Skip to content

Commit

Permalink
refact: reduce gas consumption while donating tokens (#87)
Browse files Browse the repository at this point in the history
* refact: reduce gas consumption while donating tokens

* test: fix test LiquidatorStreamFeeVault.spec

* docs: updates convex vaults diagrams
  • Loading branch information
doncesarts authored and naddison36 committed Nov 24, 2022
1 parent 186dd0a commit 87515cb
Show file tree
Hide file tree
Showing 16 changed files with 705 additions and 511 deletions.
66 changes: 40 additions & 26 deletions contracts/vault/liquidator/LiquidatorStreamAbstractVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,32 +111,7 @@ abstract contract LiquidatorStreamAbstractVault is AbstractVault, LiquidatorAbst
*/
function donate(address token, uint256 amount) external virtual override streamRewards {
(uint256 newShares, uint256 newAssets) = _convertTokens(token, amount);

StreamData memory stream = shareStream;
uint256 remainingStreamShares = _streamedShares(stream);

if (newShares > 0) {
// Not all shares have to be streamed. Some may be used as a fee.
(uint256 newStreamShares, uint256 streamAssets) = _beforeStreamShare(
newShares,
newAssets
);

uint256 sharesPerSecond = ((remainingStreamShares + newStreamShares) *
STREAM_PER_SECOND_SCALE) / STREAM_DURATION;

// Store updated stream data
shareStream = StreamData(
SafeCast.toUint32(block.timestamp),
SafeCast.toUint32(block.timestamp + STREAM_DURATION),
SafeCast.toUint128(sharesPerSecond)
);

// Mint new shares that will be burnt over time.
_mint(address(this), newStreamShares);

emit Deposit(msg.sender, address(this), streamAssets, newStreamShares);
}
_streamNewShares(newShares, newAssets);
}

/**
Expand Down Expand Up @@ -202,6 +177,45 @@ abstract contract LiquidatorStreamAbstractVault is AbstractVault, LiquidatorAbst
}
}

/**
* @notice Mints shares so the assets per share does not increase initially,
* and then burns the new shares over a period of time
* so the assets per share gradually increases.
* For example, if 1,000 DAI is being donated is worth 800 vault shares and the donation fee is 16%. 128 (800 * 16%)
* of the new vault shares are minted to the fee receiver. The remaining 672 vault shares are minted to the vault so
* the assets per shares of the shareholders does not increase. Over the next week the new vault shares will be burnt
* which will increase the assets per share to the shareholders.
*
* @param newShares The amount of shares to stream.
* @param newAssets The amount of assets to stream.
*/
function _streamNewShares(uint256 newShares, uint256 newAssets) internal virtual {
if (newShares > 0) {
StreamData memory stream = shareStream;
uint256 remainingStreamShares = _streamedShares(stream);
// Not all shares have to be streamed. Some may be used as a fee.
(uint256 newStreamShares, uint256 streamAssets) = _beforeStreamShare(
newShares,
newAssets
);

uint256 sharesPerSecond = ((remainingStreamShares + newStreamShares) *
STREAM_PER_SECOND_SCALE) / STREAM_DURATION;

// Store updated stream data
shareStream = StreamData(
SafeCast.toUint32(block.timestamp),
SafeCast.toUint32(block.timestamp + STREAM_DURATION),
SafeCast.toUint128(sharesPerSecond)
);

// Mint new shares that will be burnt over time.
_mint(address(this), newStreamShares);

emit Deposit(msg.sender, address(this), streamAssets, newStreamShares);
}
}

/***************************************
Streamed Rewards Views
****************************************/
Expand Down
95 changes: 81 additions & 14 deletions contracts/vault/liquidity/convex/Convex3CrvAbstractVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ abstract contract Convex3CrvAbstractVault is AbstractSlippage, AbstractVault {
_data.booster
).poolInfo(_data.convexPoolId);
metapoolToken = metapoolTokenAddress;
metapoolTokenScale = 10**IERC20Metadata(metapoolTokenAddress).decimals();
metapoolTokenScale = 10 ** IERC20Metadata(metapoolTokenAddress).decimals();
baseRewardPool = IConvexRewardsPool(baseRewardPoolAddress);

metapool = _data.metapool;
Expand Down Expand Up @@ -145,12 +145,23 @@ abstract contract Convex3CrvAbstractVault is AbstractSlippage, AbstractVault {
shares = _depositInternal(assets, receiver, depositSlippage);
}

/// @dev Vault assets (3Crv) -> Metapool LP tokens, eg musd3Crv -> vault shares
/**
* @dev Vault assets (3Crv) -> Metapool LP tokens, eg musd3Crv -> vault shares
* If the vault has any 3Crv balance, it is added to the mint/deposit 3Crv that is added to the Metapool
* and LP deposited to the Convex pool. The resulting shares are proportionally split between the reciever
* and the vault via _afterSharesMintedHook fn.
*
* @param _assets The amount of underlying assets to be transferred to the vault.
* @param _receiver The account that the vault shares will be minted to.
* @param _slippage Deposit slippage in basis points i.e. 1% = 100.
* @return shares The amount of vault shares that were minted.
*/
function _depositInternal(
uint256 _assets,
address _receiver,
uint256 _slippage
) internal virtual returns (uint256 shares) {
uint256 assetsToDeposit = _asset.balanceOf(address(this)) + _assets;
// Transfer vault's asssets (3Crv) from the caller.
_asset.safeTransferFrom(msg.sender, address(this), _assets);

Expand All @@ -159,29 +170,34 @@ abstract contract Convex3CrvAbstractVault is AbstractSlippage, AbstractVault {
uint256 totalMetapoolTokensBefore = baseRewardPool.balanceOf(address(this));

// Calculate fair amount of metapool LP tokens, eg musd3Crv, using virtual prices for vault assets (3Crv)
uint256 minMetapoolTokens = _getMetapoolTokensForAssets(_assets);
uint256 minMetapoolTokens = _getMetapoolTokensForAssets(assetsToDeposit);
// Calculate min amount of metapool LP tokens with max slippage
// This is used for sandwich attack protection
minMetapoolTokens = (minMetapoolTokens * (BASIS_SCALE - _slippage)) / BASIS_SCALE;

// Deposit 3Crv into metapool and the stake into Convex vault
uint256 metapoolTokensReceived = _depositAndStake(_assets, minMetapoolTokens);
uint256 metapoolTokensReceived = _depositAndStake(assetsToDeposit, minMetapoolTokens);

// Calculate the proportion of shares to mint based on the amount of Metapool LP tokens.
shares = _getSharesFromMetapoolTokens(
uint256 sharesToMint = _getSharesFromMetapoolTokens(
metapoolTokensReceived,
totalMetapoolTokensBefore,
totalSupply()
);
// Calculate the proportion of shares to mint to the receiver.
shares = (sharesToMint * _assets) / assetsToDeposit;

_mint(_receiver, shares);

emit Deposit(msg.sender, _receiver, _assets, shares);
// Account any new shares, assets.
_afterSharesMintedHook(sharesToMint - shares, assetsToDeposit - _assets);
}

/// @dev Converts vault assets to shares in two steps
/// Vault assets (3Crv) -> Metapool LP tokens, eg musd3Crv -> vault shares
/// Override `AbstractVault._previewDeposit`.
/// changes - It takes into account any asset balance to be included in the deposit.
function _previewDeposit(uint256 assets)
internal
view
Expand All @@ -190,20 +206,24 @@ abstract contract Convex3CrvAbstractVault is AbstractSlippage, AbstractVault {
returns (uint256 shares)
{
if (assets > 0) {
// Take into account any asset balance.
uint256 assetsToDeposit = _asset.balanceOf(address(this)) + assets;
// Calculate Metapool LP tokens, eg musd3Crv, for vault assets (3Crv)
(uint256 metapoolTokens, , , ) = Curve3CrvMetapoolCalculatorLibrary.calcDeposit(
metapool,
metapoolToken,
assets,
assetsToDeposit,
1
);

// Calculate the proportion of shares to mint based on the amount of metapool LP tokens, eg musd3Crv.
shares = _getSharesFromMetapoolTokens(
uint256 sharesToMint = _getSharesFromMetapoolTokens(
metapoolTokens,
baseRewardPool.balanceOf(address(this)),
totalSupply()
);
// Calculate the callers portion of shares
shares = (sharesToMint * assets) / assetsToDeposit;
}
}

Expand All @@ -215,19 +235,31 @@ abstract contract Convex3CrvAbstractVault is AbstractSlippage, AbstractVault {
override
returns (uint256 assets)
{
uint256 donatedAssets = _asset.balanceOf(address(this));
uint256 donatedMetapoolTokens = 0;
if (donatedAssets > 0) {
(donatedMetapoolTokens, , , ) = Curve3CrvMetapoolCalculatorLibrary.calcDeposit(
metapool,
metapoolToken,
donatedAssets,
1
);
}
// Calculate Curve Metapool LP tokens, eg musd3CRV, needed to mint the required amount of shares
uint256 requiredMetapoolTokens = _getMetapoolTokensFromShares(
uint256 metapoolTokens = _getMetapoolTokensFromShares(
shares,
baseRewardPool.balanceOf(address(this)),
totalSupply()
);
uint256 requiredMetapoolTokens = metapoolTokens + donatedMetapoolTokens;

// Calculate assets needed to deposit into the metapool for the for required metapool lp tokens.
// Calculate assets needed to deposit into the metapool for the required metapool lp tokens.
uint256 assetsToDeposit;
uint256 invariant;
uint256 metapoolTotalSupply;
uint256 baseVirtualPrice;
(
assets,
assetsToDeposit,
invariant,
metapoolTotalSupply,
baseVirtualPrice
Expand All @@ -237,39 +269,59 @@ abstract contract Convex3CrvAbstractVault is AbstractSlippage, AbstractVault {
requiredMetapoolTokens,
1
);
assets = (assetsToDeposit * requiredMetapoolTokens) / metapoolTokens;
// Protect against sandwich and flash loan attacks where the balance of the metapool is manipulated.
uint256 maxAssets = (requiredMetapoolTokens * invariant * VIRTUAL_PRICE_SCALE) /
(metapoolTotalSupply * baseVirtualPrice);
maxAssets = (maxAssets * (BASIS_SCALE + mintSlippage)) / BASIS_SCALE;
require(assets <= maxAssets, "too much slippage");

require(assetsToDeposit <= maxAssets, "too much slippage");

// Transfer vault's asssets (3Crv) from the caller.
_asset.safeTransferFrom(msg.sender, address(this), assets);

// Deposit 3Crv into metapool and the stake into Convex vault
_depositAndStake(assets, requiredMetapoolTokens);
_depositAndStake(assetsToDeposit, requiredMetapoolTokens);

_mint(receiver, shares);

emit Deposit(msg.sender, receiver, assets, shares);
// Account any new shares, assets.
uint256 donatedShares = donatedAssets == 0
? 0
: (shares * requiredMetapoolTokens) / donatedMetapoolTokens;
_afterSharesMintedHook(donatedShares, donatedAssets);
}

/// @dev Converts vault shares to assets in two steps
/// Vault shares -> Metapool LP tokens, eg musd3Crv -> vault assets (3Crv)
/// Override `AbstractVault._previewMint`.
/// changes - It takes into account any asset balance to be included in the next mint.
function _previewMint(uint256 shares) internal view virtual override returns (uint256 assets) {
if (shares > 0) {
uint256 donatedAssets = _asset.balanceOf(address(this));
uint256 donatedMetapoolTokens = 0;
if (donatedAssets > 0) {
(donatedMetapoolTokens, , , ) = Curve3CrvMetapoolCalculatorLibrary.calcDeposit(
metapool,
metapoolToken,
donatedAssets,
1
);
}
uint256 metapoolTokens = _getMetapoolTokensFromShares(
shares,
baseRewardPool.balanceOf(address(this)),
totalSupply()
);
(assets, , , ) = Curve3CrvMetapoolCalculatorLibrary.calcMint(
(uint256 assetsToDeposit, , , ) = Curve3CrvMetapoolCalculatorLibrary.calcMint(
metapool,
metapoolToken,
metapoolTokens,
metapoolTokens + donatedMetapoolTokens,
1
);
// Calculate receivers portion of assets.
assets = (assetsToDeposit * (metapoolTokens + donatedMetapoolTokens)) / metapoolTokens;
}
}

Expand Down Expand Up @@ -581,6 +633,21 @@ abstract contract Convex3CrvAbstractVault is AbstractSlippage, AbstractVault {
}
}

/**
* Called be the `deposit` and `mint` functions after the assets have been transferred into the vault
* but before shares are minted.
* Typically, the hook implementation deposits the assets into the underlying vaults or platforms.
*
* @dev the shares returned from `totalSupply` and `balanceOf` have not yet been updated with the minted shares.
* The assets returned from `totalAssets` and `assetsOf` are typically updated as part of the `_afterDepositHook` hook but it depends on the implementation.
*
* If an vault is implementing multiple vault capabilities, the `_afterDepositHook` function that updates the assets amounts should be executed last.
*
* @param newShares the amount of underlying assets to be transferred to the vault.
* @param newAssets the amount of vault shares to be minted.
*/
function _afterSharesMintedHook(uint256 newShares, uint256 newAssets) internal virtual;

/***************************************
Emergency Functions
****************************************/
Expand Down
4 changes: 4 additions & 0 deletions contracts/vault/liquidity/convex/Convex3CrvBasicVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,8 @@ contract Convex3CrvBasicVault is Convex3CrvAbstractVault, Initializable {
uint8 decimals_ = InitializableToken(address(metapoolToken)).decimals();
InitializableToken._initialize(_name, _symbol, decimals_);
}

function _afterSharesMintedHook(uint256, uint256) internal virtual override {
// do nothing
}
}
23 changes: 10 additions & 13 deletions contracts/vault/liquidity/convex/Convex3CrvLiquidatorVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ contract Convex3CrvLiquidatorVault is
* @dev Converts donated tokens (DAI, USDC or USDT) to vault assets (3Crv) and shares.
* Transfers token from donor to vault.
* Adds the token to the Curve 3Pool to receive the vault asset (3Crv) in exchange.
* The resulting asset (3Crv) is added to the Curve Metapool.
* The Curve Metapool LP token, eg mUSD3Crv, is added to the Convex pool and staked.
* The assets assets (3Crv) stay in the vault without changing the number of shares.
*/
function _convertTokens(address token, uint256 amount)
internal
Expand Down Expand Up @@ -190,18 +189,8 @@ contract Convex3CrvLiquidatorVault is

// Get vault's asset (3Crv) balance after adding token to Curve's 3Pool.
assets_ = _asset.balanceOf(address(this));
// Add asset (3Crv) to metapool with slippage protection.
uint256 metapoolTokens = ICurveMetapool(metapool).add_liquidity([0, assets_], minMetapoolTokens);

// Calculate share value of the new assets before depositing the metapool tokens to the Convex pool.
shares_ = _getSharesFromMetapoolTokens(
metapoolTokens,
baseRewardPool.balanceOf(address(this)),
totalSupply()
);

// Deposit Curve.fi Metapool LP token, eg musd3CRV, in Convex pool, eg cvxmusd3CRV, and stake.
booster.deposit(convexPoolId, metapoolTokens, true);
shares_ = 0;
}

/***************************************
Expand Down Expand Up @@ -382,6 +371,14 @@ contract Convex3CrvLiquidatorVault is
shares = Convex3CrvAbstractVault._withdraw(assets, receiver, owner);
}

/// @dev use LiquidatorStreamAbstractVault._streamNewShares implementation.
function _afterSharesMintedHook(
uint256 newShares,
uint256 newAssets
) internal virtual override(Convex3CrvAbstractVault) {
LiquidatorStreamAbstractVault._streamNewShares(newShares, newAssets);
}

/***************************************
Internal vault convertions
****************************************/
Expand Down
Loading

0 comments on commit 87515cb

Please sign in to comment.