StablecoinLens
The StablecoinLens
contract calculates the accurate circulating supply of crvUSD by summing the debt of all PegKeepers
and the total debt of all Controllers
. This approach is necessary because simply calling crvUSD.totalSupply()
returns an inflated number, as it includes idle crvUSD in PegKeepers
, unborrowed crvUSD in Controllers
, and crvUSD allocated to the FlashLender
or other venues.
StablecoinLens.vy
The source code for the StablecoinLens.vy
contract is available on GitHub. The contract is written using Vyper version ~=0.4
.
The contract is deployed on Ethereum at 0xe24e2db9f6bb40bbe7c1c025bc87104f5401ecd7
.
The source code was audited by ChainSecurity. The audit report is available on GitHub.
Warning: Usage of StablecoinLens.vy
contract
In theory, the calculation of the true circulating supply of crvUSD could be manipulated using MEV techniques. For example, one could take a flash loan of up to 1 million crvUSD or borrow a significant amount of crvUSD from a Controller, then take a snapshot via RewardsHandler.take_snapshot()
, and subsequently repay the debt. However, there is a lower bound defined by minimum_weight
and an upper bound defined by the FeeSplitter
cap.
Ultimately, as this calculation is a moving average, successful manipulation would require repeated MEV actions over multiple snapshots to have a substantial impact.
Nontheless, the contract should not be used by third parties before consulting with the Curve team.
circulating_supply
ΒΆ
StablecoinLens.circulating_supply() -> uint256
Function to compute the true circulating supply of crvUSD. Calling totalSupply
directly returns an inflated figure, as it includes idle crvUSD in PegKeepers
, unborrowed crvUSD in Controllers
, and crvUSD allocated to the FlashLender
contract. The true circulating supply is calculated by summing the debt of all PegKeepers
and the total debt of each Controller
in the factory.
Calculation logic:
- Fetches a predefined crvUSD
Controller
from theControllerFactory
, in this case the WETHController
. This is hardcoded and can not be changed. - Fetches the
MonetaryPolicy
from the WETHController
by calling themonetary_policy()
function. - Iterates over the
PegKeepers
in theMonetaryPolicy
, summing the debt of allPegKeepers
. Idle sitting crvUSD inPegKeepers
are not included in the calculation as they are not circulating. - Iterates over all crvUSD
Controllers
and sums the total debt of eachController
. - Returns the combined sum of the debt of all
PegKeepers
and the total debt of allControllers
.
Returns: true circulating supply of crvUSD (uint256
).
Source code
# pragma version ~=0.4
from interfaces import IPegKeeper
from interfaces import IController
from interfaces import IControllerFactory
from interfaces import IMonetaryPolicy
# bound from factory
MAX_CONTROLLERS: constant(uint256) = 50000
# bound from monetary policy
MAX_PEG_KEEPERS: constant(uint256) = 1001
# could have been any other controller
WETH_CONTROLLER_IDX: constant(uint256) = 3
# the crvusd controller factory
factory: immutable(IControllerFactory)
@deploy
def __init__(_factory: IControllerFactory):
factory = _factory
@view
@external
def circulating_supply() -> uint256:
return self._circulating_supply()
@view
@internal
def _circulating_supply() -> uint256:
"""
@notice Compute the circulating supply for crvUSD, `totalSupply` is incorrect
since it takes into account all minted crvUSD (i.e. flashloans)
@dev This function sacrifices some gas to fetch peg keepers from a unique source
of truth to avoid having to manually maintain multiple lists across several
contracts. For this reason we read the list of peg keepers contained in the
monetary policy returned by a controller in the factory. factory -> weth
controller -> monetary policy -> peg keepers This function is not exposed as
external as it can be easily manipulated and should not be used by third party
contracts.
"""
circulating_supply: uint256 = 0
# Fetch the weth controller (index 3) under the assumption that
# weth will always be a valid collateral for crvUSD, therefore its
# monetary policy should always be up to date.
controller: IController = staticcall factory.controllers(WETH_CONTROLLER_IDX)
# We obtain the address of the current monetary policy used by the
# weth controller because it contains a list of all the peg keepers.
monetary_policy: IMonetaryPolicy = staticcall controller.monetary_policy()
# Iterate over the peg keepers (since it's a fixed size array we
# wait for a zero address to stop iterating).
for i: uint256 in range(MAX_PEG_KEEPERS):
pk: IPegKeeper = staticcall monetary_policy.peg_keepers(i)
if pk.address == empty(address):
# end of array
break
circulating_supply += staticcall pk.debt()
n_controllers: uint256 = staticcall factory.n_collaterals()
for i: uint256 in range(n_controllers, bound=MAX_CONTROLLERS):
controller = staticcall factory.controllers(i)
# add crvUSD minted by controller
circulating_supply += staticcall controller.total_debt()
return circulating_supply
This example returns the true circulating supply of crvUSD.
>>> StablecoinLens.circulating_supply()