From 7754e2cd812adeb8dbee44c527443b068835958c Mon Sep 17 00:00:00 2001 From: crispymangoes <77207459+crispymangoes@users.noreply.github.com> Date: Wed, 17 Jan 2024 17:20:13 -0700 Subject: [PATCH] Feat/separate deposit and multi asset deposit (#179) * Separate normal deposits and multi asset deposits, and add preview function. * Add missing test and remove old commented code * Copy over code to more advanced cellar permutations * Add natspec clarifying who can call multi asset only owner functions. * Add missing natspec * Address warning * Implement missing advanced permutations * Separate deposit events into 2 separate events * Add better natspec to the multi asset deposit event --- src/base/Cellar.sol | 7 +- .../CellarWithMultiAssetDeposit.sol | 137 ++++++--- ...ithAaveFlashLoansWithMultiAssetDeposit.sol | 263 ++++++++++++++++++ ...WithMultiAssetDepositWithNativeSupport.sol | 44 +++ ...alancerFlashLoansWithMultiAssetDeposit.sol | 135 ++++++--- test/CellarWithMultiAssetDeposit.t.sol | 60 ++-- 6 files changed, 519 insertions(+), 127 deletions(-) create mode 100644 src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit.sol create mode 100644 src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDepositWithNativeSupport.sol diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 16db4638..ee4ddd81 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -707,11 +707,6 @@ contract Cellar is ERC4626, Owned, ERC721Holder { // =========================================== CORE LOGIC =========================================== - /** - * @notice Emitted during deposits. - */ - event Deposit(address indexed caller, address indexed owner, address depositAsset, uint256 assets, uint256 shares); - /** * @notice Attempted an action with zero shares. */ @@ -775,7 +770,7 @@ contract Cellar is ERC4626, Owned, ERC721Holder { _mint(receiver, shares); - emit Deposit(msg.sender, receiver, address(asset), assets, shares); + emit Deposit(msg.sender, receiver, assets, shares); afterDeposit(position, assets, shares, receiver); } diff --git a/src/base/permutations/CellarWithMultiAssetDeposit.sol b/src/base/permutations/CellarWithMultiAssetDeposit.sol index dc081607..e960ab1e 100644 --- a/src/base/permutations/CellarWithMultiAssetDeposit.sol +++ b/src/base/permutations/CellarWithMultiAssetDeposit.sol @@ -3,8 +3,6 @@ pragma solidity 0.8.21; import { Cellar, Registry, ERC20, Math, SafeTransferLib, Address } from "src/base/Cellar.sol"; -// TODO once audited, make a permutation for oracle, aave flashloans, multi-asset deposit -// TODO once audited, make a permutation for oracle, aave flashloans, multi-asset deposit, native support contract CellarWithMultiAssetDeposit is Cellar { using Math for uint256; using SafeTransferLib for ERC20; @@ -56,6 +54,20 @@ contract CellarWithMultiAssetDeposit is Cellar { */ event AlternativeAssetDropped(address asset); + /** + * @notice Emitted during multi asset deposits. + * @dev Multi asset deposits will emit 2 events, the ERC4626 compliant Deposit event + * and this event. These events were intentionally separated out so we can + * keep the compliant event, but also have an event that emits the depositAsset. + */ + event MultiAssetDeposit( + address indexed caller, + address indexed owner, + address depositAsset, + uint256 assets, + uint256 shares + ); + //============================== IMMUTABLES =============================== constructor( @@ -88,6 +100,7 @@ contract CellarWithMultiAssetDeposit is Cellar { /** * @notice Allows the owner to add, or update an existing alternative asset deposit. + * @dev Callable by Sommelier Strategists. * @param _alternativeAsset the ERC20 alternative asset that can be deposited * @param _alternativeHoldingPosition the holding position to direct alternative asset deposits to * @param _alternativeAssetFee the fee to charge for depositing this alternative asset @@ -117,34 +130,91 @@ contract CellarWithMultiAssetDeposit is Cellar { /** * @notice Allows the owner to stop an alternative asset from being deposited. + * @dev Callable by Sommelier Strategists. * @param _alternativeAsset the asset to not allow for alternative asset deposits anymore */ function dropAlternativeAssetData(ERC20 _alternativeAsset) external { _onlyOwner(); delete alternativeAssetData[_alternativeAsset]; - // alternativeAssetData[_alternativeAsset] = AlternativeAssetData(false, 0, 0); emit AlternativeAssetDropped(address(_alternativeAsset)); } /** * @notice Deposits assets into the cellar, and returns shares to receiver. - * @dev Compliant with ERC4626 standard, but additionally allows for multi-asset deposits - * by encoding the asset to deposit at the end of the normal deposit params. * @param assets amount of assets deposited by user. * @param receiver address to receive the shares. * @return shares amount of shares given for deposit. */ function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256 shares) { - // Use `_calculateTotalAssetsOrTotalAssetsWithdrawable` instead of totalAssets bc re-entrancy is already checked in this function. - (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + shares = _deposit(asset, assets, assets, assets, holdingPosition, receiver); + } + /** + * @notice Allows users to deposit into cellar using alternative assets. + * @param depositAsset the asset to deposit + * @param assets amount of depositAsset to deposit + * @param receiver address to receive the shares + */ + function multiAssetDeposit( + ERC20 depositAsset, + uint256 assets, + address receiver + ) public nonReentrant returns (uint256 shares) { + // Convert assets from depositAsset to asset. ( - ERC20 depositAsset, uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, uint32 position - ) = _getDepositAssetAndAdjustedAssetsAndPosition(assets); + ) = _getMultiAssetDepositData(depositAsset, assets); + + shares = _deposit( + depositAsset, + assets, + assetsConvertedToAsset, + assetsConvertedToAssetWithFeeRemoved, + position, + receiver + ); + + emit MultiAssetDeposit(msg.sender, receiver, address(depositAsset), assets, shares); + } + + //============================== PREVIEW FUNCTIONS =============================== + + /** + * @notice Preview function to see how many shares a multi asset deposit will give user. + */ + function previewMultiAssetDeposit(ERC20 depositAsset, uint256 assets) external view returns (uint256 shares) { + // Convert assets from depositAsset to asset. + (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, ) = _getMultiAssetDepositData( + depositAsset, + assets + ); + + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + shares = _convertToShares( + assetsConvertedToAssetWithFeeRemoved, + _totalAssets + (assetsConvertedToAsset - assetsConvertedToAssetWithFeeRemoved), + _totalSupply + ); + } + + //============================== HELPER FUNCTIONS =============================== + + /** + * @notice Helper function to fulfill normal deposits and multi asset deposits. + */ + function _deposit( + ERC20 depositAsset, + uint256 assets, + uint256 assetsConvertedToAsset, + uint256 assetsConvertedToAssetWithFeeRemoved, + uint32 position, + address receiver + ) internal returns (uint256 shares) { + // Use `_calculateTotalAssetsOrTotalAssetsWithdrawable` instead of totalAssets bc re-entrancy is already checked in this function. + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); // Perform share calculation using assetsConvertedToAssetWithFeeRemoved. // Check for rounding error since we round down in previewDeposit. @@ -164,46 +234,27 @@ contract CellarWithMultiAssetDeposit is Cellar { _enter(depositAsset, position, assets, shares, receiver); } - //============================== HELPER FUNCTION =============================== - /** - * @notice Reads message data to determine if user is trying to deposit with an alternative asset or wants to do a normal deposit. + * @notice Helper function to verify asset is supported for multi asset deposit, + * convert assets from depositAsset to asset, and account for alternative asset fee. */ - function _getDepositAssetAndAdjustedAssetsAndPosition( + function _getMultiAssetDepositData( + ERC20 depositAsset, uint256 assets ) internal view - returns ( - ERC20 depositAsset, - uint256 assetsConvertedToAsset, - uint256 assetsConvertedToAssetWithFeeRemoved, - uint32 position - ) + returns (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, uint32 position) { - uint256 msgDataLength = msg.data.length; - if (msgDataLength == 68) { - // Caller has not encoded an alternative asset, so return address(0). - depositAsset = asset; - assetsConvertedToAssetWithFeeRemoved = assets; - assetsConvertedToAsset = assets; - position = holdingPosition; - } else if (msgDataLength == 100) { - // Caller has encoded an extra arguments, try to decode it as an address. - (, , depositAsset) = abi.decode(msg.data[4:], (uint256, address, ERC20)); - - AlternativeAssetData memory assetData = alternativeAssetData[depositAsset]; - if (!assetData.isSupported) revert CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); - - // Convert assets from depositAsset to asset. - assetsConvertedToAsset = priceRouter.getValue(depositAsset, assets, asset); - - // Collect alternative asset fee. - assetsConvertedToAssetWithFeeRemoved = assetsConvertedToAsset.mulDivDown(1e8 - assetData.depositFee, 1e8); - - position = assetData.holdingPosition; - } else { - revert CellarWithMultiAssetDeposit__CallDataLengthNotSupported(); - } + AlternativeAssetData memory assetData = alternativeAssetData[depositAsset]; + if (!assetData.isSupported) revert CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); + + // Convert assets from depositAsset to asset. + assetsConvertedToAsset = priceRouter.getValue(depositAsset, assets, asset); + + // Collect alternative asset fee. + assetsConvertedToAssetWithFeeRemoved = assetsConvertedToAsset.mulDivDown(1e8 - assetData.depositFee, 1e8); + + position = assetData.holdingPosition; } } diff --git a/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit.sol b/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit.sol new file mode 100644 index 00000000..bbad2723 --- /dev/null +++ b/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit.sol @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Registry, ERC20, Math, SafeTransferLib, Address } from "src/base/Cellar.sol"; +import { CellarWithOracleWithAaveFlashLoans } from "src/base/permutations/CellarWithOracleWithAaveFlashLoans.sol"; + +contract CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit is CellarWithOracleWithAaveFlashLoans { + using Math for uint256; + using SafeTransferLib for ERC20; + using Address for address; + + // ========================================= STRUCTS ========================================= + + /** + * @notice Stores data needed for multi-asset deposits into this cellar. + * @param isSupported bool indicating that mapped asset is supported + * @param holdingPosition the holding position to deposit alternative assets into + * @param depositFee fee taken for depositing this alternative asset + */ + struct AlternativeAssetData { + bool isSupported; + uint32 holdingPosition; + uint32 depositFee; + } + + // ========================================= CONSTANTS ========================================= + + /** + * @notice The max possible fee that can be charged for an alternative asset deposit. + */ + uint32 internal constant MAX_ALTERNATIVE_ASSET_FEE = 0.1e8; + + // ========================================= GLOBAL STATE ========================================= + + /** + * @notice Maps alternative assets to alternative asset data. + */ + mapping(ERC20 => AlternativeAssetData) public alternativeAssetData; + + //============================== ERRORS =============================== + + error CellarWithMultiAssetDeposit__AlternativeAssetFeeTooLarge(); + error CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); + error CellarWithMultiAssetDeposit__CallDataLengthNotSupported(); + + //============================== EVENTS =============================== + + /** + * @notice Emitted when an alternative asset is added or updated. + */ + event AlternativeAssetUpdated(address asset, uint32 holdingPosition, uint32 depositFee); + + /** + * @notice Emitted when an alternative asser is removed. + */ + event AlternativeAssetDropped(address asset); + + /** + * @notice Emitted during multi asset deposits. + * @dev Multi asset deposits will emit 2 events, the ERC4626 compliant Deposit event + * and this event. These events were intentionally separated out so we can + * keep the compliant event, but also have an event that emits the depositAsset. + */ + event MultiAssetDeposit( + address indexed caller, + address indexed owner, + address depositAsset, + uint256 assets, + uint256 shares + ); + + //============================== IMMUTABLES =============================== + + constructor( + address _owner, + Registry _registry, + ERC20 _asset, + string memory _name, + string memory _symbol, + uint32 _holdingPosition, + bytes memory _holdingPositionConfig, + uint256 _initialDeposit, + uint64 _strategistPlatformCut, + uint192 _shareSupplyCap, + address _aavePool + ) + CellarWithOracleWithAaveFlashLoans( + _owner, + _registry, + _asset, + _name, + _symbol, + _holdingPosition, + _holdingPositionConfig, + _initialDeposit, + _strategistPlatformCut, + _shareSupplyCap, + _aavePool + ) + {} + + //============================== OWNER FUNCTIONS =============================== + + /** + * @notice Allows the owner to add, or update an existing alternative asset deposit. + * @dev Callable by Sommelier Strategists. + * @param _alternativeAsset the ERC20 alternative asset that can be deposited + * @param _alternativeHoldingPosition the holding position to direct alternative asset deposits to + * @param _alternativeAssetFee the fee to charge for depositing this alternative asset + */ + function setAlternativeAssetData( + ERC20 _alternativeAsset, + uint32 _alternativeHoldingPosition, + uint32 _alternativeAssetFee + ) external { + _onlyOwner(); + if (!isPositionUsed[_alternativeHoldingPosition]) revert Cellar__PositionNotUsed(_alternativeHoldingPosition); + if (_assetOf(_alternativeHoldingPosition) != _alternativeAsset) + revert Cellar__AssetMismatch(address(_alternativeAsset), address(_assetOf(_alternativeHoldingPosition))); + if (getPositionData[_alternativeHoldingPosition].isDebt) + revert Cellar__InvalidHoldingPosition(_alternativeHoldingPosition); + if (_alternativeAssetFee > MAX_ALTERNATIVE_ASSET_FEE) + revert CellarWithMultiAssetDeposit__AlternativeAssetFeeTooLarge(); + + alternativeAssetData[_alternativeAsset] = AlternativeAssetData( + true, + _alternativeHoldingPosition, + _alternativeAssetFee + ); + + emit AlternativeAssetUpdated(address(_alternativeAsset), _alternativeHoldingPosition, _alternativeAssetFee); + } + + /** + * @notice Allows the owner to stop an alternative asset from being deposited. + * @dev Callable by Sommelier Strategists. + * @param _alternativeAsset the asset to not allow for alternative asset deposits anymore + */ + function dropAlternativeAssetData(ERC20 _alternativeAsset) external { + _onlyOwner(); + delete alternativeAssetData[_alternativeAsset]; + + emit AlternativeAssetDropped(address(_alternativeAsset)); + } + + /** + * @notice Deposits assets into the cellar, and returns shares to receiver. + * @param assets amount of assets deposited by user. + * @param receiver address to receive the shares. + * @return shares amount of shares given for deposit. + */ + function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256 shares) { + shares = _deposit(asset, assets, assets, assets, holdingPosition, receiver); + } + + /** + * @notice Allows users to deposit into cellar using alternative assets. + * @param depositAsset the asset to deposit + * @param assets amount of depositAsset to deposit + * @param receiver address to receive the shares + */ + function multiAssetDeposit( + ERC20 depositAsset, + uint256 assets, + address receiver + ) public nonReentrant returns (uint256 shares) { + // Convert assets from depositAsset to asset. + ( + uint256 assetsConvertedToAsset, + uint256 assetsConvertedToAssetWithFeeRemoved, + uint32 position + ) = _getMultiAssetDepositData(depositAsset, assets); + + shares = _deposit( + depositAsset, + assets, + assetsConvertedToAsset, + assetsConvertedToAssetWithFeeRemoved, + position, + receiver + ); + + emit MultiAssetDeposit(msg.sender, receiver, address(depositAsset), assets, shares); + } + + //============================== PREVIEW FUNCTIONS =============================== + + /** + * @notice Preview function to see how many shares a multi asset deposit will give user. + */ + function previewMultiAssetDeposit(ERC20 depositAsset, uint256 assets) external view returns (uint256 shares) { + // Convert assets from depositAsset to asset. + (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, ) = _getMultiAssetDepositData( + depositAsset, + assets + ); + + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + shares = _convertToShares( + assetsConvertedToAssetWithFeeRemoved, + _totalAssets + (assetsConvertedToAsset - assetsConvertedToAssetWithFeeRemoved), + _totalSupply + ); + } + + //============================== HELPER FUNCTIONS =============================== + + /** + * @notice Helper function to fulfill normal deposits and multi asset deposits. + */ + function _deposit( + ERC20 depositAsset, + uint256 assets, + uint256 assetsConvertedToAsset, + uint256 assetsConvertedToAssetWithFeeRemoved, + uint32 position, + address receiver + ) internal returns (uint256 shares) { + // Use `_calculateTotalAssetsOrTotalAssetsWithdrawable` instead of totalAssets bc re-entrancy is already checked in this function. + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + + // Perform share calculation using assetsConvertedToAssetWithFeeRemoved. + // Check for rounding error since we round down in previewDeposit. + // NOTE for totalAssets, we add the delta between assetsConvertedToAsset, and assetsConvertedToAssetWithFeeRemoved, so that the fee the caller pays + // to join with the alternative asset is factored into share price calculation. + if ( + (shares = _convertToShares( + assetsConvertedToAssetWithFeeRemoved, + _totalAssets + (assetsConvertedToAsset - assetsConvertedToAssetWithFeeRemoved), + _totalSupply + )) == 0 + ) revert Cellar__ZeroShares(); + + if ((_totalSupply + shares) > shareSupplyCap) revert Cellar__ShareSupplyCapExceeded(); + + // _enter into holding position but passing in actual assets. + _enter(depositAsset, position, assets, shares, receiver); + } + + /** + * @notice Helper function to verify asset is supported for multi asset deposit, + * convert assets from depositAsset to asset, and account for alternative asset fee. + */ + function _getMultiAssetDepositData( + ERC20 depositAsset, + uint256 assets + ) + internal + view + returns (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, uint32 position) + { + AlternativeAssetData memory assetData = alternativeAssetData[depositAsset]; + if (!assetData.isSupported) revert CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); + + // Convert assets from depositAsset to asset. + assetsConvertedToAsset = priceRouter.getValue(depositAsset, assets, asset); + + // Collect alternative asset fee. + assetsConvertedToAssetWithFeeRemoved = assetsConvertedToAsset.mulDivDown(1e8 - assetData.depositFee, 1e8); + + position = assetData.holdingPosition; + } +} diff --git a/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDepositWithNativeSupport.sol b/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDepositWithNativeSupport.sol new file mode 100644 index 00000000..c5e91780 --- /dev/null +++ b/src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDepositWithNativeSupport.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.21; + +import { Registry, ERC20, Math, SafeTransferLib, Address } from "src/base/Cellar.sol"; +import { CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit } from "src/base/permutations/advanced/CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit.sol"; + +contract CellarWithOracleWithAaveFlashLoansWithMultiAssetDepositWithNativeSupport is + CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit +{ + //============================== IMMUTABLES =============================== + + constructor( + address _owner, + Registry _registry, + ERC20 _asset, + string memory _name, + string memory _symbol, + uint32 _holdingPosition, + bytes memory _holdingPositionConfig, + uint256 _initialDeposit, + uint64 _strategistPlatformCut, + uint192 _shareSupplyCap, + address _aavePool + ) + CellarWithOracleWithAaveFlashLoansWithMultiAssetDeposit( + _owner, + _registry, + _asset, + _name, + _symbol, + _holdingPosition, + _holdingPositionConfig, + _initialDeposit, + _strategistPlatformCut, + _shareSupplyCap, + _aavePool + ) + {} + + /** + * @notice Implement receive so Cellar can accept native transfers. + */ + receive() external payable {} +} diff --git a/src/base/permutations/advanced/CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit.sol b/src/base/permutations/advanced/CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit.sol index ebea046b..b09672c4 100644 --- a/src/base/permutations/advanced/CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit.sol +++ b/src/base/permutations/advanced/CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit.sol @@ -55,6 +55,20 @@ contract CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit is CellarWi */ event AlternativeAssetDropped(address asset); + /** + * @notice Emitted during multi asset deposits. + * @dev Multi asset deposits will emit 2 events, the ERC4626 compliant Deposit event + * and this event. These events were intentionally separated out so we can + * keep the compliant event, but also have an event that emits the depositAsset. + */ + event MultiAssetDeposit( + address indexed caller, + address indexed owner, + address depositAsset, + uint256 assets, + uint256 shares + ); + //============================== IMMUTABLES =============================== constructor( @@ -89,6 +103,7 @@ contract CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit is CellarWi /** * @notice Allows the owner to add, or update an existing alternative asset deposit. + * @dev Callable by Sommelier Strategists. * @param _alternativeAsset the ERC20 alternative asset that can be deposited * @param _alternativeHoldingPosition the holding position to direct alternative asset deposits to * @param _alternativeAssetFee the fee to charge for depositing this alternative asset @@ -118,34 +133,91 @@ contract CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit is CellarWi /** * @notice Allows the owner to stop an alternative asset from being deposited. + * @dev Callable by Sommelier Strategists. * @param _alternativeAsset the asset to not allow for alternative asset deposits anymore */ function dropAlternativeAssetData(ERC20 _alternativeAsset) external { _onlyOwner(); delete alternativeAssetData[_alternativeAsset]; - // alternativeAssetData[_alternativeAsset] = AlternativeAssetData(false, 0, 0); emit AlternativeAssetDropped(address(_alternativeAsset)); } /** * @notice Deposits assets into the cellar, and returns shares to receiver. - * @dev Compliant with ERC4626 standard, but additionally allows for multi-asset deposits - * by encoding the asset to deposit at the end of the normal deposit params. * @param assets amount of assets deposited by user. * @param receiver address to receive the shares. * @return shares amount of shares given for deposit. */ function deposit(uint256 assets, address receiver) public override nonReentrant returns (uint256 shares) { - // Use `_calculateTotalAssetsOrTotalAssetsWithdrawable` instead of totalAssets bc re-entrancy is already checked in this function. - (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + shares = _deposit(asset, assets, assets, assets, holdingPosition, receiver); + } + /** + * @notice Allows users to deposit into cellar using alternative assets. + * @param depositAsset the asset to deposit + * @param assets amount of depositAsset to deposit + * @param receiver address to receive the shares + */ + function multiAssetDeposit( + ERC20 depositAsset, + uint256 assets, + address receiver + ) public nonReentrant returns (uint256 shares) { + // Convert assets from depositAsset to asset. ( - ERC20 depositAsset, uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, uint32 position - ) = _getDepositAssetAndAdjustedAssetsAndPosition(assets); + ) = _getMultiAssetDepositData(depositAsset, assets); + + shares = _deposit( + depositAsset, + assets, + assetsConvertedToAsset, + assetsConvertedToAssetWithFeeRemoved, + position, + receiver + ); + + emit MultiAssetDeposit(msg.sender, receiver, address(depositAsset), assets, shares); + } + + //============================== PREVIEW FUNCTIONS =============================== + + /** + * @notice Preview function to see how many shares a multi asset deposit will give user. + */ + function previewMultiAssetDeposit(ERC20 depositAsset, uint256 assets) external view returns (uint256 shares) { + // Convert assets from depositAsset to asset. + (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, ) = _getMultiAssetDepositData( + depositAsset, + assets + ); + + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); + shares = _convertToShares( + assetsConvertedToAssetWithFeeRemoved, + _totalAssets + (assetsConvertedToAsset - assetsConvertedToAssetWithFeeRemoved), + _totalSupply + ); + } + + //============================== HELPER FUNCTIONS =============================== + + /** + * @notice Helper function to fulfill normal deposits and multi asset deposits. + */ + function _deposit( + ERC20 depositAsset, + uint256 assets, + uint256 assetsConvertedToAsset, + uint256 assetsConvertedToAssetWithFeeRemoved, + uint32 position, + address receiver + ) internal returns (uint256 shares) { + // Use `_calculateTotalAssetsOrTotalAssetsWithdrawable` instead of totalAssets bc re-entrancy is already checked in this function. + (uint256 _totalAssets, uint256 _totalSupply) = _getTotalAssetsAndTotalSupply(true); // Perform share calculation using assetsConvertedToAssetWithFeeRemoved. // Check for rounding error since we round down in previewDeposit. @@ -165,46 +237,27 @@ contract CellarWithOracleWithBalancerFlashLoansWithMultiAssetDeposit is CellarWi _enter(depositAsset, position, assets, shares, receiver); } - //============================== HELPER FUNCTION =============================== - /** - * @notice Reads message data to determine if user is trying to deposit with an alternative asset or wants to do a normal deposit. + * @notice Helper function to verify asset is supported for multi asset deposit, + * convert assets from depositAsset to asset, and account for alternative asset fee. */ - function _getDepositAssetAndAdjustedAssetsAndPosition( + function _getMultiAssetDepositData( + ERC20 depositAsset, uint256 assets ) internal view - returns ( - ERC20 depositAsset, - uint256 assetsConvertedToAsset, - uint256 assetsConvertedToAssetWithFeeRemoved, - uint32 position - ) + returns (uint256 assetsConvertedToAsset, uint256 assetsConvertedToAssetWithFeeRemoved, uint32 position) { - uint256 msgDataLength = msg.data.length; - if (msgDataLength == 68) { - // Caller has not encoded an alternative asset, so return address(0). - depositAsset = asset; - assetsConvertedToAssetWithFeeRemoved = assets; - assetsConvertedToAsset = assets; - position = holdingPosition; - } else if (msgDataLength == 100) { - // Caller has encoded an extra arguments, try to decode it as an address. - (, , depositAsset) = abi.decode(msg.data[4:], (uint256, address, ERC20)); - - AlternativeAssetData memory assetData = alternativeAssetData[depositAsset]; - if (!assetData.isSupported) revert CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); - - // Convert assets from depositAsset to asset. - assetsConvertedToAsset = priceRouter.getValue(depositAsset, assets, asset); - - // Collect alternative asset fee. - assetsConvertedToAssetWithFeeRemoved = assetsConvertedToAsset.mulDivDown(1e8 - assetData.depositFee, 1e8); - - position = assetData.holdingPosition; - } else { - revert CellarWithMultiAssetDeposit__CallDataLengthNotSupported(); - } + AlternativeAssetData memory assetData = alternativeAssetData[depositAsset]; + if (!assetData.isSupported) revert CellarWithMultiAssetDeposit__AlternativeAssetNotSupported(); + + // Convert assets from depositAsset to asset. + assetsConvertedToAsset = priceRouter.getValue(depositAsset, assets, asset); + + // Collect alternative asset fee. + assetsConvertedToAssetWithFeeRemoved = assetsConvertedToAsset.mulDivDown(1e8 - assetData.depositFee, 1e8); + + position = assetData.holdingPosition; } } diff --git a/test/CellarWithMultiAssetDeposit.t.sol b/test/CellarWithMultiAssetDeposit.t.sol index 877f70db..77a1b502 100644 --- a/test/CellarWithMultiAssetDeposit.t.sol +++ b/test/CellarWithMultiAssetDeposit.t.sol @@ -138,9 +138,7 @@ contract CellarWithMultiAssetDepositTest is MainnetStarterTest, AdaptorHelperFun deal(address(USDT), address(this), assets); USDT.safeApprove(address(cellar), assets); - bytes memory depositCallData = abi.encodeWithSelector(Cellar.deposit.selector, assets, address(this), USDT); - - address(cellar).functionCall(depositCallData); + cellar.multiAssetDeposit(USDT, assets, address(this)); // Since share price is 1:1, below checks should pass. assertEq(cellar.previewRedeem(1e6), 1e6, "Cellar share price should be 1."); @@ -155,9 +153,7 @@ contract CellarWithMultiAssetDepositTest is MainnetStarterTest, AdaptorHelperFun deal(address(USDC), address(this), assets); USDC.safeApprove(address(cellar), assets); - bytes memory depositCallData = abi.encodeWithSelector(Cellar.deposit.selector, assets, address(this), USDC); - - address(cellar).functionCall(depositCallData); + cellar.multiAssetDeposit(USDC, assets, address(this)); // Since share price is 1:1, below checks should pass. assertEq( @@ -182,25 +178,19 @@ contract CellarWithMultiAssetDepositTest is MainnetStarterTest, AdaptorHelperFun // Setup Cellar to accept USDT deposits. cellar.setAlternativeAssetData(USDT, usdtPosition, fee); - vm.startPrank(user); + uint256 expectedShares = cellar.previewMultiAssetDeposit(USDT, assets); + vm.startPrank(user); USDT.safeApprove(address(cellar), assets); - - bytes memory depositCallData = abi.encodeWithSelector(Cellar.deposit.selector, assets, user, USDT); - - address(cellar).functionCall(depositCallData); - + cellar.multiAssetDeposit(USDT, assets, user); vm.stopPrank(); - uint256 assetsIn = priceRouter.getValue(USDT, assets, USDC); - uint256 assetsInWithFee = assetsIn.mulDivDown(1e8 - fee, 1e8); - - uint256 expectedShares = cellar.previewDeposit(assetsInWithFee); - + // Check preview logic. uint256 userShareBalance = cellar.balanceOf(user); - assertApproxEqAbs(userShareBalance, expectedShares, 1, "User shares should equal expected."); + uint256 assetsIn = priceRouter.getValue(USDT, assets, USDC); + uint256 assetsInWithFee = assetsIn.mulDivDown(1e8 - fee, 1e8); uint256 expectedSharePrice = (initialAssets + assetsIn).mulDivDown(1e6, cellar.totalSupply()); assertApproxEqAbs( @@ -232,10 +222,7 @@ contract CellarWithMultiAssetDepositTest is MainnetStarterTest, AdaptorHelperFun deal(address(USDT), address(this), assets); USDT.safeApprove(address(cellar), assets); - bytes memory depositCallData = abi.encodeWithSelector(Cellar.deposit.selector, assets, address(this), USDT); - - // USDT deposits work. - address(cellar).functionCall(depositCallData); + cellar.multiAssetDeposit(USDT, assets, address(this)); // But if USDT is dropped, deposits revert. cellar.dropAlternativeAssetData(USDT); @@ -247,7 +234,7 @@ contract CellarWithMultiAssetDepositTest is MainnetStarterTest, AdaptorHelperFun ) ) ); - address(cellar).functionCall(depositCallData); + cellar.multiAssetDeposit(USDT, assets, address(this)); (bool isSupported, uint32 holdingPosition, uint32 fee) = cellar.alternativeAssetData(USDT); assertEq(isSupported, false, "USDT should not be supported."); @@ -255,6 +242,18 @@ contract CellarWithMultiAssetDepositTest is MainnetStarterTest, AdaptorHelperFun assertEq(fee, 0, "Fee should be zero."); } + function testSettingAlternativeAssetDataAgain() external { + cellar.setAlternativeAssetData(USDT, usdtPosition, 0); + + // Owner decides they actually want to add a fee. + cellar.setAlternativeAssetData(USDT, usdtPosition, 0.0010e8); + + (bool isSupported, uint32 holdingPosition, uint32 fee) = cellar.alternativeAssetData(USDT); + assertEq(isSupported, true, "USDT should be supported."); + assertEq(holdingPosition, usdtPosition, "Holding position should be usdt position."); + assertEq(fee, 0.0010e8, "Fee should be 10 bps."); + } + // ======================== Test Reverts ========================== function testDepositReverts() external { uint256 assets = 100e6; @@ -262,8 +261,6 @@ contract CellarWithMultiAssetDepositTest is MainnetStarterTest, AdaptorHelperFun deal(address(USDT), address(this), assets); USDT.safeApprove(address(cellar), assets); - bytes memory depositCallData = abi.encodeWithSelector(Cellar.deposit.selector, assets, address(this), USDT); - // Try depositing with an asset that is not setup. vm.expectRevert( bytes( @@ -272,18 +269,7 @@ contract CellarWithMultiAssetDepositTest is MainnetStarterTest, AdaptorHelperFun ) ) ); - address(cellar).functionCall(depositCallData); - - // User messes up the calldata. - depositCallData = abi.encodeWithSelector(Cellar.deposit.selector, assets, address(this), USDT, address(0)); - vm.expectRevert( - bytes( - abi.encodeWithSelector( - CellarWithMultiAssetDeposit.CellarWithMultiAssetDeposit__CallDataLengthNotSupported.selector - ) - ) - ); - address(cellar).functionCall(depositCallData); + cellar.multiAssetDeposit(USDT, assets, address(this)); } function testOwnerReverts() external {