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 to stETH via unwrapping (wstETH Burner)
stETH to ETH via swap through stETH/ETH curve pool (SwapStableBurner)
ETH to USDT via swap through tricrypto pool (CryptoSwapBurner)
USDT to 3CRV via depositing into 3pool (StableDepositBurner)
Simplified burn pattern:
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.
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.
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.
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.
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
@externaldefset_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 """assertmsg.senderin[self.owner,self.emergency_owner]# dev: only owner# if another burner was previous set, revoke approvalspool:address=self.swap_data[_lp_token].poolifpool!=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].coinifcoin!=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,)iflen(response)!=0:assertconvert(response,bool)# find `i` for `_coin` within the pool, approve transfers and save to storageregistry:address=AddressProvider(ADDRESS_PROVIDER).get_registry()pool=Registry(registry).get_pool_from_lp_token(_lp_token)coins:address[8]=Registry(registry).get_coins(pool)foriinrange(8):ifcoins[i]==ZERO_ADDRESS:raiseifcoins[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,)iflen(response)!=0:assertconvert(response,bool)returnTrueraise
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.
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:
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
@externaldefset_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()foriinrange(10):coin:address=_coins[i]ifcoin==ZERO_ADDRESS:breaktarget:address=_targets[i]assertRegistry(registry).find_pool_for_coins(coin,target)!=ZERO_ADDRESSifself.currency_keys[target]==EMPTY_BYTES32:# if target is not a synth, ensure target already has a target setassertself.swap_for[target]!=ZERO_ADDRESSself.swap_for[coin]=targetreturnTrue
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")defadd_synths(_synths:address[10])->bool:""" @notice Registry synth token addresses @param _synths List of synth tokens to register @return bool success """forsynthin_synths:ifsynth==ZERO_ADDRESS:break# this will revert if `_synth` is not actually a synthself.currency_keys[synth]=Synth(synth).currencyKey()returnTrue
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
defexecute()->bool:""" @notice Add liquidity to 3pool and transfer 3CRV to the fee distributor @return bool success """assertnotself.is_killed# dev: is killedamounts:uint256[3]=[ERC20(TRIPOOL_COINS[0]).balanceOf(self),ERC20(TRIPOOL_COINS[1]).balanceOf(self),ERC20(TRIPOOL_COINS[2]).balanceOf(self),]ifamounts[0]!=0andamounts[1]!=0andamounts[2]!=0:StableSwap(TRIPOOL).add_liquidity(amounts,0)amount:uint256=ERC20(TRIPOOL_LP).balanceOf(self)ifamount!=0:ERC20(TRIPOOL_LP).transfer(self.receiver,amount)returnTrue
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.
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
@externaldefburn(_coin:ERC20)->bool:""" @notice Convert `_coin` by depositing @param _coin Address of the coin being converted @return bool success """assertnotself.is_killed# dev: is killedassert_coininCOINSamount:uint256=_coin.balanceOf(msg.sender)assert_coin.transferFrom(msg.sender,self,amount,default_return_value=True)# safe transferif_coin==COINS[N_COINS-1]:# Do it onceamounts:uint256[N_COINS]=empty(uint256[N_COINS])foriinrange(N_COINS):amounts[i]=COINS[i].balanceOf(self)self._burn(amounts)returnTrue@internaldef_burn(_amounts:uint256[N_COINS]):amount:uint256=0foriinrange(N_COINS):amount+=_amounts[i]*DEC[i]min_amount:uint256=amount*ONE/POOL.get_virtual_price()min_amount-=min_amount*self.slippage/BPSPOOL.add_liquidity(_amounts,min_amount)amount=LP.balanceOf(self)LP.transfer(FEE_DISTRIBUTER,amount)
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
@externaldefwithdraw_admin_fees():# transfer coin 0 to Factory and call `convert_fees` to swap it for coin 1factory:address=self.factorycoin:address=self.coins[0]amount:uint256=ERC20(coin).balanceOf(self)-self.balances[0]ifamount>0:response:Bytes[32]=raw_call(coin,concat(method_id("transfer(address,uint256)"),convert(factory,bytes32),convert(amount,bytes32),),max_outsize=32,)iflen(response)>0:assertconvert(response,bool)Factory(factory).convert_metapool_fees()# transfer coin 1 to the receivercoin=self.coins[1]amount=ERC20(coin).balanceOf(self)-self.balances[1]ifamount>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,)iflen(response)>0:assertconvert(response,bool)
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
eventAddBurner:burner:address@external@nonreentrant('lock')defset_burner(_coin:address,_burner:address):""" @notice Set burner of `_coin` to `_burner` address @param _coin Token address @param _burner Burner contract address """assertmsg.sender==self.ownership_admin,"Access denied"self._set_burner(_coin,_burner)@internaldef_set_burner(_coin:address,_burner:address):old_burner:address=self.burners[_coin]if_coin!=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:ifold_burner!=ZERO_ADDRESS:# revoke approval on previous burnerresponse:Bytes[32]=raw_call(_coin,concat(method_id("approve(address,uint256)"),convert(old_burner,bytes32),convert(0,bytes32),),max_outsize=32,)iflen(response)!=0:assertconvert(response,bool)if_burner!=ZERO_ADDRESS:# infinite approval for current burnerresponse:Bytes[32]=raw_call(_coin,concat(method_id("approve(address,uint256)"),convert(_burner,bytes32),convert(MAX_UINT256,bytes32),),max_outsize=32,)iflen(response)!=0:assertconvert(response,bool)self.burners[_coin]=_burnerlogAddBurner(_burner)
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
eventAddBurner:burner:address@internaldef_set_burner(_coin:address,_burner:address):old_burner:address=self.burners[_coin]if_coin!=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:ifold_burner!=ZERO_ADDRESS:# revoke approval on previous burnerresponse:Bytes[32]=raw_call(_coin,concat(method_id("approve(address,uint256)"),convert(old_burner,bytes32),convert(0,bytes32),),max_outsize=32,)iflen(response)!=0:assertconvert(response,bool)if_burner!=ZERO_ADDRESS:# infinite approval for current burnerresponse:Bytes[32]=raw_call(_coin,concat(method_id("approve(address,uint256)"),convert(_burner,bytes32),convert(MAX_UINT256,bytes32),),max_outsize=32,)iflen(response)!=0:assertconvert(response,bool)self.burners[_coin]=_burnerlogAddBurner(_burner)@external@nonreentrant('lock')defset_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 """assertmsg.sender==self.ownership_admin,"Access denied"foriinrange(20):coin:address=_coins[i]ifcoin==ZERO_ADDRESS:breakself._set_burner(coin,_burners[i])