Withdraw and Burn
Withdrawing Admin Fees¶
In order to be able to burn admin fees into the fee token, those fees have to be claimed prior. Admin fees can be claimed by anyone. Somethimes, the function to claim the fees is guarded and therefore only called by the proxy contract (admin or owner of the pool). If thats the case, users can just call the claim function via the proxy contract (as the function is not guarded there).
Claiming fees can differ based on which source they are claimed from:
StableSwap Pools¶
Admin fees are stored within each exchange contract and viewable via the public getter method admin_balances
. Users may call withdraw_admin_fees
to claim the fees at any time.
Fees are usually claimed via the withdraw_many
function of the PoolProxy. This withdraws fees from multiple pools at once, pulling them into the PoolProxy contract.
Tip
Admin fees can either be claimed through the corresponding PoolProxy or directly by calling the withdraw_admin_fees
function on the pool contract itself (if the function is not guarded).
admin_balances
¶
Pool.admin_balances(i: uint256) -> uint256:
Getter for the admin fees of coin i
in a specific pool.
Returns: admin balances (uint256
).
Input | Type | Description |
---|---|---|
i | uint256 | coin index |
Source code
withdraw_admin_fees
¶
PoolProxy.withdraw_admin_fees(_pool: address):
Info
This function is called from the PoolProxy.
Function to claim admin fees from pool
into this contract. This is the first step in the fee burning process.
Input | Type | Description |
---|---|---|
_pool | address | pool address |
Source code
withdraw_many
¶
PoolProxy.withdraw_many(_pools: address[20]):
Info
This function is called from the PoolProxy.
Function to withdraw fees from multiple pools in a single call.
Input | Type | Description |
---|---|---|
_pool | address | Pool Address |
Source code
interface Curve:
def withdraw_admin_fees(): nonpayable
@external
@nonreentrant('lock')
def withdraw_many(_pools: address[20]):
"""
@notice Withdraw admin fees from multiple pools
@param _pools List of pool address to withdraw admin fees from
"""
for pool in _pools:
if pool == ZERO_ADDRESS:
break
Curve(pool).withdraw_admin_fees()
CryptoSwap Pools¶
Fees of crypto pools are a bit different from stableswap pools. These pools have an auto-rebalancing mechanism which uses parts of the admin fees for rebalancing purposes. After taking this into consideration, fees are claimed by minting the admin's share (which essentially is the admin fee) of the pool as LP tokens.
Fees are mostly claimed directly from the pool.
claim_admin_fees
¶
Pool.claim_admin_fees():
Function to claim admin fees from a crypto pool.
Source code
event ClaimAdminFee:
admin: indexed(address)
tokens: uint256
@external
@nonreentrant("lock")
def claim_admin_fees():
"""
@notice Claim admin fees. Callable by anyone.
"""
self._claim_admin_fees()
@internal
def _claim_admin_fees():
"""
@notice Claims admin fees and sends it to fee_receiver set in the factory.
"""
A_gamma: uint256[2] = self._A_gamma()
xcp_profit: uint256 = self.xcp_profit # <---------- Current pool profits.
xcp_profit_a: uint256 = self.xcp_profit_a # <- Profits at previous claim.
total_supply: uint256 = self.totalSupply
# Do not claim admin fees if:
# 1. insufficient profits accrued since last claim, and
# 2. there are less than 10**18 (or 1 unit of) lp tokens, else it can lead
# to manipulated virtual prices.
if xcp_profit <= xcp_profit_a or total_supply < 10**18:
return
# Claim tokens belonging to the admin here. This is done by 'gulping'
# pool tokens that have accrued as fees, but not accounted in pool's
# `self.balances` yet: pool balances only account for incoming and
# outgoing tokens excluding fees. Following 'gulps' fees:
for i in range(N_COINS):
if coins[i] == WETH20:
self.balances[i] = self.balance
else:
self.balances[i] = ERC20(coins[i]).balanceOf(self)
# If the pool has made no profits, `xcp_profit == xcp_profit_a`
# and the pool gulps nothing in the previous step.
vprice: uint256 = self.virtual_price
# Admin fees are calculated as follows.
# 1. Calculate accrued profit since last claim. `xcp_profit`
# is the current profits. `xcp_profit_a` is the profits
# at the previous claim.
# 2. Take out admin's share, which is hardcoded at 5 * 10**9.
# (50% => half of 100% => 10**10 / 2 => 5 * 10**9).
# 3. Since half of the profits go to rebalancing the pool, we
# are left with half; so divide by 2.
fees: uint256 = unsafe_div(
unsafe_sub(xcp_profit, xcp_profit_a) * ADMIN_FEE, 2 * 10**10
)
# ------------------------------ Claim admin fees by minting admin's share
# of the pool in LP tokens.
receiver: address = Factory(self.factory).fee_receiver()
if receiver != empty(address) and fees > 0:
frac: uint256 = vprice * 10**18 / (vprice - fees) - 10**18
claimed: uint256 = self.mint_relative(receiver, frac)
xcp_profit -= fees * 2
self.xcp_profit = xcp_profit
log ClaimAdminFee(receiver, claimed)
# ------------------------------------------- Recalculate D b/c we gulped.
D: uint256 = MATH.newton_D(A_gamma[0], A_gamma[1], self.xp(), 0)
self.D = D
# ------------------- Recalculate virtual_price following admin fee claim.
# In this instance we do not check if current virtual price is greater
# than old virtual price, since the claim process can result
# in a small decrease in pool's value.
self.virtual_price = 10**18 * self.get_xcp(D) / self.totalSupply
self.xcp_profit_a = xcp_profit # <------------ Cache last claimed profit.
Curve Stablecoin¶
crvUSD fees are based on the borrow rate of the corresponding markets. Fees are accurred in crvUSD token. They can be claimed from the according Controller.
admin_fees
¶
Controller.admin_fees() -> uint256:
Getter for the currently claimable admin fees form a Controller. These fees can be collected via the collect_fees()
function (see below).
Source code
@external
@view
def admin_fees() -> uint256:
"""
@notice Calculate the amount of fees obtained from the interest
"""
rate_mul: uint256 = AMM.get_rate_mul()
loan: Loan = self._total_debt
loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul
loan.initial_debt += self.redeemed
minted: uint256 = self.minted
return unsafe_sub(max(loan.initial_debt, minted), minted)
collect_fees
¶
Controller.collect_fees():
Function to collects all fees, including borrowing-based fees (interest rate) and AMM-based fees (swap fee, if applicable).
Source code
@external
@nonreentrant('lock')
def collect_fees() -> uint256:
"""
@notice Collect the fees charged as interest
"""
_to: address = FACTORY.fee_receiver()
# AMM-based fees
borrowed_fees: uint256 = AMM.admin_fees_x()
collateral_fees: uint256 = AMM.admin_fees_y()
if borrowed_fees > 0:
STABLECOIN.transferFrom(AMM.address, _to, borrowed_fees)
if collateral_fees > 0:
assert COLLATERAL_TOKEN.transferFrom(AMM.address, _to, collateral_fees, default_return_value=True)
AMM.reset_admin_fees()
# Borrowing-based fees
rate_mul: uint256 = self._rate_mul_w()
loan: Loan = self._total_debt
loan.initial_debt = loan.initial_debt * rate_mul / loan.rate_mul
loan.rate_mul = rate_mul
self._total_debt = loan
# Amount which would have been redeemed if all the debt was repaid now
to_be_redeemed: uint256 = loan.initial_debt + self.redeemed
# Amount which was minted when borrowing + all previously claimed admin fees
minted: uint256 = self.minted
# Difference between to_be_redeemed and minted amount is exactly due to interest charged
if to_be_redeemed > minted:
self.minted = to_be_redeemed
to_be_redeemed = unsafe_sub(to_be_redeemed, minted) # Now this is the fees to charge
STABLECOIN.transfer(_to, to_be_redeemed)
log CollectFees(to_be_redeemed, loan.initial_debt)
return to_be_redeemed
else:
log CollectFees(0, loan.initial_debt)
return 0
Burning Admin Fees¶
All admin fees are accumulated in the 0xECB contract and are burned according to the fee-burner settings designated for each specific coin.
These functions need to be called from the 0xECB contract.
burn
¶
0xECB.burn(_coin: address):
Guarded Method
This function is only callable by EOA to prevent flashloan exploits.
Transfer the contract’s balance of coin
into the according burner and execute the burn process.
Input | Type | Description |
---|---|---|
_coin | address | Token Address |
Source code
interface Burner:
def burn(_coin: address) -> bool: payable
@external
@nonreentrant('burn')
def burn(_coin: address):
"""
@notice Burn accrued `_coin` via a preset burner
@dev Only callable by an EOA to prevent flashloan exploits
@param _coin Coin address
"""
assert tx.origin == msg.sender
assert not self.burner_kill
_value: uint256 = 0
if _coin == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:
_value = self.balance
Burner(self.burners[_coin]).burn(_coin, value=_value) # dev: should implement burn()
burn_many
¶
0xECB.burn_many(_coins: address[20]):
Guarded Method
This function is only callable by EOA to prevent flashloan exploits.
Executes the burn process on many coins at once.
Note
Burning can be very gas intensive. In some cases burning 20 coins at once is not possible due to the block gas limit.
Input | Type | Description |
---|---|---|
_coins | address[20] | Token Addresses |
Source code
@external
@nonreentrant('burn')
def burn_many(_coins: address[20]):
"""
@notice Burn accrued admin fees from multiple coins
@dev Only callable by an EOA to prevent flashloan exploits
@param _coins List of coin addresses
"""
assert tx.origin == msg.sender
assert not self.burner_kill
for coin in _coins:
if coin == ZERO_ADDRESS:
break
_value: uint256 = 0
if coin == 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE:
_value = self.balance
Burner(self.burners[coin]).burn(coin, value=_value) # dev: should implement burn()
donate_admin_fees
¶
0xECB.donate_admin_fees(_pool: address):
Warning
Most pools do not have this donation function implemented!
Guarded Method
This function is only callable by the ownership_admin
or its prior approved wallets.
Function donate a pool’s current admin fees to the pool LPs.
Input | Type | Description |
---|---|---|
_pool | address | pool addresses |
Source code
interface Curve:
def donate_admin_fees(): nonpayable
@external
@nonreentrant('lock')
def donate_admin_fees(_pool: address):
"""
@notice Donate admin fees of `_pool` pool
@param _pool Pool address
"""
if msg.sender != self.ownership_admin:
assert self.donate_approval[_pool][msg.sender], "Access denied"
Curve(_pool).donate_admin_fees() # dev: if implemented by the pool
donate_admin_fees
¶
0xECB.set_donate_approval(_pool: address, _caller: address, _is_approved: bool):
Warning
Most pools do not have this donation function implemented!
Guarded Method
This function is only callable by the ownership_admin
of the contract.
Function to set donation approval for _pool
to _caller
.
Input | Type | Description |
---|---|---|
_pool | address | pool address |
_caller | address | address to set approval for |
_is_approved | bool | approval status |
Source code
# pool -> caller -> can call `donate_admin_fees`
donate_approval: public(HashMap[address, HashMap[address, bool]])
@external
def set_donate_approval(_pool: address, _caller: address, _is_approved: bool):
"""
@notice Set approval of `_caller` to donate admin fees for `_pool`
@param _pool Pool address
@param _caller Adddress to set approval for
@param _is_approved Approval status
"""
assert msg.sender == self.ownership_admin, "Access denied"
self.donate_approval[_pool][_caller] = _is_approved