Burners
Overview¶
Burning is handled on a per-coin basis. The process is initiated by calling the PoolProxy.burn
or PoolProxy.burn_many
functions. Calling to burn a coin transfers that coin into the burner and then calls the burn
function on the burner.
Each burn
action typically performs one conversion into another asset; either 3CRV itself, or something that is a step closer to reaching 3CRV. As an example, here is the sequence of conversions required to burn wstETH:
wstETH -> stETH -> ETH -> USDT
wstETH
tostETH
via unwrapping (wstETH Burner)stETH
toETH
via swap through stETH/ETH curve pool (SwapStableBurner)ETH
toUSDT
via swap through tricrypto pool (CryptoSwapBurner)USDT
to3CRV
via depositing into 3pool (StableDepositBurner)
Simplified burn pattern:
flowchart LR
p[(0xECB)]
v1(StableSwap) ..->|withdraw admin fees| p;
v2(CryptoSwap) ..-> |claim admin fees| p;
crvUSD(crvUSD Markets) ..-> |collect fees| p;
p --->|burn| b1([Burner 1]);
p -->|burn| b2([Burner 2]);
p -->|burn| b3([Burner 3]);
b1 --> ub{{StableDepositBurner}}
b1 --> |if burned directly for 3CRV| fd(((FeeDistributor)))
b2 --> ub
b3 --> ub
ub --> |burn for 3CRV| fd
Burning Efficiency
Efficiency within the intermediate conversions is the reason it is important to run the burn process in a specific order. For example, if you burn stETH prior to burning wstETH, you will have to burn stETH a second time!
There are multiple burner contracts, each of which handles a different category of fee coin.
Deployed Burner Contracts¶
Contract Source & Deployment
Source code for burners is available on Github.
Burner Type | Description | Address |
---|---|---|
ABurner | Aave lending tokens | 0x12220a63a2013133D54558C9d03c35288eAC9B34 |
CBurner | Compound lending tokens | 0xdd0e10857d952c73b2fa39ce86308299df8774b8 |
YBurner | Yearn lending tokens | 0xd16ea3e5681234da84419512eb597362135cd8c9 |
CryptoSwapBurner | Swaps crypto LP tokens | 0xdc237b4B882Fa1d1fd1dD5B59A08F8dB3416DbE3 |
SwapStableBurner | Swaps into another asset using a stable pool | 0x90B4508e8F91523e5c8854eA73AFD8c22d8c27b7 |
LPBurner | Burner for LP Tokens | 0xaa42C0CD9645A58dfeB699cCAeFBD30f19B1ff81 |
MetaBurner | USD denominated assets that are directly swappable for 3CRV | 0xE4b65889469ad896e866331f0AB5652C1EcfB3E6 |
SynthBurner | non-USD denominated assets that are synths or can be swapped into synths | 0x67a0213310202dbc2cbe788f4349b72fba90f9fa |
UniswapBurner | Assets that must be swapped on Uniswap/Sushiswap | 0xf3b64840b39121b40d8685f1576b64c157ce2e24 |
UnderlyingBurner | Assets that can be directly deposited into 3pool, or swapped for an asset that is deposited into 3pool | 0x786b374b5eef874279f4b7b4de16940e57301a58 |
Wrapped stETH Burner | Unwraps wstETH into stETH | 0x072C93f12dC274300c79E92Eb61a0feCFa8E8918 |
Stable Deposit Burner | Deposits stables into Threepool | 0x1D56495c76d99435d10ecd5b0C3bd6a8EE7cC3bb |
Tricrypto Factory LP Burner | Withdraws LP Tokens | 0xA6a0103f8F185786143f3EFe3Ddf268d8E070813 |
crvUSD Burner | Withdraws LP tokens from crvUSD pools and exchanges crvUSD | 0xA6a0103f8F185786143f3EFe3Ddf268d8E070813 |
Burners¶
ABurner / CBurner / YBurner¶
ABurner
, CBurner
and YBurner
are collectively known as “lending burners”. They unwrap lending tokens into the underlying asset and transfer those assets onward into the underlying burner.
There is no configuration required for this burner.
CryptoSwap Burner¶
The CryptoSwapBurner is used to burn fees from Crypto Pools.
StableSwap Burner¶
Swaps an asset into another asset using a Stable pool and forwards to another burner.
LP Burner¶
The LP Burner handles non-3CRV LP tokens. This burner is primarily used for FRAXBP LP tokens which are converted to USDC and then sent to 0xECB for a further burn process.
LP burner calls to StableSwap.remove_liquidity_one_coin
to unwrap the LP token. The new asset is then transferred on to another burner.
swap_data
¶
LPBurner.swap_data(arg0: adress) -> pool: address, coin: address, burner: address, i: int128
Getter method for informations about the LP Token burn process.
Retuns: pool (address
) of the LP token, coin (address
) in which the LP token in withdrawn, burner (address
) where the output token is forwarded to and i (index
) of coin
in the pool.
Input | Type | Description |
---|---|---|
arg0 | address | LP Token Address |
set_swap_data
¶
LPBurner.set_swap_data(_lp_token: address, _coin: address, _burner: address) -> bool:
Guarded Method
This function is only callable by the owner
or emergency_owner
of the contract.
Function to set the swap data of a LP token.
Returns: true (bool
).
Input | Type | Description |
---|---|---|
_lp_token | address | LP token address |
_coin | address | coin address to swap the LP token to |
_burner | address | burner address to forward to |
Source code
@external
def set_swap_data(_lp_token: address, _coin: address, _burner: address) -> bool:
"""
@notice Set conversion and transfer data for `_lp_token`
@param _lp_token LP token address
@param _coin Underlying coin to remove liquidity in
@param _burner Burner to transfer `_coin` to
@return bool success
"""
assert msg.sender in [self.owner, self.emergency_owner] # dev: only owner
# if another burner was previous set, revoke approvals
pool: address = self.swap_data[_lp_token].pool
if pool != ZERO_ADDRESS:
# we trust that LP tokens always return True, so no need for `raw_call`
ERC20(_lp_token).approve(pool, 0)
coin: address = self.swap_data[_lp_token].coin
if coin != ZERO_ADDRESS:
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(self.swap_data[_lp_token].burner, bytes32),
convert(0, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
# find `i` for `_coin` within the pool, approve transfers and save to storage
registry: address = AddressProvider(ADDRESS_PROVIDER).get_registry()
pool = Registry(registry).get_pool_from_lp_token(_lp_token)
coins: address[8] = Registry(registry).get_coins(pool)
for i in range(8):
if coins[i] == ZERO_ADDRESS:
raise
if coins[i] == _coin:
self.swap_data[_lp_token] = SwapData({
pool: pool,
coin: _coin,
burner: _burner,
i: i
})
ERC20(_lp_token).approve(pool, MAX_UINT256)
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(_burner, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
return True
raise
MetaBurner¶
The MetaBurner converts Metapool-paried coins to 3CRV and transfers to the FeeDistributor. It uses the registry’s exchange_with_best_rate
and transfers 3CRV directly to the fee distributor.
There is no configuration required for this burner.
SynthBurner¶
Swaps non-USD denominated assets for synths, converts synths to sUSD and transfers to UnderlyingBurner
. The synth burner is used to convert non-USD denominated assets into sUSD. This is accomplished via synth conversion, the same mechanism used in cross-asset swaps.
When the synth burner is called to burn a non-synthetic asset, it uses RegistrySwap.exchange_with_best_rate
to swap into a related synth. If no direct path to a synth is avaialble, a swap is made into an intermediate asset.
For synths, the burner first transfers to the Underlying. Then it calls UnderlyingBurner.convert_synth
, performing the cross-asset swap within the underlying burner. This is done to avoid requiring another transfer call after the settlement period has passed.
The optimal sequence when burning assets using the synth burner is thus:
- Coins that cannot directly swap to synths
- Coins that can directly swap to synths
- Synthetic assets
The burner is configurable via the following functions:
set_swap_for
¶
SynthBurner.set_swap_for(_coins: address[10], _targets: address[10]) -> bool:
Function to set target coins that the burner will swap into.
For assets that can be directly swapped for a synth, the target should be set as that synth. For assets that cannot be directly swapped, the target must be an asset that has already had it’s own target registered (e.g. can be swapped for a synth).
Input | Type | Description |
---|---|---|
_coin | address[10] | list of coins to be burned |
_targets | address[10] | list of coins to be swapped for |
Tip
If you wish to set less than 10 _coins
, fill the remaining array slots with ZERO_ADDRESS
.
The address as index n
within this list corresponds to the address at index n
within coins
.
Source code
@external
def set_swap_for(_coins: address[10], _targets: address[10]) -> bool:
"""
@notice Set target coins that will be swapped into
@dev If any target coin is not a synth, it must have already
had it's own target coin registered
@param _coins List of coins to be burned
@param _targets List of coins to be swapped for
@return bool success
"""
registry: address = AddressProvider(ADDRESS_PROVIDER).get_registry()
for i in range(10):
coin: address = _coins[i]
if coin == ZERO_ADDRESS:
break
target: address = _targets[i]
assert Registry(registry).find_pool_for_coins(coin, target) != ZERO_ADDRESS
if self.currency_keys[target] == EMPTY_BYTES32:
# if target is not a synth, ensure target already has a target set
assert self.swap_for[target] != ZERO_ADDRESS
self.swap_for[coin] = target
return True
add_synths
¶
SynthBurner.add_synths(_synths: address[10]) -> bool:
Register synthetic assets within the burner. This function is unguarded. For each synth to be added, a call is made to Synth.currencyKey to validate the addresss and obtain the synth currency key.
Input | Type | Description |
---|---|---|
_synths | address[10] | list of synth tokens to register |
Note
If you wish to set less than 10 _coins
, fill the remaining array slots with ZERO_ADDRESS
.
The address as index n
within this list corresponds to the address at index n
within coins
.
Source code
@external
@nonreentrant("lock")
def add_synths(_synths: address[10]) -> bool:
"""
@notice Registry synth token addresses
@param _synths List of synth tokens to register
@return bool success
"""
for synth in _synths:
if synth == ZERO_ADDRESS:
break
# this will revert if `_synth` is not actually a synth
self.currency_keys[synth] = Synth(synth).currencyKey()
return True
Wrapped stETH Burner¶
This burner unwraps wstETH to stETH and sends it back to 0xECB.
UnderlyingBurner¶
The underlying burner handles assets that can be directly swapped to USDC and deposits DAI/USDC/USDT into 3pool to obtain 3CRV. This is the final step of the burn process for many assets that require multiple intermediate swaps.
Note
Prior to burning any assets with the UnderlyingBurner, you should have completed the entire burn process with SynthBurner
, UniswapBurner
and all of the lending burners
.
The burn process consists of:
- For sUSD: First call settles to complete any pending synth conversions. Then swaps into USDC.
- For all other assets that are not DAI/USDC/USDT: Swap into USDC.
- For DAI/USDC/USDT: Only transfer the assets into the burner.
Once the entire burn process has been completed you must call execute
as the final action:
UnderlyingBurner.execute() -> bool:
Function to deposit all the tokens into 3pool and transfer the recieved 3CRV to the FeeDistributor contract.
Source code
def execute() -> bool:
"""
@notice Add liquidity to 3pool and transfer 3CRV to the fee distributor
@return bool success
"""
assert not self.is_killed # dev: is killed
amounts: uint256[3] = [
ERC20(TRIPOOL_COINS[0]).balanceOf(self),
ERC20(TRIPOOL_COINS[1]).balanceOf(self),
ERC20(TRIPOOL_COINS[2]).balanceOf(self),
]
if amounts[0] != 0 and amounts[1] != 0 and amounts[2] != 0:
StableSwap(TRIPOOL).add_liquidity(amounts, 0)
amount: uint256 = ERC20(TRIPOOL_LP).balanceOf(self)
if amount != 0:
ERC20(TRIPOOL_LP).transfer(self.receiver, amount)
return True
Note
This is the final function to be called in the burn process, after all other steps are completed. Calling this function does not do anything if the burner has a balance of zero for DAI, USDC and USDT.
Stable Deposit Burner¶
This burner converts DAI, USDC and USDT into 3CRV by adding liquidity to the 3pool and then transfers them to the FeeDistributor.
StableDepositBurner.burn(_coin: ERC20) -> bool:
Function to add the entire burner's balance of _coin
to the 3pool.
Source code
@external
def burn(_coin: ERC20) -> bool:
"""
@notice Convert `_coin` by depositing
@param _coin Address of the coin being converted
@return bool success
"""
assert not self.is_killed # dev: is killed
assert _coin in COINS
amount: uint256 = _coin.balanceOf(msg.sender)
assert _coin.transferFrom(msg.sender, self, amount, default_return_value=True) # safe transfer
if _coin == COINS[N_COINS - 1]: # Do it once
amounts: uint256[N_COINS] = empty(uint256[N_COINS])
for i in range(N_COINS):
amounts[i] = COINS[i].balanceOf(self)
self._burn(amounts)
return True
@internal
def _burn(_amounts: uint256[N_COINS]):
amount: uint256 = 0
for i in range(N_COINS):
amount += _amounts[i] * DEC[i]
min_amount: uint256 = amount * ONE / POOL.get_virtual_price()
min_amount -= min_amount * self.slippage / BPS
POOL.add_liquidity(_amounts, min_amount)
amount = LP.balanceOf(self)
LP.transfer(FEE_DISTRIBUTER, amount)
Metapool Burner¶
This is not a burner contract in itself. Some metapools transfer coin 0 of the admin fees to the Factory, where it is swapped for coin 1 (e.g., 3CRV), which is then sent directly to the FeeDistributor.
Source code
@external
def withdraw_admin_fees():
# transfer coin 0 to Factory and call `convert_fees` to swap it for coin 1
factory: address = self.factory
coin: address = self.coins[0]
amount: uint256 = ERC20(coin).balanceOf(self) - self.balances[0]
if amount > 0:
response: Bytes[32] = raw_call(
coin,
concat(
method_id("transfer(address,uint256)"),
convert(factory, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) > 0:
assert convert(response, bool)
Factory(factory).convert_metapool_fees()
# transfer coin 1 to the receiver
coin = self.coins[1]
amount = ERC20(coin).balanceOf(self) - self.balances[1]
if amount > 0:
receiver: address = Factory(factory).get_fee_receiver(self)
response: Bytes[32] = raw_call(
coin,
concat(
method_id("transfer(address,uint256)"),
convert(receiver, bytes32),
convert(amount, bytes32),
),
max_outsize=32,
)
if len(response) > 0:
assert convert(response, bool)
Configuring Fee Bruners¶
Burners are configured within the 0xECB contract.
burners
¶
PoolProxy.burners(coin: address) -> address: view
Getter for the burner contract address for coin
.
Returns: burner of a coin (address
).
Input | Type | Description |
---|---|---|
coin | address | Token Address |
set_burner
¶
PoolProxy.set_burner(_coin: address, _burner: address):
Guarded Method
This function is only callable by the ownership_admin
of the contract.
Function to set burner of _coin
to _burner
address.
Emits: AddBurner
Input | Type | Description |
---|---|---|
_coin | address | Token Address |
_burner | address | Burner Address |
Source code
event AddBurner:
burner: address
@external
@nonreentrant('lock')
def set_burner(_coin: address, _burner: address):
"""
@notice Set burner of `_coin` to `_burner` address
@param _coin Token address
@param _burner Burner contract address
"""
assert msg.sender == self.ownership_admin, "Access denied"
self._set_burner(_coin, _burner)
@internal
def _set_burner(_coin: address, _burner: address):
old_burner: address = self.burners[_coin]
if _coin != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:
if old_burner != ZERO_ADDRESS:
# revoke approval on previous burner
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(old_burner, bytes32),
convert(0, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
if _burner != ZERO_ADDRESS:
# infinite approval for current burner
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(_burner, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.burners[_coin] = _burner
log AddBurner(_burner)
set_many_burners
¶
PoolProxy.set_many_burners(_coins: address[20], _burners: address[20]):
Guarded Method
This function is only callable by the ownership_admin
of the contract.
Function to set many burner for multiple coins at once.
Emits: AddBurner
Input | Type | Description |
---|---|---|
_coins | address[20] | Token Addresses. The address at index n within this list corresponds to the address at index n within coins |
_burners | address[20] | Burner Addresses. If less than 20 burners are set, the remaining array slots need to be filled with ZERO_ADDRESS . |
Source code
event AddBurner:
burner: address
@internal
def _set_burner(_coin: address, _burner: address):
old_burner: address = self.burners[_coin]
if _coin != 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:
if old_burner != ZERO_ADDRESS:
# revoke approval on previous burner
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(old_burner, bytes32),
convert(0, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
if _burner != ZERO_ADDRESS:
# infinite approval for current burner
response: Bytes[32] = raw_call(
_coin,
concat(
method_id("approve(address,uint256)"),
convert(_burner, bytes32),
convert(MAX_UINT256, bytes32),
),
max_outsize=32,
)
if len(response) != 0:
assert convert(response, bool)
self.burners[_coin] = _burner
log AddBurner(_burner)
@external
@nonreentrant('lock')
def set_many_burners(_coins: address[20], _burners: address[20]):
"""
@notice Set burner of `_coin` to `_burner` address
@param _coins Token address
@param _burners Burner contract address
"""
assert msg.sender == self.ownership_admin, "Access denied"
for i in range(20):
coin: address = _coins[i]
if coin == ZERO_ADDRESS:
break
self._set_burner(coin, _burners[i])
set_kill_burner
¶
PoolProxy.set_burner_kill(_is_killed: bool):
Guarded Method
This function is only callable by the ownership_admin
or emergency_admin
of the contract.
Disable or enable the process of fee burning.
Input | Type | Description |
---|---|---|
_pool | address | Pool Address |
_caller | address | Caller Address |
_is_approved | boolean | True or False |
Source code
ownership_admin: public(address)
emergency_admin: public(address)
@external
def set_burner_kill(_is_killed: bool):
"""
@notice Kill or unkill `burn` functionality
@param _is_killed Burner kill status
"""
assert msg.sender == self.emergency_admin or msg.sender == self.ownership_admin, "Access denied"
self.burner_kill = _is_killed