From 673aa92c272ca609cae47d34195581e7e4eb0074 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Thu, 16 Jun 2022 21:53:07 -0700 Subject: [PATCH 01/49] feat(Cellar): add support for multiple positions and multiple assets --- src/Registry.sol | 27 ++- src/SwapRouter.sol | 2 +- src/base/Cellar.sol | 423 +++++++++++++++++++------------------------- 3 files changed, 207 insertions(+), 245 deletions(-) diff --git a/src/Registry.sol b/src/Registry.sol index fd8b9e14..00f2e14e 100644 --- a/src/Registry.sol +++ b/src/Registry.sol @@ -1,20 +1,43 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.13; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { SwapRouter } from "./SwapRouter.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; // TODO: configure defaults // TODO: add natspec // TODO: add events +interface PriceRouter { + function getValue( + ERC20[] memory baseAssets, + uint256[] memory amounts, + ERC20 quoteAsset + ) external view returns (uint256); + + function getValue( + ERC20 baseAssets, + uint256 amounts, + ERC20 quoteAsset + ) external view returns (uint256); + + function getExchangeRate(ERC20 baseAsset, ERC20 quoteAsset) external view returns (uint256); +} + contract Registry is Ownable { - address public swapRouter; + SwapRouter public swapRouter; + PriceRouter public priceRouter; address public gravityBridge; - function setSwapRouter(address newSwapRouter) external { + function setSwapRouter(SwapRouter newSwapRouter) external { swapRouter = newSwapRouter; } + function setPriceRouter(PriceRouter newPriceRouter) external { + priceRouter = newPriceRouter; + } + function setGravityBridge(address newGravityBridge) external { gravityBridge = newGravityBridge; } diff --git a/src/SwapRouter.sol b/src/SwapRouter.sol index 89171748..510277df 100644 --- a/src/SwapRouter.sol +++ b/src/SwapRouter.sol @@ -13,7 +13,7 @@ contract SwapRouter { UNIV2, UNIV3 } - /** @notice Planned additions + /** @notice Planned additions BALANCERV2, CURVE, ONEINCH diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 50aa34de..a3119090 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -5,7 +5,7 @@ import { ERC4626, ERC20 } from "./ERC4626.sol"; import { Multicall } from "./Multicall.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; -import { Registry } from "../Registry.sol"; +import { Registry, SwapRouter, PriceRouter } from "../Registry.sol"; import { IGravity } from "../interfaces/IGravity.sol"; import { AddressArray } from "src/utils/AddressArray.sol"; import { Math } from "../utils/Math.sol"; @@ -52,8 +52,8 @@ contract Cellar is ERC4626, Ownable, Multicall { // TODO: pack struct struct PositionData { - bool isLossless; uint256 balance; + uint256 storedUnrealizedGains; } address[] public positions; @@ -72,11 +72,6 @@ contract Cellar is ERC4626, Ownable, Multicall { // Check if position is already being used. if (isPositionUsed[position]) revert USR_PositionAlreadyUsed(position); - // Check if position has same underlying as cellar. - ERC20 cellarAsset = asset; - ERC20 positionAsset = ERC4626(position).asset(); - if (positionAsset != cellarAsset) revert USR_IncompatiblePosition(address(positionAsset), address(cellarAsset)); - // Add new position at a specified index. positions.add(index, position); isPositionUsed[position] = true; @@ -94,11 +89,6 @@ contract Cellar is ERC4626, Ownable, Multicall { // Check if position is already being used. if (isPositionUsed[position]) revert USR_PositionAlreadyUsed(position); - // Check if position has same underlying as cellar. - ERC20 cellarAsset = asset; - ERC20 positionAsset = ERC4626(position).asset(); - if (positionAsset != cellarAsset) revert USR_IncompatiblePosition(address(positionAsset), address(cellarAsset)); - // Add new position to the end of the positions. positions.push(position); isPositionUsed[position] = true; @@ -176,13 +166,10 @@ contract Cellar is ERC4626, Ownable, Multicall { mapping(address => bool) public isTrusted; - function trustPosition(address position, bool isLossless) external onlyOwner { + function trustPosition(address position) external onlyOwner { // Trust position. isTrusted[position] = true; - // Set position's lossless flag. - getPositionData[position].isLossless = isLossless; - // Set max approval to deposit into position if it is ERC4626. ERC4626(position).asset().safeApprove(position, type(uint256).max); @@ -205,50 +192,14 @@ contract Cellar is ERC4626, Ownable, Multicall { emit TrustChanged(position, false); } - // ============================================ ACCOUNTING STATE ============================================ - - uint256 public totalLosslessBalance; - - // ======================================== ACCRUAL CONFIG ======================================== - - /** - * @notice Emitted when accrual period is changed. - * @param oldPeriod time the period was changed from - * @param newPeriod time the period was changed to - */ - event AccrualPeriodChanged(uint32 oldPeriod, uint32 newPeriod); - - /** - * @notice Period of time over which yield since the last accrual is linearly distributed to the cellar. - * @dev Net gains are distributed gradually over a period to prevent frontrunning and sandwich attacks. - * Net losses are realized immediately otherwise users could time exits to sidestep losses. - */ - uint32 public accrualPeriod = 7 days; + // ============================================ ACCRUAL STORAGE ============================================ /** * @notice Timestamp of when the last accrual occurred. */ uint64 public lastAccrual; - /** - * @notice The amount of yield to be distributed to the cellar from the last accrual. - */ - uint160 public maxLocked; - - /** - * @notice Set the accrual period over which yield is distributed. - * @param newAccrualPeriod period of time in seconds of the new accrual period - */ - function setAccrualPeriod(uint32 newAccrualPeriod) external onlyOwner { - // Ensure that the change is not disrupting a currently ongoing distribution of accrued yield. - if (totalLocked() > 0) revert STATE_AccrualOngoing(); - - emit AccrualPeriodChanged(accrualPeriod, newAccrualPeriod); - - accrualPeriod = newAccrualPeriod; - } - - // ========================================= FEES CONFIG ========================================= + // =============================================== FEES CONFIG =============================================== /** * @notice Emitted when platform fees is changed. @@ -411,19 +362,6 @@ contract Cellar is ERC4626, Ownable, Multicall { emit ShutdownChanged(false); } - // ============================================ HOLDINGS CONFIG ============================================ - - /** - * @dev Should be set high enough that the holding pool can cover the majority of weekly - * withdraw volume without needing to pull from positions. See `beforeWithdraw` for - * more information as to why. - */ - uint256 public targetHoldingsPercent; - - function setTargetHoldings(uint256 targetPercent) external onlyOwner { - targetHoldingsPercent = targetPercent; - } - // =========================================== CONSTRUCTOR =========================================== // TODO: since registry address should never change, consider hardcoding the address once @@ -462,78 +400,94 @@ contract Cellar is ERC4626, Ownable, Multicall { if (assets > maxAssets) revert USR_DepositRestricted(assets, maxAssets); } - /** - * @dev Check if holding position has enough funds to cover the withdraw and only pull from the - * current lending position if needed. - */ - function beforeWithdraw( + function withdrawFromPositions( uint256 assets, - uint256, - address, - address - ) internal override { - uint256 totalAssetsInHolding = totalHoldings(); - + uint256 minAssetsOut, + SwapRouter.Exchanges[] calldata exchanges, + bytes[] calldata params, + address receiver, + address owner + ) external returns (uint256 assetsOut, uint256 shares) { // Only withdraw if not enough assets in the holding pool. - if (assets > totalAssetsInHolding) { - uint256 totalAssetsInCellar = totalAssets(); + if (assets > totalHoldings()) return (assets, withdraw(assets, receiver, owner)); - // The amounts needed to cover this withdraw and reach the target holdings percentage. - uint256 assetsMissingForWithdraw = assets - totalAssetsInHolding; - uint256 assetsMissingForTargetHoldings = (totalAssetsInCellar - assets).mulWadDown(targetHoldingsPercent); + shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up. - // Pull enough to cover the withdraw and reach the target holdings percentage. - uint256 assetsleftToWithdraw = assetsMissingForWithdraw + assetsMissingForTargetHoldings; + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. - uint256 newTotalLosslessBalance = totalLosslessBalance; + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + } - for (uint256 i; ; i++) { - ERC4626 position = ERC4626(positions[i]); + _burn(owner, shares); - uint256 totalPositionAssets = position.maxWithdraw(address(this)); + assetsOut = _withdrawAndSwapFromPositions(assets, exchanges, params); - // Move on to next position if this one is empty. - if (totalPositionAssets == 0) continue; + require(assetsOut >= minAssetsOut, "NOT_ENOUGH_ASSETS_OUT"); - // We want to pull as much as we can from this position, but no more than needed. - uint256 assetsWithdrawn = Math.min(totalPositionAssets, assetsleftToWithdraw); + asset.safeTransfer(receiver, assetsOut); - PositionData storage positionData = getPositionData[address(position)]; + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + + function _withdrawAndSwapFromPositions( + uint256 assets, + SwapRouter.Exchanges[] calldata exchanges, + bytes[] calldata params + ) internal returns (uint256 assetsOut) { + for (uint256 i; ; i++) { + ERC4626 position = ERC4626(positions[i]); - if (positionData.isLossless) newTotalLosslessBalance -= assetsWithdrawn; + uint256 totalPositionBalance = position.maxWithdraw(address(this)); - // Without this the next accrual would count this withdrawal as a loss. - positionData.balance -= assetsWithdrawn; + // Move on to next position if this one is empty. + if (totalPositionBalance == 0) continue; - // Update the assets left to withdraw. - assetsleftToWithdraw -= assetsWithdrawn; + // Although it would be more efficient to store `position.asset()` and + // `registry.priceRouter()`, doing so would cause a stack error. + uint256 onePositionAsset = 10**position.asset().decimals(); + uint256 positionAssetToAssetExchangeRate = registry.priceRouter().getExchangeRate(position.asset(), asset); - // Pull from this position. - position.withdraw(assetsWithdrawn, address(this), address(this)); + // Denominate position balance in cellar's asset. + uint256 totalPositionBalanceInAssets = totalPositionBalance.mulDivDown( + positionAssetToAssetExchangeRate, + onePositionAsset + ); - if (assetsleftToWithdraw == 0) break; + // We want to pull as much as we can from this position, but no more than needed. + uint256 amount; + if (totalPositionBalanceInAssets > assets) { + assets -= assets; + amount = assets.mulDivDown(onePositionAsset, positionAssetToAssetExchangeRate); + } else { + assets -= totalPositionBalanceInAssets; + amount = totalPositionBalance; } - totalLosslessBalance = newTotalLosslessBalance; + // Pull from this position. + assetsOut += _withdrawAndSwapFromPosition(position, asset, amount, exchanges[i], params[i]); + + // Stop if no more assets to withdraw. + if (assets == 0) break; } } // ========================================= ACCOUNTING LOGIC ========================================= - function denominateInAsset(address token, uint256 amount) public view returns (uint256 assets) {} - /** * @notice The total amount of assets in the cellar. * @dev Excludes locked yield that hasn't been distributed. */ function totalAssets() public view override returns (uint256 assets) { - for (uint256 i; i < positions.length; i++) { - address position = positions[i]; + assets = totalHoldings(); - if (getPositionData[position].isLossless) assets += ERC4626(position).maxWithdraw(address(this)); - } + PriceRouter priceRouter = PriceRouter(registry.priceRouter()); - assets += totalLosslessBalance + totalHoldings() - totalLocked(); + ERC20 denominationAsset = asset; + for (uint256 i; i < positions.length; i++) { + ERC4626 position = ERC4626(positions[i]); + assets += priceRouter.getValue(position.asset(), position.maxWithdraw(address(this)), denominationAsset); + } } /** @@ -543,24 +497,6 @@ contract Cellar is ERC4626, Ownable, Multicall { return asset.balanceOf(address(this)); } - /** - * @notice The total amount of locked yield still being distributed. - */ - function totalLocked() public view returns (uint256) { - // Get the last accrual and accrual period. - uint256 previousAccrual = lastAccrual; - uint256 accrualInterval = accrualPeriod; - - // If the accrual period has passed, there is no locked yield. - if (block.timestamp >= previousAccrual + accrualInterval) return 0; - - // Get the maximum amount we could return. - uint256 maxLockedYield = maxLocked; - - // Get how much yield remains locked. - return maxLockedYield - (maxLockedYield * (block.timestamp - previousAccrual)) / accrualInterval; - } - // =========================================== ACCRUAL LOGIC =========================================== /** @@ -574,63 +510,60 @@ contract Cellar is ERC4626, Ownable, Multicall { * @notice Accrue platform fees and performance fees. May also accrue yield. */ function accrue() public { - uint256 totalLockedYield = totalLocked(); - - // Without this check, malicious actors could do a slowdown attack on the distribution of - // yield by continuously resetting the accrual period. - if (msg.sender != owner() && totalLockedYield > 0) revert STATE_AccrualOngoing(); - - uint256 totalBalanceLastAccrual = totalLosslessBalance; + // Record the balance of this and last accrual. uint256 totalBalanceThisAccrual; + uint256 totalBalanceLastAccrual; - uint256 newTotalLosslessBalance; + // Get the latest address of the price router. + PriceRouter priceRouter = PriceRouter(registry.priceRouter()); for (uint256 i; i < positions.length; i++) { ERC4626 position = ERC4626(positions[i]); PositionData storage positionData = getPositionData[address(position)]; + // Get the current position balance. uint256 balanceThisAccrual = position.maxWithdraw(address(this)); - // Check whether position is lossless. - if (positionData.isLossless) { - // Update total lossless balance. No need to add to last accrual balance - // because since it is already accounted for in `totalLosslessBalance`. - newTotalLosslessBalance += balanceThisAccrual; - } else { - // Add to balance for last accrual. - totalBalanceLastAccrual += positionData.balance; - } + // Get exchange rate. + ERC20 positionAsset = position.asset(); + uint256 onePositionAsset = 10**positionAsset.decimals(); + uint256 positionAssetToAssetExchangeRate = priceRouter.getExchangeRate(positionAsset, asset); + + // Add to balance for last accrual. + totalBalanceLastAccrual += positionData.balance.mulDivDown( + positionAssetToAssetExchangeRate, + onePositionAsset + ); // Add to balance for this accrual. - totalBalanceThisAccrual += balanceThisAccrual; + totalBalanceThisAccrual += (balanceThisAccrual + positionData.storedUnrealizedGains).mulDivDown( + positionAssetToAssetExchangeRate, + onePositionAsset + ); - // Store position's balance this accrual. + // Update position's data. positionData.balance = balanceThisAccrual; + positionData.storedUnrealizedGains = 0; } // Compute and store current exchange rate between assets and shares for gas efficiency. - uint256 exchangeRate = convertToShares(1e18); + uint256 assetToSharesExchangeRate = convertToShares(1e18); // Calculate platform fees accrued. uint256 elapsedTime = block.timestamp - lastAccrual; uint256 platformFeeInAssets = (totalBalanceThisAccrual * elapsedTime * platformFee) / 1e18 / 365 days; - uint256 platformFees = platformFeeInAssets.mulWadDown(exchangeRate); // Convert to shares. + uint256 platformFees = platformFeeInAssets.mulWadDown(assetToSharesExchangeRate); // Convert to shares. // Calculate performance fees accrued. uint256 yield = totalBalanceThisAccrual.subMinZero(totalBalanceLastAccrual); uint256 performanceFeeInAssets = yield.mulWadDown(performanceFee); - uint256 performanceFees = performanceFeeInAssets.mulWadDown(exchangeRate); // Convert to shares. + uint256 performanceFees = performanceFeeInAssets.mulWadDown(assetToSharesExchangeRate); // Convert to shares. // Mint accrued fees as shares. _mint(address(this), platformFees + performanceFees); - // Do not count assets set aside for fees as yield. Allows fees to be immediately withdrawable. - maxLocked = uint160(totalLockedYield + yield.subMinZero(platformFeeInAssets + performanceFeeInAssets)); - lastAccrual = uint32(block.timestamp); - totalLosslessBalance = uint240(newTotalLosslessBalance); - emit Accrual(platformFees, performanceFees); } @@ -645,118 +578,64 @@ contract Cellar is ERC4626, Ownable, Multicall { * @param position address of the position to enter holdings into * @param assets amount of assets to exit from the position */ - function enterPosition(address position, uint256 assets) public onlyOwner { + function enterPosition(ERC4626 position, uint256 assets) public onlyOwner { // Check that position is a valid position. - if (!isPositionUsed[position]) revert USR_InvalidPosition(position); + if (!isPositionUsed[address(position)]) revert USR_InvalidPosition(address(position)); - PositionData storage positionData = getPositionData[address(position)]; + // TODO: + // // Swap to the holding pool asset if necessary. + // ERC20 positionAsset = position.asset(); + // if (positionAsset != asset) _swap(positionAsset, assets, exchange, params); - if (positionData.isLossless) totalLosslessBalance += assets; + // Get position data. + PositionData storage positionData = getPositionData[address(position)]; + // Update position balance. positionData.balance += assets; // Deposit into position. ERC4626(position).deposit(assets, address(this)); } - /** - * @notice Pushes all assets in holding into a position. - * @param position address of the position to enter all holdings into - */ - function enterPosition(address position) external { - enterPosition(position, totalHoldings()); - } - /** * @notice Pulls assets from a position back into holdings. - * @param position address of the position to exit - * @param assets amount of assets to exit from the position - */ - function exitPosition(address position, uint256 assets) external onlyOwner { - PositionData storage positionData = getPositionData[address(position)]; - - if (positionData.isLossless) totalLosslessBalance -= assets; - - positionData.balance -= assets; - - // Withdraw from specified position. - ERC4626(position).withdraw(assets, address(this), address(this)); - } - - /** - * @notice Pulls all assets from a position back into holdings. * @param position address of the position to completely exit + * @param assets amount of assets to exit from the position + * @param params encoded arguments for the function that will perform the swap on the selected exchange */ - function exitPosition(address position) external onlyOwner { - PositionData storage positionData = getPositionData[position]; - - uint256 balanceLastAccrual = positionData.balance; - uint256 balanceThisAccrual; - - // uint256 totalPositionBalance = positionData.positionType == PositionType.ERC4626 - // ? ERC4626(position).redeem(ERC4626(position).balanceOf(address(this)), address(this), address(this)) - // : ERC20(position).balanceOf(address(this)); - - // if (!isPositionUsingSameAsset(position)) - // registry.getSwapRouter().swapExactAmount( - // totalPositionBalance, - // // amountOutMin, - // positionData.pathToAsset - // ); - - positionData.balance = 0; - - if (positionData.isLossless) totalLosslessBalance -= balanceLastAccrual; - - if (balanceThisAccrual == 0) return; - - // Calculate performance fees accrued. - uint256 yield = balanceThisAccrual.subMinZero(balanceLastAccrual); - uint256 performanceFeeInAssets = yield.mulWadDown(performanceFee); - uint256 performanceFees = convertToShares(performanceFeeInAssets); // Convert to shares. - - // Mint accrued fees as shares. - _mint(address(this), performanceFees); - - // Do not count assets set aside for fees as yield. Allows fees to be immediately withdrawable. - maxLocked = uint160(totalLocked() + yield.subMinZero(performanceFeeInAssets)); + function exitPosition( + ERC4626 position, + uint256 assets, + SwapRouter.Exchanges exchange, + bytes calldata params + ) external onlyOwner { + _withdrawAndSwapFromPosition(position, asset, assets, exchange, params); } /** * @notice Move assets between positions. * @param fromPosition address of the position to move assets from * @param toPosition address of the position to move assets to - * @param assets amount of assets to move + * @param assetsFrom amount of assets to move from the from position */ function rebalance( - address fromPosition, - address toPosition, - uint256 assets - ) external onlyOwner { + ERC4626 fromPosition, + ERC4626 toPosition, + uint256 assetsFrom, + SwapRouter.Exchanges exchange, + bytes calldata params + ) external onlyOwner returns (uint256 assetsTo) { // Check that position being rebalanced to is a valid position. - if (!isPositionUsed[toPosition]) revert USR_InvalidPosition(toPosition); - - // Get data for both positions. - PositionData storage fromPositionData = getPositionData[fromPosition]; - PositionData storage toPositionData = getPositionData[toPosition]; + if (!isPositionUsed[address(toPosition)]) revert USR_InvalidPosition(address(toPosition)); - // Update tracked balance of both positions. - fromPositionData.balance -= assets; - toPositionData.balance += assets; + // Withdraw from the from position and update related position data. + assetsTo = _withdrawAndSwapFromPosition(fromPosition, toPosition.asset(), assetsFrom, exchange, params); - // Update total lossless balance. - uint256 newTotalLosslessBalance = totalLosslessBalance; + // Update stored balance of the to position. + getPositionData[address(toPosition)].balance += assetsTo; - if (fromPositionData.isLossless) newTotalLosslessBalance -= assets; - if (toPositionData.isLossless) newTotalLosslessBalance += assets; - - totalLosslessBalance = newTotalLosslessBalance; - - // Withdraw from specified position. - ERC4626(fromPosition).withdraw(assets, address(this), address(this)); - - // Deposit into destination position. - ERC4626(toPosition).deposit(assets, address(this)); + // Deposit into the to position. + toPosition.deposit(assetsTo, address(this)); } // ============================================ LIMITS LOGIC ============================================ @@ -826,7 +705,7 @@ contract Cellar is ERC4626, Ownable, Multicall { // Transfer assets to a fee distributor on the Sommelier chain. IGravity gravityBridge = IGravity(registry.gravityBridge()); - asset.safeApprove(address(gravityBridge), assets); + asset.safeApprove(address(gravityBridge), assets); // TODO: change to send the asset withdrawn gravityBridge.sendToCosmos(address(asset), feesDistributor, assets); emit SendFees(totalFees, assets); @@ -857,4 +736,64 @@ contract Cellar is ERC4626, Ownable, Multicall { emit Sweep(address(token), to, amount); } + + // ========================================== HELPER FUNCTIONS ========================================== + + function _withdrawAndSwapFromPosition( + ERC4626 position, + ERC20 toAsset, + uint256 amount, + SwapRouter.Exchanges exchange, + bytes calldata params + ) internal returns (uint256 amountOut) { + // Get position data. + PositionData storage positionData = getPositionData[address(position)]; + + // Update position balance. + _subtractFromPositionBalance(positionData, amount); + + // Withdraw from position. + position.withdraw(amount, address(this), address(this)); + + // Swap to the holding pool asset if necessary. + ERC20 positionAsset = position.asset(); + amountOut = positionAsset != toAsset ? _swap(positionAsset, amount, exchange, params) : amount; + } + + function _subtractFromPositionBalance(PositionData storage positionData, uint256 amount) internal { + // Update position balance. + uint256 positionBalance = positionData.balance; + if (positionBalance > amount) { + positionData.balance -= amount; + } else { + positionData.balance = 0; + + // Without these, the unrealized gains that were withdrawn would be not be counted next accrual. + positionData.storedUnrealizedGains = amount - positionBalance; + } + } + + function _swap( + ERC20 assetIn, + uint256 amountIn, + SwapRouter.Exchanges exchange, + bytes calldata params + ) internal returns (uint256 assetsOut) { + // Store the expected amount of the asset in that we expect to have after the swap. + uint256 expectedAssetsInAfter = assetIn.balanceOf(address(this)) - amountIn; + + // Get the address of the latest swap router. + SwapRouter swapRouter = registry.swapRouter(); + + // Approve swap router to swap assets. + assetIn.safeApprove(address(swapRouter), amountIn); + + // Perform swap. + assetsOut = swapRouter.swap(exchange, params); + + // Check that the amount of assets swapped is what is expected. Will revert if the `params` + // specified a different amount of assets to swap then `assets`. + // TODO: consider replacing with revert statement + require(assetIn.balanceOf(address(this)) == expectedAssetsInAfter, "INCORRECT_PARAMS_AMOUNT"); + } } From 65e0708a4c3323f817b0399aed75f38ac5610aa9 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Fri, 17 Jun 2022 09:59:01 -0700 Subject: [PATCH 02/49] feat(Cellar): add ability for enterPosition to handle converting between assets --- src/Registry.sol | 17 +++++-- src/SwapRouter.sol | 100 ------------------------------------- src/base/Cellar.sol | 83 ++++++++++++++++++++---------- src/modules/SwapRouter.sol | 4 +- 4 files changed, 74 insertions(+), 130 deletions(-) delete mode 100644 src/SwapRouter.sol diff --git a/src/Registry.sol b/src/Registry.sol index 00f2e14e..1ff93972 100644 --- a/src/Registry.sol +++ b/src/Registry.sol @@ -2,7 +2,8 @@ pragma solidity 0.8.13; import { ERC20 } from "@solmate/tokens/ERC20.sol"; -import { SwapRouter } from "./SwapRouter.sol"; +import { SwapRouter } from "./modules/SwapRouter.sol"; +import { IGravity } from "./interfaces/IGravity.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; // TODO: configure defaults @@ -28,7 +29,17 @@ interface PriceRouter { contract Registry is Ownable { SwapRouter public swapRouter; PriceRouter public priceRouter; - address public gravityBridge; + IGravity public gravityBridge; + + constructor( + SwapRouter _swapRouter, + PriceRouter _priceRouter, + IGravity _gravityBridge + ) { + swapRouter = _swapRouter; + priceRouter = _priceRouter; + gravityBridge = _gravityBridge; + } function setSwapRouter(SwapRouter newSwapRouter) external { swapRouter = newSwapRouter; @@ -38,7 +49,7 @@ contract Registry is Ownable { priceRouter = newPriceRouter; } - function setGravityBridge(address newGravityBridge) external { + function setGravityBridge(IGravity newGravityBridge) external { gravityBridge = newGravityBridge; } } diff --git a/src/SwapRouter.sol b/src/SwapRouter.sol deleted file mode 100644 index aea1dfe7..00000000 --- a/src/SwapRouter.sol +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; - -import { ERC20 } from "@solmate/tokens/ERC20.sol"; -import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; -import { IUniswapV2Router02 as IUniswapV2Router } from "./interfaces/IUniswapV2Router02.sol"; -import { IUniswapV3Router } from "./interfaces/IUniswapV3Router.sol"; - -contract SwapRouter { - using SafeTransferLib for ERC20; - - enum Exchanges { - UNIV2, - UNIV3 - } - /** @notice Planned additions - BALANCERV2, - CURVE, - ONEINCH - */ - mapping(Exchanges => bytes4) public idToSelector; - - // ========================================== CONSTRUCTOR ========================================== - - /** - * @notice Uniswap V2 swap router contract. Used for swapping if pool fees are not specified. - */ - IUniswapV2Router public immutable uniswapV2Router; // 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D - - /** - * @notice Uniswap V3 swap router contract. Used for swapping if pool fees are specified. - */ - IUniswapV3Router public immutable uniswapV3Router; // 0xE592427A0AEce92De3Edee1F18E0157C05861564 - - /** - * - */ - constructor(IUniswapV2Router _uniswapV2Router, IUniswapV3Router _uniswapV3Router) { - //set up all exchanges - uniswapV2Router = _uniswapV2Router; - uniswapV3Router = _uniswapV3Router; - - //set up mapping between ids and selectors - idToSelector[Exchanges.UNIV2] = SwapRouter(this).swapWithUniV2.selector; - idToSelector[Exchanges.UNIV3] = SwapRouter(this).swapWithUniV3.selector; - } - - // ======================================= SWAP OPERATIONS ======================================= - - function swap(Exchanges id, bytes memory swapData) external returns (uint256 swapOutAmount) { - (bool success, bytes memory result) = address(this).call(abi.encodeWithSelector(idToSelector[id], swapData)); - require(success, "Failed to perform swap"); - swapOutAmount = abi.decode(result, (uint256)); - } - - function swapWithUniV2(bytes memory swapData) public returns (uint256 swapOutAmount) { - (address[] memory path, uint256 assets, uint256 assetsOutMin, address recipient) = abi.decode( - swapData, - (address[], uint256, uint256, address) - ); - ERC20 assetIn = ERC20(path[0]); - // Approve assets to be swapped through the router. - assetIn.safeApprove(address(uniswapV2Router), assets); - - // Execute the swap. - uint256[] memory amountsOut = uniswapV2Router.swapExactTokensForTokens( - assets, - assetsOutMin, - path, - recipient, - block.timestamp + 60 - ); - swapOutAmount = amountsOut[1]; - } - - function swapWithUniV3(bytes memory swapData) public returns (uint256 swapOutAmount) { - (address[] memory path, uint24[] memory poolFees, uint256 assets, uint256 assetsOutMin, address recipient) = abi - .decode(swapData, (address[], uint24[], uint256, uint256, address)); - ERC20 assetIn = ERC20(path[0]); - - // Approve assets to be swapped through the router. - assetIn.safeApprove(address(uniswapV3Router), assets); - - // Encode swap parameters. - bytes memory encodePackedPath = abi.encodePacked(address(assetIn)); - for (uint256 i = 1; i < path.length; i++) - encodePackedPath = abi.encodePacked(encodePackedPath, poolFees[i - 1], path[i]); - - // Execute the swap. - swapOutAmount = uniswapV3Router.exactInput( - IUniswapV3Router.ExactInputParams({ - path: encodePackedPath, - recipient: recipient, - deadline: block.timestamp + 60, - amountIn: assets, - amountOutMinimum: assetsOutMin - }) - ); - } -} diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index a3119090..41271c93 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -386,7 +386,7 @@ contract Cellar is ERC4626, Ownable, Multicall { registry = _registry; // Transfer ownership to the Gravity Bridge. - transferOwnership(_registry.gravityBridge()); + transferOwnership(address(_registry.gravityBridge())); } // =========================================== CORE LOGIC =========================================== @@ -421,7 +421,7 @@ contract Cellar is ERC4626, Ownable, Multicall { _burn(owner, shares); - assetsOut = _withdrawAndSwapFromPositions(assets, exchanges, params); + assetsOut = _pullFromPositions(assets, exchanges, params); require(assetsOut >= minAssetsOut, "NOT_ENOUGH_ASSETS_OUT"); @@ -430,7 +430,7 @@ contract Cellar is ERC4626, Ownable, Multicall { emit Withdraw(msg.sender, receiver, owner, assets, shares); } - function _withdrawAndSwapFromPositions( + function _pullFromPositions( uint256 assets, SwapRouter.Exchanges[] calldata exchanges, bytes[] calldata params @@ -444,7 +444,7 @@ contract Cellar is ERC4626, Ownable, Multicall { if (totalPositionBalance == 0) continue; // Although it would be more efficient to store `position.asset()` and - // `registry.priceRouter()`, doing so would cause a stack error. + // `registry.priceRouter()`, this is done to avoid stack errors. uint256 onePositionAsset = 10**position.asset().decimals(); uint256 positionAssetToAssetExchangeRate = registry.priceRouter().getExchangeRate(position.asset(), asset); @@ -479,15 +479,18 @@ contract Cellar is ERC4626, Ownable, Multicall { * @dev Excludes locked yield that hasn't been distributed. */ function totalAssets() public view override returns (uint256 assets) { - assets = totalHoldings(); + uint256 numOfPositions = positions.length; + ERC20[] memory positionAssets = new ERC20[](numOfPositions); + uint256[] memory balances = new uint256[](numOfPositions); - PriceRouter priceRouter = PriceRouter(registry.priceRouter()); - - ERC20 denominationAsset = asset; - for (uint256 i; i < positions.length; i++) { + for (uint256 i; i < numOfPositions; i++) { ERC4626 position = ERC4626(positions[i]); - assets += priceRouter.getValue(position.asset(), position.maxWithdraw(address(this)), denominationAsset); + + positionAssets[i] = position.asset(); + balances[i] = position.maxWithdraw(address(this)); } + + assets = totalHoldings() + registry.priceRouter().getValue(positionAssets, balances, asset); } /** @@ -515,7 +518,7 @@ contract Cellar is ERC4626, Ownable, Multicall { uint256 totalBalanceLastAccrual; // Get the latest address of the price router. - PriceRouter priceRouter = PriceRouter(registry.priceRouter()); + PriceRouter priceRouter = registry.priceRouter(); for (uint256 i; i < positions.length; i++) { ERC4626 position = ERC4626(positions[i]); @@ -578,23 +581,26 @@ contract Cellar is ERC4626, Ownable, Multicall { * @param position address of the position to enter holdings into * @param assets amount of assets to exit from the position */ - function enterPosition(ERC4626 position, uint256 assets) public onlyOwner { + function enterPosition( + ERC4626 position, + uint256 assets, + SwapRouter.Exchanges exchange, + bytes calldata params + ) external onlyOwner { // Check that position is a valid position. if (!isPositionUsed[address(position)]) revert USR_InvalidPosition(address(position)); - // TODO: - // // Swap to the holding pool asset if necessary. - // ERC20 positionAsset = position.asset(); - // if (positionAsset != asset) _swap(positionAsset, assets, exchange, params); - - // Get position data. - PositionData storage positionData = getPositionData[address(position)]; + // Swap from the holding pool asset if necessary. + ERC20 denominationAsset = asset; + ERC20 positionAsset = position.asset(); + if (positionAsset != denominationAsset) + _swapForExactAssets(positionAsset, denominationAsset, assets, exchange, params); // Update position balance. - positionData.balance += assets; + getPositionData[address(position)].balance += assets; // Deposit into position. - ERC4626(position).deposit(assets, address(this)); + position.deposit(assets, address(this)); } /** @@ -757,7 +763,7 @@ contract Cellar is ERC4626, Ownable, Multicall { // Swap to the holding pool asset if necessary. ERC20 positionAsset = position.asset(); - amountOut = positionAsset != toAsset ? _swap(positionAsset, amount, exchange, params) : amount; + amountOut = positionAsset != toAsset ? _swapExactAssets(positionAsset, amount, exchange, params) : amount; } function _subtractFromPositionBalance(PositionData storage positionData, uint256 amount) internal { @@ -773,12 +779,12 @@ contract Cellar is ERC4626, Ownable, Multicall { } } - function _swap( + function _swapExactAssets( ERC20 assetIn, uint256 amountIn, SwapRouter.Exchanges exchange, bytes calldata params - ) internal returns (uint256 assetsOut) { + ) internal returns (uint256 amountOut) { // Store the expected amount of the asset in that we expect to have after the swap. uint256 expectedAssetsInAfter = assetIn.balanceOf(address(this)) - amountIn; @@ -789,11 +795,36 @@ contract Cellar is ERC4626, Ownable, Multicall { assetIn.safeApprove(address(swapRouter), amountIn); // Perform swap. - assetsOut = swapRouter.swap(exchange, params); + amountOut = swapRouter.swapExactAssets(exchange, params); // Check that the amount of assets swapped is what is expected. Will revert if the `params` - // specified a different amount of assets to swap then `assets`. + // specified a different amount of assets to swap then `amountIn`. // TODO: consider replacing with revert statement require(assetIn.balanceOf(address(this)) == expectedAssetsInAfter, "INCORRECT_PARAMS_AMOUNT"); } + + function _swapForExactAssets( + ERC20 assetIn, + ERC20 assetOut, + uint256 amountOut, + SwapRouter.Exchanges exchange, + bytes calldata params + ) internal returns (uint256 amountIn) { + // Store the expected amount of the asset out that we expect to have after the swap. + uint256 expectedAssetsOutAfter = assetOut.balanceOf(address(this)) + amountOut; + + // Get the address of the latest swap router. + SwapRouter swapRouter = registry.swapRouter(); + + // Approve swap router to swap assets. + assetIn.safeApprove(address(swapRouter), amountIn); + + // Perform swap. + amountIn = swapRouter.swapForExactAssets(exchange, params); + + // Check that the amount of assets received is what is expected. Will revert if the `params` + // specified a different amount of assets to receive then `amountOut`. + // TODO: consider replacing with revert statement + require(assetOut.balanceOf(address(this)) == expectedAssetsOutAfter, "INCORRECT_PARAMS_AMOUNT"); + } } diff --git a/src/modules/SwapRouter.sol b/src/modules/SwapRouter.sol index 053940cf..60972f20 100644 --- a/src/modules/SwapRouter.sol +++ b/src/modules/SwapRouter.sol @@ -47,12 +47,14 @@ contract SwapRouter { // ======================================= SWAP OPERATIONS ======================================= - function swap(Exchanges id, bytes memory swapData) external returns (uint256 swapOutAmount) { + function swapExactAssets(Exchanges id, bytes memory swapData) external returns (uint256 swapOutAmount) { (bool success, bytes memory result) = address(this).call(abi.encodeWithSelector(idToSelector[id], swapData)); require(success, "Failed to perform swap"); swapOutAmount = abi.decode(result, (uint256)); } + function swapForExactAssets(Exchanges id, bytes memory swapData) external returns (uint256 swapInAmount) {} + function swapWithUniV2(bytes memory swapData) public returns (uint256 swapOutAmount) { (address[] memory path, uint256 assets, uint256 assetsOutMin, address recipient) = abi.decode( swapData, From ab1ebcd92ecf03247165e029c4f5cb8206bc7dd6 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Fri, 17 Jun 2022 11:24:38 -0700 Subject: [PATCH 03/49] perf(Cellar): add an internal getter function to fetch data efficiently --- src/base/Cellar.sol | 174 +++++++++++++++++++++++++++++++++----------- 1 file changed, 133 insertions(+), 41 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 41271c93..2e487efd 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -400,56 +400,84 @@ contract Cellar is ERC4626, Ownable, Multicall { if (assets > maxAssets) revert USR_DepositRestricted(assets, maxAssets); } + // TODO: move to ICellar once done + event WithdrawFromPositions( + address indexed caller, + address indexed receiver, + address indexed owner, + ERC20[] receivedAssets, + uint256[] amountsOut, + uint256 shares + ); + + event PulledFromPosition(address indexed position, uint256 amount); + function withdrawFromPositions( uint256 assets, - uint256 minAssetsOut, - SwapRouter.Exchanges[] calldata exchanges, - bytes[] calldata params, address receiver, address owner - ) external returns (uint256 assetsOut, uint256 shares) { + ) + external + returns ( + uint256 shares, + ERC20[] memory receivedAssets, + uint256[] memory amountsOut + ) + { // Only withdraw if not enough assets in the holding pool. - if (assets > totalHoldings()) return (assets, withdraw(assets, receiver, owner)); - - shares = previewWithdraw(assets); // No need to check for rounding error, previewWithdraw rounds up. - - if (msg.sender != owner) { - uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. - - if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; - } - - _burn(owner, shares); + if (assets > totalHoldings()) { + receivedAssets[0] = asset; + amountsOut[0] = assets; + shares = withdraw(assets, receiver, owner); + } else { + // Get data efficiently. + ( + uint256 _totalAssets, + , + ERC4626[] memory _positions, + ERC20[] memory positionAssets, + uint256[] memory positionBalances + ) = _getData(); + + // Get the amount of share needed to redeem. + shares = _previewWithdraw(assets, _totalAssets); + + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + } - assetsOut = _pullFromPositions(assets, exchanges, params); + _burn(owner, shares); - require(assetsOut >= minAssetsOut, "NOT_ENOUGH_ASSETS_OUT"); + (receivedAssets, amountsOut) = _pullFromPositions(assets, _positions, positionAssets, positionBalances); - asset.safeTransfer(receiver, assetsOut); + // Transfer withdrawn assets to the receiver. + for (uint256 i; i < receivedAssets.length; i++) receivedAssets[i].safeTransfer(receiver, amountsOut[i]); - emit Withdraw(msg.sender, receiver, owner, assets, shares); + emit WithdrawFromPositions(msg.sender, receiver, owner, receivedAssets, amountsOut, shares); + } } function _pullFromPositions( uint256 assets, - SwapRouter.Exchanges[] calldata exchanges, - bytes[] calldata params - ) internal returns (uint256 assetsOut) { - for (uint256 i; ; i++) { - ERC4626 position = ERC4626(positions[i]); - - uint256 totalPositionBalance = position.maxWithdraw(address(this)); + ERC4626[] memory _positions, + ERC20[] memory positionAssets, + uint256[] memory positionBalances + ) internal returns (ERC20[] memory receivedAssets, uint256[] memory amountsOut) { + // Get the price router. + PriceRouter priceRouter = registry.priceRouter(); + for (uint256 i; ; i++) { // Move on to next position if this one is empty. - if (totalPositionBalance == 0) continue; + if (positionBalances[i] == 0) continue; - // Although it would be more efficient to store `position.asset()` and - // `registry.priceRouter()`, this is done to avoid stack errors. - uint256 onePositionAsset = 10**position.asset().decimals(); - uint256 positionAssetToAssetExchangeRate = registry.priceRouter().getExchangeRate(position.asset(), asset); + ERC20 positionAsset = positionAssets[i]; + uint256 onePositionAsset = 10**positionAsset.decimals(); + uint256 positionAssetToAssetExchangeRate = priceRouter.getExchangeRate(positionAsset, asset); // Denominate position balance in cellar's asset. - uint256 totalPositionBalanceInAssets = totalPositionBalance.mulDivDown( + uint256 totalPositionBalanceInAssets = positionBalances[i].mulDivDown( positionAssetToAssetExchangeRate, onePositionAsset ); @@ -461,11 +489,20 @@ contract Cellar is ERC4626, Ownable, Multicall { amount = assets.mulDivDown(onePositionAsset, positionAssetToAssetExchangeRate); } else { assets -= totalPositionBalanceInAssets; - amount = totalPositionBalance; + amount = positionBalances[i]; } - // Pull from this position. - assetsOut += _withdrawAndSwapFromPosition(position, asset, amount, exchanges[i], params[i]); + // Return the asset and amount that will be received. + amountsOut[i] = amount; + receivedAssets[i] = positionAsset; + + // Update position balance. + _subtractFromPositionBalance(getPositionData[address(_positions[i])], amount); + + // Withdraw from position. + _positions[i].withdraw(amount, address(this), address(this)); + + emit PulledFromPosition(address(_positions[i]), amount); // Stop if no more assets to withdraw. if (assets == 0) break; @@ -520,15 +557,23 @@ contract Cellar is ERC4626, Ownable, Multicall { // Get the latest address of the price router. PriceRouter priceRouter = registry.priceRouter(); - for (uint256 i; i < positions.length; i++) { - ERC4626 position = ERC4626(positions[i]); - PositionData storage positionData = getPositionData[address(position)]; + // Get data efficiently. + ( + uint256 _totalAssets, + , + ERC4626[] memory _positions, + ERC20[] memory positionAssets, + uint256[] memory positionBalances + ) = _getData(); + + for (uint256 i; i < _positions.length; i++) { + PositionData storage positionData = getPositionData[address(_positions[i])]; // Get the current position balance. - uint256 balanceThisAccrual = position.maxWithdraw(address(this)); + uint256 balanceThisAccrual = positionBalances[i]; // Get exchange rate. - ERC20 positionAsset = position.asset(); + ERC20 positionAsset = positionAssets[i]; uint256 onePositionAsset = 10**positionAsset.decimals(); uint256 positionAssetToAssetExchangeRate = priceRouter.getExchangeRate(positionAsset, asset); @@ -550,7 +595,7 @@ contract Cellar is ERC4626, Ownable, Multicall { } // Compute and store current exchange rate between assets and shares for gas efficiency. - uint256 assetToSharesExchangeRate = convertToShares(1e18); + uint256 assetToSharesExchangeRate = _convertToShares(1e18, _totalAssets); // Calculate platform fees accrued. uint256 elapsedTime = block.timestamp - lastAccrual; @@ -779,6 +824,53 @@ contract Cellar is ERC4626, Ownable, Multicall { } } + function _getData() + internal + view + returns ( + uint256 _totalAssets, + uint256 _totalHoldings, + ERC4626[] memory _positions, + ERC20[] memory positionAssets, + uint256[] memory positionBalances + ) + { + for (uint256 i; i < positions.length; i++) { + ERC4626 position = ERC4626(positions[i]); + + _positions[i] = position; + positionAssets[i] = position.asset(); + positionBalances[i] = position.maxWithdraw(address(this)); + } + + _totalHoldings = totalHoldings(); + _totalAssets = _totalHoldings + registry.priceRouter().getValue(positionAssets, positionBalances, asset); + } + + function _convertToShares(uint256 assets, uint256 _totalAssets) internal view returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? assets : assets.mulDivDown(supply, _totalAssets); + } + + function _convertToAssets(uint256 shares, uint256 _totalAssets) internal view returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? shares : shares.mulDivDown(_totalAssets, supply); + } + + function _previewMint(uint256 shares, uint256 _totalAssets) internal view returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? shares : shares.mulDivUp(_totalAssets, supply); + } + + function _previewWithdraw(uint256 assets, uint256 _totalAssets) internal view returns (uint256) { + uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + + return supply == 0 ? assets : assets.mulDivUp(supply, _totalAssets); + } + function _swapExactAssets( ERC20 assetIn, uint256 amountIn, From e67a35465bf6793ffcc407bf607cf875312a9c9e Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Fri, 17 Jun 2022 12:43:10 -0700 Subject: [PATCH 04/49] refactor(Cellar): initialize cellar with positions --- src/base/Cellar.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 2e487efd..c5ac6707 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -380,10 +380,14 @@ contract Cellar is ERC4626, Ownable, Multicall { constructor( Registry _registry, ERC20 _asset, + address[] memory _positions, string memory _name, string memory _symbol ) ERC4626(_asset, _name, _symbol, 18) Ownable() { registry = _registry; + positions = _positions; + + for (uint256 i; i < _positions.length; i++) isTrusted[_positions[i]] = true; // Transfer ownership to the Gravity Bridge. transferOwnership(address(_registry.gravityBridge())); From f7683f36ed9b4fb4401fb75dec6c0410a92c39d5 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Fri, 17 Jun 2022 12:43:29 -0700 Subject: [PATCH 05/49] tests(Cellar): update test --- src/mocks/MockPriceRouter.sol | 33 + test/Cellar.t.sol | 1515 ++++++++++++++++----------------- 2 files changed, 779 insertions(+), 769 deletions(-) create mode 100644 src/mocks/MockPriceRouter.sol diff --git a/src/mocks/MockPriceRouter.sol b/src/mocks/MockPriceRouter.sol new file mode 100644 index 00000000..9e98b3ca --- /dev/null +++ b/src/mocks/MockPriceRouter.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { MockExchange } from "./MockExchange.sol"; + +contract MockPriceRouter { + MockExchange public exchange; + + constructor(MockExchange _exchange) { + exchange = _exchange; + } + + function getValue( + ERC20[] memory baseAssets, + uint256[] memory amounts, + ERC20 quoteAsset + ) external view returns (uint256 value) { + for (uint256 i; i < baseAssets.length; i++) value += getValue(baseAssets[i], amounts[i], quoteAsset); + } + + function getValue( + ERC20 baseAsset, + uint256 amounts, + ERC20 quoteAsset + ) public view returns (uint256) { + return exchange.convert(address(baseAsset), address(quoteAsset), amounts); + } + + function getExchangeRate(ERC20 baseAsset, ERC20 quoteAsset) external view returns (uint256) { + return exchange.getExchangeRate(address(baseAsset), address(quoteAsset)); + } +} diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 14bc5b20..35d0a3f7 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -1,850 +1,827 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.13; -// TODO: uncomment and refactor test once src/base/Cellar.sol contract is ready - -// import { ERC20 } from "@solmate/tokens/ERC20.sol"; -// import { ERC4626 } from "src/base/ERC4626.sol"; -// import { MockMultipositionCellar } from "src/mocks/MockMultipositionCellar.sol"; -// import { MockERC20 } from "src/mocks/MockERC20.sol"; -// import { MockERC4626 } from "src/mocks/MockERC4626.sol"; -// import { MockSwapRouter } from "src/mocks/MockSwapRouter.sol"; -// import { ISwapRouter } from "src/interfaces/ISwapRouter.sol"; - -// import { Test } from "@forge-std/Test.sol"; -// import { Math } from "src/utils/Math.sol"; - -// // TODO: test with fuzzing - -// contract MultipositionCellarTest is Test { -// using Math for uint256; - -// MockMultipositionCellar private cellar; -// MockSwapRouter private swapRouter; - -// MockERC20 private USDC; -// MockERC4626 private usdcCLR; - -// MockERC20 private WETH; -// MockERC4626 private wethCLR; - -// MockERC20 private WBTC; -// MockERC4626 private wbtcCLR; - -// function setUp() external { -// swapRouter = new MockSwapRouter(); -// vm.label(address(swapRouter), "swapRouter"); - -// USDC = new MockERC20("USDC", 6); -// vm.label(address(USDC), "USDC"); -// usdcCLR = new MockERC4626(ERC20(address(USDC)), "USDC Cellar LP Token", "USDC-CLR", 6); -// vm.label(address(usdcCLR), "usdcCLR"); - -// WETH = new MockERC20("WETH", 18); -// vm.label(address(WETH), "WETH"); -// wethCLR = new MockERC4626(ERC20(address(WETH)), "WETH Cellar LP Token", "WETH-CLR", 18); -// vm.label(address(wethCLR), "wethCLR"); - -// WBTC = new MockERC20("WBTC", 8); -// vm.label(address(WBTC), "WBTC"); -// wbtcCLR = new MockERC4626(ERC20(address(WBTC)), "WBTC Cellar LP Token", "WBTC-CLR", 8); -// vm.label(address(wbtcCLR), "wbtcCLR"); - -// // Setup exchange rates: -// swapRouter.setExchangeRate(address(USDC), address(USDC), 1e6); -// swapRouter.setExchangeRate(address(WETH), address(WETH), 1e18); -// swapRouter.setExchangeRate(address(WBTC), address(WBTC), 1e8); - -// swapRouter.setExchangeRate(address(USDC), address(WETH), 0.0005e18); -// swapRouter.setExchangeRate(address(WETH), address(USDC), 2000e6); - -// swapRouter.setExchangeRate(address(USDC), address(WBTC), 0.000033e8); -// swapRouter.setExchangeRate(address(WBTC), address(USDC), 30_000e6); - -// swapRouter.setExchangeRate(address(WETH), address(WBTC), 0.06666666e8); -// swapRouter.setExchangeRate(address(WBTC), address(WETH), 15e18); - -// // Setup cellar: -// ERC4626[] memory positions = new ERC4626[](3); -// positions[0] = ERC4626(address(usdcCLR)); -// positions[1] = ERC4626(address(wethCLR)); -// positions[2] = ERC4626(address(wbtcCLR)); - -// uint256 len = positions.length; - -// address[][] memory paths = new address[][](len); -// for (uint256 i; i < len; i++) { -// address[] memory path = new address[](2); -// path[0] = address(positions[i].asset()); -// path[1] = address(USDC); - -// paths[i] = path; -// } - -// uint32[] memory maxSlippages = new uint32[](len); -// for (uint256 i; i < len; i++) maxSlippages[i] = uint32(swapRouter.PRICE_IMPACT()); - -// cellar = new MockMultipositionCellar( -// USDC, -// positions, -// paths, -// maxSlippages, -// ISwapRouter(address(swapRouter)), -// "Multiposition Cellar LP Token", -// "multiposition-CLR", -// 6 -// ); -// vm.label(address(cellar), "cellar"); - -// // Transfer ownership to this contract for testing. -// vm.prank(address(cellar.gravityBridge())); -// cellar.transferOwnership(address(this)); - -// // Mint enough liquidity to swap router for swaps. -// for (uint256 i; i < positions.length; i++) { -// MockERC20 asset = MockERC20(address(positions[i].asset())); -// asset.mint(address(swapRouter), type(uint112).max); -// } -// } - -// // ========================================= DEPOSIT/WITHDRAW TEST ========================================= - -// function testDepositWithdraw() external { -// // assets = bound(assets, 1, cellar.maxDeposit(address(this))); -// // NOTE: last time this was run, all test pass with the line below uncommented -// // assets = bound(assets, 1, type(uint128).max); -// uint256 assets = 100e18; - -// // Test single deposit. -// USDC.mint(address(this), assets); -// USDC.approve(address(cellar), assets); -// uint256 shares = cellar.deposit(assets, address(this)); - -// assertEq(shares, assets); // Expect exchange rate to be 1:1 on initial deposit. -// assertEq(cellar.previewWithdraw(assets), shares); -// assertEq(cellar.previewDeposit(assets), shares); -// assertEq(cellar.totalBalance(), 0); -// assertEq(cellar.totalHoldings(), assets); -// assertEq(cellar.totalAssets(), assets); -// assertEq(cellar.totalSupply(), shares); -// assertEq(cellar.balanceOf(address(this)), shares); -// assertEq(cellar.convertToAssets(cellar.balanceOf(address(this))), assets); -// assertEq(USDC.balanceOf(address(this)), 0); - -// // Test single withdraw. -// cellar.withdraw(assets, address(this), address(this)); - -// assertEq(cellar.totalBalance(), 0); -// assertEq(cellar.totalHoldings(), 0); -// assertEq(cellar.totalAssets(), 0); -// assertEq(cellar.totalSupply(), 0); -// assertEq(cellar.balanceOf(address(this)), 0); -// assertEq(cellar.convertToAssets(cellar.balanceOf(address(this))), 0); -// assertEq(USDC.balanceOf(address(this)), assets); -// } - -// function testFailDepositWithNotEnoughApproval(uint256 assets) external { -// USDC.mint(address(this), assets / 2); -// USDC.approve(address(cellar), assets / 2); - -// cellar.deposit(assets, address(this)); -// } - -// function testFailWithdrawWithNotEnoughBalance(uint256 assets) external { -// USDC.mint(address(this), assets / 2); -// USDC.approve(address(cellar), assets / 2); - -// cellar.deposit(assets / 2, address(this)); - -// cellar.withdraw(assets, address(this), address(this)); -// } - -// function testFailRedeemWithNotEnoughBalance(uint256 assets) external { -// USDC.mint(address(this), assets / 2); -// USDC.approve(address(cellar), assets / 2); - -// cellar.deposit(assets / 2, address(this)); - -// cellar.redeem(assets, address(this), address(this)); -// } - -// function testFailWithdrawWithNoBalance(uint256 assets) external { -// if (assets == 0) assets = 1; -// cellar.withdraw(assets, address(this), address(this)); -// } - -// function testFailRedeemWithNoBalance(uint256 assets) external { -// cellar.redeem(assets, address(this), address(this)); -// } - -// function testFailDepositWithNoApproval(uint256 assets) external { -// cellar.deposit(assets, address(this)); -// } - -// function testFailWithdrawWithSwapOverMaxSlippage() external { -// WETH.mint(address(this), 1e18); -// WETH.approve(address(cellar), 1e18); -// cellar.depositIntoPosition(wethCLR, 1e18, address(this)); - -// assertEq(cellar.totalAssets(), 2000e6); - -// cellar.setMaxSlippage(wethCLR, 0); - -// cellar.withdraw(1e6, address(this), address(this)); -// } - -// function testWithdrawWithoutEnoughHoldings() external { -// // Deposit assets directly into position. -// WETH.mint(address(this), 1e18); -// WETH.approve(address(cellar), 1e18); -// cellar.depositIntoPosition(wethCLR, 1e18, address(this)); // $2000 - -// WBTC.mint(address(this), 1e8); -// WBTC.approve(address(cellar), 1e8); -// cellar.depositIntoPosition(wbtcCLR, 1e8, address(this)); // $30,000 - -// assertEq(cellar.totalHoldings(), 0); -// assertEq(cellar.totalAssets(), 32_000e6); +import { Cellar, ERC4626, ERC20 } from "src/base/Cellar.sol"; +import { Registry, PriceRouter, IGravity } from "src/Registry.sol"; +import { SwapRouter, IUniswapV2Router, IUniswapV3Router } from "src/modules/SwapRouter.sol"; +import { MockPriceRouter } from "src/mocks/MockPriceRouter.sol"; +import { MockERC20 } from "src/mocks/MockERC20.sol"; +import { MockERC4626 } from "src/mocks/MockERC4626.sol"; +import { MockGravity } from "src/mocks/MockGravity.sol"; +import { MockExchange } from "src/mocks/MockExchange.sol"; + +import { Test } from "@forge-std/Test.sol"; +import { Math } from "src/utils/Math.sol"; + +// TODO: test with fuzzing + +contract CellarTest is Test { + using Math for uint256; + + Cellar private cellar; + MockGravity private gravity; + MockExchange private exchange; + + IUniswapV2Router private constant uniswapV2Router = IUniswapV2Router(0xE592427A0AEce92De3Edee1F18E0157C05861564); + IUniswapV3Router private constant uniswapV3Router = IUniswapV3Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + SwapRouter private swapRouter; + PriceRouter private priceRouter; + + Registry private registry; + + MockERC20 private USDC; + MockERC4626 private usdcCLR; + + MockERC20 private WETH; + MockERC4626 private wethCLR; + + MockERC20 private WBTC; + MockERC4626 private wbtcCLR; + + function setUp() external { + exchange = new MockExchange(); + vm.label(address(exchange), "exchange"); + + USDC = new MockERC20("USDC", 6); + vm.label(address(USDC), "USDC"); + usdcCLR = new MockERC4626(ERC20(address(USDC)), "USDC Cellar LP Token", "USDC-CLR", 6); + vm.label(address(usdcCLR), "usdcCLR"); + + WETH = new MockERC20("WETH", 18); + vm.label(address(WETH), "WETH"); + wethCLR = new MockERC4626(ERC20(address(WETH)), "WETH Cellar LP Token", "WETH-CLR", 18); + vm.label(address(wethCLR), "wethCLR"); + + WBTC = new MockERC20("WBTC", 8); + vm.label(address(WBTC), "WBTC"); + wbtcCLR = new MockERC4626(ERC20(address(WBTC)), "WBTC Cellar LP Token", "WBTC-CLR", 8); + vm.label(address(wbtcCLR), "wbtcCLR"); + + // Setup exchange rates: + exchange.setExchangeRate(address(USDC), address(USDC), 1e6); + exchange.setExchangeRate(address(WETH), address(WETH), 1e18); + exchange.setExchangeRate(address(WBTC), address(WBTC), 1e8); + + exchange.setExchangeRate(address(USDC), address(WETH), 0.0005e18); + exchange.setExchangeRate(address(WETH), address(USDC), 2000e6); + + exchange.setExchangeRate(address(USDC), address(WBTC), 0.000033e8); + exchange.setExchangeRate(address(WBTC), address(USDC), 30_000e6); + + exchange.setExchangeRate(address(WETH), address(WBTC), 0.06666666e8); + exchange.setExchangeRate(address(WBTC), address(WETH), 15e18); + + // Setup Registry and modules: + swapRouter = new SwapRouter(uniswapV2Router, uniswapV3Router); + priceRouter = PriceRouter(address(new MockPriceRouter(exchange))); + gravity = new MockGravity(); + + registry = new Registry(swapRouter, priceRouter, IGravity(address(gravity))); + + // Setup Cellar: + address[] memory positions = new address[](3); + positions[0] = address(usdcCLR); + positions[1] = address(wethCLR); + positions[2] = address(wbtcCLR); + + cellar = new Cellar(registry, USDC, positions, "Multiposition Cellar LP Token", "multiposition-CLR"); + vm.label(address(cellar), "cellar"); + + // Transfer ownership to this contract for testing. + vm.prank(address(registry.gravityBridge())); + cellar.transferOwnership(address(this)); + + // Mint enough liquidity to swap router for swaps. + for (uint256 i; i < positions.length; i++) { + MockERC20 asset = MockERC20(address(ERC4626(positions[i]).asset())); + asset.mint(address(exchange), type(uint112).max); + } + } + + // ========================================= DEPOSIT/WITHDRAW TEST ========================================= -// // Test withdraw returns assets to receiver and replenishes holding position. -// cellar.withdraw(10e6, address(this), address(this)); + function testDepositWithdraw() external { + // assets = bound(assets, 1, cellar.maxDeposit(address(this))); + // NOTE: last time this was run, all test pass with the line below uncommented + // assets = bound(assets, 1, type(uint128).max); + uint256 assets = 100e18; -// assertEq(USDC.balanceOf(address(this)), 10e6); -// // $1,600 = 5% of $32,000 (tolerate some assets loss due to swap slippage). -// assertApproxEqAbs(USDC.balanceOf(address(cellar)), 1600e6, 100e6); -// } + // Test single deposit. + USDC.mint(address(this), assets); + USDC.approve(address(cellar), assets); + uint256 shares = cellar.deposit(assets, address(this)); + + assertEq(shares, assets); // Expect exchange rate to be 1:1 on initial deposit. + assertEq(cellar.previewWithdraw(assets), shares); + assertEq(cellar.previewDeposit(assets), shares); + assertEq(cellar.totalHoldings(), assets); + assertEq(cellar.totalAssets(), assets); + assertEq(cellar.totalSupply(), shares); + assertEq(cellar.balanceOf(address(this)), shares); + assertEq(cellar.convertToAssets(cellar.balanceOf(address(this))), assets); + assertEq(USDC.balanceOf(address(this)), 0); + + // Test single withdraw. + cellar.withdraw(assets, address(this), address(this)); + + assertEq(cellar.totalHoldings(), 0); + assertEq(cellar.totalAssets(), 0); + assertEq(cellar.totalSupply(), 0); + assertEq(cellar.balanceOf(address(this)), 0); + assertEq(cellar.convertToAssets(cellar.balanceOf(address(this))), 0); + assertEq(USDC.balanceOf(address(this)), assets); + } -// function testWithdrawAllWithHomogenousPositions() external { -// USDC.mint(address(this), 100e18); -// USDC.approve(address(cellar), 100e18); -// cellar.depositIntoPosition(usdcCLR, 100e18, address(this)); + function testFailDepositWithNotEnoughApproval(uint256 assets) external { + USDC.mint(address(this), assets / 2); + USDC.approve(address(cellar), assets / 2); + + cellar.deposit(assets, address(this)); + } + + function testFailWithdrawWithNotEnoughBalance(uint256 assets) external { + USDC.mint(address(this), assets / 2); + USDC.approve(address(cellar), assets / 2); + + cellar.deposit(assets / 2, address(this)); + + cellar.withdraw(assets, address(this), address(this)); + } + + function testFailRedeemWithNotEnoughBalance(uint256 assets) external { + USDC.mint(address(this), assets / 2); + USDC.approve(address(cellar), assets / 2); -// assertEq(cellar.totalAssets(), 100e18); + cellar.deposit(assets / 2, address(this)); -// cellar.withdraw(100e18, address(this), address(this)); + cellar.redeem(assets, address(this), address(this)); + } -// assertEq(USDC.balanceOf(address(this)), 100e18); -// } + function testFailWithdrawWithNoBalance(uint256 assets) external { + if (assets == 0) assets = 1; + cellar.withdraw(assets, address(this), address(this)); + } -// // NOTE: Although this behavior is not desired, it should be anticipated that this will occur when -// // withdrawing from a cellar with positions that are not all in the same asset as the holding -// // position due to the swap slippage involved in needing to convert them all to single asset -// // received by the user. -// function testFailWithdrawAllWithHeterogenousPositions() external { -// USDC.mint(address(this), 100e6); -// USDC.approve(address(cellar), 100e6); -// cellar.depositIntoPosition(usdcCLR, 100e6, address(this)); // $100 + function testFailRedeemWithNoBalance(uint256 assets) external { + cellar.redeem(assets, address(this), address(this)); + } + + function testFailDepositWithNoApproval(uint256 assets) external { + cellar.deposit(assets, address(this)); + } + + // function testWithdrawWithoutEnoughHoldings() external { + // // Deposit assets directly into position. + // WETH.mint(address(this), 1e18); + // WETH.approve(address(cellar), 1e18); + // cellar.depositIntoPosition(wethCLR, 1e18, address(this)); // $2000 + + // WBTC.mint(address(this), 1e8); + // WBTC.approve(address(cellar), 1e8); + // cellar.depositIntoPosition(wbtcCLR, 1e8, address(this)); // $30,000 + + // assertEq(cellar.totalHoldings(), 0); + // assertEq(cellar.totalAssets(), 32_000e6); -// WETH.mint(address(this), 1e18); -// WETH.approve(address(cellar), 1e18); -// cellar.depositIntoPosition(wethCLR, 1e18, address(this)); // $2,000 + // // Test withdraw returns assets to receiver and replenishes holding position. + // cellar.withdraw(10e6, address(this), address(this)); + + // assertEq(USDC.balanceOf(address(this)), 10e6); + // // $1,600 = 5% of $32,000 (tolerate some assets loss due to swap slippage). + // assertApproxEqAbs(USDC.balanceOf(address(cellar)), 1600e6, 100e6); + // } -// WBTC.mint(address(this), 1e8); -// WBTC.approve(address(cellar), 1e8); -// cellar.depositIntoPosition(wbtcCLR, 1e8, address(this)); // $30,000 + // function testWithdrawAllWithHomogenousPositions() external { + // USDC.mint(address(this), 100e18); + // USDC.approve(address(cellar), 100e18); + // cellar.depositIntoPosition(usdcCLR, 100e18, address(this)); -// assertEq(cellar.totalAssets(), 32_100e6); + // assertEq(cellar.totalAssets(), 100e18); -// cellar.withdraw(32_100e6, address(this), address(this)); -// } + // cellar.withdraw(100e18, address(this), address(this)); -// // =========================================== REBALANCE TEST =========================================== + // assertEq(USDC.balanceOf(address(this)), 100e18); + // } -// function testRebalance() external { -// USDC.mint(address(this), 10_000e6); -// USDC.approve(address(cellar), 10_000e6); -// cellar.deposit(10_000e6, address(this)); + // // NOTE: Although this behavior is not desired, it should be anticipated that this will occur when + // // withdrawing from a cellar with positions that are not all in the same asset as the holding + // // position due to the swap slippage involved in needing to convert them all to single asset + // // received by the user. + // function testFailWithdrawAllWithHeterogenousPositions() external { + // USDC.mint(address(this), 100e6); + // USDC.approve(address(cellar), 100e6); + // cellar.depositIntoPosition(usdcCLR, 100e6, address(this)); // $100 -// address[] memory path = new address[](2); + // WETH.mint(address(this), 1e18); + // WETH.approve(address(cellar), 1e18); + // cellar.depositIntoPosition(wethCLR, 1e18, address(this)); // $2,000 -// // Test rebalancing from holding position. -// path[0] = address(USDC); -// path[1] = address(USDC); + // WBTC.mint(address(this), 1e8); + // WBTC.approve(address(cellar), 1e8); + // cellar.depositIntoPosition(wbtcCLR, 1e8, address(this)); // $30,000 -// uint256 assetsRebalanced = cellar.rebalance(cellar, usdcCLR, 10_000e6, 10_000e6, path); + // assertEq(cellar.totalAssets(), 32_100e6); -// assertEq(assetsRebalanced, 10_000e6); -// assertEq(cellar.totalHoldings(), 0); -// assertEq(usdcCLR.balanceOf(address(cellar)), 10_000e6); -// (, , uint112 fromBalance) = cellar.getPositionData(usdcCLR); -// assertEq(fromBalance, 10_000e6); - -// // Test rebalancing between positions. -// path[0] = address(USDC); -// path[1] = address(WETH); - -// uint256 expectedAssetsOut = swapRouter.quote(10_000e6, path); -// assetsRebalanced = cellar.rebalance(usdcCLR, wethCLR, 10_000e6, expectedAssetsOut, path); - -// assertEq(assetsRebalanced, expectedAssetsOut); -// assertEq(usdcCLR.balanceOf(address(cellar)), 0); -// assertEq(wethCLR.balanceOf(address(cellar)), assetsRebalanced); -// (, , fromBalance) = cellar.getPositionData(usdcCLR); -// assertEq(fromBalance, 0); -// (, , uint112 toBalance) = cellar.getPositionData(wethCLR); -// assertEq(toBalance, assetsRebalanced); - -// // Test rebalancing back to holding position. -// path[0] = address(WETH); -// path[1] = address(USDC); - -// expectedAssetsOut = swapRouter.quote(assetsRebalanced, path); -// assetsRebalanced = cellar.rebalance(wethCLR, cellar, assetsRebalanced, expectedAssetsOut, path); - -// assertEq(assetsRebalanced, expectedAssetsOut); -// assertEq(wethCLR.balanceOf(address(cellar)), 0); -// assertEq(cellar.totalHoldings(), assetsRebalanced); -// (, , toBalance) = cellar.getPositionData(wethCLR); -// assertEq(toBalance, 0); -// } - -// function testFailRebalanceFromPositionWithNotEnoughBalance() external { -// uint256 assets = 100e18; - -// USDC.mint(address(this), assets / 2); -// USDC.approve(address(cellar), assets / 2); - -// cellar.depositIntoPosition(usdcCLR, assets / 2, address(this)); - -// address[] memory path = new address[](2); -// path[0] = address(USDC); -// path[1] = address(WBTC); - -// uint256 expectedAssetsOut = swapRouter.quote(assets, path); -// cellar.rebalance(usdcCLR, wbtcCLR, assets, expectedAssetsOut, path); -// } - -// function testFailRebalanceIntoUntrustedPosition() external { -// uint256 assets = 100e18; - -// ERC4626[] memory positions = cellar.getPositions(); -// ERC4626 untrustedPosition = positions[positions.length - 1]; - -// cellar.setTrust(untrustedPosition, false); - -// MockERC20 asset = MockERC20(address(cellar.asset())); - -// asset.mint(address(this), assets); -// asset.approve(address(cellar), assets); -// cellar.deposit(assets, address(this)); - -// address[] memory path = new address[](2); - -// // Test rebalancing from holding position to untrusted position. -// path[0] = address(asset); -// path[1] = address(untrustedPosition.asset()); - -// cellar.rebalance(cellar, untrustedPosition, assets, 0, path); -// } - -// // ============================================= ACCRUE TEST ============================================= - -// function testAccrue() external { -// // Scenario: -// // - Multiposition cellar has 3 positions. -// // -// // Testcases Covered: -// // - Test accrual with positive performance. -// // - Test accrual with negative performance. -// // - Test accrual with no performance (nothing changes). -// // - Test accrual reverting previous accrual period is still ongoing. -// // - Test accrual not starting an accrual period if negative performance or no performance. -// // - Test accrual for single position. -// // - Test accrual for multiple positions. -// // - Test accrued yield is distributed linearly as expected. -// // - Test deposits / withdraws do not effect accrual and yield distribution. - -// // NOTE: The amounts in each column are approximations. Actual results may differ due -// // to swaps and decimal conversions, however, it should not be significant. -// // +==============+==============+==================+================+===================+==============+ -// // | Total Assets | Total Locked | Performance Fees | Platform Fees | Last Accrual Time | Current Time | -// // | (in USD) | (in USD) | (in shares) | (in shares) | (in seconds) | (in seconds) | -// // +==============+==============+==================+================+===================+==============+ -// // | 1. Deposit $100 worth of assets into each position. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $300 | $0 | 0 | 0 | 0 | 0 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | 2. An entire year passes. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $300 | $0 | 0 | 0 | 0 | 31536000 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | 3. Test accrual of platform fees. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $300 | $0 | 0 | 3 | 31536000 | 31536000 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | 4. Each position gains $50 worth of assets of yield. | -// // | NOTE: Nothing should change because yield has not been accrued. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $300 | $0 | 0 | 3 | 31536000 | 31536000 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | 5. Accrue with positive performance. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $315 | $135 | 15 | 3 | 31536000 | 31536000 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | 6. Half of accrual period passes. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $382.5 | $67.5 | 15 | 3 | 31536000 | 31838400 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | 7. Deposit $200 worth of assets into a position. | -// // | NOTE: For testing that deposit does not effect yield and is not factored in to later accrual. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $582.5 | $67.5 | 15 | 3 | 31536000 | 31838400 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | 8. Entire accrual period passes. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $650 | $0 | 15 | 3 | 31536000 | 32140800 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | 9. Withdraw $100 worth of assets from a position. | -// // | NOTE: For testing that withdraw does not effect yield and is not factored in to later accrual. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $550 | $0 | 15 | 3 | 31536000 | 32140800 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | 10. Accrue with no performance. | -// // | NOTE: Ignore platform fees from now on because we've already tested they work and amounts at | -// // | this timescale are very small. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $550 | $0 | 15 | 3 | 32140800 | 32140800 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | 11. A position loses $150 worth of assets of yield. | -// // | NOTE: Nothing should change because losses have not been accrued. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $550 | $0 | 15 | 3 | 32140800 | 32140800 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | 12. Accrue with negative performance. | -// // | NOTE: Losses are realized immediately. | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ -// // | $400 | $0 | 15 | 3 | 32745600 | 32745600 | -// // +--------------+--------------+------------------+----------------+-------------------+--------------+ - -// ERC4626[] memory positions = cellar.getPositions(); - -// // 1. Deposit $100 worth of assets into each position. -// for (uint256 i; i < positions.length; i++) { -// ERC4626 position = positions[i]; -// MockERC20 positionAsset = MockERC20(address(position.asset())); - -// uint256 assets = swapRouter.convert(address(USDC), address(positionAsset), 100e6); -// positionAsset.mint(address(this), assets); -// positionAsset.approve(address(cellar), assets); -// cellar.depositIntoPosition(position, assets, address(this)); - -// assertEq(position.totalAssets(), assets); -// (, , uint112 balance) = cellar.getPositionData(position); -// assertEq(balance, assets); -// assertApproxEqAbs(cellar.totalBalance(), 100e6 * (i + 1), 1e6); -// } - -// assertApproxEqAbs(cellar.totalAssets(), 300e6, 1e6); - -// // 2. An entire year passes. -// vm.warp(block.timestamp + 365 days); -// uint256 lastAccrualTimestamp = block.timestamp; - -// // 3. Accrue platform fees. -// cellar.accrue(); - -// assertEq(cellar.totalLocked(), 0); -// assertApproxEqAbs(cellar.totalAssets(), 300e6, 1e6); -// assertApproxEqAbs(cellar.totalBalance(), 300e6, 1e6); -// assertApproxEqAbs(cellar.balanceOf(address(cellar)), 3e6, 0.01e6); -// assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - -// // 4. Each position gains $50 worth of assets of yield. -// for (uint256 i; i < positions.length; i++) { -// ERC4626 position = positions[i]; -// MockERC20 positionAsset = MockERC20(address(position.asset())); - -// uint256 assets = swapRouter.convert(address(USDC), address(positionAsset), 50e6); -// MockERC4626(address(position)).simulateGain(assets, address(cellar)); -// assertApproxEqAbs(cellar.convertToAssets(positionAsset, position.maxWithdraw(address(cellar))), 150e6, 2e6); -// } - -// uint256 priceOfShareBefore = cellar.convertToShares(1e6); - -// // 5. Accrue with positive performance. -// cellar.accrue(); - -// uint256 priceOfShareAfter = cellar.convertToShares(1e6); -// assertEq(priceOfShareAfter, priceOfShareBefore); -// assertApproxEqAbs(cellar.totalLocked(), 135e6, 1e6); -// assertApproxEqAbs(cellar.totalAssets(), 315e6, 2e6); -// assertApproxEqAbs(cellar.totalBalance(), 450e6, 2e6); -// assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); -// assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - -// // Position balances should have updated to reflect yield accrued per position. -// for (uint256 i; i < positions.length; i++) { -// ERC4626 position = positions[i]; - -// (, , uint112 balance) = cellar.getPositionData(position); -// assertApproxEqAbs(cellar.convertToAssets(position.asset(), balance), 150e6, 2e6); -// } - -// // 6. Half of accrual period passes. -// uint256 accrualPeriod = cellar.accrualPeriod(); -// vm.warp(block.timestamp + accrualPeriod / 2); - -// assertApproxEqAbs(cellar.totalLocked(), 67.5e6, 1e6); -// assertApproxEqAbs(cellar.totalAssets(), 382.5e6, 2e6); -// assertApproxEqAbs(cellar.totalBalance(), 450e6, 2e6); -// assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); -// assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - -// // 7. Deposit $200 worth of assets into a position. -// USDC.mint(address(this), 200e6); -// USDC.approve(address(cellar), 200e6); -// cellar.depositIntoPosition(usdcCLR, 200e6, address(this)); - -// assertApproxEqAbs(cellar.totalLocked(), 67.5e6, 1e6); -// assertApproxEqAbs(cellar.totalAssets(), 582.5e6, 2e6); -// assertApproxEqAbs(cellar.totalBalance(), 650e6, 2e6); -// assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); -// assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - -// // 8. Entire accrual period passes. -// vm.warp(block.timestamp + accrualPeriod / 2); - -// assertEq(cellar.totalLocked(), 0); -// assertApproxEqAbs(cellar.totalAssets(), 650e6, 2e6); -// assertApproxEqAbs(cellar.totalBalance(), 650e6, 2e6); -// assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); -// assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - -// // 9. Withdraw $100 worth of assets from a position. -// cellar.withdrawFromPosition( -// wethCLR, -// swapRouter.convert(address(USDC), address(WETH), 100e6), -// address(this), -// address(this) -// ); - -// assertEq(cellar.totalLocked(), 0); -// assertApproxEqAbs(cellar.totalAssets(), 550e6, 2e6); -// assertApproxEqAbs(cellar.totalBalance(), 550e6, 2e6); -// assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); -// assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - -// // 10. Accrue with no performance. -// cellar.accrue(); -// lastAccrualTimestamp = block.timestamp; - -// assertEq(cellar.totalLocked(), 0); -// assertApproxEqAbs(cellar.totalAssets(), 550e6, 2e6); -// assertApproxEqAbs(cellar.totalBalance(), 550e6, 2e6); -// assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); -// assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - -// // 11. A position loses $150 worth of assets of yield. -// MockERC4626(address(usdcCLR)).simulateLoss(150e6); - -// assertEq(cellar.totalLocked(), 0); -// assertApproxEqAbs(cellar.totalAssets(), 550e6, 2e6); -// assertApproxEqAbs(cellar.totalBalance(), 550e6, 2e6); -// assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); -// assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - -// // 12. Accrue with negative performance. -// cellar.accrue(); - -// assertEq(cellar.totalLocked(), 0); -// assertApproxEqAbs(cellar.totalAssets(), 400e6, 2e6); -// assertApproxEqAbs(cellar.totalBalance(), 400e6, 2e6); -// assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); -// assertEq(cellar.lastAccrual(), lastAccrualTimestamp); -// } - -// function testAccrueWithZeroTotalLocked() external { -// cellar.accrue(); - -// assertEq(cellar.totalLocked(), 0); - -// cellar.accrue(); -// } - -// function testFailAccrueWithNonzeroTotalLocked() external { -// MockERC4626(address(usdcCLR)).simulateGain(100e6, address(cellar)); -// cellar.accrue(); - -// // $90 locked after taking $10 for 10% performance fees. -// assertEq(cellar.totalLocked(), 90e6); - -// cellar.accrue(); -// } - -// // ============================================= POSITIONS TEST ============================================= - -// function testSetPositions() external { -// ERC4626[] memory positions = new ERC4626[](3); -// positions[0] = ERC4626(address(wethCLR)); -// positions[1] = ERC4626(address(usdcCLR)); -// positions[2] = ERC4626(address(wbtcCLR)); - -// uint32[] memory maxSlippages = new uint32[](3); -// for (uint256 i; i < 3; i++) maxSlippages[i] = 1_00; - -// cellar.setPositions(positions, maxSlippages); - -// // Test that positions were updated. -// ERC4626[] memory newPositions = cellar.getPositions(); -// uint32 maxSlippage; -// for (uint256 i; i < 3; i++) { -// ERC4626 position = positions[i]; - -// assertEq(address(position), address(newPositions[i])); -// (, maxSlippage, ) = cellar.getPositionData(position); -// assertEq(maxSlippage, 1_00); -// } -// } - -// function testFailSetUntrustedPosition() external { -// MockERC20 XYZ = new MockERC20("XYZ", 18); -// MockERC4626 xyzCLR = new MockERC4626(ERC20(address(XYZ)), "XYZ Cellar LP Token", "XYZ-CLR", 18); - -// (bool isTrusted, , ) = cellar.getPositionData(xyzCLR); -// assertFalse(isTrusted); - -// ERC4626[] memory positions = new ERC4626[](4); -// positions[0] = ERC4626(address(wethCLR)); -// positions[1] = ERC4626(address(usdcCLR)); -// positions[2] = ERC4626(address(wbtcCLR)); -// positions[3] = ERC4626(address(xyzCLR)); - -// // Test attempting to setting with an untrusted position. -// cellar.setPositions(positions); -// } - -// function testFailAddingUntrustedPosition() external { -// MockERC20 XYZ = new MockERC20("XYZ", 18); -// MockERC4626 xyzCLR = new MockERC4626(ERC20(address(XYZ)), "XYZ Cellar LP Token", "XYZ-CLR", 18); + // cellar.withdraw(32_100e6, address(this), address(this)); + // } -// (bool isTrusted, , ) = cellar.getPositionData(xyzCLR); -// assertFalse(isTrusted); + // // =========================================== REBALANCE TEST =========================================== -// // Test attempting to add untrusted position. -// cellar.addPosition(xyzCLR); -// } + // function testRebalance() external { + // USDC.mint(address(this), 10_000e6); + // USDC.approve(address(cellar), 10_000e6); + // cellar.deposit(10_000e6, address(this)); -// function testTrustingPosition() external { -// MockERC20 XYZ = new MockERC20("XYZ", 18); -// MockERC4626 xyzCLR = new MockERC4626(ERC20(address(XYZ)), "XYZ Cellar LP Token", "XYZ-CLR", 18); + // address[] memory path = new address[](2); -// (bool isTrusted, , ) = cellar.getPositionData(xyzCLR); -// assertFalse(isTrusted); + // // Test rebalancing from holding position. + // path[0] = address(USDC); + // path[1] = address(USDC); -// // Test that position is trusted. -// cellar.setTrust(xyzCLR, true); + // uint256 assetsRebalanced = cellar.rebalance(cellar, usdcCLR, 10_000e6, 10_000e6, path); -// (isTrusted, , ) = cellar.getPositionData(xyzCLR); -// assertTrue(isTrusted); + // assertEq(assetsRebalanced, 10_000e6); + // assertEq(cellar.totalHoldings(), 0); + // assertEq(usdcCLR.balanceOf(address(cellar)), 10_000e6); + // (, , uint112 fromBalance) = cellar.getPositionData(usdcCLR); + // assertEq(fromBalance, 10_000e6); + + // // Test rebalancing between positions. + // path[0] = address(USDC); + // path[1] = address(WETH); + + // uint256 expectedAssetsOut = exchange.quote(10_000e6, path); + // assetsRebalanced = cellar.rebalance(usdcCLR, wethCLR, 10_000e6, expectedAssetsOut, path); + + // assertEq(assetsRebalanced, expectedAssetsOut); + // assertEq(usdcCLR.balanceOf(address(cellar)), 0); + // assertEq(wethCLR.balanceOf(address(cellar)), assetsRebalanced); + // (, , fromBalance) = cellar.getPositionData(usdcCLR); + // assertEq(fromBalance, 0); + // (, , uint112 toBalance) = cellar.getPositionData(wethCLR); + // assertEq(toBalance, assetsRebalanced); + + // // Test rebalancing back to holding position. + // path[0] = address(WETH); + // path[1] = address(USDC); + + // expectedAssetsOut = exchange.quote(assetsRebalanced, path); + // assetsRebalanced = cellar.rebalance(wethCLR, cellar, assetsRebalanced, expectedAssetsOut, path); + + // assertEq(assetsRebalanced, expectedAssetsOut); + // assertEq(wethCLR.balanceOf(address(cellar)), 0); + // assertEq(cellar.totalHoldings(), assetsRebalanced); + // (, , toBalance) = cellar.getPositionData(wethCLR); + // assertEq(toBalance, 0); + // } + + // function testFailRebalanceFromPositionWithNotEnoughBalance() external { + // uint256 assets = 100e18; + + // USDC.mint(address(this), assets / 2); + // USDC.approve(address(cellar), assets / 2); + + // cellar.depositIntoPosition(usdcCLR, assets / 2, address(this)); + + // address[] memory path = new address[](2); + // path[0] = address(USDC); + // path[1] = address(WBTC); + + // uint256 expectedAssetsOut = exchange.quote(assets, path); + // cellar.rebalance(usdcCLR, wbtcCLR, assets, expectedAssetsOut, path); + // } + + // function testFailRebalanceIntoUntrustedPosition() external { + // uint256 assets = 100e18; + + // ERC4626[] memory positions = cellar.getPositions(); + // ERC4626 untrustedPosition = positions[positions.length - 1]; + + // cellar.setTrust(untrustedPosition, false); + + // MockERC20 asset = MockERC20(address(cellar.asset())); + + // asset.mint(address(this), assets); + // asset.approve(address(cellar), assets); + // cellar.deposit(assets, address(this)); + + // address[] memory path = new address[](2); + + // // Test rebalancing from holding position to untrusted position. + // path[0] = address(asset); + // path[1] = address(untrustedPosition.asset()); + + // cellar.rebalance(cellar, untrustedPosition, assets, 0, path); + // } + + // // ============================================= ACCRUE TEST ============================================= + + // function testAccrue() external { + // // Scenario: + // // - Multiposition cellar has 3 positions. + // // + // // Testcases Covered: + // // - Test accrual with positive performance. + // // - Test accrual with negative performance. + // // - Test accrual with no performance (nothing changes). + // // - Test accrual reverting previous accrual period is still ongoing. + // // - Test accrual not starting an accrual period if negative performance or no performance. + // // - Test accrual for single position. + // // - Test accrual for multiple positions. + // // - Test accrued yield is distributed linearly as expected. + // // - Test deposits / withdraws do not effect accrual and yield distribution. + + // // NOTE: The amounts in each column are approximations. Actual results may differ due + // // to swaps and decimal conversions, however, it should not be significant. + // // +==============+==============+==================+================+===================+==============+ + // // | Total Assets | Total Locked | Performance Fees | Platform Fees | Last Accrual Time | Current Time | + // // | (in USD) | (in USD) | (in shares) | (in shares) | (in seconds) | (in seconds) | + // // +==============+==============+==================+================+===================+==============+ + // // | 1. Deposit $100 worth of assets into each position. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $300 | $0 | 0 | 0 | 0 | 0 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | 2. An entire year passes. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $300 | $0 | 0 | 0 | 0 | 31536000 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | 3. Test accrual of platform fees. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $300 | $0 | 0 | 3 | 31536000 | 31536000 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | 4. Each position gains $50 worth of assets of yield. | + // // | NOTE: Nothing should change because yield has not been accrued. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $300 | $0 | 0 | 3 | 31536000 | 31536000 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | 5. Accrue with positive performance. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $315 | $135 | 15 | 3 | 31536000 | 31536000 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | 6. Half of accrual period passes. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $382.5 | $67.5 | 15 | 3 | 31536000 | 31838400 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | 7. Deposit $200 worth of assets into a position. | + // // | NOTE: For testing that deposit does not effect yield and is not factored in to later accrual. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $582.5 | $67.5 | 15 | 3 | 31536000 | 31838400 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | 8. Entire accrual period passes. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $650 | $0 | 15 | 3 | 31536000 | 32140800 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | 9. Withdraw $100 worth of assets from a position. | + // // | NOTE: For testing that withdraw does not effect yield and is not factored in to later accrual. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $550 | $0 | 15 | 3 | 31536000 | 32140800 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | 10. Accrue with no performance. | + // // | NOTE: Ignore platform fees from now on because we've already tested they work and amounts at | + // // | this timescale are very small. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $550 | $0 | 15 | 3 | 32140800 | 32140800 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | 11. A position loses $150 worth of assets of yield. | + // // | NOTE: Nothing should change because losses have not been accrued. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $550 | $0 | 15 | 3 | 32140800 | 32140800 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | 12. Accrue with negative performance. | + // // | NOTE: Losses are realized immediately. | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + // // | $400 | $0 | 15 | 3 | 32745600 | 32745600 | + // // +--------------+--------------+------------------+----------------+-------------------+--------------+ + + // ERC4626[] memory positions = cellar.getPositions(); + + // // 1. Deposit $100 worth of assets into each position. + // for (uint256 i; i < positions.length; i++) { + // ERC4626 position = positions[i]; + // MockERC20 positionAsset = MockERC20(address(position.asset())); + + // uint256 assets = exchange.convert(address(USDC), address(positionAsset), 100e6); + // positionAsset.mint(address(this), assets); + // positionAsset.approve(address(cellar), assets); + // cellar.depositIntoPosition(position, assets, address(this)); + + // assertEq(position.totalAssets(), assets); + // (, , uint112 balance) = cellar.getPositionData(position); + // assertEq(balance, assets); + // assertApproxEqAbs(cellar.totalBalance(), 100e6 * (i + 1), 1e6); + // } + + // assertApproxEqAbs(cellar.totalAssets(), 300e6, 1e6); + + // // 2. An entire year passes. + // vm.warp(block.timestamp + 365 days); + // uint256 lastAccrualTimestamp = block.timestamp; + + // // 3. Accrue platform fees. + // cellar.accrue(); + + // assertEq(cellar.totalLocked(), 0); + // assertApproxEqAbs(cellar.totalAssets(), 300e6, 1e6); + // assertApproxEqAbs(cellar.totalBalance(), 300e6, 1e6); + // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 3e6, 0.01e6); + // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); + + // // 4. Each position gains $50 worth of assets of yield. + // for (uint256 i; i < positions.length; i++) { + // ERC4626 position = positions[i]; + // MockERC20 positionAsset = MockERC20(address(position.asset())); + + // uint256 assets = exchange.convert(address(USDC), address(positionAsset), 50e6); + // MockERC4626(address(position)).simulateGain(assets, address(cellar)); + // assertApproxEqAbs(cellar.convertToAssets(positionAsset, position.maxWithdraw(address(cellar))), 150e6, 2e6); + // } + + // uint256 priceOfShareBefore = cellar.convertToShares(1e6); + + // // 5. Accrue with positive performance. + // cellar.accrue(); + + // uint256 priceOfShareAfter = cellar.convertToShares(1e6); + // assertEq(priceOfShareAfter, priceOfShareBefore); + // assertApproxEqAbs(cellar.totalLocked(), 135e6, 1e6); + // assertApproxEqAbs(cellar.totalAssets(), 315e6, 2e6); + // assertApproxEqAbs(cellar.totalBalance(), 450e6, 2e6); + // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); + // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); + + // // Position balances should have updated to reflect yield accrued per position. + // for (uint256 i; i < positions.length; i++) { + // ERC4626 position = positions[i]; + + // (, , uint112 balance) = cellar.getPositionData(position); + // assertApproxEqAbs(cellar.convertToAssets(position.asset(), balance), 150e6, 2e6); + // } + + // // 6. Half of accrual period passes. + // uint256 accrualPeriod = cellar.accrualPeriod(); + // vm.warp(block.timestamp + accrualPeriod / 2); + + // assertApproxEqAbs(cellar.totalLocked(), 67.5e6, 1e6); + // assertApproxEqAbs(cellar.totalAssets(), 382.5e6, 2e6); + // assertApproxEqAbs(cellar.totalBalance(), 450e6, 2e6); + // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); + // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); + + // // 7. Deposit $200 worth of assets into a position. + // USDC.mint(address(this), 200e6); + // USDC.approve(address(cellar), 200e6); + // cellar.depositIntoPosition(usdcCLR, 200e6, address(this)); + + // assertApproxEqAbs(cellar.totalLocked(), 67.5e6, 1e6); + // assertApproxEqAbs(cellar.totalAssets(), 582.5e6, 2e6); + // assertApproxEqAbs(cellar.totalBalance(), 650e6, 2e6); + // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); + // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); + + // // 8. Entire accrual period passes. + // vm.warp(block.timestamp + accrualPeriod / 2); + + // assertEq(cellar.totalLocked(), 0); + // assertApproxEqAbs(cellar.totalAssets(), 650e6, 2e6); + // assertApproxEqAbs(cellar.totalBalance(), 650e6, 2e6); + // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); + // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); + + // // 9. Withdraw $100 worth of assets from a position. + // cellar.withdrawFromPosition( + // wethCLR, + // exchange.convert(address(USDC), address(WETH), 100e6), + // address(this), + // address(this) + // ); + + // assertEq(cellar.totalLocked(), 0); + // assertApproxEqAbs(cellar.totalAssets(), 550e6, 2e6); + // assertApproxEqAbs(cellar.totalBalance(), 550e6, 2e6); + // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); + // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); + + // // 10. Accrue with no performance. + // cellar.accrue(); + // lastAccrualTimestamp = block.timestamp; + + // assertEq(cellar.totalLocked(), 0); + // assertApproxEqAbs(cellar.totalAssets(), 550e6, 2e6); + // assertApproxEqAbs(cellar.totalBalance(), 550e6, 2e6); + // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); + // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); + + // // 11. A position loses $150 worth of assets of yield. + // MockERC4626(address(usdcCLR)).simulateLoss(150e6); + + // assertEq(cellar.totalLocked(), 0); + // assertApproxEqAbs(cellar.totalAssets(), 550e6, 2e6); + // assertApproxEqAbs(cellar.totalBalance(), 550e6, 2e6); + // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); + // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); + + // // 12. Accrue with negative performance. + // cellar.accrue(); + + // assertEq(cellar.totalLocked(), 0); + // assertApproxEqAbs(cellar.totalAssets(), 400e6, 2e6); + // assertApproxEqAbs(cellar.totalBalance(), 400e6, 2e6); + // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); + // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); + // } + + // function testAccrueWithZeroTotalLocked() external { + // cellar.accrue(); + + // assertEq(cellar.totalLocked(), 0); + + // cellar.accrue(); + // } + + // function testFailAccrueWithNonzeroTotalLocked() external { + // MockERC4626(address(usdcCLR)).simulateGain(100e6, address(cellar)); + // cellar.accrue(); + + // // $90 locked after taking $10 for 10% performance fees. + // assertEq(cellar.totalLocked(), 90e6); + + // cellar.accrue(); + // } + + // // ============================================= POSITIONS TEST ============================================= + + // function testSetPositions() external { + // ERC4626[] memory positions = new ERC4626[](3); + // positions[0] = ERC4626(address(wethCLR)); + // positions[1] = ERC4626(address(usdcCLR)); + // positions[2] = ERC4626(address(wbtcCLR)); + + // uint32[] memory maxSlippages = new uint32[](3); + // for (uint256 i; i < 3; i++) maxSlippages[i] = 1_00; + + // cellar.setPositions(positions, maxSlippages); + + // // Test that positions were updated. + // ERC4626[] memory newPositions = cellar.getPositions(); + // uint32 maxSlippage; + // for (uint256 i; i < 3; i++) { + // ERC4626 position = positions[i]; + + // assertEq(address(position), address(newPositions[i])); + // (, maxSlippage, ) = cellar.getPositionData(position); + // assertEq(maxSlippage, 1_00); + // } + // } + + // function testFailSetUntrustedPosition() external { + // MockERC20 XYZ = new MockERC20("XYZ", 18); + // MockERC4626 xyzCLR = new MockERC4626(ERC20(address(XYZ)), "XYZ Cellar LP Token", "XYZ-CLR", 18); + + // (bool isTrusted, , ) = cellar.getPositionData(xyzCLR); + // assertFalse(isTrusted); + + // ERC4626[] memory positions = new ERC4626[](4); + // positions[0] = ERC4626(address(wethCLR)); + // positions[1] = ERC4626(address(usdcCLR)); + // positions[2] = ERC4626(address(wbtcCLR)); + // positions[3] = ERC4626(address(xyzCLR)); + + // // Test attempting to setting with an untrusted position. + // cellar.setPositions(positions); + // } + + // function testFailAddingUntrustedPosition() external { + // MockERC20 XYZ = new MockERC20("XYZ", 18); + // MockERC4626 xyzCLR = new MockERC4626(ERC20(address(XYZ)), "XYZ Cellar LP Token", "XYZ-CLR", 18); -// // Test that newly trusted position can now be added. -// cellar.addPosition(xyzCLR); + // (bool isTrusted, , ) = cellar.getPositionData(xyzCLR); + // assertFalse(isTrusted); -// ERC4626[] memory positions = cellar.getPositions(); -// assertEq(address(positions[positions.length - 1]), address(xyzCLR)); -// } + // // Test attempting to add untrusted position. + // cellar.addPosition(xyzCLR); + // } -// function testDistrustingAndRemovingPosition() external { -// ERC4626 distrustedPosition = wethCLR; + // function testTrustingPosition() external { + // MockERC20 XYZ = new MockERC20("XYZ", 18); + // MockERC4626 xyzCLR = new MockERC4626(ERC20(address(XYZ)), "XYZ Cellar LP Token", "XYZ-CLR", 18); -// // Deposit assets into position before distrusting. -// uint256 assets = swapRouter.convert(address(USDC), address(WETH), 100e6); -// WETH.mint(address(this), assets); -// WETH.approve(address(cellar), assets); -// cellar.depositIntoPosition(distrustedPosition, assets, address(this)); + // (bool isTrusted, , ) = cellar.getPositionData(xyzCLR); + // assertFalse(isTrusted); -// // Simulate position gaining yield. -// MockERC4626(address(distrustedPosition)).simulateGain(assets / 2, address(cellar)); + // // Test that position is trusted. + // cellar.setTrust(xyzCLR, true); -// (, , uint112 balance) = cellar.getPositionData(distrustedPosition); -// assertEq(balance, assets); -// assertEq(cellar.totalBalance(), 100e6); -// assertEq(cellar.totalAssets(), 100e6); -// assertEq(cellar.totalHoldings(), 0); + // (isTrusted, , ) = cellar.getPositionData(xyzCLR); + // assertTrue(isTrusted); -// // Distrust and removing position. -// cellar.setTrust(distrustedPosition, false); + // // Test that newly trusted position can now be added. + // cellar.addPosition(xyzCLR); -// // Test that assets have been pulled from untrusted position and state has updated accordingly. -// (, , balance) = cellar.getPositionData(distrustedPosition); -// assertEq(balance, 0); -// // Expected 142.5 assets to be received after swapping 150 assets with simulated 5% slippage. -// assertEq(cellar.totalBalance(), 0); -// assertEq(cellar.totalAssets(), 142.5e6); -// assertEq(cellar.totalHoldings(), 142.5e6); + // ERC4626[] memory positions = cellar.getPositions(); + // assertEq(address(positions[positions.length - 1]), address(xyzCLR)); + // } -// // Test that position has been distrusted. -// (bool isTrusted, , ) = cellar.getPositionData(distrustedPosition); -// assertFalse(isTrusted); + // function testDistrustingAndRemovingPosition() external { + // ERC4626 distrustedPosition = wethCLR; -// // Test that position has been removed from list of positions. -// ERC4626[] memory expectedPositions = new ERC4626[](2); -// expectedPositions[0] = ERC4626(address(usdcCLR)); -// expectedPositions[1] = ERC4626(address(wbtcCLR)); + // // Deposit assets into position before distrusting. + // uint256 assets = exchange.convert(address(USDC), address(WETH), 100e6); + // WETH.mint(address(this), assets); + // WETH.approve(address(cellar), assets); + // cellar.depositIntoPosition(distrustedPosition, assets, address(this)); -// ERC4626[] memory positions = cellar.getPositions(); -// for (uint256 i; i < positions.length; i++) assertTrue(positions[i] == expectedPositions[i]); -// } + // // Simulate position gaining yield. + // MockERC4626(address(distrustedPosition)).simulateGain(assets / 2, address(cellar)); -// // ============================================== SWEEP TEST ============================================== + // (, , uint112 balance) = cellar.getPositionData(distrustedPosition); + // assertEq(balance, assets); + // assertEq(cellar.totalBalance(), 100e6); + // assertEq(cellar.totalAssets(), 100e6); + // assertEq(cellar.totalHoldings(), 0); -// function testSweep() external { -// MockERC20 XYZ = new MockERC20("XYZ", 18); -// XYZ.mint(address(cellar), 100e18); + // // Distrust and removing position. + // cellar.setTrust(distrustedPosition, false); -// // Test sweep. -// cellar.sweep(address(XYZ), 100e18, address(this)); + // // Test that assets have been pulled from untrusted position and state has updated accordingly. + // (, , balance) = cellar.getPositionData(distrustedPosition); + // assertEq(balance, 0); + // // Expected 142.5 assets to be received after swapping 150 assets with simulated 5% slippage. + // assertEq(cellar.totalBalance(), 0); + // assertEq(cellar.totalAssets(), 142.5e6); + // assertEq(cellar.totalHoldings(), 142.5e6); -// assertEq(XYZ.balanceOf(address(this)), 100e18); -// } + // // Test that position has been distrusted. + // (bool isTrusted, , ) = cellar.getPositionData(distrustedPosition); + // assertFalse(isTrusted); -// function testFailSweep() external { -// wbtcCLR.mint(address(cellar), 100e18); + // // Test that position has been removed from list of positions. + // ERC4626[] memory expectedPositions = new ERC4626[](2); + // expectedPositions[0] = ERC4626(address(usdcCLR)); + // expectedPositions[1] = ERC4626(address(wbtcCLR)); -// // Test sweep of protected asset. -// cellar.sweep(address(wbtcCLR), 100e18, address(this)); -// } + // ERC4626[] memory positions = cellar.getPositions(); + // for (uint256 i; i < positions.length; i++) assertTrue(positions[i] == expectedPositions[i]); + // } -// function testFailAttemptingToStealFundsByRemovingPositionThenSweeping() external { -// // Deposit assets into position before distrusting. -// uint256 assets = swapRouter.convert(address(USDC), address(WBTC), 100e6); -// WBTC.mint(address(this), assets); -// WBTC.approve(address(cellar), assets); -// cellar.depositIntoPosition(wbtcCLR, assets, address(this)); + // // ============================================== SWEEP TEST ============================================== -// // Simulate position gaining yield. -// MockERC4626(address(wbtcCLR)).simulateGain(assets / 2, address(cellar)); + // function testSweep() external { + // MockERC20 XYZ = new MockERC20("XYZ", 18); + // XYZ.mint(address(cellar), 100e18); -// uint256 totalAssets = assets + assets / 2; -// assertEq(wbtcCLR.balanceOf(address(cellar)), totalAssets); + // // Test sweep. + // cellar.sweep(address(XYZ), 100e18, address(this)); -// // Remove position. -// cellar.removePosition(wbtcCLR); + // assertEq(XYZ.balanceOf(address(this)), 100e18); + // } -// assertEq(wbtcCLR.balanceOf(address(cellar)), 0); + // function testFailSweep() external { + // wbtcCLR.mint(address(cellar), 100e18); -// // Test attempting to steal assets after removing position from list. -// cellar.sweep(address(wbtcCLR), totalAssets, address(this)); -// } + // // Test sweep of protected asset. + // cellar.sweep(address(wbtcCLR), 100e18, address(this)); + // } -// // ============================================= EMERGENCY TEST ============================================= + // function testFailAttemptingToStealFundsByRemovingPositionThenSweeping() external { + // // Deposit assets into position before distrusting. + // uint256 assets = exchange.convert(address(USDC), address(WBTC), 100e6); + // WBTC.mint(address(this), assets); + // WBTC.approve(address(cellar), assets); + // cellar.depositIntoPosition(wbtcCLR, assets, address(this)); -// function testFailShutdownDeposit() external { -// cellar.setShutdown(true, false); + // // Simulate position gaining yield. + // MockERC4626(address(wbtcCLR)).simulateGain(assets / 2, address(cellar)); -// USDC.mint(address(this), 1); -// USDC.approve(address(cellar), 1); -// cellar.deposit(1, address(this)); -// } + // uint256 totalAssets = assets + assets / 2; + // assertEq(wbtcCLR.balanceOf(address(cellar)), totalAssets); -// function testFailShutdownDepositIntoPosition() external { -// USDC.mint(address(this), 1e18); -// USDC.approve(address(cellar), 1e18); -// cellar.deposit(1e18, address(this)); + // // Remove position. + // cellar.removePosition(wbtcCLR); -// cellar.setShutdown(true, false); + // assertEq(wbtcCLR.balanceOf(address(cellar)), 0); -// address[] memory path = new address[](2); -// path[0] = address(USDC); -// path[1] = address(USDC); + // // Test attempting to steal assets after removing position from list. + // cellar.sweep(address(wbtcCLR), totalAssets, address(this)); + // } -// cellar.rebalance(cellar, usdcCLR, 1e18, 0, path); -// } + // // ============================================= EMERGENCY TEST ============================================= -// function testShutdownExitsAllPositions() external { -// // Deposit 100 assets into each position with 50 assets of unrealized yield. -// ERC4626[] memory positions = cellar.getPositions(); -// for (uint256 i; i < positions.length; i++) { -// ERC4626 position = positions[i]; -// MockERC20 positionAsset = MockERC20(address(position.asset())); + // function testFailShutdownDeposit() external { + // cellar.setShutdown(true, false); -// uint256 assets = swapRouter.convert(address(USDC), address(positionAsset), 100e6); -// positionAsset.mint(address(this), assets); -// positionAsset.approve(address(cellar), assets); -// cellar.depositIntoPosition(position, assets, address(this)); + // USDC.mint(address(this), 1); + // USDC.approve(address(cellar), 1); + // cellar.deposit(1, address(this)); + // } -// MockERC4626(address(position)).simulateGain(assets / 2, address(cellar)); -// } + // function testFailShutdownDepositIntoPosition() external { + // USDC.mint(address(this), 1e18); + // USDC.approve(address(cellar), 1e18); + // cellar.deposit(1e18, address(this)); -// assertApproxEqAbs(cellar.totalBalance(), 300e6, 1e6); -// assertApproxEqAbs(cellar.totalAssets(), 300e6, 1e6); -// assertEq(cellar.totalHoldings(), 0); + // cellar.setShutdown(true, false); -// cellar.setShutdown(true, true); + // address[] memory path = new address[](2); + // path[0] = address(USDC); + // path[1] = address(USDC); -// assertTrue(cellar.isShutdown()); -// assertEq(cellar.totalBalance(), 0); -// // Expect to receive 435 assets after 450 total assets from positions are swapped with 5% slippage. -// assertApproxEqAbs(cellar.totalAssets(), 435e6, 2e6); -// assertApproxEqAbs(cellar.totalHoldings(), 435e6, 2e6); -// } + // cellar.rebalance(cellar, usdcCLR, 1e18, 0, path); + // } -// function testShutdownExitsAllPositionsWithNoBalances() external { -// cellar.setShutdown(true, true); + // function testShutdownExitsAllPositions() external { + // // Deposit 100 assets into each position with 50 assets of unrealized yield. + // ERC4626[] memory positions = cellar.getPositions(); + // for (uint256 i; i < positions.length; i++) { + // ERC4626 position = positions[i]; + // MockERC20 positionAsset = MockERC20(address(position.asset())); -// assertTrue(cellar.isShutdown()); -// } + // uint256 assets = exchange.convert(address(USDC), address(positionAsset), 100e6); + // positionAsset.mint(address(this), assets); + // positionAsset.approve(address(cellar), assets); + // cellar.depositIntoPosition(position, assets, address(this)); -// // ============================================== LIMITS TEST ============================================== + // MockERC4626(address(position)).simulateGain(assets / 2, address(cellar)); + // } -// function testLimits() external { -// USDC.mint(address(this), 100e6); -// USDC.approve(address(cellar), 100e6); -// cellar.deposit(100e6, address(this)); + // assertApproxEqAbs(cellar.totalBalance(), 300e6, 1e6); + // assertApproxEqAbs(cellar.totalAssets(), 300e6, 1e6); + // assertEq(cellar.totalHoldings(), 0); -// assertEq(cellar.maxDeposit(address(this)), type(uint256).max); -// assertEq(cellar.maxMint(address(this)), type(uint256).max); + // cellar.setShutdown(true, true); -// cellar.setDepositLimit(200e6); -// cellar.setLiquidityLimit(100e6); + // assertTrue(cellar.isShutdown()); + // assertEq(cellar.totalBalance(), 0); + // // Expect to receive 435 assets after 450 total assets from positions are swapped with 5% slippage. + // assertApproxEqAbs(cellar.totalAssets(), 435e6, 2e6); + // assertApproxEqAbs(cellar.totalHoldings(), 435e6, 2e6); + // } -// assertEq(cellar.depositLimit(), 200e6); -// assertEq(cellar.liquidityLimit(), 100e6); -// assertEq(cellar.maxDeposit(address(this)), 0); -// assertEq(cellar.maxMint(address(this)), 0); + // function testShutdownExitsAllPositionsWithNoBalances() external { + // cellar.setShutdown(true, true); -// cellar.setLiquidityLimit(300e6); + // assertTrue(cellar.isShutdown()); + // } -// assertEq(cellar.depositLimit(), 200e6); -// assertEq(cellar.liquidityLimit(), 300e6); -// assertEq(cellar.maxDeposit(address(this)), 100e6); -// assertEq(cellar.maxMint(address(this)), 100e6); + // // ============================================== LIMITS TEST ============================================== -// cellar.setShutdown(true, false); + // function testLimits() external { + // USDC.mint(address(this), 100e6); + // USDC.approve(address(cellar), 100e6); + // cellar.deposit(100e6, address(this)); -// assertEq(cellar.maxDeposit(address(this)), 0); -// assertEq(cellar.maxMint(address(this)), 0); -// } + // assertEq(cellar.maxDeposit(address(this)), type(uint256).max); + // assertEq(cellar.maxMint(address(this)), type(uint256).max); -// function testFailDepositAboveDepositLimit() external { -// cellar.setDepositLimit(100e6); + // cellar.setDepositLimit(200e6); + // cellar.setLiquidityLimit(100e6); -// USDC.mint(address(this), 101e6); -// USDC.approve(address(cellar), 101e6); -// cellar.deposit(101e6, address(this)); -// } + // assertEq(cellar.depositLimit(), 200e6); + // assertEq(cellar.liquidityLimit(), 100e6); + // assertEq(cellar.maxDeposit(address(this)), 0); + // assertEq(cellar.maxMint(address(this)), 0); -// function testFailMintAboveDepositLimit() external { -// cellar.setDepositLimit(100e6); + // cellar.setLiquidityLimit(300e6); -// USDC.mint(address(this), 101e6); -// USDC.approve(address(cellar), 101e6); -// cellar.mint(101e6, address(this)); -// } + // assertEq(cellar.depositLimit(), 200e6); + // assertEq(cellar.liquidityLimit(), 300e6); + // assertEq(cellar.maxDeposit(address(this)), 100e6); + // assertEq(cellar.maxMint(address(this)), 100e6); -// function testFailDepositAboveLiquidityLimit() external { -// cellar.setLiquidityLimit(100e6); + // cellar.setShutdown(true, false); -// USDC.mint(address(this), 101e6); -// USDC.approve(address(cellar), 101e6); -// cellar.deposit(101e6, address(this)); -// } + // assertEq(cellar.maxDeposit(address(this)), 0); + // assertEq(cellar.maxMint(address(this)), 0); + // } -// function testFailMintAboveLiquidityLimit() external { -// cellar.setLiquidityLimit(100e6); + // function testFailDepositAboveDepositLimit() external { + // cellar.setDepositLimit(100e6); -// USDC.mint(address(this), 101e6); -// USDC.approve(address(cellar), 101e6); -// cellar.mint(101e6, address(this)); -// } -// } + // USDC.mint(address(this), 101e6); + // USDC.approve(address(cellar), 101e6); + // cellar.deposit(101e6, address(this)); + // } + + // function testFailMintAboveDepositLimit() external { + // cellar.setDepositLimit(100e6); + + // USDC.mint(address(this), 101e6); + // USDC.approve(address(cellar), 101e6); + // cellar.mint(101e6, address(this)); + // } + + // function testFailDepositAboveLiquidityLimit() external { + // cellar.setLiquidityLimit(100e6); + + // USDC.mint(address(this), 101e6); + // USDC.approve(address(cellar), 101e6); + // cellar.deposit(101e6, address(this)); + // } + + // function testFailMintAboveLiquidityLimit() external { + // cellar.setLiquidityLimit(100e6); + + // USDC.mint(address(this), 101e6); + // USDC.approve(address(cellar), 101e6); + // cellar.mint(101e6, address(this)); + // } +} From a58428107617c14ae3fb4e5a6600020eb1356503 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Fri, 17 Jun 2022 12:53:16 -0700 Subject: [PATCH 06/49] feat(Cellar): add ability to handle assets of different decimals --- src/base/Cellar.sol | 116 +++++++++++++++++++++++++++++++++++--------- test/Cellar.t.sol | 13 +++-- 2 files changed, 98 insertions(+), 31 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index c5ac6707..5b076ca0 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -541,6 +541,98 @@ contract Cellar is ERC4626, Ownable, Multicall { return asset.balanceOf(address(this)); } + /** + * @notice The amount of assets that the cellar would exchange for the amount of shares provided. + * @param shares amount of shares to convert + * @return assets the shares can be exchanged for + */ + function convertToAssets(uint256 shares) public view override returns (uint256 assets) { + uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + uint8 assetDecimals = asset.decimals(); + uint256 totalAssetsNormalized = totalAssets().changeDecimals(assetDecimals, 18); + + assets = totalShares == 0 ? shares : shares.mulDivDown(totalAssetsNormalized, totalShares); + assets = assets.changeDecimals(18, assetDecimals); + } + + /** + * @notice The amount of shares that the cellar would exchange for the amount of assets provided. + * @param assets amount of assets to convert + * @return shares the assets can be exchanged for + */ + function convertToShares(uint256 assets) public view override returns (uint256 shares) { + uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + uint8 assetDecimals = asset.decimals(); + uint256 assetsNormalized = assets.changeDecimals(assetDecimals, 18); + uint256 totalAssetsNormalized = totalAssets().changeDecimals(assetDecimals, 18); + + shares = totalShares == 0 ? assetsNormalized : assetsNormalized.mulDivDown(totalShares, totalAssetsNormalized); + } + + /** + * @notice Simulate the effects of minting shares at the current block, given current on-chain conditions. + * @param shares amount of shares to mint + * @return assets that will be deposited + */ + function previewMint(uint256 shares) public view override returns (uint256 assets) { + uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + uint8 assetDecimals = asset.decimals(); + uint256 totalAssetsNormalized = totalAssets().changeDecimals(assetDecimals, 18); + + assets = totalShares == 0 ? shares : shares.mulDivUp(totalAssetsNormalized, totalShares); + assets = assets.changeDecimals(18, assetDecimals); + } + + /** + * @notice Simulate the effects of withdrawing assets at the current block, given current on-chain conditions. + * @param assets amount of assets to withdraw + * @return shares that will be redeemed + */ + function previewWithdraw(uint256 assets) public view override returns (uint256 shares) { + uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + uint8 assetDecimals = asset.decimals(); + uint256 assetsNormalized = assets.changeDecimals(assetDecimals, 18); + uint256 totalAssetsNormalized = totalAssets().changeDecimals(assetDecimals, 18); + + shares = totalShares == 0 ? assetsNormalized : assetsNormalized.mulDivUp(totalShares, totalAssetsNormalized); + } + + function _convertToAssets(uint256 shares, uint256 _totalAssets) internal view returns (uint256 assets) { + uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + uint8 assetDecimals = asset.decimals(); + uint256 totalAssetsNormalized = _totalAssets.changeDecimals(assetDecimals, 18); + + assets = totalShares == 0 ? shares : shares.mulDivDown(totalAssetsNormalized, totalShares); + assets = assets.changeDecimals(18, assetDecimals); + } + + function _convertToShares(uint256 assets, uint256 _totalAssets) internal view returns (uint256 shares) { + uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + uint8 assetDecimals = asset.decimals(); + uint256 assetsNormalized = assets.changeDecimals(assetDecimals, 18); + uint256 totalAssetsNormalized = _totalAssets.changeDecimals(assetDecimals, 18); + + shares = totalShares == 0 ? assetsNormalized : assetsNormalized.mulDivDown(totalShares, totalAssetsNormalized); + } + + function _previewMint(uint256 shares, uint256 _totalAssets) internal view returns (uint256 assets) { + uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + uint8 assetDecimals = asset.decimals(); + uint256 totalAssetsNormalized = _totalAssets.changeDecimals(assetDecimals, 18); + + assets = totalShares == 0 ? shares : shares.mulDivUp(totalAssetsNormalized, totalShares); + assets = assets.changeDecimals(18, assetDecimals); + } + + function _previewWithdraw(uint256 assets, uint256 _totalAssets) internal view returns (uint256 shares) { + uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. + uint8 assetDecimals = asset.decimals(); + uint256 assetsNormalized = assets.changeDecimals(assetDecimals, 18); + uint256 totalAssetsNormalized = _totalAssets.changeDecimals(assetDecimals, 18); + + shares = totalShares == 0 ? assetsNormalized : assetsNormalized.mulDivUp(totalShares, totalAssetsNormalized); + } + // =========================================== ACCRUAL LOGIC =========================================== /** @@ -851,30 +943,6 @@ contract Cellar is ERC4626, Ownable, Multicall { _totalAssets = _totalHoldings + registry.priceRouter().getValue(positionAssets, positionBalances, asset); } - function _convertToShares(uint256 assets, uint256 _totalAssets) internal view returns (uint256) { - uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. - - return supply == 0 ? assets : assets.mulDivDown(supply, _totalAssets); - } - - function _convertToAssets(uint256 shares, uint256 _totalAssets) internal view returns (uint256) { - uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. - - return supply == 0 ? shares : shares.mulDivDown(_totalAssets, supply); - } - - function _previewMint(uint256 shares, uint256 _totalAssets) internal view returns (uint256) { - uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. - - return supply == 0 ? shares : shares.mulDivUp(_totalAssets, supply); - } - - function _previewWithdraw(uint256 assets, uint256 _totalAssets) internal view returns (uint256) { - uint256 supply = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. - - return supply == 0 ? assets : assets.mulDivUp(supply, _totalAssets); - } - function _swapExactAssets( ERC20 assetIn, uint256 amountIn, diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 35d0a3f7..3a9166ec 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -101,9 +101,8 @@ contract CellarTest is Test { // ========================================= DEPOSIT/WITHDRAW TEST ========================================= function testDepositWithdraw() external { - // assets = bound(assets, 1, cellar.maxDeposit(address(this))); // NOTE: last time this was run, all test pass with the line below uncommented - // assets = bound(assets, 1, type(uint128).max); + // assets = bound(assets, 1, type(uint72).max); uint256 assets = 100e18; // Test single deposit. @@ -111,7 +110,7 @@ contract CellarTest is Test { USDC.approve(address(cellar), assets); uint256 shares = cellar.deposit(assets, address(this)); - assertEq(shares, assets); // Expect exchange rate to be 1:1 on initial deposit. + assertEq(shares, assets.changeDecimals(6, 18)); // Expect exchange rate to be 1:1 on initial deposit. assertEq(cellar.previewWithdraw(assets), shares); assertEq(cellar.previewDeposit(assets), shares); assertEq(cellar.totalHoldings(), assets); @@ -149,12 +148,12 @@ contract CellarTest is Test { } function testFailRedeemWithNotEnoughBalance(uint256 assets) external { - USDC.mint(address(this), assets / 2); - USDC.approve(address(cellar), assets / 2); + USDC.mint(address(this), assets); + USDC.approve(address(cellar), assets); - cellar.deposit(assets / 2, address(this)); + uint256 shares = cellar.deposit(assets, address(this)); - cellar.redeem(assets, address(this), address(this)); + cellar.redeem(shares * 2, address(this), address(this)); } function testFailWithdrawWithNoBalance(uint256 assets) external { From ab301e35e65c017c7d5a549e50774747cef57140 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Sun, 19 Jun 2022 19:31:48 -0700 Subject: [PATCH 07/49] build: add chainlink dependency --- .gitmodules | 3 +++ lib/chainlink | 1 + remappings.txt | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) create mode 160000 lib/chainlink diff --git a/.gitmodules b/.gitmodules index f4cb72bd..cee553a6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/openzeppelin-contracts"] path = lib/openzeppelin-contracts url = https://github.com/OpenZeppelin/openzeppelin-contracts +[submodule "lib/chainlink"] + path = lib/chainlink + url = https://github.com/smartcontractkit/chainlink diff --git a/lib/chainlink b/lib/chainlink new file mode 160000 index 00000000..8843bef9 --- /dev/null +++ b/lib/chainlink @@ -0,0 +1 @@ +Subproject commit 8843bef969cb8728820d3d3ccd9891a85f97603c diff --git a/remappings.txt b/remappings.txt index db49e56d..e1383bdc 100644 --- a/remappings.txt +++ b/remappings.txt @@ -3,4 +3,5 @@ @ds-test/=lib/forge-std/lib/ds-test/src/ @openzeppelin/=lib/openzeppelin-contracts/ @uniswap/v3-periphery/=lib/v3-periphery/ -@uniswap/v3-core/=lib/v3-core/ \ No newline at end of file +@uniswap/v3-core/=lib/v3-core/ +@chainlink/=lib/chainlink/ \ No newline at end of file From 8e3d393b2a49bf1716e7a45a7e70476ab168dcfb Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Sun, 19 Jun 2022 22:38:16 -0700 Subject: [PATCH 08/49] feat(PriceRouter): add price router --- src/Registry.sol | 17 +- src/mocks/MockPriceRouter.sol | 33 -- src/modules/PriceRouter.sol | 112 +++++ test/Cellar.t.sol | 789 ++-------------------------------- test/PriceRouter.t.sol | 37 ++ 5 files changed, 181 insertions(+), 807 deletions(-) delete mode 100644 src/mocks/MockPriceRouter.sol create mode 100644 src/modules/PriceRouter.sol create mode 100644 test/PriceRouter.t.sol diff --git a/src/Registry.sol b/src/Registry.sol index 1ff93972..650defb1 100644 --- a/src/Registry.sol +++ b/src/Registry.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.13; import { ERC20 } from "@solmate/tokens/ERC20.sol"; import { SwapRouter } from "./modules/SwapRouter.sol"; +import { PriceRouter } from "./modules/PriceRouter.sol"; import { IGravity } from "./interfaces/IGravity.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; @@ -10,22 +11,6 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; // TODO: add natspec // TODO: add events -interface PriceRouter { - function getValue( - ERC20[] memory baseAssets, - uint256[] memory amounts, - ERC20 quoteAsset - ) external view returns (uint256); - - function getValue( - ERC20 baseAssets, - uint256 amounts, - ERC20 quoteAsset - ) external view returns (uint256); - - function getExchangeRate(ERC20 baseAsset, ERC20 quoteAsset) external view returns (uint256); -} - contract Registry is Ownable { SwapRouter public swapRouter; PriceRouter public priceRouter; diff --git a/src/mocks/MockPriceRouter.sol b/src/mocks/MockPriceRouter.sol deleted file mode 100644 index 9e98b3ca..00000000 --- a/src/mocks/MockPriceRouter.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.13; - -import { ERC20 } from "@solmate/tokens/ERC20.sol"; -import { MockExchange } from "./MockExchange.sol"; - -contract MockPriceRouter { - MockExchange public exchange; - - constructor(MockExchange _exchange) { - exchange = _exchange; - } - - function getValue( - ERC20[] memory baseAssets, - uint256[] memory amounts, - ERC20 quoteAsset - ) external view returns (uint256 value) { - for (uint256 i; i < baseAssets.length; i++) value += getValue(baseAssets[i], amounts[i], quoteAsset); - } - - function getValue( - ERC20 baseAsset, - uint256 amounts, - ERC20 quoteAsset - ) public view returns (uint256) { - return exchange.convert(address(baseAsset), address(quoteAsset), amounts); - } - - function getExchangeRate(ERC20 baseAsset, ERC20 quoteAsset) external view returns (uint256) { - return exchange.getExchangeRate(address(baseAsset), address(quoteAsset)); - } -} diff --git a/src/modules/PriceRouter.sol b/src/modules/PriceRouter.sol new file mode 100644 index 00000000..0c0a5715 --- /dev/null +++ b/src/modules/PriceRouter.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.13; + +import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { Registry } from "../Registry.sol"; +import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; +import { FeedRegistryInterface } from "@chainlink/contracts/src/v0.8/interfaces/FeedRegistryInterface.sol"; +import { Denominations } from "@chainlink/contracts/src/v0.8/Denominations.sol"; +import { Math } from "src/utils/Math.sol"; + +contract PriceRouter is Ownable { + using Math for uint256; + + FeedRegistryInterface public feedRegistry = FeedRegistryInterface(0x47Fb2585D2C56Fe188D0E6ec628a38b74fCeeeDf); + + // =========================================== ASSET CONFIG =========================================== + + mapping(ERC20 => address) public baseAssetOverride; + mapping(ERC20 => address) public quoteAssetOverride; + mapping(ERC20 => bool) private _isSupportedQuoteAsset; + + function isSupportedQuoteAsset(ERC20 quoteAsset) public view returns (bool) { + return _isSupportedQuoteAsset[quoteAsset] || _isSupportedQuoteAsset[ERC20(baseAssetOverride[quoteAsset])]; + } + + function setAssetOverride(ERC20 asset, address _override) external onlyOwner { + baseAssetOverride[asset] = _override; + } + + function setIsSupportedQuoteAsset(ERC20 asset, bool isSupported) external onlyOwner { + _isSupportedQuoteAsset[asset] = isSupported; + } + + // TODO: transfer ownership to the gravity contract + constructor() Ownable() { + _isSupportedQuoteAsset[ERC20(Denominations.ETH)] = true; + _isSupportedQuoteAsset[ERC20(Denominations.USD)] = true; + + ERC20 WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + baseAssetOverride[WETH] = Denominations.ETH; + quoteAssetOverride[WETH] = Denominations.ETH; + _isSupportedQuoteAsset[WETH] = true; + + ERC20 USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + quoteAssetOverride[USDC] = Denominations.USD; + _isSupportedQuoteAsset[USDC] = true; + + ERC20 WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + baseAssetOverride[WBTC] = Denominations.BTC; + quoteAssetOverride[WBTC] = Denominations.BTC; + } + + // =========================================== PRICING LOGIC =========================================== + + function getValues( + ERC20[] memory baseAssets, + uint256[] memory amounts, + ERC20 quoteAsset + ) external view returns (uint256 value) { + uint8 quoteAssetDecimals = quoteAsset.decimals(); + for (uint256 i; i < baseAssets.length; i++) + value += _getValue(baseAssets[i], amounts[i], quoteAsset, quoteAssetDecimals); + } + + function getValue( + ERC20 baseAsset, + uint256 amounts, + ERC20 quoteAsset + ) public view returns (uint256 value) { + value = _getValue(baseAsset, amounts, quoteAsset, quoteAsset.decimals()); + } + + function _getValue( + ERC20 baseAsset, + uint256 amounts, + ERC20 quoteAsset, + uint8 quoteAssetDecimals + ) internal view returns (uint256 value) { + value = amounts.mulWadDown(getExchangeRate(baseAsset, quoteAsset)).changeDecimals( + baseAsset.decimals(), + quoteAssetDecimals + ); + } + + function getExchangeRate(ERC20 baseAsset, ERC20 quoteAsset) public view returns (uint256 exchangeRate) { + address baseOverride = baseAssetOverride[baseAsset]; + address base = baseOverride == address(0) ? address(baseAsset) : baseOverride; + + address quoteOverride = quoteAssetOverride[quoteAsset]; + address quote = quoteOverride == address(0) ? address(quoteAsset) : quoteOverride; + + if (base == quote) return 1e18; + + exchangeRate = isSupportedQuoteAsset(quoteAsset) + ? _getExchangeRate(base, quote) + : _getExchangeRateInETH(base).mulDivDown(1e18, _getExchangeRateInETH(quote)); + } + + function _getExchangeRate(address base, address quote) internal view returns (uint256 exchangeRate) { + (, int256 price, , , ) = feedRegistry.latestRoundData(base, quote); + + exchangeRate = uint256(price).changeDecimals(feedRegistry.decimals(base, quote), 18); + } + + function _getExchangeRateInETH(address base) internal view returns (uint256 exchangeRate) { + if (base == Denominations.ETH) return 1e18; + + (, int256 price, , , ) = feedRegistry.latestRoundData(base, Denominations.ETH); + + exchangeRate = uint256(price); + } +} diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 3a9166ec..f0cdf4c9 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -2,78 +2,50 @@ pragma solidity 0.8.13; import { Cellar, ERC4626, ERC20 } from "src/base/Cellar.sol"; -import { Registry, PriceRouter, IGravity } from "src/Registry.sol"; -import { SwapRouter, IUniswapV2Router, IUniswapV3Router } from "src/modules/SwapRouter.sol"; -import { MockPriceRouter } from "src/mocks/MockPriceRouter.sol"; -import { MockERC20 } from "src/mocks/MockERC20.sol"; +import { Registry, PriceRouter, SwapRouter, IGravity } from "src/Registry.sol"; +import { IUniswapV2Router, IUniswapV3Router } from "src/modules/SwapRouter.sol"; +import { PriceRouter } from "src/modules/PriceRouter.sol"; import { MockERC4626 } from "src/mocks/MockERC4626.sol"; import { MockGravity } from "src/mocks/MockGravity.sol"; -import { MockExchange } from "src/mocks/MockExchange.sol"; import { Test } from "@forge-std/Test.sol"; import { Math } from "src/utils/Math.sol"; -// TODO: test with fuzzing - contract CellarTest is Test { using Math for uint256; Cellar private cellar; MockGravity private gravity; - MockExchange private exchange; IUniswapV2Router private constant uniswapV2Router = IUniswapV2Router(0xE592427A0AEce92De3Edee1F18E0157C05861564); IUniswapV3Router private constant uniswapV3Router = IUniswapV3Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); - SwapRouter private swapRouter; PriceRouter private priceRouter; + SwapRouter private swapRouter; Registry private registry; - MockERC20 private USDC; + ERC20 private USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); MockERC4626 private usdcCLR; - MockERC20 private WETH; + ERC20 private WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); MockERC4626 private wethCLR; - MockERC20 private WBTC; + ERC20 private WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); MockERC4626 private wbtcCLR; function setUp() external { - exchange = new MockExchange(); - vm.label(address(exchange), "exchange"); - - USDC = new MockERC20("USDC", 6); - vm.label(address(USDC), "USDC"); - usdcCLR = new MockERC4626(ERC20(address(USDC)), "USDC Cellar LP Token", "USDC-CLR", 6); + usdcCLR = new MockERC4626(USDC, "USDC Cellar LP Token", "USDC-CLR", 6); vm.label(address(usdcCLR), "usdcCLR"); - WETH = new MockERC20("WETH", 18); - vm.label(address(WETH), "WETH"); - wethCLR = new MockERC4626(ERC20(address(WETH)), "WETH Cellar LP Token", "WETH-CLR", 18); + wethCLR = new MockERC4626(WETH, "WETH Cellar LP Token", "WETH-CLR", 18); vm.label(address(wethCLR), "wethCLR"); - WBTC = new MockERC20("WBTC", 8); - vm.label(address(WBTC), "WBTC"); - wbtcCLR = new MockERC4626(ERC20(address(WBTC)), "WBTC Cellar LP Token", "WBTC-CLR", 8); + wbtcCLR = new MockERC4626(WBTC, "WBTC Cellar LP Token", "WBTC-CLR", 8); vm.label(address(wbtcCLR), "wbtcCLR"); - // Setup exchange rates: - exchange.setExchangeRate(address(USDC), address(USDC), 1e6); - exchange.setExchangeRate(address(WETH), address(WETH), 1e18); - exchange.setExchangeRate(address(WBTC), address(WBTC), 1e8); - - exchange.setExchangeRate(address(USDC), address(WETH), 0.0005e18); - exchange.setExchangeRate(address(WETH), address(USDC), 2000e6); - - exchange.setExchangeRate(address(USDC), address(WBTC), 0.000033e8); - exchange.setExchangeRate(address(WBTC), address(USDC), 30_000e6); - - exchange.setExchangeRate(address(WETH), address(WBTC), 0.06666666e8); - exchange.setExchangeRate(address(WBTC), address(WETH), 15e18); - // Setup Registry and modules: swapRouter = new SwapRouter(uniswapV2Router, uniswapV3Router); - priceRouter = PriceRouter(address(new MockPriceRouter(exchange))); + priceRouter = new PriceRouter(); gravity = new MockGravity(); registry = new Registry(swapRouter, priceRouter, IGravity(address(gravity))); @@ -91,736 +63,37 @@ contract CellarTest is Test { vm.prank(address(registry.gravityBridge())); cellar.transferOwnership(address(this)); - // Mint enough liquidity to swap router for swaps. - for (uint256 i; i < positions.length; i++) { - MockERC20 asset = MockERC20(address(ERC4626(positions[i]).asset())); - asset.mint(address(exchange), type(uint112).max); - } + // Approve cellar to spend all assets. + USDC.approve(address(cellar), type(uint256).max); + WETH.approve(address(cellar), type(uint256).max); + WBTC.approve(address(cellar), type(uint256).max); } // ========================================= DEPOSIT/WITHDRAW TEST ========================================= - function testDepositWithdraw() external { - // NOTE: last time this was run, all test pass with the line below uncommented - // assets = bound(assets, 1, type(uint72).max); - uint256 assets = 100e18; + function testDepositAndWithdraw(uint256 assets) external { + assets = bound(assets, 1, type(uint72).max); + + deal(address(USDC), address(this), assets); // Test single deposit. - USDC.mint(address(this), assets); - USDC.approve(address(cellar), assets); uint256 shares = cellar.deposit(assets, address(this)); - assertEq(shares, assets.changeDecimals(6, 18)); // Expect exchange rate to be 1:1 on initial deposit. - assertEq(cellar.previewWithdraw(assets), shares); - assertEq(cellar.previewDeposit(assets), shares); - assertEq(cellar.totalHoldings(), assets); - assertEq(cellar.totalAssets(), assets); - assertEq(cellar.totalSupply(), shares); - assertEq(cellar.balanceOf(address(this)), shares); - assertEq(cellar.convertToAssets(cellar.balanceOf(address(this))), assets); - assertEq(USDC.balanceOf(address(this)), 0); + assertEq(shares, assets.changeDecimals(6, 18), "Should have 1:1 exchange rate for initial deposit."); + assertEq(cellar.previewWithdraw(assets), shares, "Withdrawing assets should burn shares given."); + assertEq(cellar.previewDeposit(assets), shares, "Depositing assets should mint shares given."); + assertEq(cellar.totalSupply(), shares, "Should have updated total supply with shares minted."); + assertEq(cellar.totalAssets(), assets, "Should have updated total assets with assets deposited."); + assertEq(cellar.balanceOf(address(this)), shares, "Should have updated user's share balance."); + assertEq(cellar.convertToAssets(cellar.balanceOf(address(this))), assets, "Should return all user's assets."); + assertEq(USDC.balanceOf(address(this)), 0, "Should have deposited assets from user."); // Test single withdraw. cellar.withdraw(assets, address(this), address(this)); - assertEq(cellar.totalHoldings(), 0); - assertEq(cellar.totalAssets(), 0); - assertEq(cellar.totalSupply(), 0); - assertEq(cellar.balanceOf(address(this)), 0); - assertEq(cellar.convertToAssets(cellar.balanceOf(address(this))), 0); - assertEq(USDC.balanceOf(address(this)), assets); - } - - function testFailDepositWithNotEnoughApproval(uint256 assets) external { - USDC.mint(address(this), assets / 2); - USDC.approve(address(cellar), assets / 2); - - cellar.deposit(assets, address(this)); + assertEq(cellar.totalAssets(), 0, "Should have updated total assets with assets withdrawn."); + assertEq(cellar.balanceOf(address(this)), 0, "Should have redeemed user's share balance."); + assertEq(cellar.convertToAssets(cellar.balanceOf(address(this))), 0, "Should return zero assets."); + assertEq(USDC.balanceOf(address(this)), assets, "Should have withdrawn assets to user."); } - - function testFailWithdrawWithNotEnoughBalance(uint256 assets) external { - USDC.mint(address(this), assets / 2); - USDC.approve(address(cellar), assets / 2); - - cellar.deposit(assets / 2, address(this)); - - cellar.withdraw(assets, address(this), address(this)); - } - - function testFailRedeemWithNotEnoughBalance(uint256 assets) external { - USDC.mint(address(this), assets); - USDC.approve(address(cellar), assets); - - uint256 shares = cellar.deposit(assets, address(this)); - - cellar.redeem(shares * 2, address(this), address(this)); - } - - function testFailWithdrawWithNoBalance(uint256 assets) external { - if (assets == 0) assets = 1; - cellar.withdraw(assets, address(this), address(this)); - } - - function testFailRedeemWithNoBalance(uint256 assets) external { - cellar.redeem(assets, address(this), address(this)); - } - - function testFailDepositWithNoApproval(uint256 assets) external { - cellar.deposit(assets, address(this)); - } - - // function testWithdrawWithoutEnoughHoldings() external { - // // Deposit assets directly into position. - // WETH.mint(address(this), 1e18); - // WETH.approve(address(cellar), 1e18); - // cellar.depositIntoPosition(wethCLR, 1e18, address(this)); // $2000 - - // WBTC.mint(address(this), 1e8); - // WBTC.approve(address(cellar), 1e8); - // cellar.depositIntoPosition(wbtcCLR, 1e8, address(this)); // $30,000 - - // assertEq(cellar.totalHoldings(), 0); - // assertEq(cellar.totalAssets(), 32_000e6); - - // // Test withdraw returns assets to receiver and replenishes holding position. - // cellar.withdraw(10e6, address(this), address(this)); - - // assertEq(USDC.balanceOf(address(this)), 10e6); - // // $1,600 = 5% of $32,000 (tolerate some assets loss due to swap slippage). - // assertApproxEqAbs(USDC.balanceOf(address(cellar)), 1600e6, 100e6); - // } - - // function testWithdrawAllWithHomogenousPositions() external { - // USDC.mint(address(this), 100e18); - // USDC.approve(address(cellar), 100e18); - // cellar.depositIntoPosition(usdcCLR, 100e18, address(this)); - - // assertEq(cellar.totalAssets(), 100e18); - - // cellar.withdraw(100e18, address(this), address(this)); - - // assertEq(USDC.balanceOf(address(this)), 100e18); - // } - - // // NOTE: Although this behavior is not desired, it should be anticipated that this will occur when - // // withdrawing from a cellar with positions that are not all in the same asset as the holding - // // position due to the swap slippage involved in needing to convert them all to single asset - // // received by the user. - // function testFailWithdrawAllWithHeterogenousPositions() external { - // USDC.mint(address(this), 100e6); - // USDC.approve(address(cellar), 100e6); - // cellar.depositIntoPosition(usdcCLR, 100e6, address(this)); // $100 - - // WETH.mint(address(this), 1e18); - // WETH.approve(address(cellar), 1e18); - // cellar.depositIntoPosition(wethCLR, 1e18, address(this)); // $2,000 - - // WBTC.mint(address(this), 1e8); - // WBTC.approve(address(cellar), 1e8); - // cellar.depositIntoPosition(wbtcCLR, 1e8, address(this)); // $30,000 - - // assertEq(cellar.totalAssets(), 32_100e6); - - // cellar.withdraw(32_100e6, address(this), address(this)); - // } - - // // =========================================== REBALANCE TEST =========================================== - - // function testRebalance() external { - // USDC.mint(address(this), 10_000e6); - // USDC.approve(address(cellar), 10_000e6); - // cellar.deposit(10_000e6, address(this)); - - // address[] memory path = new address[](2); - - // // Test rebalancing from holding position. - // path[0] = address(USDC); - // path[1] = address(USDC); - - // uint256 assetsRebalanced = cellar.rebalance(cellar, usdcCLR, 10_000e6, 10_000e6, path); - - // assertEq(assetsRebalanced, 10_000e6); - // assertEq(cellar.totalHoldings(), 0); - // assertEq(usdcCLR.balanceOf(address(cellar)), 10_000e6); - // (, , uint112 fromBalance) = cellar.getPositionData(usdcCLR); - // assertEq(fromBalance, 10_000e6); - - // // Test rebalancing between positions. - // path[0] = address(USDC); - // path[1] = address(WETH); - - // uint256 expectedAssetsOut = exchange.quote(10_000e6, path); - // assetsRebalanced = cellar.rebalance(usdcCLR, wethCLR, 10_000e6, expectedAssetsOut, path); - - // assertEq(assetsRebalanced, expectedAssetsOut); - // assertEq(usdcCLR.balanceOf(address(cellar)), 0); - // assertEq(wethCLR.balanceOf(address(cellar)), assetsRebalanced); - // (, , fromBalance) = cellar.getPositionData(usdcCLR); - // assertEq(fromBalance, 0); - // (, , uint112 toBalance) = cellar.getPositionData(wethCLR); - // assertEq(toBalance, assetsRebalanced); - - // // Test rebalancing back to holding position. - // path[0] = address(WETH); - // path[1] = address(USDC); - - // expectedAssetsOut = exchange.quote(assetsRebalanced, path); - // assetsRebalanced = cellar.rebalance(wethCLR, cellar, assetsRebalanced, expectedAssetsOut, path); - - // assertEq(assetsRebalanced, expectedAssetsOut); - // assertEq(wethCLR.balanceOf(address(cellar)), 0); - // assertEq(cellar.totalHoldings(), assetsRebalanced); - // (, , toBalance) = cellar.getPositionData(wethCLR); - // assertEq(toBalance, 0); - // } - - // function testFailRebalanceFromPositionWithNotEnoughBalance() external { - // uint256 assets = 100e18; - - // USDC.mint(address(this), assets / 2); - // USDC.approve(address(cellar), assets / 2); - - // cellar.depositIntoPosition(usdcCLR, assets / 2, address(this)); - - // address[] memory path = new address[](2); - // path[0] = address(USDC); - // path[1] = address(WBTC); - - // uint256 expectedAssetsOut = exchange.quote(assets, path); - // cellar.rebalance(usdcCLR, wbtcCLR, assets, expectedAssetsOut, path); - // } - - // function testFailRebalanceIntoUntrustedPosition() external { - // uint256 assets = 100e18; - - // ERC4626[] memory positions = cellar.getPositions(); - // ERC4626 untrustedPosition = positions[positions.length - 1]; - - // cellar.setTrust(untrustedPosition, false); - - // MockERC20 asset = MockERC20(address(cellar.asset())); - - // asset.mint(address(this), assets); - // asset.approve(address(cellar), assets); - // cellar.deposit(assets, address(this)); - - // address[] memory path = new address[](2); - - // // Test rebalancing from holding position to untrusted position. - // path[0] = address(asset); - // path[1] = address(untrustedPosition.asset()); - - // cellar.rebalance(cellar, untrustedPosition, assets, 0, path); - // } - - // // ============================================= ACCRUE TEST ============================================= - - // function testAccrue() external { - // // Scenario: - // // - Multiposition cellar has 3 positions. - // // - // // Testcases Covered: - // // - Test accrual with positive performance. - // // - Test accrual with negative performance. - // // - Test accrual with no performance (nothing changes). - // // - Test accrual reverting previous accrual period is still ongoing. - // // - Test accrual not starting an accrual period if negative performance or no performance. - // // - Test accrual for single position. - // // - Test accrual for multiple positions. - // // - Test accrued yield is distributed linearly as expected. - // // - Test deposits / withdraws do not effect accrual and yield distribution. - - // // NOTE: The amounts in each column are approximations. Actual results may differ due - // // to swaps and decimal conversions, however, it should not be significant. - // // +==============+==============+==================+================+===================+==============+ - // // | Total Assets | Total Locked | Performance Fees | Platform Fees | Last Accrual Time | Current Time | - // // | (in USD) | (in USD) | (in shares) | (in shares) | (in seconds) | (in seconds) | - // // +==============+==============+==================+================+===================+==============+ - // // | 1. Deposit $100 worth of assets into each position. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $300 | $0 | 0 | 0 | 0 | 0 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | 2. An entire year passes. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $300 | $0 | 0 | 0 | 0 | 31536000 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | 3. Test accrual of platform fees. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $300 | $0 | 0 | 3 | 31536000 | 31536000 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | 4. Each position gains $50 worth of assets of yield. | - // // | NOTE: Nothing should change because yield has not been accrued. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $300 | $0 | 0 | 3 | 31536000 | 31536000 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | 5. Accrue with positive performance. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $315 | $135 | 15 | 3 | 31536000 | 31536000 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | 6. Half of accrual period passes. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $382.5 | $67.5 | 15 | 3 | 31536000 | 31838400 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | 7. Deposit $200 worth of assets into a position. | - // // | NOTE: For testing that deposit does not effect yield and is not factored in to later accrual. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $582.5 | $67.5 | 15 | 3 | 31536000 | 31838400 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | 8. Entire accrual period passes. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $650 | $0 | 15 | 3 | 31536000 | 32140800 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | 9. Withdraw $100 worth of assets from a position. | - // // | NOTE: For testing that withdraw does not effect yield and is not factored in to later accrual. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $550 | $0 | 15 | 3 | 31536000 | 32140800 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | 10. Accrue with no performance. | - // // | NOTE: Ignore platform fees from now on because we've already tested they work and amounts at | - // // | this timescale are very small. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $550 | $0 | 15 | 3 | 32140800 | 32140800 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | 11. A position loses $150 worth of assets of yield. | - // // | NOTE: Nothing should change because losses have not been accrued. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $550 | $0 | 15 | 3 | 32140800 | 32140800 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | 12. Accrue with negative performance. | - // // | NOTE: Losses are realized immediately. | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - // // | $400 | $0 | 15 | 3 | 32745600 | 32745600 | - // // +--------------+--------------+------------------+----------------+-------------------+--------------+ - - // ERC4626[] memory positions = cellar.getPositions(); - - // // 1. Deposit $100 worth of assets into each position. - // for (uint256 i; i < positions.length; i++) { - // ERC4626 position = positions[i]; - // MockERC20 positionAsset = MockERC20(address(position.asset())); - - // uint256 assets = exchange.convert(address(USDC), address(positionAsset), 100e6); - // positionAsset.mint(address(this), assets); - // positionAsset.approve(address(cellar), assets); - // cellar.depositIntoPosition(position, assets, address(this)); - - // assertEq(position.totalAssets(), assets); - // (, , uint112 balance) = cellar.getPositionData(position); - // assertEq(balance, assets); - // assertApproxEqAbs(cellar.totalBalance(), 100e6 * (i + 1), 1e6); - // } - - // assertApproxEqAbs(cellar.totalAssets(), 300e6, 1e6); - - // // 2. An entire year passes. - // vm.warp(block.timestamp + 365 days); - // uint256 lastAccrualTimestamp = block.timestamp; - - // // 3. Accrue platform fees. - // cellar.accrue(); - - // assertEq(cellar.totalLocked(), 0); - // assertApproxEqAbs(cellar.totalAssets(), 300e6, 1e6); - // assertApproxEqAbs(cellar.totalBalance(), 300e6, 1e6); - // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 3e6, 0.01e6); - // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - - // // 4. Each position gains $50 worth of assets of yield. - // for (uint256 i; i < positions.length; i++) { - // ERC4626 position = positions[i]; - // MockERC20 positionAsset = MockERC20(address(position.asset())); - - // uint256 assets = exchange.convert(address(USDC), address(positionAsset), 50e6); - // MockERC4626(address(position)).simulateGain(assets, address(cellar)); - // assertApproxEqAbs(cellar.convertToAssets(positionAsset, position.maxWithdraw(address(cellar))), 150e6, 2e6); - // } - - // uint256 priceOfShareBefore = cellar.convertToShares(1e6); - - // // 5. Accrue with positive performance. - // cellar.accrue(); - - // uint256 priceOfShareAfter = cellar.convertToShares(1e6); - // assertEq(priceOfShareAfter, priceOfShareBefore); - // assertApproxEqAbs(cellar.totalLocked(), 135e6, 1e6); - // assertApproxEqAbs(cellar.totalAssets(), 315e6, 2e6); - // assertApproxEqAbs(cellar.totalBalance(), 450e6, 2e6); - // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); - // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - - // // Position balances should have updated to reflect yield accrued per position. - // for (uint256 i; i < positions.length; i++) { - // ERC4626 position = positions[i]; - - // (, , uint112 balance) = cellar.getPositionData(position); - // assertApproxEqAbs(cellar.convertToAssets(position.asset(), balance), 150e6, 2e6); - // } - - // // 6. Half of accrual period passes. - // uint256 accrualPeriod = cellar.accrualPeriod(); - // vm.warp(block.timestamp + accrualPeriod / 2); - - // assertApproxEqAbs(cellar.totalLocked(), 67.5e6, 1e6); - // assertApproxEqAbs(cellar.totalAssets(), 382.5e6, 2e6); - // assertApproxEqAbs(cellar.totalBalance(), 450e6, 2e6); - // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); - // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - - // // 7. Deposit $200 worth of assets into a position. - // USDC.mint(address(this), 200e6); - // USDC.approve(address(cellar), 200e6); - // cellar.depositIntoPosition(usdcCLR, 200e6, address(this)); - - // assertApproxEqAbs(cellar.totalLocked(), 67.5e6, 1e6); - // assertApproxEqAbs(cellar.totalAssets(), 582.5e6, 2e6); - // assertApproxEqAbs(cellar.totalBalance(), 650e6, 2e6); - // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); - // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - - // // 8. Entire accrual period passes. - // vm.warp(block.timestamp + accrualPeriod / 2); - - // assertEq(cellar.totalLocked(), 0); - // assertApproxEqAbs(cellar.totalAssets(), 650e6, 2e6); - // assertApproxEqAbs(cellar.totalBalance(), 650e6, 2e6); - // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); - // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - - // // 9. Withdraw $100 worth of assets from a position. - // cellar.withdrawFromPosition( - // wethCLR, - // exchange.convert(address(USDC), address(WETH), 100e6), - // address(this), - // address(this) - // ); - - // assertEq(cellar.totalLocked(), 0); - // assertApproxEqAbs(cellar.totalAssets(), 550e6, 2e6); - // assertApproxEqAbs(cellar.totalBalance(), 550e6, 2e6); - // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); - // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - - // // 10. Accrue with no performance. - // cellar.accrue(); - // lastAccrualTimestamp = block.timestamp; - - // assertEq(cellar.totalLocked(), 0); - // assertApproxEqAbs(cellar.totalAssets(), 550e6, 2e6); - // assertApproxEqAbs(cellar.totalBalance(), 550e6, 2e6); - // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); - // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - - // // 11. A position loses $150 worth of assets of yield. - // MockERC4626(address(usdcCLR)).simulateLoss(150e6); - - // assertEq(cellar.totalLocked(), 0); - // assertApproxEqAbs(cellar.totalAssets(), 550e6, 2e6); - // assertApproxEqAbs(cellar.totalBalance(), 550e6, 2e6); - // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); - // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - - // // 12. Accrue with negative performance. - // cellar.accrue(); - - // assertEq(cellar.totalLocked(), 0); - // assertApproxEqAbs(cellar.totalAssets(), 400e6, 2e6); - // assertApproxEqAbs(cellar.totalBalance(), 400e6, 2e6); - // assertApproxEqAbs(cellar.balanceOf(address(cellar)), 18e6, 1e6); - // assertEq(cellar.lastAccrual(), lastAccrualTimestamp); - // } - - // function testAccrueWithZeroTotalLocked() external { - // cellar.accrue(); - - // assertEq(cellar.totalLocked(), 0); - - // cellar.accrue(); - // } - - // function testFailAccrueWithNonzeroTotalLocked() external { - // MockERC4626(address(usdcCLR)).simulateGain(100e6, address(cellar)); - // cellar.accrue(); - - // // $90 locked after taking $10 for 10% performance fees. - // assertEq(cellar.totalLocked(), 90e6); - - // cellar.accrue(); - // } - - // // ============================================= POSITIONS TEST ============================================= - - // function testSetPositions() external { - // ERC4626[] memory positions = new ERC4626[](3); - // positions[0] = ERC4626(address(wethCLR)); - // positions[1] = ERC4626(address(usdcCLR)); - // positions[2] = ERC4626(address(wbtcCLR)); - - // uint32[] memory maxSlippages = new uint32[](3); - // for (uint256 i; i < 3; i++) maxSlippages[i] = 1_00; - - // cellar.setPositions(positions, maxSlippages); - - // // Test that positions were updated. - // ERC4626[] memory newPositions = cellar.getPositions(); - // uint32 maxSlippage; - // for (uint256 i; i < 3; i++) { - // ERC4626 position = positions[i]; - - // assertEq(address(position), address(newPositions[i])); - // (, maxSlippage, ) = cellar.getPositionData(position); - // assertEq(maxSlippage, 1_00); - // } - // } - - // function testFailSetUntrustedPosition() external { - // MockERC20 XYZ = new MockERC20("XYZ", 18); - // MockERC4626 xyzCLR = new MockERC4626(ERC20(address(XYZ)), "XYZ Cellar LP Token", "XYZ-CLR", 18); - - // (bool isTrusted, , ) = cellar.getPositionData(xyzCLR); - // assertFalse(isTrusted); - - // ERC4626[] memory positions = new ERC4626[](4); - // positions[0] = ERC4626(address(wethCLR)); - // positions[1] = ERC4626(address(usdcCLR)); - // positions[2] = ERC4626(address(wbtcCLR)); - // positions[3] = ERC4626(address(xyzCLR)); - - // // Test attempting to setting with an untrusted position. - // cellar.setPositions(positions); - // } - - // function testFailAddingUntrustedPosition() external { - // MockERC20 XYZ = new MockERC20("XYZ", 18); - // MockERC4626 xyzCLR = new MockERC4626(ERC20(address(XYZ)), "XYZ Cellar LP Token", "XYZ-CLR", 18); - - // (bool isTrusted, , ) = cellar.getPositionData(xyzCLR); - // assertFalse(isTrusted); - - // // Test attempting to add untrusted position. - // cellar.addPosition(xyzCLR); - // } - - // function testTrustingPosition() external { - // MockERC20 XYZ = new MockERC20("XYZ", 18); - // MockERC4626 xyzCLR = new MockERC4626(ERC20(address(XYZ)), "XYZ Cellar LP Token", "XYZ-CLR", 18); - - // (bool isTrusted, , ) = cellar.getPositionData(xyzCLR); - // assertFalse(isTrusted); - - // // Test that position is trusted. - // cellar.setTrust(xyzCLR, true); - - // (isTrusted, , ) = cellar.getPositionData(xyzCLR); - // assertTrue(isTrusted); - - // // Test that newly trusted position can now be added. - // cellar.addPosition(xyzCLR); - - // ERC4626[] memory positions = cellar.getPositions(); - // assertEq(address(positions[positions.length - 1]), address(xyzCLR)); - // } - - // function testDistrustingAndRemovingPosition() external { - // ERC4626 distrustedPosition = wethCLR; - - // // Deposit assets into position before distrusting. - // uint256 assets = exchange.convert(address(USDC), address(WETH), 100e6); - // WETH.mint(address(this), assets); - // WETH.approve(address(cellar), assets); - // cellar.depositIntoPosition(distrustedPosition, assets, address(this)); - - // // Simulate position gaining yield. - // MockERC4626(address(distrustedPosition)).simulateGain(assets / 2, address(cellar)); - - // (, , uint112 balance) = cellar.getPositionData(distrustedPosition); - // assertEq(balance, assets); - // assertEq(cellar.totalBalance(), 100e6); - // assertEq(cellar.totalAssets(), 100e6); - // assertEq(cellar.totalHoldings(), 0); - - // // Distrust and removing position. - // cellar.setTrust(distrustedPosition, false); - - // // Test that assets have been pulled from untrusted position and state has updated accordingly. - // (, , balance) = cellar.getPositionData(distrustedPosition); - // assertEq(balance, 0); - // // Expected 142.5 assets to be received after swapping 150 assets with simulated 5% slippage. - // assertEq(cellar.totalBalance(), 0); - // assertEq(cellar.totalAssets(), 142.5e6); - // assertEq(cellar.totalHoldings(), 142.5e6); - - // // Test that position has been distrusted. - // (bool isTrusted, , ) = cellar.getPositionData(distrustedPosition); - // assertFalse(isTrusted); - - // // Test that position has been removed from list of positions. - // ERC4626[] memory expectedPositions = new ERC4626[](2); - // expectedPositions[0] = ERC4626(address(usdcCLR)); - // expectedPositions[1] = ERC4626(address(wbtcCLR)); - - // ERC4626[] memory positions = cellar.getPositions(); - // for (uint256 i; i < positions.length; i++) assertTrue(positions[i] == expectedPositions[i]); - // } - - // // ============================================== SWEEP TEST ============================================== - - // function testSweep() external { - // MockERC20 XYZ = new MockERC20("XYZ", 18); - // XYZ.mint(address(cellar), 100e18); - - // // Test sweep. - // cellar.sweep(address(XYZ), 100e18, address(this)); - - // assertEq(XYZ.balanceOf(address(this)), 100e18); - // } - - // function testFailSweep() external { - // wbtcCLR.mint(address(cellar), 100e18); - - // // Test sweep of protected asset. - // cellar.sweep(address(wbtcCLR), 100e18, address(this)); - // } - - // function testFailAttemptingToStealFundsByRemovingPositionThenSweeping() external { - // // Deposit assets into position before distrusting. - // uint256 assets = exchange.convert(address(USDC), address(WBTC), 100e6); - // WBTC.mint(address(this), assets); - // WBTC.approve(address(cellar), assets); - // cellar.depositIntoPosition(wbtcCLR, assets, address(this)); - - // // Simulate position gaining yield. - // MockERC4626(address(wbtcCLR)).simulateGain(assets / 2, address(cellar)); - - // uint256 totalAssets = assets + assets / 2; - // assertEq(wbtcCLR.balanceOf(address(cellar)), totalAssets); - - // // Remove position. - // cellar.removePosition(wbtcCLR); - - // assertEq(wbtcCLR.balanceOf(address(cellar)), 0); - - // // Test attempting to steal assets after removing position from list. - // cellar.sweep(address(wbtcCLR), totalAssets, address(this)); - // } - - // // ============================================= EMERGENCY TEST ============================================= - - // function testFailShutdownDeposit() external { - // cellar.setShutdown(true, false); - - // USDC.mint(address(this), 1); - // USDC.approve(address(cellar), 1); - // cellar.deposit(1, address(this)); - // } - - // function testFailShutdownDepositIntoPosition() external { - // USDC.mint(address(this), 1e18); - // USDC.approve(address(cellar), 1e18); - // cellar.deposit(1e18, address(this)); - - // cellar.setShutdown(true, false); - - // address[] memory path = new address[](2); - // path[0] = address(USDC); - // path[1] = address(USDC); - - // cellar.rebalance(cellar, usdcCLR, 1e18, 0, path); - // } - - // function testShutdownExitsAllPositions() external { - // // Deposit 100 assets into each position with 50 assets of unrealized yield. - // ERC4626[] memory positions = cellar.getPositions(); - // for (uint256 i; i < positions.length; i++) { - // ERC4626 position = positions[i]; - // MockERC20 positionAsset = MockERC20(address(position.asset())); - - // uint256 assets = exchange.convert(address(USDC), address(positionAsset), 100e6); - // positionAsset.mint(address(this), assets); - // positionAsset.approve(address(cellar), assets); - // cellar.depositIntoPosition(position, assets, address(this)); - - // MockERC4626(address(position)).simulateGain(assets / 2, address(cellar)); - // } - - // assertApproxEqAbs(cellar.totalBalance(), 300e6, 1e6); - // assertApproxEqAbs(cellar.totalAssets(), 300e6, 1e6); - // assertEq(cellar.totalHoldings(), 0); - - // cellar.setShutdown(true, true); - - // assertTrue(cellar.isShutdown()); - // assertEq(cellar.totalBalance(), 0); - // // Expect to receive 435 assets after 450 total assets from positions are swapped with 5% slippage. - // assertApproxEqAbs(cellar.totalAssets(), 435e6, 2e6); - // assertApproxEqAbs(cellar.totalHoldings(), 435e6, 2e6); - // } - - // function testShutdownExitsAllPositionsWithNoBalances() external { - // cellar.setShutdown(true, true); - - // assertTrue(cellar.isShutdown()); - // } - - // // ============================================== LIMITS TEST ============================================== - - // function testLimits() external { - // USDC.mint(address(this), 100e6); - // USDC.approve(address(cellar), 100e6); - // cellar.deposit(100e6, address(this)); - - // assertEq(cellar.maxDeposit(address(this)), type(uint256).max); - // assertEq(cellar.maxMint(address(this)), type(uint256).max); - - // cellar.setDepositLimit(200e6); - // cellar.setLiquidityLimit(100e6); - - // assertEq(cellar.depositLimit(), 200e6); - // assertEq(cellar.liquidityLimit(), 100e6); - // assertEq(cellar.maxDeposit(address(this)), 0); - // assertEq(cellar.maxMint(address(this)), 0); - - // cellar.setLiquidityLimit(300e6); - - // assertEq(cellar.depositLimit(), 200e6); - // assertEq(cellar.liquidityLimit(), 300e6); - // assertEq(cellar.maxDeposit(address(this)), 100e6); - // assertEq(cellar.maxMint(address(this)), 100e6); - - // cellar.setShutdown(true, false); - - // assertEq(cellar.maxDeposit(address(this)), 0); - // assertEq(cellar.maxMint(address(this)), 0); - // } - - // function testFailDepositAboveDepositLimit() external { - // cellar.setDepositLimit(100e6); - - // USDC.mint(address(this), 101e6); - // USDC.approve(address(cellar), 101e6); - // cellar.deposit(101e6, address(this)); - // } - - // function testFailMintAboveDepositLimit() external { - // cellar.setDepositLimit(100e6); - - // USDC.mint(address(this), 101e6); - // USDC.approve(address(cellar), 101e6); - // cellar.mint(101e6, address(this)); - // } - - // function testFailDepositAboveLiquidityLimit() external { - // cellar.setLiquidityLimit(100e6); - - // USDC.mint(address(this), 101e6); - // USDC.approve(address(cellar), 101e6); - // cellar.deposit(101e6, address(this)); - // } - - // function testFailMintAboveLiquidityLimit() external { - // cellar.setLiquidityLimit(100e6); - - // USDC.mint(address(this), 101e6); - // USDC.approve(address(cellar), 101e6); - // cellar.mint(101e6, address(this)); - // } } diff --git a/test/PriceRouter.t.sol b/test/PriceRouter.t.sol new file mode 100644 index 00000000..b0f781b4 --- /dev/null +++ b/test/PriceRouter.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.13; + +import { PriceRouter, Registry, ERC20 } from "src/modules/PriceRouter.sol"; +import { MockGravity } from "src/mocks/MockGravity.sol"; +import { IGravity } from "src/interfaces/IGravity.sol"; + +import { Test, console } from "@forge-std/Test.sol"; +import { Math } from "src/utils/Math.sol"; + +contract PriceRouterTest is Test { + using Math for uint256; + + PriceRouter private priceRouter; + + ERC20 private USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + ERC20 private WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + ERC20 private WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + + function setUp() external { + priceRouter = new PriceRouter(); + } + + function testExchangeRate() external view { + console.log("WETH/WBTC", priceRouter.getExchangeRate(WETH, WBTC)); + console.log("WBTC/WETH", priceRouter.getExchangeRate(WBTC, WETH)); + console.log("WETH/USDC", priceRouter.getExchangeRate(WETH, USDC)); + console.log("USDC/WETH", priceRouter.getExchangeRate(USDC, WETH)); + } + + function testGetValue() external view { + console.log("1 WETH in WBTC", priceRouter.getValue(WETH, 1e18, WBTC)); + console.log("1 WBTC in WETH", priceRouter.getValue(WBTC, 1e8, WETH)); + console.log("1 WETH in USDC", priceRouter.getValue(WETH, 1e18, USDC)); + console.log("1 USDC in WETH", priceRouter.getValue(USDC, 1e6, WETH)); + } +} From b923f42c09688084c50d4945fd7fcfe6c0876adf Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Sun, 19 Jun 2022 22:38:23 -0700 Subject: [PATCH 09/49] fix(Cellar): fix stack errors, fix decimal conversions, optimize data fetching --- src/base/Cellar.sol | 105 ++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 62 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 5b076ca0..44d9d4e3 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -435,13 +435,7 @@ contract Cellar is ERC4626, Ownable, Multicall { shares = withdraw(assets, receiver, owner); } else { // Get data efficiently. - ( - uint256 _totalAssets, - , - ERC4626[] memory _positions, - ERC20[] memory positionAssets, - uint256[] memory positionBalances - ) = _getData(); + (uint256 _totalAssets, , , ERC20[] memory positionAssets, uint256[] memory positionBalances) = _getData(); // Get the amount of share needed to redeem. shares = _previewWithdraw(assets, _totalAssets); @@ -454,7 +448,7 @@ contract Cellar is ERC4626, Ownable, Multicall { _burn(owner, shares); - (receivedAssets, amountsOut) = _pullFromPositions(assets, _positions, positionAssets, positionBalances); + (receivedAssets, amountsOut) = _pullFromPositions(assets, positionAssets, positionBalances); // Transfer withdrawn assets to the receiver. for (uint256 i; i < receivedAssets.length; i++) receivedAssets[i].safeTransfer(receiver, amountsOut[i]); @@ -465,7 +459,6 @@ contract Cellar is ERC4626, Ownable, Multicall { function _pullFromPositions( uint256 assets, - ERC4626[] memory _positions, ERC20[] memory positionAssets, uint256[] memory positionBalances ) internal returns (ERC20[] memory receivedAssets, uint256[] memory amountsOut) { @@ -476,21 +469,21 @@ contract Cellar is ERC4626, Ownable, Multicall { // Move on to next position if this one is empty. if (positionBalances[i] == 0) continue; - ERC20 positionAsset = positionAssets[i]; - uint256 onePositionAsset = 10**positionAsset.decimals(); - uint256 positionAssetToAssetExchangeRate = priceRouter.getExchangeRate(positionAsset, asset); + uint256 positionAssetToAssetExchangeRate = priceRouter.getExchangeRate(positionAssets[i], asset); // Denominate position balance in cellar's asset. - uint256 totalPositionBalanceInAssets = positionBalances[i].mulDivDown( - positionAssetToAssetExchangeRate, - onePositionAsset - ); + uint256 totalPositionBalanceInAssets = positionBalances[i] + .mulWadDown(positionAssetToAssetExchangeRate) + .changeDecimals(positionAssets[i].decimals(), asset.decimals()); // We want to pull as much as we can from this position, but no more than needed. uint256 amount; if (totalPositionBalanceInAssets > assets) { assets -= assets; - amount = assets.mulDivDown(onePositionAsset, positionAssetToAssetExchangeRate); + amount = assets.mulDivDown(1e18, positionAssetToAssetExchangeRate).changeDecimals( + asset.decimals(), + positionAssets[i].decimals() + ); } else { assets -= totalPositionBalanceInAssets; amount = positionBalances[i]; @@ -498,15 +491,15 @@ contract Cellar is ERC4626, Ownable, Multicall { // Return the asset and amount that will be received. amountsOut[i] = amount; - receivedAssets[i] = positionAsset; + receivedAssets[i] = positionAssets[i]; // Update position balance. - _subtractFromPositionBalance(getPositionData[address(_positions[i])], amount); + _subtractFromPositionBalance(getPositionData[positions[i]], amount); // Withdraw from position. - _positions[i].withdraw(amount, address(this), address(this)); + ERC4626(positions[i]).withdraw(amount, address(this), address(this)); - emit PulledFromPosition(address(_positions[i]), amount); + emit PulledFromPosition(positions[i], amount); // Stop if no more assets to withdraw. if (assets == 0) break; @@ -531,7 +524,7 @@ contract Cellar is ERC4626, Ownable, Multicall { balances[i] = position.maxWithdraw(address(this)); } - assets = totalHoldings() + registry.priceRouter().getValue(positionAssets, balances, asset); + assets = registry.priceRouter().getValues(positionAssets, balances, asset) + totalHoldings(); } /** @@ -547,12 +540,7 @@ contract Cellar is ERC4626, Ownable, Multicall { * @return assets the shares can be exchanged for */ function convertToAssets(uint256 shares) public view override returns (uint256 assets) { - uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. - uint8 assetDecimals = asset.decimals(); - uint256 totalAssetsNormalized = totalAssets().changeDecimals(assetDecimals, 18); - - assets = totalShares == 0 ? shares : shares.mulDivDown(totalAssetsNormalized, totalShares); - assets = assets.changeDecimals(18, assetDecimals); + assets = _convertToAssets(shares, totalAssets()); } /** @@ -561,12 +549,7 @@ contract Cellar is ERC4626, Ownable, Multicall { * @return shares the assets can be exchanged for */ function convertToShares(uint256 assets) public view override returns (uint256 shares) { - uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. - uint8 assetDecimals = asset.decimals(); - uint256 assetsNormalized = assets.changeDecimals(assetDecimals, 18); - uint256 totalAssetsNormalized = totalAssets().changeDecimals(assetDecimals, 18); - - shares = totalShares == 0 ? assetsNormalized : assetsNormalized.mulDivDown(totalShares, totalAssetsNormalized); + shares = _convertToShares(assets, totalAssets()); } /** @@ -575,12 +558,7 @@ contract Cellar is ERC4626, Ownable, Multicall { * @return assets that will be deposited */ function previewMint(uint256 shares) public view override returns (uint256 assets) { - uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. - uint8 assetDecimals = asset.decimals(); - uint256 totalAssetsNormalized = totalAssets().changeDecimals(assetDecimals, 18); - - assets = totalShares == 0 ? shares : shares.mulDivUp(totalAssetsNormalized, totalShares); - assets = assets.changeDecimals(18, assetDecimals); + assets = _previewMint(shares, totalAssets()); } /** @@ -589,12 +567,7 @@ contract Cellar is ERC4626, Ownable, Multicall { * @return shares that will be redeemed */ function previewWithdraw(uint256 assets) public view override returns (uint256 shares) { - uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. - uint8 assetDecimals = asset.decimals(); - uint256 assetsNormalized = assets.changeDecimals(assetDecimals, 18); - uint256 totalAssetsNormalized = totalAssets().changeDecimals(assetDecimals, 18); - - shares = totalShares == 0 ? assetsNormalized : assetsNormalized.mulDivUp(totalShares, totalAssetsNormalized); + shares = _previewWithdraw(assets, totalAssets()); } function _convertToAssets(uint256 shares, uint256 _totalAssets) internal view returns (uint256 assets) { @@ -662,6 +635,8 @@ contract Cellar is ERC4626, Ownable, Multicall { uint256[] memory positionBalances ) = _getData(); + uint8 assetDecimals = asset.decimals(); + for (uint256 i; i < _positions.length; i++) { PositionData storage positionData = getPositionData[address(_positions[i])]; @@ -670,20 +645,18 @@ contract Cellar is ERC4626, Ownable, Multicall { // Get exchange rate. ERC20 positionAsset = positionAssets[i]; - uint256 onePositionAsset = 10**positionAsset.decimals(); + uint8 positionDecimals = positionAsset.decimals(); uint256 positionAssetToAssetExchangeRate = priceRouter.getExchangeRate(positionAsset, asset); // Add to balance for last accrual. - totalBalanceLastAccrual += positionData.balance.mulDivDown( - positionAssetToAssetExchangeRate, - onePositionAsset - ); + totalBalanceLastAccrual += (positionData.balance) + .mulWadDown(positionAssetToAssetExchangeRate) + .changeDecimals(positionDecimals, assetDecimals); // Add to balance for this accrual. - totalBalanceThisAccrual += (balanceThisAccrual + positionData.storedUnrealizedGains).mulDivDown( - positionAssetToAssetExchangeRate, - onePositionAsset - ); + totalBalanceThisAccrual += (balanceThisAccrual + positionData.storedUnrealizedGains) + .mulWadDown(positionAssetToAssetExchangeRate) + .changeDecimals(positionDecimals, assetDecimals); // Update position's data. positionData.balance = balanceThisAccrual; @@ -691,7 +664,7 @@ contract Cellar is ERC4626, Ownable, Multicall { } // Compute and store current exchange rate between assets and shares for gas efficiency. - uint256 assetToSharesExchangeRate = _convertToShares(1e18, _totalAssets); + uint256 assetToSharesExchangeRate = _convertToShares(10**assetDecimals, _totalAssets); // Calculate platform fees accrued. uint256 elapsedTime = block.timestamp - lastAccrual; @@ -800,8 +773,12 @@ contract Cellar is ERC4626, Ownable, Multicall { if (asssetDepositLimit == type(uint256).max && asssetLiquidityLimit == type(uint256).max) return type(uint256).max; - uint256 leftUntilDepositLimit = asssetDepositLimit.subMinZero(maxWithdraw(receiver)); - uint256 leftUntilLiquidityLimit = asssetLiquidityLimit.subMinZero(totalAssets()); + // Get data efficiently. + uint256 _totalAssets = totalAssets(); + uint256 ownedAssets = _convertToAssets(balanceOf[receiver], _totalAssets); + + uint256 leftUntilDepositLimit = asssetDepositLimit.subMinZero(ownedAssets); + uint256 leftUntilLiquidityLimit = asssetLiquidityLimit.subMinZero(_totalAssets); // Only return the more relevant of the two. assets = Math.min(leftUntilDepositLimit, leftUntilLiquidityLimit); @@ -820,11 +797,15 @@ contract Cellar is ERC4626, Ownable, Multicall { if (asssetDepositLimit == type(uint256).max && asssetLiquidityLimit == type(uint256).max) return type(uint256).max; - uint256 leftUntilDepositLimit = asssetDepositLimit.subMinZero(maxWithdraw(receiver)); - uint256 leftUntilLiquidityLimit = asssetLiquidityLimit.subMinZero(totalAssets()); + // Get data efficiently. + uint256 _totalAssets = totalAssets(); + uint256 ownedAssets = _convertToAssets(balanceOf[receiver], _totalAssets); + + uint256 leftUntilDepositLimit = asssetDepositLimit.subMinZero(ownedAssets); + uint256 leftUntilLiquidityLimit = asssetLiquidityLimit.subMinZero(_totalAssets); // Only return the more relevant of the two. - shares = convertToShares(Math.min(leftUntilDepositLimit, leftUntilLiquidityLimit)); + shares = _convertToShares(Math.min(leftUntilDepositLimit, leftUntilLiquidityLimit), _totalAssets); } // ========================================= FEES LOGIC ========================================= @@ -940,7 +921,7 @@ contract Cellar is ERC4626, Ownable, Multicall { } _totalHoldings = totalHoldings(); - _totalAssets = _totalHoldings + registry.priceRouter().getValue(positionAssets, positionBalances, asset); + _totalAssets = registry.priceRouter().getValues(positionAssets, positionBalances, asset) + _totalHoldings; } function _swapExactAssets( From 4bbb67de7ee40e34f4e8aebe2cb3f8acfdee3163 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Sun, 19 Jun 2022 23:33:56 -0700 Subject: [PATCH 10/49] refactor(PriceRouter): change getExchangeRate to return data in decimals of quote --- src/base/Cellar.sol | 51 ++++++++++++++++++++++--------------- src/modules/PriceRouter.sol | 31 +++++++++++++--------- 2 files changed, 49 insertions(+), 33 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 44d9d4e3..7bec1ad5 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -435,7 +435,13 @@ contract Cellar is ERC4626, Ownable, Multicall { shares = withdraw(assets, receiver, owner); } else { // Get data efficiently. - (uint256 _totalAssets, , , ERC20[] memory positionAssets, uint256[] memory positionBalances) = _getData(); + ( + uint256 _totalAssets, + , + ERC4626[] memory _positions, + ERC20[] memory positionAssets, + uint256[] memory positionBalances + ) = _getData(); // Get the amount of share needed to redeem. shares = _previewWithdraw(assets, _totalAssets); @@ -448,7 +454,7 @@ contract Cellar is ERC4626, Ownable, Multicall { _burn(owner, shares); - (receivedAssets, amountsOut) = _pullFromPositions(assets, positionAssets, positionBalances); + (receivedAssets, amountsOut) = _pullFromPositions(assets, _positions, positionAssets, positionBalances); // Transfer withdrawn assets to the receiver. for (uint256 i; i < receivedAssets.length; i++) receivedAssets[i].safeTransfer(receiver, amountsOut[i]); @@ -459,6 +465,7 @@ contract Cellar is ERC4626, Ownable, Multicall { function _pullFromPositions( uint256 assets, + ERC4626[] memory _positions, ERC20[] memory positionAssets, uint256[] memory positionBalances ) internal returns (ERC20[] memory receivedAssets, uint256[] memory amountsOut) { @@ -469,21 +476,20 @@ contract Cellar is ERC4626, Ownable, Multicall { // Move on to next position if this one is empty. if (positionBalances[i] == 0) continue; + uint256 onePositionAsset = 10**positionAssets[i].decimals(); uint256 positionAssetToAssetExchangeRate = priceRouter.getExchangeRate(positionAssets[i], asset); // Denominate position balance in cellar's asset. - uint256 totalPositionBalanceInAssets = positionBalances[i] - .mulWadDown(positionAssetToAssetExchangeRate) - .changeDecimals(positionAssets[i].decimals(), asset.decimals()); + uint256 totalPositionBalanceInAssets = positionBalances[i].mulDivDown( + positionAssetToAssetExchangeRate, + onePositionAsset + ); // We want to pull as much as we can from this position, but no more than needed. uint256 amount; if (totalPositionBalanceInAssets > assets) { assets -= assets; - amount = assets.mulDivDown(1e18, positionAssetToAssetExchangeRate).changeDecimals( - asset.decimals(), - positionAssets[i].decimals() - ); + amount = assets.mulDivDown(onePositionAsset, positionAssetToAssetExchangeRate); } else { assets -= totalPositionBalanceInAssets; amount = positionBalances[i]; @@ -494,12 +500,12 @@ contract Cellar is ERC4626, Ownable, Multicall { receivedAssets[i] = positionAssets[i]; // Update position balance. - _subtractFromPositionBalance(getPositionData[positions[i]], amount); + _subtractFromPositionBalance(getPositionData[address(_positions[i])], amount); // Withdraw from position. - ERC4626(positions[i]).withdraw(amount, address(this), address(this)); + _positions[i].withdraw(amount, address(this), address(this)); - emit PulledFromPosition(positions[i], amount); + emit PulledFromPosition(address(_positions[i]), amount); // Stop if no more assets to withdraw. if (assets == 0) break; @@ -635,7 +641,8 @@ contract Cellar is ERC4626, Ownable, Multicall { uint256[] memory positionBalances ) = _getData(); - uint8 assetDecimals = asset.decimals(); + ERC20 denominationAsset = asset; + uint8 assetDecimals = denominationAsset.decimals(); for (uint256 i; i < _positions.length; i++) { PositionData storage positionData = getPositionData[address(_positions[i])]; @@ -645,18 +652,20 @@ contract Cellar is ERC4626, Ownable, Multicall { // Get exchange rate. ERC20 positionAsset = positionAssets[i]; - uint8 positionDecimals = positionAsset.decimals(); - uint256 positionAssetToAssetExchangeRate = priceRouter.getExchangeRate(positionAsset, asset); + uint256 onePositionAsset = 10**positionAsset.decimals(); + uint256 positionAssetToAssetExchangeRate = priceRouter.getExchangeRate(positionAsset, denominationAsset); // Add to balance for last accrual. - totalBalanceLastAccrual += (positionData.balance) - .mulWadDown(positionAssetToAssetExchangeRate) - .changeDecimals(positionDecimals, assetDecimals); + totalBalanceLastAccrual += (positionData.balance).mulDivDown( + positionAssetToAssetExchangeRate, + onePositionAsset + ); // Add to balance for this accrual. - totalBalanceThisAccrual += (balanceThisAccrual + positionData.storedUnrealizedGains) - .mulWadDown(positionAssetToAssetExchangeRate) - .changeDecimals(positionDecimals, assetDecimals); + totalBalanceThisAccrual += (balanceThisAccrual + positionData.storedUnrealizedGains).mulDivDown( + positionAssetToAssetExchangeRate, + onePositionAsset + ); // Update position's data. positionData.balance = balanceThisAccrual; diff --git a/src/modules/PriceRouter.sol b/src/modules/PriceRouter.sol index 0c0a5715..05b3bcdc 100644 --- a/src/modules/PriceRouter.sol +++ b/src/modules/PriceRouter.sol @@ -70,19 +70,27 @@ contract PriceRouter is Ownable { value = _getValue(baseAsset, amounts, quoteAsset, quoteAsset.decimals()); } + function getExchangeRate(ERC20 baseAsset, ERC20 quoteAsset) external view returns (uint256 exchangeRate) { + exchangeRate = _getExchangeRate(baseAsset, quoteAsset, quoteAsset.decimals()); + } + function _getValue( ERC20 baseAsset, uint256 amounts, ERC20 quoteAsset, uint8 quoteAssetDecimals ) internal view returns (uint256 value) { - value = amounts.mulWadDown(getExchangeRate(baseAsset, quoteAsset)).changeDecimals( - baseAsset.decimals(), - quoteAssetDecimals + value = amounts.mulDivDown( + _getExchangeRate(baseAsset, quoteAsset, quoteAssetDecimals), + 10**baseAsset.decimals() ); } - function getExchangeRate(ERC20 baseAsset, ERC20 quoteAsset) public view returns (uint256 exchangeRate) { + function _getExchangeRate( + ERC20 baseAsset, + ERC20 quoteAsset, + uint8 quoteDecimals + ) internal view returns (uint256 exchangeRate) { address baseOverride = baseAssetOverride[baseAsset]; address base = baseOverride == address(0) ? address(baseAsset) : baseOverride; @@ -91,15 +99,14 @@ contract PriceRouter is Ownable { if (base == quote) return 1e18; - exchangeRate = isSupportedQuoteAsset(quoteAsset) - ? _getExchangeRate(base, quote) - : _getExchangeRateInETH(base).mulDivDown(1e18, _getExchangeRateInETH(quote)); - } - - function _getExchangeRate(address base, address quote) internal view returns (uint256 exchangeRate) { - (, int256 price, , , ) = feedRegistry.latestRoundData(base, quote); + if (isSupportedQuoteAsset(quoteAsset)) { + (, int256 price, , , ) = feedRegistry.latestRoundData(base, quote); - exchangeRate = uint256(price).changeDecimals(feedRegistry.decimals(base, quote), 18); + exchangeRate = uint256(price).changeDecimals(feedRegistry.decimals(base, quote), quoteDecimals); + } else { + exchangeRate = _getExchangeRateInETH(base).mulDivDown(1e18, _getExchangeRateInETH(quote)); + exchangeRate = exchangeRate.changeDecimals(18, quoteDecimals); + } } function _getExchangeRateInETH(address base) internal view returns (uint256 exchangeRate) { From f1792c6c239a8df5263cc5ab516e5806b3c53427 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Sun, 19 Jun 2022 23:56:45 -0700 Subject: [PATCH 11/49] refactor(Cellar): fix pullFromPositions logic --- src/base/Cellar.sol | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 7bec1ad5..e8235676 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -487,17 +487,13 @@ contract Cellar is ERC4626, Ownable, Multicall { // We want to pull as much as we can from this position, but no more than needed. uint256 amount; - if (totalPositionBalanceInAssets > assets) { - assets -= assets; - amount = assets.mulDivDown(onePositionAsset, positionAssetToAssetExchangeRate); - } else { - assets -= totalPositionBalanceInAssets; - amount = positionBalances[i]; - } + (amount, assets) = totalPositionBalanceInAssets > assets + ? (assets.mulDivDown(onePositionAsset, positionAssetToAssetExchangeRate), 0) + : (positionBalances[i], assets - totalPositionBalanceInAssets); // Return the asset and amount that will be received. - amountsOut[i] = amount; - receivedAssets[i] = positionAssets[i]; + amountsOut[amountsOut.length] = amount; + receivedAssets[receivedAssets.length] = positionAssets[i]; // Update position balance. _subtractFromPositionBalance(getPositionData[address(_positions[i])], amount); From f01ee4667a6849e05fb3b5414c8e965381c004e7 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Mon, 20 Jun 2022 14:33:15 -0700 Subject: [PATCH 12/49] feat(Mocks): add mock price router --- src/mocks/MockExchange.sol | 69 ++++++++++++++----------------- src/mocks/MockPriceOracle.sol | 12 ------ src/mocks/MockPriceRouter.sol | 36 ++++++++++++++++ src/modules/PriceRouter.sol | 6 +-- test/AaveV2StablecoinCellar.t.sol | 14 ++++--- test/Cellar.t.sol | 28 ++++++++++--- test/CellarRouter.t.sol | 10 +++-- 7 files changed, 108 insertions(+), 67 deletions(-) delete mode 100644 src/mocks/MockPriceOracle.sol create mode 100644 src/mocks/MockPriceRouter.sol diff --git a/src/mocks/MockExchange.sol b/src/mocks/MockExchange.sol index 1bc32c19..5eefa997 100644 --- a/src/mocks/MockExchange.sol +++ b/src/mocks/MockExchange.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.13; import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { MockPriceRouter } from "src/mocks/MockPriceRouter.sol"; import { Math } from "src/utils/Math.sol"; import { IUniswapV3Router } from "../interfaces/IUniswapV3Router.sol"; @@ -161,22 +162,10 @@ contract MockExchange { uint256 public constant PRICE_IMPACT = 5_00; uint256 public constant DENOMINATOR = 100_00; - mapping(address => mapping(address => uint256)) public getExchangeRate; + MockPriceRouter public priceRouter; - function setExchangeRate( - address _base, - address _quote, - uint256 _price - ) external { - getExchangeRate[_base][_quote] = _price; - } - - function convert( - address fromToken, - address toToken, - uint256 amount - ) public view returns (uint256) { - return (amount * getExchangeRate[fromToken][toToken]) / 10**ERC20(fromToken).decimals(); + constructor(MockPriceRouter _priceRouter) { + priceRouter = _priceRouter; } struct ExactInputSingleParams { @@ -191,33 +180,39 @@ contract MockExchange { } function exactInputSingle(ExactInputSingleParams calldata params) external returns (uint256) { - ERC20(params.tokenIn).transferFrom(msg.sender, address(this), params.amountIn); + ERC20 tokenIn = ERC20(params.tokenIn); + ERC20 tokenOut = ERC20(params.tokenOut); - uint256 amountOut = convert(params.tokenIn, params.tokenOut, params.amountIn); + tokenIn.transferFrom(msg.sender, address(this), params.amountIn); + + uint256 amountOut = priceRouter.getValue(tokenIn, params.amountIn, tokenOut); amountOut = amountOut.mulDivDown(DENOMINATOR - PRICE_IMPACT, DENOMINATOR); require(amountOut >= params.amountOutMinimum, "amountOutMin invariant failed"); - ERC20(params.tokenOut).transfer(params.recipient, amountOut); + tokenOut.transfer(params.recipient, amountOut); return amountOut; } function exactInput(IUniswapV3Router.ExactInputParams memory params) external returns (uint256) { - (address tokenIn, address tokenOut, ) = params.path.decodeFirstPool(); + (address _tokenIn, address _tokenOut, ) = params.path.decodeFirstPool(); + ERC20 tokenIn = ERC20(_tokenIn); + ERC20 tokenOut = ERC20(_tokenOut); while (params.path.hasMultiplePools()) { params.path = params.path.skipToken(); - (, tokenOut, ) = params.path.decodeFirstPool(); + (, _tokenOut, ) = params.path.decodeFirstPool(); + tokenOut = ERC20(_tokenOut); } - ERC20(tokenIn).transferFrom(msg.sender, address(this), params.amountIn); + tokenIn.transferFrom(msg.sender, address(this), params.amountIn); - uint256 amountOut = convert(tokenIn, tokenOut, params.amountIn); + uint256 amountOut = priceRouter.getValue(tokenIn, params.amountIn, tokenOut); amountOut = amountOut.mulDivDown(DENOMINATOR - PRICE_IMPACT, DENOMINATOR); require(amountOut >= params.amountOutMinimum, "amountOutMin invariant failed"); - ERC20(tokenOut).transfer(params.recipient, amountOut); + tokenOut.transfer(params.recipient, amountOut); return amountOut; } @@ -228,17 +223,17 @@ contract MockExchange { address to, uint256 ) external returns (uint256[] memory) { - address tokenIn = path[0]; - address tokenOut = path[path.length - 1]; + ERC20 tokenIn = ERC20(path[0]); + ERC20 tokenOut = ERC20(path[path.length - 1]); - ERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn); + tokenIn.transferFrom(msg.sender, address(this), amountIn); - uint256 amountOut = convert(tokenIn, tokenOut, amountIn); + uint256 amountOut = priceRouter.getValue(tokenIn, amountIn, tokenOut); amountOut = amountOut.mulDivDown(DENOMINATOR - PRICE_IMPACT, DENOMINATOR); require(amountOut >= amountOutMin, "amountOutMin invariant failed"); - ERC20(tokenOut).transfer(to, amountOut); + tokenOut.transfer(to, amountOut); uint256[] memory amounts = new uint256[](1); amounts[0] = amountOut; @@ -252,33 +247,33 @@ contract MockExchange { uint256 _amount, uint256 _expected ) external returns (uint256) { - address tokenIn = _route[0]; + ERC20 tokenIn = ERC20(_route[0]); - address tokenOut; + ERC20 tokenOut; for (uint256 i; ; i += 2) { if (i == 8 || _route[i + 1] == address(0)) { - tokenOut = _route[i]; + tokenOut = ERC20(_route[i]); break; } } - ERC20(tokenIn).transferFrom(msg.sender, address(this), _amount); + tokenIn.transferFrom(msg.sender, address(this), _amount); - uint256 amountOut = convert(tokenIn, tokenOut, _amount); + uint256 amountOut = priceRouter.getValue(tokenIn, _amount, tokenOut); amountOut = amountOut.mulDivDown(DENOMINATOR - PRICE_IMPACT, DENOMINATOR); require(amountOut > _expected, "received less than expected"); - ERC20(tokenOut).transfer(msg.sender, amountOut); + tokenOut.transfer(msg.sender, amountOut); return amountOut; } function quote(uint256 amountIn, address[] calldata path) external view returns (uint256 amountOut) { - address tokenIn = path[0]; - address tokenOut = path[path.length - 1]; + ERC20 tokenIn = ERC20(path[0]); + ERC20 tokenOut = ERC20(path[path.length - 1]); - amountOut = convert(tokenIn, tokenOut, amountIn); + amountOut = priceRouter.getValue(tokenIn, amountIn, tokenOut); amountOut = amountOut.mulDivDown(DENOMINATOR - PRICE_IMPACT, DENOMINATOR); } } diff --git a/src/mocks/MockPriceOracle.sol b/src/mocks/MockPriceOracle.sol deleted file mode 100644 index 3910148e..00000000 --- a/src/mocks/MockPriceOracle.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; - -import { ERC20 } from "@solmate/tokens/ERC20.sol"; - -contract MockPriceOracle { - mapping(address => uint256) public getLatestPrice; - - function setPrice(address token, uint256 price) external { - getLatestPrice[token] = price; - } -} diff --git a/src/mocks/MockPriceRouter.sol b/src/mocks/MockPriceRouter.sol new file mode 100644 index 00000000..39b212cf --- /dev/null +++ b/src/mocks/MockPriceRouter.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.13; + +import { MockExchange } from "src/mocks/MockExchange.sol"; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { Math } from "src/utils/Math.sol"; + +contract MockPriceRouter { + using Math for uint256; + + mapping(ERC20 => mapping(ERC20 => uint256)) public getExchangeRate; + + function setExchangeRate( + ERC20 baseAsset, + ERC20 quoteAsset, + uint256 exchangeRate + ) external { + getExchangeRate[baseAsset][quoteAsset] = exchangeRate; + } + + function getValues( + ERC20[] memory baseAssets, + uint256[] memory amounts, + ERC20 quoteAsset + ) external view returns (uint256 value) { + for (uint256 i; i < baseAssets.length; i++) value += getValue(baseAssets[i], amounts[i], quoteAsset); + } + + function getValue( + ERC20 baseAsset, + uint256 amount, + ERC20 quoteAsset + ) public view returns (uint256 value) { + value = amount.mulDivDown(getExchangeRate[baseAsset][quoteAsset], 10**baseAsset.decimals()); + } +} diff --git a/src/modules/PriceRouter.sol b/src/modules/PriceRouter.sol index 05b3bcdc..1ca79631 100644 --- a/src/modules/PriceRouter.sol +++ b/src/modules/PriceRouter.sol @@ -66,7 +66,7 @@ contract PriceRouter is Ownable { ERC20 baseAsset, uint256 amounts, ERC20 quoteAsset - ) public view returns (uint256 value) { + ) external view returns (uint256 value) { value = _getValue(baseAsset, amounts, quoteAsset, quoteAsset.decimals()); } @@ -76,11 +76,11 @@ contract PriceRouter is Ownable { function _getValue( ERC20 baseAsset, - uint256 amounts, + uint256 amount, ERC20 quoteAsset, uint8 quoteAssetDecimals ) internal view returns (uint256 value) { - value = amounts.mulDivDown( + value = amount.mulDivDown( _getExchangeRate(baseAsset, quoteAsset, quoteAssetDecimals), 10**baseAsset.decimals() ); diff --git a/test/AaveV2StablecoinCellar.t.sol b/test/AaveV2StablecoinCellar.t.sol index 7a280a84..1489f2ca 100644 --- a/test/AaveV2StablecoinCellar.t.sol +++ b/test/AaveV2StablecoinCellar.t.sol @@ -11,7 +11,7 @@ import { IGravity } from "src/interfaces/IGravity.sol"; import { ILendingPool } from "src/interfaces/ILendingPool.sol"; import { MockERC20 } from "src/mocks/MockERC20.sol"; import { MockAToken } from "src/mocks/MockAToken.sol"; -import { MockExchange } from "src/mocks/MockExchange.sol"; +import { MockExchange, MockPriceRouter } from "src/mocks/MockExchange.sol"; import { MockLendingPool } from "src/mocks/MockLendingPool.sol"; import { MockIncentivesController } from "src/mocks/MockIncentivesController.sol"; import { MockGravity } from "src/mocks/MockGravity.sol"; @@ -32,6 +32,7 @@ contract AaveV2StablecoinCellarTest is Test { MockAToken private aUSDC; MockAToken private aDAI; MockLendingPool private lendingPool; + MockPriceRouter private priceRouter; MockExchange private exchange; MockIncentivesController private incentivesController; MockGravity private gravity; @@ -60,7 +61,8 @@ contract AaveV2StablecoinCellarTest is Test { ERC20[] memory approvedPositions = new ERC20[](1); approvedPositions[0] = ERC20(DAI); - exchange = new MockExchange(); + priceRouter = new MockPriceRouter(); + exchange = new MockExchange(priceRouter); vm.label(address(exchange), "exchange"); AAVE = new MockERC20("AAVE", 18); @@ -74,10 +76,10 @@ contract AaveV2StablecoinCellarTest is Test { vm.label(address(gravity), "gravity"); // Setup exchange rates: - exchange.setExchangeRate(address(USDC), address(DAI), 1e18); - exchange.setExchangeRate(address(DAI), address(USDC), 1e6); - exchange.setExchangeRate(address(AAVE), address(USDC), 100e6); - exchange.setExchangeRate(address(AAVE), address(DAI), 100e18); + priceRouter.setExchangeRate(ERC20(address(USDC)), ERC20(address(DAI)), 1e18); + priceRouter.setExchangeRate(ERC20(address(DAI)), ERC20(address(USDC)), 1e6); + priceRouter.setExchangeRate(ERC20(address(AAVE)), ERC20(address(USDC)), 100e6); + priceRouter.setExchangeRate(ERC20(address(AAVE)), ERC20(address(DAI)), 100e18); // Declare unnecessary variables with address 0. cellar = new AaveV2StablecoinCellar( diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index f0cdf4c9..19063404 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.13; import { Cellar, ERC4626, ERC20 } from "src/base/Cellar.sol"; import { Registry, PriceRouter, SwapRouter, IGravity } from "src/Registry.sol"; import { IUniswapV2Router, IUniswapV3Router } from "src/modules/SwapRouter.sol"; -import { PriceRouter } from "src/modules/PriceRouter.sol"; +import { MockPriceRouter } from "src/mocks/MockPriceRouter.sol"; import { MockERC4626 } from "src/mocks/MockERC4626.sol"; import { MockGravity } from "src/mocks/MockGravity.sol"; @@ -19,7 +19,7 @@ contract CellarTest is Test { IUniswapV2Router private constant uniswapV2Router = IUniswapV2Router(0xE592427A0AEce92De3Edee1F18E0157C05861564); IUniswapV3Router private constant uniswapV3Router = IUniswapV3Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); - PriceRouter private priceRouter; + MockPriceRouter private priceRouter; SwapRouter private swapRouter; Registry private registry; @@ -45,10 +45,28 @@ contract CellarTest is Test { // Setup Registry and modules: swapRouter = new SwapRouter(uniswapV2Router, uniswapV3Router); - priceRouter = new PriceRouter(); + priceRouter = new MockPriceRouter(); gravity = new MockGravity(); - registry = new Registry(swapRouter, priceRouter, IGravity(address(gravity))); + registry = new Registry(swapRouter, PriceRouter(address(priceRouter)), IGravity(address(gravity))); + + // Setup exchange rates: + // USDC Simulated Price: $1 + // WETH Simulated Price: $2000 + // WBTC Simulated Price: $30,000 + + swapRouter.setExchangeRate(USDC, USDC, 1e6); + swapRouter.setExchangeRate(WETH, WETH, 1e18); + swapRouter.setExchangeRate(WBTC, WBTC, 1e8); + + swapRouter.setExchangeRate(USDC, WETH, 0.0005e18); + swapRouter.setExchangeRate(WETH, USDC, 2000e6); + + swapRouter.setExchangeRate(USDC, WBTC, 0.000033e8); + swapRouter.setExchangeRate(WBTC, USDC, 30_000e6); + + swapRouter.setExchangeRate(WETH, WBTC, 0.06666666e8); + swapRouter.setExchangeRate(WBTC, WETH, 15e18); // Setup Cellar: address[] memory positions = new address[](3); @@ -71,7 +89,7 @@ contract CellarTest is Test { // ========================================= DEPOSIT/WITHDRAW TEST ========================================= - function testDepositAndWithdraw(uint256 assets) external { + function testDepositAndWithdraw(uint256 assets) internal { assets = bound(assets, 1, type(uint72).max); deal(address(USDC), address(this), assets); diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index 49798c0a..2c41b071 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -8,7 +8,7 @@ import { IUniswapV3Router } from "src/interfaces/IUniswapV3Router.sol"; import { IUniswapV2Router02 as IUniswapV2Router } from "src/interfaces/IUniswapV2Router02.sol"; import { MockERC20 } from "src/mocks/MockERC20.sol"; import { MockERC4626 } from "src/mocks/MockERC4626.sol"; -import { MockExchange } from "src/mocks/MockExchange.sol"; +import { MockExchange, MockPriceRouter } from "src/mocks/MockExchange.sol"; import { Test, console } from "@forge-std/Test.sol"; import { Math } from "src/utils/Math.sol"; @@ -18,6 +18,7 @@ contract CellarRouterTest is Test { MockERC20 private ABC; MockERC20 private XYZ; + MockPriceRouter private priceRouter; MockExchange private exchange; MockERC4626 private cellar; @@ -38,7 +39,8 @@ contract CellarRouterTest is Test { ERC20 private DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); function setUp() public { - exchange = new MockExchange(); + priceRouter = new MockPriceRouter(); + exchange = new MockExchange(priceRouter); router = new CellarRouter(IUniswapV3Router(address(exchange)), IUniswapV2Router(address(exchange))); forkedRouter = new CellarRouter(IUniswapV3Router(uniV3Router), IUniswapV2Router(uniV2Router)); @@ -47,8 +49,8 @@ contract CellarRouterTest is Test { XYZ = new MockERC20("XYZ", 18); // Set up exchange rates: - exchange.setExchangeRate(address(ABC), address(XYZ), 1e18); - exchange.setExchangeRate(address(XYZ), address(ABC), 1e18); + priceRouter.setExchangeRate(ERC20(address(ABC)), ERC20(address(XYZ)), 1e18); + priceRouter.setExchangeRate(ERC20(address(XYZ)), ERC20(address(ABC)), 1e18); // Set up two cellars: cellar = new MockERC4626(ERC20(address(ABC)), "ABC Cellar", "abcCLR", 18); From e098b8479c350f88abcc6815faefc10f46635eaf Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Wed, 22 Jun 2022 09:49:27 -0700 Subject: [PATCH 13/49] tests(Cellar): add test for withdrawFromPositions --- src/Errors.sol | 16 ++++-- src/base/Cellar.sol | 94 +++++++++++++++++++++++----------- src/mocks/MockCellar.sol | 31 +++++++----- src/modules/SwapRouter.sol | 26 ++++++++-- test/Cellar.t.sol | 100 ++++++++++++++++++++++++++++++------- 5 files changed, 202 insertions(+), 65 deletions(-) diff --git a/src/Errors.sol b/src/Errors.sol index 0083cf10..380056fc 100644 --- a/src/Errors.sol +++ b/src/Errors.sol @@ -44,7 +44,6 @@ error USR_InvalidSwap(address assetOut, address currentAsset); * @notice Attempted to sweep an asset that is managed by the cellar. * @param token address of the token that can't be sweeped */ -// TODO: change to ERC20 error USR_ProtectedAsset(address token); /** @@ -63,7 +62,6 @@ error USR_UnsupportedPosition(address unsupportedPosition); * @notice Attempted an operation on an untrusted position. * @param position address of the position */ -// TODO: change to ERC4626 error USR_UntrustedPosition(address position); /** @@ -77,7 +75,6 @@ error USR_TooManyDecimals(uint8 newDecimals, uint8 maxDecimals); * @notice Attempted set the cellar's asset to WETH with an asset that is not WETH compatible. * @param asset address of the asset that is not WETH compatible */ -// TODO: change to ERC20 error USR_AssetNotWETH(address asset); /** @@ -154,6 +151,19 @@ error USR_IncompatiblePosition(address incompatibleAsset, address expectedAsset) */ error USR_PositionAlreadyUsed(address position); +/** + * @notice Attempted an action on a position that is not being used by the cellar. + * @param position address of the invalid position + */ +error USR_InvalidPosition(address position); + +/** + * @notice Attempted an action on a position that is required to be empty before the action can be performed. + * @param position address of the non-empty position + * @param sharesRemaining amount of shares remaining in the position + */ +error USR_PositionNotEmpty(address position, uint256 sharesRemaining); + // ========================================== STATE ERRORS =========================================== /** diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index e8235676..3d260997 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -101,7 +101,8 @@ contract Cellar is ERC4626, Ownable, Multicall { address position = positions[index]; // Only remove position if it is empty. - if (ERC4626(position).balanceOf(address(this)) > 0) revert USR_PositionNotEmpty(position); + uint256 positionBalance = ERC4626(position).balanceOf(address(this)); + if (positionBalance > 0) revert USR_PositionNotEmpty(position, positionBalance); // Remove position at the given index. positions.remove(index); @@ -120,7 +121,8 @@ contract Cellar is ERC4626, Ownable, Multicall { address position = positions[index]; // Only remove position if it is empty. - if (ERC4626(position).balanceOf(address(this)) > 0) revert USR_PositionNotEmpty(position); + uint256 positionBalance = ERC4626(position).balanceOf(address(this)); + if (positionBalance > 0) revert USR_PositionNotEmpty(position, positionBalance); // Remove last position. positions.pop(); @@ -134,7 +136,8 @@ contract Cellar is ERC4626, Ownable, Multicall { address oldPosition = positions[index]; // Only remove position if it is empty. - if (ERC4626(oldPosition).balanceOf(address(this)) > 0) revert USR_PositionNotEmpty(oldPosition); + uint256 positionBalance = ERC4626(oldPosition).balanceOf(address(this)); + if (positionBalance > 0) revert USR_PositionNotEmpty(oldPosition, positionBalance); // Replace old position with new position. positions[index] = newPosition; @@ -387,7 +390,14 @@ contract Cellar is ERC4626, Ownable, Multicall { registry = _registry; positions = _positions; - for (uint256 i; i < _positions.length; i++) isTrusted[_positions[i]] = true; + for (uint256 i; i < _positions.length; i++) { + address position = _positions[i]; + + isTrusted[position] = true; + isPositionUsed[position] = true; + + ERC4626(position).asset().safeApprove(position, type(uint256).max); + } // Transfer ownership to the Gravity Bridge. transferOwnership(address(_registry.gravityBridge())); @@ -429,9 +439,13 @@ contract Cellar is ERC4626, Ownable, Multicall { ) { // Only withdraw if not enough assets in the holding pool. - if (assets > totalHoldings()) { + if (totalHoldings() > assets) { + receivedAssets = new ERC20[](1); + amountsOut = new uint256[](1); + receivedAssets[0] = asset; amountsOut[0] = assets; + shares = withdraw(assets, receiver, owner); } else { // Get data efficiently. @@ -454,10 +468,30 @@ contract Cellar is ERC4626, Ownable, Multicall { _burn(owner, shares); - (receivedAssets, amountsOut) = _pullFromPositions(assets, _positions, positionAssets, positionBalances); + (uint256[] memory amountsReceived, uint256 numOfReceivedAssets) = _pullFromPositions( + assets, + _positions, + positionAssets, + positionBalances + ); + + receivedAssets = new ERC20[](numOfReceivedAssets); + amountsOut = new uint256[](numOfReceivedAssets); + + uint256 j; + for (uint256 i; i < amountsReceived.length; i++) { + uint256 amountOut = amountsReceived[i]; - // Transfer withdrawn assets to the receiver. - for (uint256 i; i < receivedAssets.length; i++) receivedAssets[i].safeTransfer(receiver, amountsOut[i]); + if (amountOut == 0) continue; + + ERC20 positionAsset = positionAssets[i]; + receivedAssets[j] = positionAsset; + amountsOut[j] = amountOut; + j++; + + // Transfer withdrawn assets to the receiver. + positionAsset.safeTransfer(receiver, amountOut); + } emit WithdrawFromPositions(msg.sender, receiver, owner, receivedAssets, amountsOut, shares); } @@ -468,7 +502,9 @@ contract Cellar is ERC4626, Ownable, Multicall { ERC4626[] memory _positions, ERC20[] memory positionAssets, uint256[] memory positionBalances - ) internal returns (ERC20[] memory receivedAssets, uint256[] memory amountsOut) { + ) internal returns (uint256[] memory amountsReceived, uint256 numOfReceivedAssets) { + amountsReceived = new uint256[](_positions.length); + // Get the price router. PriceRouter priceRouter = registry.priceRouter(); @@ -491,9 +527,9 @@ contract Cellar is ERC4626, Ownable, Multicall { ? (assets.mulDivDown(onePositionAsset, positionAssetToAssetExchangeRate), 0) : (positionBalances[i], assets - totalPositionBalanceInAssets); - // Return the asset and amount that will be received. - amountsOut[amountsOut.length] = amount; - receivedAssets[receivedAssets.length] = positionAssets[i]; + // Return the amount that will be received and increment number of received assets. + amountsReceived[i] = amount; + numOfReceivedAssets++; // Update position balance. _subtractFromPositionBalance(getPositionData[address(_positions[i])], amount); @@ -691,14 +727,10 @@ contract Cellar is ERC4626, Ownable, Multicall { // =========================================== POSITION LOGIC =========================================== - // TODO: move to Errors.sol - error USR_InvalidPosition(address position); - error USR_PositionNotEmpty(address position); - /** * @notice Pushes assets in holdings into a position. * @param position address of the position to enter holdings into - * @param assets amount of assets to exit from the position + * @param assets amount of cellar assets to enter into the position */ function enterPosition( ERC4626 position, @@ -711,9 +743,8 @@ contract Cellar is ERC4626, Ownable, Multicall { // Swap from the holding pool asset if necessary. ERC20 denominationAsset = asset; - ERC20 positionAsset = position.asset(); - if (positionAsset != denominationAsset) - _swapForExactAssets(positionAsset, denominationAsset, assets, exchange, params); + if (position.asset() != denominationAsset) + assets = _swapExactAssets(denominationAsset, assets, exchange, params); // Update position balance. getPositionData[address(position)].balance += assets; @@ -725,16 +756,15 @@ contract Cellar is ERC4626, Ownable, Multicall { /** * @notice Pulls assets from a position back into holdings. * @param position address of the position to completely exit - * @param assets amount of assets to exit from the position - * @param params encoded arguments for the function that will perform the swap on the selected exchange + * @param balance amount of position's balance to pull and swap into the cellar's asset */ function exitPosition( ERC4626 position, - uint256 assets, + uint256 balance, SwapRouter.Exchanges exchange, bytes calldata params ) external onlyOwner { - _withdrawAndSwapFromPosition(position, asset, assets, exchange, params); + _withdrawAndSwapFromPosition(position, asset, balance, exchange, params); } /** @@ -875,7 +905,7 @@ contract Cellar is ERC4626, Ownable, Multicall { function _withdrawAndSwapFromPosition( ERC4626 position, ERC20 toAsset, - uint256 amount, + uint256 balance, SwapRouter.Exchanges exchange, bytes calldata params ) internal returns (uint256 amountOut) { @@ -883,14 +913,14 @@ contract Cellar is ERC4626, Ownable, Multicall { PositionData storage positionData = getPositionData[address(position)]; // Update position balance. - _subtractFromPositionBalance(positionData, amount); + _subtractFromPositionBalance(positionData, balance); // Withdraw from position. - position.withdraw(amount, address(this), address(this)); + position.withdraw(balance, address(this), address(this)); // Swap to the holding pool asset if necessary. ERC20 positionAsset = position.asset(); - amountOut = positionAsset != toAsset ? _swapExactAssets(positionAsset, amount, exchange, params) : amount; + amountOut = positionAsset != toAsset ? _swapExactAssets(positionAsset, balance, exchange, params) : balance; } function _subtractFromPositionBalance(PositionData storage positionData, uint256 amount) internal { @@ -917,7 +947,13 @@ contract Cellar is ERC4626, Ownable, Multicall { uint256[] memory positionBalances ) { - for (uint256 i; i < positions.length; i++) { + uint256 len = positions.length; + + _positions = new ERC4626[](len); + positionAssets = new ERC20[](len); + positionBalances = new uint256[](len); + + for (uint256 i; i < len; i++) { ERC4626 position = ERC4626(positions[i]); _positions[i] = position; diff --git a/src/mocks/MockCellar.sol b/src/mocks/MockCellar.sol index be3646e9..98628840 100644 --- a/src/mocks/MockCellar.sol +++ b/src/mocks/MockCellar.sol @@ -1,18 +1,25 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.13; -// import { Cellar, ERC20 } from "src/base/Cellar.sol"; +import { Cellar, Registry, ERC4626, ERC20 } from "src/base/Cellar.sol"; +import { Test, console } from "@forge-std/Test.sol"; -// contract MockCellar is Cellar { -// constructor( -// ERC20 _asset, -// string memory _name, -// string memory _symbol -// ) Cellar(_asset, _name, _symbol) {} +contract MockCellar is Cellar, Test { + constructor( + Registry _registry, + ERC20 _asset, + address[] memory _positions, + string memory _name, + string memory _symbol + ) Cellar(_registry, _asset, _positions, _name, _symbol) {} -// function totalAssets() public view override returns (uint256) { -// return asset.balanceOf(address(this)); -// } + function increasePositionBalance(address position, uint256 amount) external { + deal(address(ERC4626(position).asset()), address(this), amount); -// function accrue() public override {} -// } + // Update position balance. + getPositionData[position].balance += amount; + + // Deposit into position. + ERC4626(position).deposit(amount, address(this)); + } +} diff --git a/src/modules/SwapRouter.sol b/src/modules/SwapRouter.sol index 60972f20..8753ef9e 100644 --- a/src/modules/SwapRouter.sol +++ b/src/modules/SwapRouter.sol @@ -48,8 +48,22 @@ contract SwapRouter { // ======================================= SWAP OPERATIONS ======================================= function swapExactAssets(Exchanges id, bytes memory swapData) external returns (uint256 swapOutAmount) { - (bool success, bytes memory result) = address(this).call(abi.encodeWithSelector(idToSelector[id], swapData)); - require(success, "Failed to perform swap"); + (bool success, bytes memory result) = address(this).delegatecall( + abi.encodeWithSelector(idToSelector[id], swapData) + ); + + if (!success) { + // If there is return data, the call reverted with a reason or a custom error. + if (result.length > 0) { + assembly { + let returndata_size := mload(result) + revert(add(32, result), returndata_size) + } + } else { + revert("Execution reverted."); + } + } + swapOutAmount = abi.decode(result, (uint256)); } @@ -60,7 +74,10 @@ contract SwapRouter { swapData, (address[], uint256, uint256, address) ); + ERC20 assetIn = ERC20(path[0]); + assetIn.safeTransferFrom(msg.sender, address(this), assets); + // Approve assets to be swapped through the router. assetIn.safeApprove(address(uniswapV2Router), assets); @@ -72,13 +89,16 @@ contract SwapRouter { recipient, block.timestamp + 60 ); - swapOutAmount = amountsOut[1]; + + swapOutAmount = amountsOut[amountsOut.length - 1]; } function swapWithUniV3(bytes memory swapData) public returns (uint256 swapOutAmount) { (address[] memory path, uint24[] memory poolFees, uint256 assets, uint256 assetsOutMin, address recipient) = abi .decode(swapData, (address[], uint24[], uint256, uint256, address)); + ERC20 assetIn = ERC20(path[0]); + assetIn.safeTransferFrom(msg.sender, address(this), assets); // Approve assets to be swapped through the router. assetIn.safeApprove(address(uniswapV3Router), assets); diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 19063404..cd84a17e 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -1,24 +1,24 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.13; -import { Cellar, ERC4626, ERC20 } from "src/base/Cellar.sol"; +import { MockCellar, ERC4626, ERC20 } from "src/mocks/MockCellar.sol"; import { Registry, PriceRouter, SwapRouter, IGravity } from "src/Registry.sol"; import { IUniswapV2Router, IUniswapV3Router } from "src/modules/SwapRouter.sol"; +import { MockExchange } from "src/mocks/MockExchange.sol"; import { MockPriceRouter } from "src/mocks/MockPriceRouter.sol"; import { MockERC4626 } from "src/mocks/MockERC4626.sol"; import { MockGravity } from "src/mocks/MockGravity.sol"; -import { Test } from "@forge-std/Test.sol"; +import { Test, console } from "@forge-std/Test.sol"; import { Math } from "src/utils/Math.sol"; contract CellarTest is Test { using Math for uint256; - Cellar private cellar; + MockCellar private cellar; MockGravity private gravity; - IUniswapV2Router private constant uniswapV2Router = IUniswapV2Router(0xE592427A0AEce92De3Edee1F18E0157C05861564); - IUniswapV3Router private constant uniswapV3Router = IUniswapV3Router(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D); + MockExchange private exchange; MockPriceRouter private priceRouter; SwapRouter private swapRouter; @@ -44,29 +44,34 @@ contract CellarTest is Test { vm.label(address(wbtcCLR), "wbtcCLR"); // Setup Registry and modules: - swapRouter = new SwapRouter(uniswapV2Router, uniswapV3Router); priceRouter = new MockPriceRouter(); + exchange = new MockExchange(priceRouter); + swapRouter = new SwapRouter(IUniswapV2Router(address(exchange)), IUniswapV3Router(address(exchange))); gravity = new MockGravity(); - registry = new Registry(swapRouter, PriceRouter(address(priceRouter)), IGravity(address(gravity))); + registry = new Registry( + SwapRouter(address(swapRouter)), + PriceRouter(address(priceRouter)), + IGravity(address(gravity)) + ); // Setup exchange rates: // USDC Simulated Price: $1 // WETH Simulated Price: $2000 // WBTC Simulated Price: $30,000 - swapRouter.setExchangeRate(USDC, USDC, 1e6); - swapRouter.setExchangeRate(WETH, WETH, 1e18); - swapRouter.setExchangeRate(WBTC, WBTC, 1e8); + priceRouter.setExchangeRate(USDC, USDC, 1e6); + priceRouter.setExchangeRate(WETH, WETH, 1e18); + priceRouter.setExchangeRate(WBTC, WBTC, 1e8); - swapRouter.setExchangeRate(USDC, WETH, 0.0005e18); - swapRouter.setExchangeRate(WETH, USDC, 2000e6); + priceRouter.setExchangeRate(USDC, WETH, 0.0005e18); + priceRouter.setExchangeRate(WETH, USDC, 2000e6); - swapRouter.setExchangeRate(USDC, WBTC, 0.000033e8); - swapRouter.setExchangeRate(WBTC, USDC, 30_000e6); + priceRouter.setExchangeRate(USDC, WBTC, 0.000033e8); + priceRouter.setExchangeRate(WBTC, USDC, 30_000e6); - swapRouter.setExchangeRate(WETH, WBTC, 0.06666666e8); - swapRouter.setExchangeRate(WBTC, WETH, 15e18); + priceRouter.setExchangeRate(WETH, WBTC, 0.06666666e8); + priceRouter.setExchangeRate(WBTC, WETH, 15e18); // Setup Cellar: address[] memory positions = new address[](3); @@ -74,13 +79,18 @@ contract CellarTest is Test { positions[1] = address(wethCLR); positions[2] = address(wbtcCLR); - cellar = new Cellar(registry, USDC, positions, "Multiposition Cellar LP Token", "multiposition-CLR"); + cellar = new MockCellar(registry, USDC, positions, "Multiposition Cellar LP Token", "multiposition-CLR"); vm.label(address(cellar), "cellar"); // Transfer ownership to this contract for testing. vm.prank(address(registry.gravityBridge())); cellar.transferOwnership(address(this)); + // Mint enough liquidity to swap router for swaps. + deal(address(USDC), address(exchange), type(uint224).max); + deal(address(WETH), address(exchange), type(uint224).max); + deal(address(WBTC), address(exchange), type(uint224).max); + // Approve cellar to spend all assets. USDC.approve(address(cellar), type(uint256).max); WETH.approve(address(cellar), type(uint256).max); @@ -89,7 +99,7 @@ contract CellarTest is Test { // ========================================= DEPOSIT/WITHDRAW TEST ========================================= - function testDepositAndWithdraw(uint256 assets) internal { + function testDepositAndWithdraw(uint256 assets) external { assets = bound(assets, 1, type(uint72).max); deal(address(USDC), address(this), assets); @@ -114,4 +124,58 @@ contract CellarTest is Test { assertEq(cellar.convertToAssets(cellar.balanceOf(address(this))), 0, "Should return zero assets."); assertEq(USDC.balanceOf(address(this)), assets, "Should have withdrawn assets to user."); } + + function testWithdrawFromPositions() external { + cellar.increasePositionBalance(address(wethCLR), 1e18); + + assertEq(cellar.totalAssets(), 2000e6, "Should have updated total assets with assets deposited."); + + // Mint shares to user to redeem. + deal(address(cellar), address(this), cellar.previewWithdraw(1000e6)); + + // Withdraw from position. + (uint256 shares, ERC20[] memory receivedAssets, uint256[] memory amountsOut) = cellar.withdrawFromPositions( + 1000e6, + address(this), + address(this) + ); + + assertEq(cellar.balanceOf(address(this)), 0, "Should have redeemed all shares."); + assertEq(shares, 1000e18, "Should returned all redeemed shares."); + assertEq(receivedAssets.length, 1, "Should have received one asset."); + assertEq(amountsOut.length, 1, "Should have gotten out one amount."); + assertEq(address(receivedAssets[0]), address(WETH), "Should have received WETH."); + assertEq(amountsOut[0], 0.5e18, "Should have gotten out 0.5 WETH."); + assertEq(WETH.balanceOf(address(this)), 0.5e18, "Should have transferred position balance to user."); + assertEq(cellar.totalAssets(), 1000e6, "Should have updated cellar total assets."); + } + + function testWithdrawFromPositionsCompletely() external { + cellar.increasePositionBalance(address(wethCLR), 1e18); + cellar.increasePositionBalance(address(wbtcCLR), 1e8); + + assertEq(cellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); + + // Mint shares to user to redeem. + deal(address(cellar), address(this), cellar.previewWithdraw(32_000e6)); + + // Withdraw from position. + (uint256 shares, ERC20[] memory receivedAssets, uint256[] memory amountsOut) = cellar.withdrawFromPositions( + 32_000e6, + address(this), + address(this) + ); + + assertEq(cellar.balanceOf(address(this)), 0, "Should have redeemed all shares."); + assertEq(shares, 32_000e18, "Should returned all redeemed shares."); + assertEq(receivedAssets.length, 2, "Should have received two assets."); + assertEq(amountsOut.length, 2, "Should have gotten out two amount."); + assertEq(address(receivedAssets[0]), address(WETH), "Should have received WETH."); + assertEq(address(receivedAssets[1]), address(WBTC), "Should have received WBTC."); + assertEq(amountsOut[0], 1e18, "Should have gotten out 1 WETH."); + assertEq(amountsOut[1], 1e8, "Should have gotten out 1 WBTC."); + assertEq(WETH.balanceOf(address(this)), 1e18, "Should have transferred position balance to user."); + assertEq(WBTC.balanceOf(address(this)), 1e8, "Should have transferred position balance to user."); + assertEq(cellar.totalAssets(), 0, "Should have emptied cellar."); + } } From 3e1dcb07f55e161ff8cd914655cfdaeb13a793c8 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Thu, 23 Jun 2022 12:35:22 -0700 Subject: [PATCH 14/49] build: change remaining 0.8.13 to 0.8.15 --- .vscode/settings.json | 2 +- src/Registry.sol | 2 +- src/base/Cellar.sol | 2 +- src/mocks/MockCellar.sol | 2 +- src/mocks/MockMultipositionCellar.sol | 2 +- src/mocks/MockPriceRouter.sol | 2 +- src/mocks/MockWETH.sol | 2 +- src/modules/PriceRouter.sol | 2 +- src/modules/SwapRouter.sol | 2 +- src/utils/AddressArray.sol | 2 +- test/Cellar.t.sol | 2 +- test/PriceRouter.t.sol | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a9bb54b..f996eae1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "solidity.packageDefaultDependenciesContractsDirectory": "src", "solidity.packageDefaultDependenciesDirectory": "lib", - "solidity.compileUsingRemoteVersion": "v0.8.13", + "solidity.compileUsingRemoteVersion": "v0.8.15", "search.exclude": { "lib": true }, "editor.formatOnSave": true, "solidity.formatter": "prettier", diff --git a/src/Registry.sol b/src/Registry.sol index 650defb1..916ebcb8 100644 --- a/src/Registry.sol +++ b/src/Registry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; +pragma solidity 0.8.15; import { ERC20 } from "@solmate/tokens/ERC20.sol"; import { SwapRouter } from "./modules/SwapRouter.sol"; diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 3d260997..6513c7f5 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; +pragma solidity 0.8.15; import { ERC4626, ERC20 } from "./ERC4626.sol"; import { Multicall } from "./Multicall.sol"; diff --git a/src/mocks/MockCellar.sol b/src/mocks/MockCellar.sol index 98628840..e2e6e9df 100644 --- a/src/mocks/MockCellar.sol +++ b/src/mocks/MockCellar.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; +pragma solidity 0.8.15; import { Cellar, Registry, ERC4626, ERC20 } from "src/base/Cellar.sol"; import { Test, console } from "@forge-std/Test.sol"; diff --git a/src/mocks/MockMultipositionCellar.sol b/src/mocks/MockMultipositionCellar.sol index 054e9ab7..c6268a85 100644 --- a/src/mocks/MockMultipositionCellar.sol +++ b/src/mocks/MockMultipositionCellar.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; +pragma solidity 0.8.15; // import { MultipositionCellar } from "src/base/MultipositionCellar.sol"; // import { ERC20 } from "@solmate/tokens/ERC20.sol"; diff --git a/src/mocks/MockPriceRouter.sol b/src/mocks/MockPriceRouter.sol index 39b212cf..bacc0999 100644 --- a/src/mocks/MockPriceRouter.sol +++ b/src/mocks/MockPriceRouter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; +pragma solidity 0.8.15; import { MockExchange } from "src/mocks/MockExchange.sol"; import { ERC20 } from "@solmate/tokens/ERC20.sol"; diff --git a/src/mocks/MockWETH.sol b/src/mocks/MockWETH.sol index 9a07f83e..42f2f1ae 100644 --- a/src/mocks/MockWETH.sol +++ b/src/mocks/MockWETH.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; +pragma solidity 0.8.15; import { WETH } from "@solmate/tokens/WETH.sol"; diff --git a/src/modules/PriceRouter.sol b/src/modules/PriceRouter.sol index 1ca79631..58155dcf 100644 --- a/src/modules/PriceRouter.sol +++ b/src/modules/PriceRouter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.13; +pragma solidity 0.8.15; import { ERC20 } from "@solmate/tokens/ERC20.sol"; import { Registry } from "../Registry.sol"; diff --git a/src/modules/SwapRouter.sol b/src/modules/SwapRouter.sol index 8753ef9e..80924262 100644 --- a/src/modules/SwapRouter.sol +++ b/src/modules/SwapRouter.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; +pragma solidity 0.8.15; import { ERC20 } from "@solmate/tokens/ERC20.sol"; import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; diff --git a/src/utils/AddressArray.sol b/src/utils/AddressArray.sol index 965dd2c2..43c22c31 100644 --- a/src/utils/AddressArray.sol +++ b/src/utils/AddressArray.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; +pragma solidity 0.8.15; // TODO: add natspec diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index cd84a17e..0a21fa28 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; +pragma solidity 0.8.15; import { MockCellar, ERC4626, ERC20 } from "src/mocks/MockCellar.sol"; import { Registry, PriceRouter, SwapRouter, IGravity } from "src/Registry.sol"; diff --git a/test/PriceRouter.t.sol b/test/PriceRouter.t.sol index b0f781b4..d13693a4 100644 --- a/test/PriceRouter.t.sol +++ b/test/PriceRouter.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; +pragma solidity 0.8.15; import { PriceRouter, Registry, ERC20 } from "src/modules/PriceRouter.sol"; import { MockGravity } from "src/mocks/MockGravity.sol"; From 6b80a03bbbb2805d4e068e17768c27b363e1bb71 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Thu, 23 Jun 2022 13:19:38 -0700 Subject: [PATCH 15/49] fix(CellarRouter): withdrawFromPositionsIntoSingleAsset failed compiling with stack too deep errors, memory vars removed to fix it --- src/CellarRouter.sol | 57 ++++++++++++++++++++++++++++---- src/Errors.sol | 15 +++------ src/interfaces/ICellarRouter.sol | 12 +++---- test/CellarRouter.t.sol | 12 +++---- 4 files changed, 67 insertions(+), 29 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index 3dd9183a..6f7fe53d 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.15; import { ERC20 } from "@solmate/tokens/ERC20.sol"; import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; -import { ERC4626 } from "./base/ERC4626.sol"; +import { Cellar } from "./base/Cellar.sol"; import { IUniswapV3Router } from "./interfaces/IUniswapV3Router.sol"; import { IUniswapV2Router02 as IUniswapV2Router } from "./interfaces/IUniswapV2Router02.sol"; import { ICellarRouter } from "./interfaces/ICellarRouter.sol"; @@ -45,7 +45,7 @@ contract CellarRouter is ICellarRouter { * @return shares amount of shares minted */ function depositIntoCellarWithPermit( - ERC4626 cellar, + Cellar cellar, uint256 assets, address receiver, uint256 deadline, @@ -84,7 +84,7 @@ contract CellarRouter is ICellarRouter { * @return shares amount of shares minted */ function depositAndSwapIntoCellar( - ERC4626 cellar, + Cellar cellar, address[] calldata path, uint24[] calldata poolFees, uint256 assets, @@ -126,7 +126,7 @@ contract CellarRouter is ICellarRouter { * @return shares amount of shares minted */ function depositAndSwapIntoCellarWithPermit( - ERC4626 cellar, + Cellar cellar, address[] calldata path, uint24[] calldata poolFees, uint256 assets, @@ -167,7 +167,7 @@ contract CellarRouter is ICellarRouter { * @return shares amount of shares burned */ function withdrawAndSwapFromCellar( - ERC4626 cellar, + Cellar cellar, address[] calldata path, uint24[] calldata poolFees, uint256 assets, @@ -206,7 +206,7 @@ contract CellarRouter is ICellarRouter { * @return shares amount of shares burned */ function withdrawAndSwapFromCellarWithPermit( - ERC4626 cellar, + Cellar cellar, address[] calldata path, uint24[] calldata poolFees, uint256 assets, @@ -223,6 +223,51 @@ contract CellarRouter is ICellarRouter { shares = withdrawAndSwapFromCellar(cellar, path, poolFees, assets, assetsOutMin, receiver); } + /** + * @notice Withdraws from a multi assset cellar and then performs swaps to another desired asset, if the + * withdrawn asset is not already, using permit. + * @dev If using Uniswap V3 for swap, must specify the pool fee tier to use for each swap. For + * example, if there are "n" addresses in path, there should be "n-1" values specifying the + * fee tiers of each pool used for each swap. The current possible pool fee tiers for + * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap + * V2, leave pool fees empty to use Uniswap V2 for swap. + * @param cellar address of the cellar + * @param paths array of arrays of [token1, token2, token3] that specifies the swap path on swap + * @param poolFees array of amounts out of 1e4 (eg. 10000 == 1%) that represents the fee tier to use for each swap + * @param assets amount of assets to withdraw + * @param assetsOutMins array of minimum amounts of assets received from swaps + * @param receiver address receiving the assets + * @return shares amount of shares burned + */ + function withdrawFromPositionsIntoSingleAsset( + Cellar cellar, + address[][] calldata paths, + uint24[][] calldata poolFees, + uint256 assets, + uint256[] calldata assetsOutMins, //technicallly couldn't this be one value, then pass in zero for all the min amounts, and do a final check at the end? + address receiver + ) public returns (uint256 shares) { + ERC20 assetOut = ERC20(paths[0][paths.length - 1]); // get the last asset from the first path + require(paths.length == poolFees.length && paths.length == assetsOutMins.length, "Array length mismatch"); + + // Withdraw assets from the cellar. + ERC20[] memory receivedAssets; + uint256[] memory amountsOut; + (shares, receivedAssets, amountsOut) = cellar.withdrawFromPositions(assets, address(this), msg.sender); + assets = 0; //zero out for use in for loop + for (uint256 i = 0; i < paths.length; i++) { + if (receivedAssets[i] == ERC20(paths[i][paths[i].length - 1])) { + assets += amountsOut[i]; // asset is already in desired asset + continue; //no need to swap + } + require(assetOut == ERC20(paths[i][paths[i].length - 1]), "Paths have different ends"); + assets += _swap(paths[i], poolFees[i], amountsOut[i], assetsOutMins[i]); + } + + // Transfer assets from the router to the receiver. + assetOut.safeTransfer(receiver, assets); + } + // ========================================= HELPER FUNCTIONS ========================================= /** diff --git a/src/Errors.sol b/src/Errors.sol index 6ac4acd1..ff5c176c 100644 --- a/src/Errors.sol +++ b/src/Errors.sol @@ -139,11 +139,11 @@ error USR_ZeroRewardsPerEpoch(); error USR_InvalidLockValue(uint256 lock); /** - * @notice Attempted to trust a position that had an incompatible underlying asset. - * @param incompatibleAsset address of the asset is incompatible with the asset of this cellar - * @param expectedAsset address of the cellar's underlying asset + * @notice The caller attempted an signed action with an invalid signature. + * @param signatureLength length of the signature passed in + * @param expectedSignatureLength expected length of the signature passed in */ -error USR_IncompatiblePosition(address incompatibleAsset, address expectedAsset); +error USR_InvalidSignature(uint256 signatureLength, uint256 expectedSignatureLength); /** * @notice Attempted to add a position that is already being used. @@ -164,13 +164,6 @@ error USR_InvalidPosition(address position); */ error USR_PositionNotEmpty(address position, uint256 sharesRemaining); -/** - * @notice The caller attempted an signed action with an invalid signature. - * @param signatureLength length of the signature passed in - * @param expectedSignatureLength expected length of the signature passed in - */ -error USR_InvalidSignature(uint256 signatureLength, uint256 expectedSignatureLength); - // ========================================== STATE ERRORS =========================================== /** diff --git a/src/interfaces/ICellarRouter.sol b/src/interfaces/ICellarRouter.sol index 9861e5e3..02bb9ccd 100644 --- a/src/interfaces/ICellarRouter.sol +++ b/src/interfaces/ICellarRouter.sol @@ -2,13 +2,13 @@ pragma solidity 0.8.15; import { ERC20 } from "@solmate/tokens/ERC20.sol"; -import { ERC4626 } from "src/base/ERC4626.sol"; +import { Cellar } from "src/base/Cellar.sol"; interface ICellarRouter { // ======================================= ROUTER OPERATIONS ======================================= function depositIntoCellarWithPermit( - ERC4626 cellar, + Cellar cellar, uint256 assets, address receiver, uint256 deadline, @@ -16,7 +16,7 @@ interface ICellarRouter { ) external returns (uint256 shares); function depositAndSwapIntoCellar( - ERC4626 cellar, + Cellar cellar, address[] calldata path, uint24[] calldata poolFees, uint256 assets, @@ -25,7 +25,7 @@ interface ICellarRouter { ) external returns (uint256 shares); function depositAndSwapIntoCellarWithPermit( - ERC4626 cellar, + Cellar cellar, address[] calldata path, uint24[] calldata poolFees, uint256 assets, @@ -36,7 +36,7 @@ interface ICellarRouter { ) external returns (uint256 shares); function withdrawAndSwapFromCellar( - ERC4626 cellar, + Cellar cellar, address[] calldata path, uint24[] calldata poolFees, uint256 assets, @@ -45,7 +45,7 @@ interface ICellarRouter { ) external returns (uint256 shares); function withdrawAndSwapFromCellarWithPermit( - ERC4626 cellar, + Cellar cellar, address[] calldata path, uint24[] calldata poolFees, uint256 assets, diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index c5cd2d7f..a8bb0bfa 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.15; import { ERC20 } from "@solmate/tokens/ERC20.sol"; -import { ERC4626 } from "src/base/ERC4626.sol"; +import { Cellar } from "src/base/Cellar.sol"; import { CellarRouter } from "src/CellarRouter.sol"; import { IUniswapV3Router } from "src/interfaces/IUniswapV3Router.sol"; import { IUniswapV2Router02 as IUniswapV2Router } from "src/interfaces/IUniswapV2Router02.sol"; @@ -77,7 +77,7 @@ contract CellarRouterTest is Test { vm.startPrank(owner); XYZ.approve(address(router), assets); XYZ.mint(owner, assets); - uint256 shares = router.depositAndSwapIntoCellar(ERC4626(address(cellar)), path, poolFees, assets, 0, owner); + uint256 shares = router.depositAndSwapIntoCellar(Cellar(address(cellar)), path, poolFees, assets, 0, owner); vm.stopPrank(); // Assets received by the cellar will be different from the amount of assets a user attempted @@ -114,7 +114,7 @@ contract CellarRouterTest is Test { deal(address(DAI), owner, assets, true); DAI.approve(address(forkedRouter), assets); uint256 shares = forkedRouter.depositAndSwapIntoCellar( - ERC4626(address(forkedCellar)), + Cellar(address(forkedCellar)), path, poolFees, assets, @@ -162,7 +162,7 @@ contract CellarRouterTest is Test { deal(address(DAI), owner, assets, true); DAI.approve(address(forkedRouter), assets); uint256 shares = forkedRouter.depositAndSwapIntoCellar( - ERC4626(address(forkedCellar)), + Cellar(address(forkedCellar)), path, poolFees, assets, @@ -210,7 +210,7 @@ contract CellarRouterTest is Test { vm.startPrank(owner); XYZ.approve(address(router), assets); XYZ.mint(owner, assets); - router.depositAndSwapIntoCellar(ERC4626(address(cellar)), path, poolFees, assets, 0, owner); + router.depositAndSwapIntoCellar(Cellar(address(cellar)), path, poolFees, assets, 0, owner); // Assets received by the cellar will be different from the amount of assets a user attempted // to deposit due to slippage swaps. @@ -222,7 +222,7 @@ contract CellarRouterTest is Test { // Test withdraw and swap. cellar.approve(address(router), assetsReceivedAfterDeposit); uint256 sharesRedeemed = router.withdrawAndSwapFromCellar( - ERC4626(address(cellar)), + Cellar(address(cellar)), path, poolFees, assetsReceivedAfterDeposit, From 8be75adbb4eea77a25a0fdc04c480f9de6fbb758 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Fri, 24 Jun 2022 09:48:19 -0700 Subject: [PATCH 16/49] test(CellarRouter): add tests to Cellar.t.sol to check that withdrawing form multiple positions into one position works --- src/CellarRouter.sol | 5 +-- test/Cellar.t.sol | 74 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index 6f7fe53d..7708160c 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -247,13 +247,14 @@ contract CellarRouter is ICellarRouter { uint256[] calldata assetsOutMins, //technicallly couldn't this be one value, then pass in zero for all the min amounts, and do a final check at the end? address receiver ) public returns (uint256 shares) { - ERC20 assetOut = ERC20(paths[0][paths.length - 1]); // get the last asset from the first path - require(paths.length == poolFees.length && paths.length == assetsOutMins.length, "Array length mismatch"); + ERC20 assetOut = ERC20(paths[0][paths[0].length - 1]); // get the last asset from the first path + require(paths.length == assetsOutMins.length, "Array length mismatch"); // Withdraw assets from the cellar. ERC20[] memory receivedAssets; uint256[] memory amountsOut; (shares, receivedAssets, amountsOut) = cellar.withdrawFromPositions(assets, address(this), msg.sender); + assets = 0; //zero out for use in for loop for (uint256 i = 0; i < paths.length; i++) { if (receivedAssets[i] == ERC20(paths[i][paths[i].length - 1])) { diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 0a21fa28..15935969 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -9,6 +9,8 @@ import { MockPriceRouter } from "src/mocks/MockPriceRouter.sol"; import { MockERC4626 } from "src/mocks/MockERC4626.sol"; import { MockGravity } from "src/mocks/MockGravity.sol"; +import { CellarRouter } from "src/CellarRouter.sol"; + import { Test, console } from "@forge-std/Test.sol"; import { Math } from "src/utils/Math.sol"; @@ -33,6 +35,13 @@ contract CellarTest is Test { ERC20 private WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); MockERC4626 private wbtcCLR; + //========================= CRISPY TEMPORARY ========================== + // Mainnet contracts: + address private constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + address private constant uniV2Router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + ERC20 private DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + CellarRouter private cellarRouter; + function setUp() external { usdcCLR = new MockERC4626(USDC, "USDC Cellar LP Token", "USDC-CLR", 6); vm.label(address(usdcCLR), "usdcCLR"); @@ -95,6 +104,8 @@ contract CellarTest is Test { USDC.approve(address(cellar), type(uint256).max); WETH.approve(address(cellar), type(uint256).max); WBTC.approve(address(cellar), type(uint256).max); + + cellarRouter = new CellarRouter(IUniswapV3Router(address(exchange)), IUniswapV2Router(address(exchange))); } // ========================================= DEPOSIT/WITHDRAW TEST ========================================= @@ -178,4 +189,67 @@ contract CellarTest is Test { assertEq(WBTC.balanceOf(address(this)), 1e8, "Should have transferred position balance to user."); assertEq(cellar.totalAssets(), 0, "Should have emptied cellar."); } + + function testWithdrawFromPositionsIntoSingleAssetWTwoSwaps() external { + cellar.increasePositionBalance(address(wethCLR), 1e18); + cellar.increasePositionBalance(address(wbtcCLR), 1e8); + + assertEq(cellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); + + // Mint shares to user to redeem. + deal(address(cellar), address(this), cellar.previewWithdraw(32_000e6)); + + //create paths + address[][] memory paths = new address[][](2); + paths[0] = new address[](2); + paths[0][0] = address(WETH); + paths[0][1] = address(USDC); + paths[1] = new address[](2); + paths[1][0] = address(WBTC); + paths[1][1] = address(USDC); + uint24[][] memory poolFees = new uint24[][](2); + poolFees[0] = new uint24[](0); + poolFees[1] = new uint24[](0); + uint256 assets = 32_000e6; + uint256[] memory minOuts = new uint256[](2); + minOuts[0] = 0; + minOuts[1] = 0; + + cellar.approve(address(cellarRouter), type(uint256).max); + cellarRouter.withdrawFromPositionsIntoSingleAsset(cellar, paths, poolFees, assets, minOuts, address(this)); + + assertEq(USDC.balanceOf(address(this)), 30_400e6, "Did not recieve expected assets"); + } + + /** + * @notice if the asset wanted is an asset given, then it should just be added to the output with no swaps needed + */ + function testWithdrawFromPositionsIntoSingleAssetWOneSwap() external { + cellar.increasePositionBalance(address(wethCLR), 1e18); + cellar.increasePositionBalance(address(wbtcCLR), 1e8); + + assertEq(cellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); + + // Mint shares to user to redeem. + deal(address(cellar), address(this), cellar.previewWithdraw(32_000e6)); + + //create paths + address[][] memory paths = new address[][](2); + paths[0] = new address[](1); + paths[0][0] = address(WETH); + paths[1] = new address[](2); + paths[1][0] = address(WBTC); + paths[1][1] = address(WETH); + uint24[][] memory poolFees = new uint24[][](2); + poolFees[0] = new uint24[](0); + poolFees[1] = new uint24[](0); + uint256 assets = 32_000e6; + uint256[] memory minOuts = new uint256[](2); + minOuts[0] = 0; + minOuts[1] = 0; + + cellar.approve(address(cellarRouter), type(uint256).max); + cellarRouter.withdrawFromPositionsIntoSingleAsset(cellar, paths, poolFees, assets, minOuts, address(this)); + assertEq(WETH.balanceOf(address(this)), 15.25e18, "Did not recieve expected assets"); + } } From 70a02d23a1e5094f2ce07baf7e9cfcec52c12d97 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Fri, 24 Jun 2022 14:22:50 -0700 Subject: [PATCH 17/49] style(SwapRouter): removed unused code --- src/modules/swap-router/SwapRouter.sol | 33 -------------------------- 1 file changed, 33 deletions(-) diff --git a/src/modules/swap-router/SwapRouter.sol b/src/modules/swap-router/SwapRouter.sol index 86c4c8e3..dc0ae91e 100644 --- a/src/modules/swap-router/SwapRouter.sol +++ b/src/modules/swap-router/SwapRouter.sol @@ -76,39 +76,6 @@ contract SwapRouter { amountOut = abi.decode(result, (uint256)); } - //in the cellar - - function doExcecute(address adaptor, bytes memory adaptorData) public { - //require adaptor is approved - (bool success, bytes memory result) = adaptor.call( - adaptorData - ); - IAdaptor(adaptor).execute(adaptorData) - } - - //in adaptors - function execute( bytes memory adaptorData) external returns (uint256 amountOut) { - // Route swap call to appropriate function using selector. - (bool success, bytes memory result) = address(this).call( - abi.encodeWithSelector(getExchangeSelector[exchange], swapData) - ); - - if (!success) { - // If there is return data, the call reverted with a reason or a custom error so we - // bubble up the error message. - if (result.length > 0) { - assembly { - let returndata_size := mload(result) - revert(add(32, result), returndata_size) - } - } else { - revert("Swap reverted."); - } - } - - amountOut = abi.decode(result, (uint256)); - } - /** * @notice Allows caller to make swaps using the UniswapV2 Exchange. * @param swapData bytes variable storing the following swap information: From de4272a8b3115b2f631105119d540f903a3d0272 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Mon, 27 Jun 2022 09:31:16 -0700 Subject: [PATCH 18/49] feat(SwapRouter): add `multiSwap` function to allow for swaps at multiple exchanges --- src/modules/swap-router/SwapRouter.sol | 3 +- test/SwapRouter.t.sol | 86 ++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/modules/swap-router/SwapRouter.sol b/src/modules/swap-router/SwapRouter.sol index 7315c78f..e47f8e4f 100644 --- a/src/modules/swap-router/SwapRouter.sol +++ b/src/modules/swap-router/SwapRouter.sol @@ -6,6 +6,8 @@ import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; import { IUniswapV2Router02 as IUniswapV2Router } from "src/interfaces/IUniswapV2Router02.sol"; import { IUniswapV3Router } from "src/interfaces/IUniswapV3Router.sol"; +import { Test, console } from "@forge-std/Test.sol"; + contract SwapRouter { using SafeTransferLib for ERC20; @@ -47,7 +49,6 @@ contract SwapRouter { } // ======================================= SWAP OPERATIONS ======================================= - //TODO add array of exchanges and array of swap data /** * @notice Route swap calls to the appropriate exchanges. * @param exchange value dictating which exchange to use to make the swap diff --git a/test/SwapRouter.t.sol b/test/SwapRouter.t.sol index 3c4059f2..3d256f17 100644 --- a/test/SwapRouter.t.sol +++ b/test/SwapRouter.t.sol @@ -34,7 +34,7 @@ contract SwapRouterTest is Test { // ======================================= SWAP TESTS ======================================= - function testSimpleSwapV2(uint256 assets) external { + function testSimplePathSwapV2(uint256 assets) external { // Ignore if not on mainnet. if (block.chainid != 1) return; @@ -56,7 +56,7 @@ contract SwapRouterTest is Test { assertEq(out, WETH.balanceOf(reciever), "Amount Out should equal WETH Balance of reciever"); } - function testMultiSwapV2(uint256 assets) external { + function testMultiPathSwapV2(uint256 assets) external { // Ignore if not on mainnet. if (block.chainid != 1) return; @@ -79,7 +79,7 @@ contract SwapRouterTest is Test { assertEq(out, USDC.balanceOf(reciever), "Amount Out should equal USDC Balance of reciever"); } - function testSimpleSwapV3(uint256 assets) external { + function testSimplePathSwapV3(uint256 assets) external { // Ignore if not on mainnet. if (block.chainid != 1) return; @@ -105,7 +105,7 @@ contract SwapRouterTest is Test { assertEq(out, WETH.balanceOf(reciever), "Amount Out should equal WETH Balance of reciever"); } - function testMultiSwapV3(uint256 assets) external { + function testMultiPathSwapV3(uint256 assets) external { // Ignore if not on mainnet. if (block.chainid != 1) return; @@ -132,4 +132,82 @@ contract SwapRouterTest is Test { assertTrue(USDC.balanceOf(reciever) > 0, "USDC Balance of Reciever should be greater than 0"); assertEq(out, USDC.balanceOf(reciever), "Amount Out should equal USDC Balance of reciever"); } + + function testMultiSwapV2(uint256 assets) external { + // Ignore if not on mainnet. + if (block.chainid != 1) return; + + assets = bound(assets, 1e18, type(uint96).max); + + // Specify the swap path. + address[] memory path = new address[](2); + path[0] = address(DAI); + path[1] = address(WETH); + + // Test swap. + deal(address(DAI), sender, 2 * assets, true); + DAI.approve(address(swapRouter), 2 * assets); + bytes memory swapData = abi.encode(path, assets, 0, reciever, sender); + + SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](2); + exchanges[0] = SwapRouter.Exchange.UNIV2; + exchanges[1] = SwapRouter.Exchange.UNIV2; + + bytes[] memory multiSwapData = new bytes[](2); + multiSwapData[0] = swapData; + multiSwapData[1] = swapData; + + uint256[] memory amountsOut = swapRouter.multiSwap(exchanges, multiSwapData); + uint256 sum; + for (uint256 i = 0; i < 2; i++) { + sum += amountsOut[i]; + } + + assertTrue(DAI.balanceOf(sender) == 0, "DAI Balance of sender should be 0"); + assertTrue(WETH.balanceOf(reciever) > 0, "WETH Balance of Reciever should be greater than 0"); + assertEq(sum, WETH.balanceOf(reciever), "Amount Out should equal WETH Balance of reciever"); + } + + ///@dev makes three swaps using multiSwap, first 2 on UNIV2, and the last one on UNIV3 + function testMultiSwapV2V3(uint256 assets) external { + // Ignore if not on mainnet. + if (block.chainid != 1) return; + + assets = bound(assets, 1e18, type(uint96).max); + + // Specify the swap path. + address[] memory path = new address[](2); + path[0] = address(DAI); + path[1] = address(WETH); + + // Test swap. + deal(address(DAI), sender, 3 * assets, true); + DAI.approve(address(swapRouter), 3 * assets); + bytes memory swapData = abi.encode(path, assets, 0, reciever, sender); + + SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](3); + exchanges[0] = SwapRouter.Exchange.UNIV2; + exchanges[1] = SwapRouter.Exchange.UNIV2; + exchanges[2] = SwapRouter.Exchange.UNIV3; + + bytes[] memory multiSwapData = new bytes[](3); + multiSwapData[0] = swapData; + multiSwapData[1] = swapData; + + // Alter swap data to work with UniV3 + uint24[] memory poolFees = new uint24[](1); + poolFees[0] = 3000; + swapData = abi.encode(path, poolFees, assets, 0, reciever, sender); + multiSwapData[2] = swapData; + + uint256[] memory amountsOut = swapRouter.multiSwap(exchanges, multiSwapData); + uint256 sum; + for (uint256 i = 0; i < 3; i++) { + sum += amountsOut[i]; + } + + assertTrue(DAI.balanceOf(sender) == 0, "DAI Balance of sender should be 0"); + assertTrue(WETH.balanceOf(reciever) > 0, "WETH Balance of Reciever should be greater than 0"); + assertEq(sum, WETH.balanceOf(reciever), "Amount Out should equal WETH Balance of reciever"); + } } From c83e52b509b7a4b49ac45563d0f0e7370f64f20e Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Mon, 27 Jun 2022 09:46:25 -0700 Subject: [PATCH 19/49] docs(CellarRouter): updated natspec for `withdrawFromPositionsIntoSingleAsset` --- src/CellarRouter.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index 9f2b7261..6b84350e 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -225,7 +225,7 @@ contract CellarRouter is ICellarRouter { /** * @notice Withdraws from a multi assset cellar and then performs swaps to another desired asset, if the - * withdrawn asset is not already, using permit. + * withdrawn asset is not already. * @dev If using Uniswap V3 for swap, must specify the pool fee tier to use for each swap. For * example, if there are "n" addresses in path, there should be "n-1" values specifying the * fee tiers of each pool used for each swap. The current possible pool fee tiers for @@ -234,9 +234,9 @@ contract CellarRouter is ICellarRouter { * @param cellar address of the cellar * @param paths array of arrays of [token1, token2, token3] that specifies the swap path on swap, * if paths[i].length is 1, then no swap will be made - * @param poolFees array of amounts out of 1e4 (eg. 10000 == 1%) that represents the fee tier to use for each swap + * @param poolFees array of arrays of amounts out of 1e4 (eg. 10000 == 1%) that represents the fee tier to use for each swap * @param assets amount of assets to withdraw - * @param assetsIn description + * @param assetsIn array of amounts in for each swap, allows caller to swap the same asset at multiple different exchanges * @param assetsOutMins array of minimum amounts of assets received from swaps * @param receiver address receiving the assets * @return shares amount of shares burned From 955af79dcd054c59b4dd33b6401452eabd85c1d4 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Mon, 27 Jun 2022 10:22:17 -0700 Subject: [PATCH 20/49] style(SwapRouter): remove unused console and test import --- src/modules/swap-router/SwapRouter.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/swap-router/SwapRouter.sol b/src/modules/swap-router/SwapRouter.sol index e47f8e4f..94f31264 100644 --- a/src/modules/swap-router/SwapRouter.sol +++ b/src/modules/swap-router/SwapRouter.sol @@ -6,8 +6,6 @@ import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; import { IUniswapV2Router02 as IUniswapV2Router } from "src/interfaces/IUniswapV2Router02.sol"; import { IUniswapV3Router } from "src/interfaces/IUniswapV3Router.sol"; -import { Test, console } from "@forge-std/Test.sol"; - contract SwapRouter { using SafeTransferLib for ERC20; From efef140a449e00da350a84a2cf3b9407dee2903a Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Mon, 27 Jun 2022 11:27:22 -0700 Subject: [PATCH 21/49] refactor(Cellar): merge enterPosition and exitPosition into rebalance, add high watermark fee accounting --- src/base/Cellar.sol | 201 +++++++++++++++----------------------- src/mocks/MockCellar.sol | 32 +++++- src/mocks/MockERC4626.sol | 16 +-- 3 files changed, 110 insertions(+), 139 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 2977086b..51d03af1 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -5,6 +5,7 @@ import { ERC4626, ERC20 } from "./ERC4626.sol"; import { Multicall } from "./Multicall.sol"; import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import { Registry, SwapRouter, PriceRouter } from "../Registry.sol"; import { IGravity } from "../interfaces/IGravity.sol"; import { AddressArray } from "src/utils/AddressArray.sol"; @@ -15,6 +16,8 @@ import "../Errors.sol"; contract Cellar is ERC4626, Ownable, Multicall { using AddressArray for address[]; using SafeTransferLib for ERC20; + using SafeCast for uint256; + using SafeCast for int256; using Math for uint256; // ========================================= MULTI-POSITION CONFIG ========================================= @@ -50,10 +53,16 @@ contract Cellar is ERC4626, Ownable, Multicall { */ event PositionSwapped(address indexed newPosition1, address indexed newPosition2, uint256 index1, uint256 index2); + enum PositionType { + ERC20, + ERC4626, + Cellar + } + // TODO: pack struct struct PositionData { - uint256 balance; - uint256 storedUnrealizedGains; + PositionType positionType; + int256 highWatermark; } address[] public positions; @@ -393,12 +402,18 @@ contract Cellar is ERC4626, Ownable, Multicall { for (uint256 i; i < _positions.length; i++) { address position = _positions[i]; + if (isPositionUsed[position]) revert USR_PositionAlreadyUsed(position); + isTrusted[position] = true; isPositionUsed[position] = true; ERC4626(position).asset().safeApprove(position, type(uint256).max); } + // Initialize last accrual timestamp to time that cellar was created, otherwise the first + // `accrue` will take platform fees from 1970 to the time it is called. + lastAccrual = uint64(block.timestamp); + // Transfer ownership to the Gravity Bridge. transferOwnership(address(_registry.gravityBridge())); } @@ -439,7 +454,7 @@ contract Cellar is ERC4626, Ownable, Multicall { ) { // Only withdraw if not enough assets in the holding pool. - if (totalHoldings() > assets) { + if (totalHoldings() >= assets) { receivedAssets = new ERC20[](1); amountsOut = new uint256[](1); @@ -448,6 +463,10 @@ contract Cellar is ERC4626, Ownable, Multicall { shares = withdraw(assets, receiver, owner); } else { + // Would be more efficient to store `totalHoldings` to avoid calling twice, but will + // cause stack errors. + assets -= totalHoldings(); + // Get data efficiently. ( uint256 _totalAssets, @@ -480,17 +499,15 @@ contract Cellar is ERC4626, Ownable, Multicall { uint256 j; for (uint256 i; i < amountsReceived.length; i++) { - uint256 amountOut = amountsReceived[i]; - - if (amountOut == 0) continue; + if (amountsReceived[i] == 0) continue; ERC20 positionAsset = positionAssets[i]; receivedAssets[j] = positionAsset; - amountsOut[j] = amountOut; + amountsOut[j] = amountsReceived[i]; j++; // Transfer withdrawn assets to the receiver. - positionAsset.safeTransfer(receiver, amountOut); + positionAsset.safeTransfer(receiver, amountsReceived[i]); } emit WithdrawFromPositions(msg.sender, receiver, owner, receivedAssets, amountsOut, shares); @@ -532,7 +549,7 @@ contract Cellar is ERC4626, Ownable, Multicall { numOfReceivedAssets++; // Update position balance. - _subtractFromPositionBalance(getPositionData[address(_positions[i])], amount); + getPositionData[address(_positions[i])].highWatermark -= amount.toInt256(); // Withdraw from position. _positions[i].withdraw(amount, address(this), address(this)); @@ -657,10 +674,6 @@ contract Cellar is ERC4626, Ownable, Multicall { * @notice Accrue platform fees and performance fees. May also accrue yield. */ function accrue() public { - // Record the balance of this and last accrual. - uint256 totalBalanceThisAccrual; - uint256 totalBalanceLastAccrual; - // Get the latest address of the price router. PriceRouter priceRouter = registry.priceRouter(); @@ -673,8 +686,11 @@ contract Cellar is ERC4626, Ownable, Multicall { uint256[] memory positionBalances ) = _getData(); + // Record the total yield earned this accrual. + uint256 totalYield; + + // Saves SLOADs during looping. ERC20 denominationAsset = asset; - uint8 assetDecimals = denominationAsset.decimals(); for (uint256 i; i < _positions.length; i++) { PositionData storage positionData = getPositionData[address(_positions[i])]; @@ -682,40 +698,30 @@ contract Cellar is ERC4626, Ownable, Multicall { // Get the current position balance. uint256 balanceThisAccrual = positionBalances[i]; - // Get exchange rate. - ERC20 positionAsset = positionAssets[i]; - uint256 onePositionAsset = 10**positionAsset.decimals(); - uint256 positionAssetToAssetExchangeRate = priceRouter.getExchangeRate(positionAsset, denominationAsset); + // Measure yield earned against this position's high watermark. + int256 yield = balanceThisAccrual.toInt256() - positionData.highWatermark; - // Add to balance for last accrual. - totalBalanceLastAccrual += (positionData.balance).mulDivDown( - positionAssetToAssetExchangeRate, - onePositionAsset - ); + // Move on if there is no yield to accrue. + if (yield <= 0) continue; - // Add to balance for this accrual. - totalBalanceThisAccrual += (balanceThisAccrual + positionData.storedUnrealizedGains).mulDivDown( - positionAssetToAssetExchangeRate, - onePositionAsset - ); + // Denominate yield in cellar's asset and count it towards our total yield for this accural. + totalYield += priceRouter.getValue(positionAssets[i], yield.toUint256(), denominationAsset); - // Update position's data. - positionData.balance = balanceThisAccrual; - positionData.storedUnrealizedGains = 0; + // Update position's high watermark. + positionData.highWatermark = balanceThisAccrual.toInt256(); } // Compute and store current exchange rate between assets and shares for gas efficiency. - uint256 assetToSharesExchangeRate = _convertToShares(10**assetDecimals, _totalAssets); + uint256 exchangeRate = _convertToShares(1, _totalAssets); // Calculate platform fees accrued. uint256 elapsedTime = block.timestamp - lastAccrual; - uint256 platformFeeInAssets = (totalBalanceThisAccrual * elapsedTime * platformFee) / 1e18 / 365 days; - uint256 platformFees = platformFeeInAssets.mulWadDown(assetToSharesExchangeRate); // Convert to shares. + uint256 platformFeeInAssets = (_totalAssets * elapsedTime * platformFee) / 1e18 / 365 days; + uint256 platformFees = _convertToFees(platformFeeInAssets, exchangeRate); // Calculate performance fees accrued. - uint256 yield = totalBalanceThisAccrual.subMinZero(totalBalanceLastAccrual); - uint256 performanceFeeInAssets = yield.mulWadDown(performanceFee); - uint256 performanceFees = performanceFeeInAssets.mulWadDown(assetToSharesExchangeRate); // Convert to shares. + uint256 performanceFeeInAssets = totalYield.mulWadDown(performanceFee); + uint256 performanceFees = _convertToFees(performanceFeeInAssets, exchangeRate); // Mint accrued fees as shares. _mint(address(this), platformFees + performanceFees); @@ -728,46 +734,8 @@ contract Cellar is ERC4626, Ownable, Multicall { // =========================================== POSITION LOGIC =========================================== /** - * @notice Pushes assets in holdings into a position. - * @param position address of the position to enter holdings into - * @param assets amount of cellar assets to enter into the position - */ - function enterPosition( - ERC4626 position, - uint256 assets, - SwapRouter.Exchange exchange, - bytes calldata params - ) external onlyOwner { - // Check that position is a valid position. - if (!isPositionUsed[address(position)]) revert USR_InvalidPosition(address(position)); - - // Swap from the holding pool asset if necessary. - ERC20 denominationAsset = asset; - if (position.asset() != denominationAsset) assets = _swap(denominationAsset, assets, exchange, params); - - // Update position balance. - getPositionData[address(position)].balance += assets; - - // Deposit into position. - position.deposit(assets, address(this)); - } - - /** - * @notice Pulls assets from a position back into holdings. - * @param position address of the position to completely exit - * @param balance amount of position's balance to pull and swap into the cellar's asset - */ - function exitPosition( - ERC4626 position, - uint256 balance, - SwapRouter.Exchange exchange, - bytes calldata params - ) external onlyOwner { - _withdrawAndSwapFromPosition(position, asset, balance, exchange, params); - } - - /** - * @notice Move assets between positions. + * @notice Move assets between positions. To move assets from/to this cellar's holdings, specify + * the address of this cellar as the `fromPosition`/`toPosition`. * @param fromPosition address of the position to move assets from * @param toPosition address of the position to move assets to * @param assetsFrom amount of assets to move from the from position @@ -779,17 +747,31 @@ contract Cellar is ERC4626, Ownable, Multicall { SwapRouter.Exchange exchange, bytes calldata params ) external onlyOwner returns (uint256 assetsTo) { - // Check that position being rebalanced to is a valid position. - if (!isPositionUsed[address(toPosition)]) revert USR_InvalidPosition(address(toPosition)); + // Withdraw from position, if not the rebalancing from the holding pool. + if (address(fromPosition) != address(this)) { + // Without this, withdrawals from this position would be counted as losses during the + // next fee accrual. + getPositionData[address(fromPosition)].highWatermark -= assetsFrom.toInt256(); + + fromPosition.withdraw(assetsFrom, address(this), address(this)); + } - // Withdraw from the from position and update related position data. - assetsTo = _withdrawAndSwapFromPosition(fromPosition, toPosition.asset(), assetsFrom, exchange, params); + // Swap to the asset of the other position if necessary. + ERC20 fromAsset = fromPosition.asset(); + ERC20 toAsset = toPosition.asset(); + assetsTo = fromAsset != toAsset ? _swap(fromAsset, assetsFrom, exchange, params) : assetsFrom; - // Update stored balance of the to position. - getPositionData[address(toPosition)].balance += assetsTo; + // Deposit to position, if not the rebalancing to the holding pool + if (address(toPosition) != address(this)) { + // Check that position being rebalanced to is currently being used. + if (!isPositionUsed[address(toPosition)]) revert USR_InvalidPosition(address(toPosition)); - // Deposit into the to position. - toPosition.deposit(assetsTo, address(this)); + // Without this, deposits to this position would be counted as yield during the next fee + // accrual. + getPositionData[address(toPosition)].highWatermark += assetsTo.toInt256(); + + toPosition.deposit(assetsTo, address(this)); + } } // ============================================ LIMITS LOGIC ============================================ @@ -901,40 +883,6 @@ contract Cellar is ERC4626, Ownable, Multicall { // ========================================== HELPER FUNCTIONS ========================================== - function _withdrawAndSwapFromPosition( - ERC4626 position, - ERC20 toAsset, - uint256 balance, - SwapRouter.Exchange exchange, - bytes calldata params - ) internal returns (uint256 amountOut) { - // Get position data. - PositionData storage positionData = getPositionData[address(position)]; - - // Update position balance. - _subtractFromPositionBalance(positionData, balance); - - // Withdraw from position. - position.withdraw(balance, address(this), address(this)); - - // Swap to the holding pool asset if necessary. - ERC20 positionAsset = position.asset(); - amountOut = positionAsset != toAsset ? _swap(positionAsset, balance, exchange, params) : balance; - } - - function _subtractFromPositionBalance(PositionData storage positionData, uint256 amount) internal { - // Update position balance. - uint256 positionBalance = positionData.balance; - if (positionBalance > amount) { - positionData.balance -= amount; - } else { - positionData.balance = 0; - - // Without these, the unrealized gains that were withdrawn would be not be counted next accrual. - positionData.storedUnrealizedGains = amount - positionBalance; - } - } - function _getData() internal view @@ -964,8 +912,6 @@ contract Cellar is ERC4626, Ownable, Multicall { _totalAssets = registry.priceRouter().getValues(positionAssets, positionBalances, asset) + _totalHoldings; } - // =========================================== HELPER FUNCTIONS =========================================== - function _swap( ERC20 assetIn, uint256 amountIn, @@ -989,4 +935,17 @@ contract Cellar is ERC4626, Ownable, Multicall { // TODO: consider replacing with revert statement require(assetIn.balanceOf(address(this)) == expectedAssetsInAfter, "INCORRECT_PARAMS_AMOUNT"); } + + function _convertToFees(uint256 assets, uint256 exchangeRate) internal view returns (uint256 fees) { + // Convert amount of assets to take as fees to shares. + uint256 feesInShares = assets * exchangeRate; + + // Saves an SLOAD. + uint256 totalShares = totalSupply; + + // Get the amount of fees to mint. Without this, the value of fees minted would be slightly + // diluted because total shares increased while total assets did not. This counteracts that. + uint256 denominator = totalShares - feesInShares; + fees = denominator > 0 ? feesInShares.mulDivUp(totalShares, denominator) : 0; + } } diff --git a/src/mocks/MockCellar.sol b/src/mocks/MockCellar.sol index e2e6e9df..c6a31eb6 100644 --- a/src/mocks/MockCellar.sol +++ b/src/mocks/MockCellar.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.15; -import { Cellar, Registry, ERC4626, ERC20 } from "src/base/Cellar.sol"; +import { Cellar, Registry, ERC4626, ERC20, SafeCast } from "src/base/Cellar.sol"; import { Test, console } from "@forge-std/Test.sol"; contract MockCellar is Cellar, Test { + using SafeCast for uint256; + using SafeCast for int256; + constructor( Registry _registry, ERC20 _asset, @@ -13,13 +16,32 @@ contract MockCellar is Cellar, Test { string memory _symbol ) Cellar(_registry, _asset, _positions, _name, _symbol) {} - function increasePositionBalance(address position, uint256 amount) external { + function depositIntoPosition( + address position, + uint256 amount, + address mintSharesTo + ) external returns (uint256 shares) { + uint256 amountInAssets = registry.priceRouter().getValue(ERC4626(position).asset(), amount, asset); + shares = previewDeposit(amountInAssets); + + deal(address(ERC4626(position).asset()), address(this), amount); + + getPositionData[position].highWatermark += amount.toInt256(); + + ERC4626(position).deposit(amount, address(this)); + + _mint(mintSharesTo, shares); + } + + function depositIntoPosition(address position, uint256 amount) public returns (uint256 shares) { + uint256 amountInAssets = registry.priceRouter().getValue(ERC4626(position).asset(), amount, asset); + shares = previewDeposit(amountInAssets); + deal(address(ERC4626(position).asset()), address(this), amount); - // Update position balance. - getPositionData[position].balance += amount; + getPositionData[position].highWatermark += amount.toInt256(); + totalSupply += shares; - // Deposit into position. ERC4626(position).deposit(amount, address(this)); } } diff --git a/src/mocks/MockERC4626.sol b/src/mocks/MockERC4626.sol index fb8264e0..e5a509fd 100644 --- a/src/mocks/MockERC4626.sol +++ b/src/mocks/MockERC4626.sol @@ -5,7 +5,9 @@ import { ERC4626 } from "src/base/ERC4626.sol"; import { ERC20 } from "@solmate/tokens/ERC20.sol"; import { MockERC20 } from "./MockERC20.sol"; -contract MockERC4626 is ERC4626 { +import { Test } from "@forge-std/Test.sol"; + +contract MockERC4626 is ERC4626, Test { constructor( ERC20 _asset, string memory _name, @@ -21,18 +23,6 @@ contract MockERC4626 is ERC4626 { _burn(from, value); } - function simulateGain(uint256 assets, address receiver) external returns (uint256 shares) { - require((shares = previewDeposit(assets)) != 0, "ZERO_SHARES"); - - MockERC20(address(asset)).mint(address(this), assets); - - _mint(receiver, shares); - } - - function simulateLoss(uint256 assets) external { - MockERC20(address(asset)).burn(address(this), assets); - } - function totalAssets() public view override returns (uint256) { return asset.balanceOf(address(this)); } From 7270309cb0f262a999a72314fefb8281c7a4b07a Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Mon, 27 Jun 2022 11:33:07 -0700 Subject: [PATCH 22/49] tests(Cellar): add test for accrue --- test/Cellar.t.sol | 149 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 3 deletions(-) diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 700aeeac..51061e6a 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.15; import { MockCellar, ERC4626, ERC20 } from "src/mocks/MockCellar.sol"; import { Registry, PriceRouter, SwapRouter, IGravity } from "src/Registry.sol"; +import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; import { IUniswapV2Router, IUniswapV3Router } from "src/modules/swap-router/SwapRouter.sol"; import { MockExchange } from "src/mocks/MockExchange.sol"; import { MockPriceRouter } from "src/mocks/MockPriceRouter.sol"; @@ -13,6 +14,7 @@ import { Test, console } from "@forge-std/Test.sol"; import { Math } from "src/utils/Math.sol"; contract CellarTest is Test { + using SafeTransferLib for ERC20; using Math for uint256; MockCellar private cellar; @@ -97,6 +99,27 @@ contract CellarTest is Test { WBTC.approve(address(cellar), type(uint256).max); } + // ============================================ HELPER FUNCTIONS ============================================ + + // For some reason `deal(address(position.asset()), address(position), assets)` isn't working at + // the time of writing but dealing to this address is. This is a workaround. + function simulateGains(address position, uint256 assets) internal { + ERC20 asset = ERC4626(position).asset(); + + deal(address(asset), address(this), assets); + + asset.safeTransfer(position, assets); + } + + function simulateLoss(address position, uint256 assets) internal { + ERC20 asset = ERC4626(position).asset(); + + vm.prank(position); + asset.approve(address(this), assets); + + asset.safeTransferFrom(position, address(1), assets); + } + // ========================================= DEPOSIT/WITHDRAW TEST ========================================= function testDepositAndWithdraw(uint256 assets) external { @@ -126,7 +149,7 @@ contract CellarTest is Test { } function testWithdrawFromPositions() external { - cellar.increasePositionBalance(address(wethCLR), 1e18); + cellar.depositIntoPosition(address(wethCLR), 1e18); assertEq(cellar.totalAssets(), 2000e6, "Should have updated total assets with assets deposited."); @@ -151,8 +174,8 @@ contract CellarTest is Test { } function testWithdrawFromPositionsCompletely() external { - cellar.increasePositionBalance(address(wethCLR), 1e18); - cellar.increasePositionBalance(address(wbtcCLR), 1e8); + cellar.depositIntoPosition(address(wethCLR), 1e18); + cellar.depositIntoPosition(address(wbtcCLR), 1e8); assertEq(cellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); @@ -178,4 +201,124 @@ contract CellarTest is Test { assertEq(WBTC.balanceOf(address(this)), 1e8, "Should have transferred position balance to user."); assertEq(cellar.totalAssets(), 0, "Should have emptied cellar."); } + + // =========================================== ACCRUE TEST =========================================== + + // TODO: DRY this up. + // TODO: Fuzz. + // TODO: Add checks that highwatermarks for each position were updated. + + function testAccrueWithPositivePerformance() external { + // Initialize position balances. + cellar.depositIntoPosition(address(usdcCLR), 1000e6, address(this)); // $1000 + cellar.depositIntoPosition(address(wethCLR), 1e18, address(this)); // $2000 + cellar.depositIntoPosition(address(wbtcCLR), 1e8, address(this)); // $30,000 + + assertEq(cellar.totalAssets(), 33_000e6, "Should have initialized total assets with assets deposited."); + assertEq(cellar.balanceOf(address(this)), 33_000e18, "Should have initialized total shares."); + + // Simulate gains. + simulateGains(address(usdcCLR), 500e6); // $500 + simulateGains(address(wethCLR), 0.5e18); // $1000 + simulateGains(address(wbtcCLR), 0.5e8); // $15,000 + + assertEq(cellar.totalAssets(), 49_500e6, "Should have updated total assets with gains."); + + cellar.accrue(); + + assertApproxEqAbs( + cellar.convertToAssets(cellar.balanceOf(address(cellar))), + 1650e6, + 1, // May be off by 1 due to rounding. + "Should have minted performance fees to cellar." + ); + } + + function testAccrueWithNegativePerformance() external { + // Initialize position balances. + cellar.depositIntoPosition(address(usdcCLR), 1000e6, address(this)); // $1000 + cellar.depositIntoPosition(address(wethCLR), 1e18, address(this)); // $2000 + cellar.depositIntoPosition(address(wbtcCLR), 1e8, address(this)); // $30,000 + + assertEq(cellar.totalAssets(), 33_000e6, "Should have initialized total assets with assets deposited."); + assertEq(cellar.balanceOf(address(this)), 33_000e18, "Should have initialized total shares."); + + // Simulate losses. + simulateLoss(address(usdcCLR), 500e6); // -$500 + simulateLoss(address(wethCLR), 0.5e18); // -$1000 + simulateLoss(address(wbtcCLR), 0.5e8); // -$15,000 + + assertEq(cellar.totalAssets(), 16_500e6, "Should have updated total assets with losses."); + + cellar.accrue(); + + assertEq( + cellar.convertToAssets(cellar.balanceOf(address(cellar))), + 0, + "Should have minted no performance fees to cellar." + ); + } + + function testAccrueWithNoPerformance() external { + // Initialize position balances. + cellar.depositIntoPosition(address(usdcCLR), 1000e6, address(this)); // $1000 + cellar.depositIntoPosition(address(wethCLR), 1e18, address(this)); // $2000 + cellar.depositIntoPosition(address(wbtcCLR), 1e8, address(this)); // $30,000 + + assertEq(cellar.totalAssets(), 33_000e6, "Should have initialized total assets with assets deposited."); + assertEq(cellar.balanceOf(address(this)), 33_000e18, "Should have initialized total shares."); + + cellar.accrue(); + + assertEq( + cellar.convertToAssets(cellar.balanceOf(address(cellar))), + 0, + "Should have minted no performance fees to cellar." + ); + } + + function testAccrueDepositsAndWithdrawsAreNotCountedAsYield(uint256 assets) external { + assets = bound(assets, 1, type(uint72).max); + + // Deposit into cellar. + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + cellar.accrue(); + assertEq(cellar.balanceOf(address(cellar)), 0, "Should not have counted deposit into cellar as yield."); + + // Deposit assets from holding pool to USDC cellar position + cellar.rebalance( + ERC4626(address(cellar)), + ERC4626(address(usdcCLR)), + assets, + SwapRouter.Exchange.UNIV2, // Does not matter, no swap is involved. + abi.encode(0) // Does not matter, no swap is involved. + ); + + cellar.accrue(); + assertEq(cellar.balanceOf(address(cellar)), 0, "Should not have counted deposit into position as yield."); + + // Withdraw some assets from USDC cellar position to holding position. + cellar.rebalance( + ERC4626(address(usdcCLR)), + ERC4626(address(cellar)), + assets / 2, + SwapRouter.Exchange.UNIV2, // Does not matter, no swap is involved. + abi.encode(0) // Does not matter, no swap is involved. + ); + + cellar.accrue(); + assertEq(cellar.balanceOf(address(cellar)), 0, "Should not have counted withdrawals from position as yield."); + + // Withdraw assets from holding pool and USDC cellar position. + cellar.withdrawFromPositions(assets, address(this), address(this)); + + cellar.accrue(); + assertEq( + cellar.balanceOf(address(cellar)), + 0, + "Should not have counted withdrawals from holdings and position as yield." + ); + } } From 7a68af2bdcd7f060f0032c7149f7a3bff01d97e1 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Mon, 27 Jun 2022 11:34:04 -0700 Subject: [PATCH 23/49] build: add remapping for ds-test --- remappings.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/remappings.txt b/remappings.txt index e1383bdc..7ce96140 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,6 +1,7 @@ @solmate/=lib/solmate/src/ @forge-std/=lib/forge-std/src/ @ds-test/=lib/forge-std/lib/ds-test/src/ +ds-test/=lib/forge-std/lib/ds-test/src/ @openzeppelin/=lib/openzeppelin-contracts/ @uniswap/v3-periphery/=lib/v3-periphery/ @uniswap/v3-core/=lib/v3-core/ From ce8cbf77fd76db09af7c1aa2d6a92dc7ae12a131 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Mon, 27 Jun 2022 12:26:57 -0700 Subject: [PATCH 24/49] feat(PositionLib): add PositionLib to handle differing function calls between position types --- src/base/PositionLib.sol | 65 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/base/PositionLib.sol diff --git a/src/base/PositionLib.sol b/src/base/PositionLib.sol new file mode 100644 index 00000000..bde7ba11 --- /dev/null +++ b/src/base/PositionLib.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.15; + +import { ERC4626, ERC20 } from "./ERC4626.sol"; +import { Cellar } from "./Cellar.sol"; + +//TODO move PositionType struct into here? +library PositionLib { + function asset(ERC4626 vault, Cellar.PositionType positionType) internal view returns (ERC20) { + if (positionType == Cellar.PositionType.ERC4626 || positionType == Cellar.PositionType.Cellar) { + return vault.asset(); + } else if (positionType == Cellar.PositionType.ERC20) { + return ERC20(address(vault)); + } else { + revert("Unsupported Position Type"); + } + } + + function maxWithdraw( + ERC4626 vault, + Cellar.PositionType positionType, + address holder + ) internal view returns (uint256) { + if (positionType == Cellar.PositionType.ERC4626 || positionType == Cellar.PositionType.Cellar) { + return vault.maxWithdraw(holder); + } else if (positionType == Cellar.PositionType.ERC20) { + return ERC20(address(vault)).balanceOf(holder); + } else { + revert("Unsupported Position Type"); + } + } + + function withdraw( + ERC4626 vault, + Cellar.PositionType positionType, + uint256 assets, + address receiver, + address owner + ) internal returns (uint256 shares) { + if (positionType == Cellar.PositionType.ERC4626 || positionType == Cellar.PositionType.Cellar) { + shares = vault.withdraw(assets, receiver, owner); + } else if (positionType == Cellar.PositionType.ERC20) { + //since we dont need to actually do anything just return how many tokens the cellar has? Assumes the amount of tokens the cellar has is equal to the amount of shares it has + shares = ERC20(address(vault)).balanceOf(owner); + } else { + revert("Unsupported Position Type"); + } + } + + function deposit( + ERC4626 vault, + Cellar.PositionType positionType, + uint256 assets, + address receiver + ) internal returns (uint256 shares) { + if (positionType == Cellar.PositionType.ERC4626 || positionType == Cellar.PositionType.Cellar) { + shares = vault.deposit(assets, receiver); + } else if (positionType == Cellar.PositionType.ERC20) { + //since we dont need to actually do anything just return how many tokens the cellar has? Assumes the amount of tokens the cellar has is equal to the amount of shares it has + shares = ERC20(address(vault)).balanceOf(receiver); + } else { + revert("Unsupported Position Type"); + } + } +} From 21fca78c8649fa7f3f587fe3720ee87178787817 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Mon, 27 Jun 2022 13:16:10 -0700 Subject: [PATCH 25/49] tests(Cellar): add test for rebalance --- test/Cellar.t.sol | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 51061e6a..988af551 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -202,6 +202,92 @@ contract CellarTest is Test { assertEq(cellar.totalAssets(), 0, "Should have emptied cellar."); } + // ========================================== REBALANCE TEST ========================================== + + function testRebalanceBetweenPositions(uint256 assets) external { + assets = bound(assets, 1, type(uint72).max); + + cellar.depositIntoPosition(address(usdcCLR), assets); + + address[] memory path = new address[](2); + path[0] = address(USDC); + path[1] = address(WETH); + + uint256 assetsTo = cellar.rebalance( + ERC4626(address(usdcCLR)), + ERC4626(address(wethCLR)), + assets, + SwapRouter.Exchange.UNIV2, // Using a mock exchange to swap, this param does not matter. + abi.encode(path, assets, 0, address(cellar), address(cellar)) + ); + + assertEq(assetsTo, exchange.quote(assets, path), "Should received expected assets from swap."); + assertEq(usdcCLR.balanceOf(address(cellar)), 0, "Should have rebalanced from position."); + assertEq(wethCLR.balanceOf(address(cellar)), assetsTo, "Should have rebalanced to position."); + } + + function testRebalanceFromHoldings(uint256 assets) external { + assets = bound(assets, 1, type(uint72).max); + + deal(address(USDC), address(this), assets); + cellar.deposit(assets, address(this)); + + address[] memory path = new address[](2); + path[0] = address(USDC); + path[1] = address(WETH); + + uint256 assetsTo = cellar.rebalance( + ERC4626(address(cellar)), + ERC4626(address(wethCLR)), + assets, + SwapRouter.Exchange.UNIV2, // Using a mock exchange to swap, this param does not matter. + abi.encode(path, assets, 0, address(cellar), address(cellar)) + ); + + assertEq(assetsTo, exchange.quote(assets, path), "Should received expected assets from swap."); + assertEq(usdcCLR.balanceOf(address(cellar)), 0, "Should have rebalanced from position."); + assertEq(wethCLR.balanceOf(address(cellar)), assetsTo, "Should have rebalanced to position."); + } + + function testRebalanceToHoldings(uint256 assets) external { + assets = bound(assets, 1, type(uint112).max); + + cellar.depositIntoPosition(address(wethCLR), assets); + + address[] memory path = new address[](2); + path[0] = address(WETH); + path[1] = address(USDC); + + uint256 assetsTo = cellar.rebalance( + ERC4626(address(wethCLR)), + ERC4626(address(cellar)), + assets, + SwapRouter.Exchange.UNIV2, // Using a mock exchange to swap, this param does not matter. + abi.encode(path, assets, 0, address(cellar), address(cellar)) + ); + + assertEq(assetsTo, exchange.quote(assets, path), "Should received expected assets from swap."); + assertEq(wethCLR.balanceOf(address(cellar)), 0, "Should have rebalanced from position."); + assertEq(cellar.totalHoldings(), assetsTo, "Should have rebalanced to position."); + } + + function testRebalanceToSamePosition(uint256 assets) external { + assets = bound(assets, 1, type(uint72).max); + + cellar.depositIntoPosition(address(usdcCLR), assets); + + uint256 assetsTo = cellar.rebalance( + ERC4626(address(usdcCLR)), + ERC4626(address(usdcCLR)), + assets, + SwapRouter.Exchange.UNIV2, // Will be ignored because no swap is necessary. + abi.encode(0) // Will be ignored because no swap is necessary. + ); + + assertEq(assetsTo, assets, "Should received expected assets from swap."); + assertEq(usdcCLR.balanceOf(address(cellar)), assets, "Should have not changed position balance."); + } + // =========================================== ACCRUE TEST =========================================== // TODO: DRY this up. From 88cc81a3b56d07e72d1e073bfe14007a25b12b35 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Mon, 27 Jun 2022 13:25:54 -0700 Subject: [PATCH 26/49] test(CellarRouter): move withdrawIntoSingleAsset tests from Cellar.t.sol into CellarRouter.t.sol --- test/Cellar.t.sol | 153 ----------------------------- test/CellarRouter.t.sol | 207 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 206 insertions(+), 154 deletions(-) diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 7ecd2b86..51061e6a 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -10,8 +10,6 @@ import { MockPriceRouter } from "src/mocks/MockPriceRouter.sol"; import { MockERC4626 } from "src/mocks/MockERC4626.sol"; import { MockGravity } from "src/mocks/MockGravity.sol"; -import { CellarRouter } from "src/CellarRouter.sol"; - import { Test, console } from "@forge-std/Test.sol"; import { Math } from "src/utils/Math.sol"; @@ -37,13 +35,6 @@ contract CellarTest is Test { ERC20 private WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); MockERC4626 private wbtcCLR; - //========================= CRISPY TEMPORARY ========================== - // Mainnet contracts: - address private constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; - address private constant uniV2Router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; - ERC20 private DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); - CellarRouter private cellarRouter; - function setUp() external { usdcCLR = new MockERC4626(USDC, "USDC Cellar LP Token", "USDC-CLR", 6); vm.label(address(usdcCLR), "usdcCLR"); @@ -106,8 +97,6 @@ contract CellarTest is Test { USDC.approve(address(cellar), type(uint256).max); WETH.approve(address(cellar), type(uint256).max); WBTC.approve(address(cellar), type(uint256).max); - - cellarRouter = new CellarRouter(IUniswapV3Router(address(exchange)), IUniswapV2Router(address(exchange))); } // ============================================ HELPER FUNCTIONS ============================================ @@ -213,148 +202,6 @@ contract CellarTest is Test { assertEq(cellar.totalAssets(), 0, "Should have emptied cellar."); } - function testWithdrawFromPositionsIntoSingleAssetWTwoSwaps() external { - cellar.depositIntoPosition(address(wethCLR), 1e18); - cellar.depositIntoPosition(address(wbtcCLR), 1e8); - - assertEq(cellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); - - // Mint shares to user to redeem. - deal(address(cellar), address(this), cellar.previewWithdraw(32_000e6)); - - //create paths - address[][] memory paths = new address[][](2); - paths[0] = new address[](2); - paths[0][0] = address(WETH); - paths[0][1] = address(USDC); - paths[1] = new address[](2); - paths[1][0] = address(WBTC); - paths[1][1] = address(USDC); - uint24[][] memory poolFees = new uint24[][](2); - poolFees[0] = new uint24[](0); - poolFees[1] = new uint24[](0); - uint256 assets = 32_000e6; - uint256[] memory minOuts = new uint256[](2); - minOuts[0] = 0; - minOuts[1] = 0; - - uint256[] memory assetsIn = new uint256[](2); - assetsIn[0] = 1e18; - assetsIn[1] = 1e8; - - cellar.approve(address(cellarRouter), type(uint256).max); - cellarRouter.withdrawFromPositionsIntoSingleAsset( - cellar, - paths, - poolFees, - assets, - assetsIn, - minOuts, - address(this) - ); - - assertEq(USDC.balanceOf(address(this)), 30_400e6, "Did not recieve expected assets"); - } - - /** - * @notice if the asset wanted is an asset given, then it should just be added to the output with no swaps needed - */ - function testWithdrawFromPositionsIntoSingleAssetWOneSwap() external { - cellar.depositIntoPosition(address(wethCLR), 1e18); - cellar.depositIntoPosition(address(wbtcCLR), 1e8); - - assertEq(cellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); - - // Mint shares to user to redeem. - deal(address(cellar), address(this), cellar.previewWithdraw(32_000e6)); - - //create paths - address[][] memory paths = new address[][](2); - paths[0] = new address[](1); - paths[0][0] = address(WETH); - paths[1] = new address[](2); - paths[1][0] = address(WBTC); - paths[1][1] = address(WETH); - uint24[][] memory poolFees = new uint24[][](2); - poolFees[0] = new uint24[](0); - poolFees[1] = new uint24[](0); - uint256 assets = 32_000e6; - uint256[] memory minOuts = new uint256[](2); - minOuts[0] = 0; - minOuts[1] = 0; - - uint256[] memory assetsIn = new uint256[](2); - assetsIn[0] = 1e18; - assetsIn[1] = 1e8; - - cellar.approve(address(cellarRouter), type(uint256).max); - cellarRouter.withdrawFromPositionsIntoSingleAsset( - cellar, - paths, - poolFees, - assets, - assetsIn, - minOuts, - address(this) - ); - assertEq(WETH.balanceOf(address(this)), 15.25e18, "Did not recieve expected assets"); - } - - function testWithdrawFromPositionsIntoSingleAssetWFourSwaps() external { - cellar.depositIntoPosition(address(wethCLR), 1e18); - cellar.depositIntoPosition(address(wbtcCLR), 1e8); - - assertEq(cellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); - - // Mint shares to user to redeem. - deal(address(cellar), address(this), cellar.previewWithdraw(32_000e6)); - - //create paths - address[][] memory paths = new address[][](4); - paths[0] = new address[](2); - paths[0][0] = address(WETH); - paths[0][1] = address(USDC); - paths[1] = new address[](2); - paths[1][0] = address(WBTC); - paths[1][1] = address(USDC); - paths[2] = new address[](2); - paths[2][0] = address(WETH); - paths[2][1] = address(USDC); - paths[3] = new address[](2); - paths[3][0] = address(WBTC); - paths[3][1] = address(USDC); - uint24[][] memory poolFees = new uint24[][](4); - poolFees[0] = new uint24[](0); - poolFees[1] = new uint24[](0); - poolFees[2] = new uint24[](0); - poolFees[3] = new uint24[](0); - uint256 assets = 32_000e6; - uint256[] memory minOuts = new uint256[](4); - minOuts[0] = 0; - minOuts[1] = 0; - minOuts[2] = 0; - minOuts[3] = 0; - - uint256[] memory assetsIn = new uint256[](4); - assetsIn[0] = 0.5e18; - assetsIn[1] = 0.5e8; - assetsIn[2] = 0.5e18; - assetsIn[3] = 0.5e8; - - cellar.approve(address(cellarRouter), type(uint256).max); - cellarRouter.withdrawFromPositionsIntoSingleAsset( - cellar, - paths, - poolFees, - assets, - assetsIn, - minOuts, - address(this) - ); - - assertEq(USDC.balanceOf(address(this)), 30_400e6, "Did not recieve expected assets"); - } - // =========================================== ACCRUE TEST =========================================== // TODO: DRY this up. diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index a8bb0bfa..75101b26 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -9,6 +9,9 @@ import { IUniswapV2Router02 as IUniswapV2Router } from "src/interfaces/IUniswapV import { MockERC20 } from "src/mocks/MockERC20.sol"; import { MockERC4626 } from "src/mocks/MockERC4626.sol"; import { MockExchange, MockPriceRouter } from "src/mocks/MockExchange.sol"; +import { MockCellar, ERC4626, ERC20 } from "src/mocks/MockCellar.sol"; +import { Registry, PriceRouter, SwapRouter, IGravity } from "src/Registry.sol"; +import { MockGravity } from "src/mocks/MockGravity.sol"; import { Test, console } from "@forge-std/Test.sol"; import { Math } from "src/utils/Math.sol"; @@ -20,8 +23,12 @@ contract CellarRouterTest is Test { MockERC20 private XYZ; MockPriceRouter private priceRouter; MockExchange private exchange; + MockGravity private gravity; + Registry private registry; + SwapRouter private swapRouter; MockERC4626 private cellar; + MockCellar private multiCellar; //cellar with multiple assets CellarRouter private router; MockERC4626 private forkedCellar; @@ -35,15 +42,40 @@ contract CellarRouterTest is Test { // Mainnet contracts: address private constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address private constant uniV2Router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; - ERC20 private WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); ERC20 private DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + ERC20 private USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + MockERC4626 private usdcCLR; + + ERC20 private WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + MockERC4626 private wethCLR; + + ERC20 private WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + MockERC4626 private wbtcCLR; + function setUp() public { + usdcCLR = new MockERC4626(USDC, "USDC Cellar LP Token", "USDC-CLR", 6); + vm.label(address(usdcCLR), "usdcCLR"); + + wethCLR = new MockERC4626(WETH, "WETH Cellar LP Token", "WETH-CLR", 18); + vm.label(address(wethCLR), "wethCLR"); + + wbtcCLR = new MockERC4626(WBTC, "WBTC Cellar LP Token", "WBTC-CLR", 8); + vm.label(address(wbtcCLR), "wbtcCLR"); + priceRouter = new MockPriceRouter(); exchange = new MockExchange(priceRouter); router = new CellarRouter(IUniswapV3Router(address(exchange)), IUniswapV2Router(address(exchange))); forkedRouter = new CellarRouter(IUniswapV3Router(uniV3Router), IUniswapV2Router(uniV2Router)); + swapRouter = new SwapRouter(IUniswapV2Router(address(exchange)), IUniswapV3Router(address(exchange))); + gravity = new MockGravity(); + + registry = new Registry( + SwapRouter(address(swapRouter)), + PriceRouter(address(priceRouter)), + IGravity(address(gravity)) + ); ABC = new MockERC20("ABC", 18); XYZ = new MockERC20("XYZ", 18); @@ -51,10 +83,41 @@ contract CellarRouterTest is Test { // Set up exchange rates: priceRouter.setExchangeRate(ERC20(address(ABC)), ERC20(address(XYZ)), 1e18); priceRouter.setExchangeRate(ERC20(address(XYZ)), ERC20(address(ABC)), 1e18); + priceRouter.setExchangeRate(USDC, USDC, 1e6); + priceRouter.setExchangeRate(WETH, WETH, 1e18); + priceRouter.setExchangeRate(WBTC, WBTC, 1e8); + priceRouter.setExchangeRate(USDC, WETH, 0.0005e18); + priceRouter.setExchangeRate(WETH, USDC, 2000e6); + priceRouter.setExchangeRate(USDC, WBTC, 0.000033e8); + priceRouter.setExchangeRate(WBTC, USDC, 30_000e6); + priceRouter.setExchangeRate(WETH, WBTC, 0.06666666e8); + priceRouter.setExchangeRate(WBTC, WETH, 15e18); // Set up two cellars: cellar = new MockERC4626(ERC20(address(ABC)), "ABC Cellar", "abcCLR", 18); forkedCellar = new MockERC4626(ERC20(address(WETH)), "WETH Cellar", "WETHCLR", 18); // For mainnet fork test. + + address[] memory positions = new address[](3); + positions[0] = address(usdcCLR); + positions[1] = address(wethCLR); + positions[2] = address(wbtcCLR); + + multiCellar = new MockCellar(registry, USDC, positions, "Multiposition Cellar LP Token", "multiposition-CLR"); + vm.label(address(cellar), "cellar"); + + // Transfer ownership to this contract for testing. + vm.prank(address(registry.gravityBridge())); + multiCellar.transferOwnership(address(this)); + + // Mint enough liquidity to swap router for swaps. + deal(address(USDC), address(exchange), type(uint224).max); + deal(address(WETH), address(exchange), type(uint224).max); + deal(address(WBTC), address(exchange), type(uint224).max); + + // Approve cellar to spend all assets. + USDC.approve(address(cellar), type(uint256).max); + WETH.approve(address(cellar), type(uint256).max); + WBTC.approve(address(cellar), type(uint256).max); } // ======================================= DEPOSIT TESTS ======================================= @@ -240,4 +303,146 @@ contract CellarRouterTest is Test { assertEq(cellar.balanceOf(owner), 0, "Should have updated user's share balance."); assertEq(XYZ.balanceOf(owner), assetsReceivedAfterWithdraw, "Should have withdrawn assets to the user."); } + + function testWithdrawFromPositionsIntoSingleAssetWTwoSwaps() external { + multiCellar.depositIntoPosition(address(wethCLR), 1e18); + multiCellar.depositIntoPosition(address(wbtcCLR), 1e8); + + assertEq(multiCellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); + + // Mint shares to user to redeem. + deal(address(multiCellar), address(this), multiCellar.previewWithdraw(32_000e6)); + + //create paths + address[][] memory paths = new address[][](2); + paths[0] = new address[](2); + paths[0][0] = address(WETH); + paths[0][1] = address(USDC); + paths[1] = new address[](2); + paths[1][0] = address(WBTC); + paths[1][1] = address(USDC); + uint24[][] memory poolFees = new uint24[][](2); + poolFees[0] = new uint24[](0); + poolFees[1] = new uint24[](0); + uint256 assets = 32_000e6; + uint256[] memory minOuts = new uint256[](2); + minOuts[0] = 0; + minOuts[1] = 0; + + uint256[] memory assetsIn = new uint256[](2); + assetsIn[0] = 1e18; + assetsIn[1] = 1e8; + + multiCellar.approve(address(router), type(uint256).max); + router.withdrawFromPositionsIntoSingleAsset( + multiCellar, + paths, + poolFees, + assets, + assetsIn, + minOuts, + address(this) + ); + + assertEq(USDC.balanceOf(address(this)), 30_400e6, "Did not recieve expected assets"); + } + + /** + * @notice if the asset wanted is an asset given, then it should just be added to the output with no swaps needed + */ + function testWithdrawFromPositionsIntoSingleAssetWOneSwap() external { + multiCellar.depositIntoPosition(address(wethCLR), 1e18); + multiCellar.depositIntoPosition(address(wbtcCLR), 1e8); + + assertEq(multiCellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); + + // Mint shares to user to redeem. + deal(address(multiCellar), address(this), multiCellar.previewWithdraw(32_000e6)); + + //create paths + address[][] memory paths = new address[][](2); + paths[0] = new address[](1); + paths[0][0] = address(WETH); + paths[1] = new address[](2); + paths[1][0] = address(WBTC); + paths[1][1] = address(WETH); + uint24[][] memory poolFees = new uint24[][](2); + poolFees[0] = new uint24[](0); + poolFees[1] = new uint24[](0); + uint256 assets = 32_000e6; + uint256[] memory minOuts = new uint256[](2); + minOuts[0] = 0; + minOuts[1] = 0; + + uint256[] memory assetsIn = new uint256[](2); + assetsIn[0] = 1e18; + assetsIn[1] = 1e8; + + multiCellar.approve(address(router), type(uint256).max); + router.withdrawFromPositionsIntoSingleAsset( + multiCellar, + paths, + poolFees, + assets, + assetsIn, + minOuts, + address(this) + ); + assertEq(WETH.balanceOf(address(this)), 15.25e18, "Did not recieve expected assets"); + } + + function testWithdrawFromPositionsIntoSingleAssetWFourSwaps() external { + multiCellar.depositIntoPosition(address(wethCLR), 1e18); + multiCellar.depositIntoPosition(address(wbtcCLR), 1e8); + + assertEq(multiCellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); + + // Mint shares to user to redeem. + deal(address(multiCellar), address(this), multiCellar.previewWithdraw(32_000e6)); + + //create paths + address[][] memory paths = new address[][](4); + paths[0] = new address[](2); + paths[0][0] = address(WETH); + paths[0][1] = address(USDC); + paths[1] = new address[](2); + paths[1][0] = address(WBTC); + paths[1][1] = address(USDC); + paths[2] = new address[](2); + paths[2][0] = address(WETH); + paths[2][1] = address(USDC); + paths[3] = new address[](2); + paths[3][0] = address(WBTC); + paths[3][1] = address(USDC); + uint24[][] memory poolFees = new uint24[][](4); + poolFees[0] = new uint24[](0); + poolFees[1] = new uint24[](0); + poolFees[2] = new uint24[](0); + poolFees[3] = new uint24[](0); + uint256 assets = 32_000e6; + uint256[] memory minOuts = new uint256[](4); + minOuts[0] = 0; + minOuts[1] = 0; + minOuts[2] = 0; + minOuts[3] = 0; + + uint256[] memory assetsIn = new uint256[](4); + assetsIn[0] = 0.5e18; + assetsIn[1] = 0.5e8; + assetsIn[2] = 0.5e18; + assetsIn[3] = 0.5e8; + + multiCellar.approve(address(router), type(uint256).max); + router.withdrawFromPositionsIntoSingleAsset( + multiCellar, + paths, + poolFees, + assets, + assetsIn, + minOuts, + address(this) + ); + + assertEq(USDC.balanceOf(address(this)), 30_400e6, "Did not recieve expected assets"); + } } From c189ea00496595770de0477bc96a861e0c1e48e4 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Mon, 27 Jun 2022 13:32:14 -0700 Subject: [PATCH 27/49] tests(Cellar): add test for high watermark --- test/Cellar.t.sol | 55 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 988af551..3e6736b0 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.15; -import { MockCellar, ERC4626, ERC20 } from "src/mocks/MockCellar.sol"; +import { Cellar, MockCellar, ERC4626, ERC20 } from "src/mocks/MockCellar.sol"; import { Registry, PriceRouter, SwapRouter, IGravity } from "src/Registry.sol"; import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; import { IUniswapV2Router, IUniswapV3Router } from "src/modules/swap-router/SwapRouter.sol"; @@ -290,10 +290,6 @@ contract CellarTest is Test { // =========================================== ACCRUE TEST =========================================== - // TODO: DRY this up. - // TODO: Fuzz. - // TODO: Add checks that highwatermarks for each position were updated. - function testAccrueWithPositivePerformance() external { // Initialize position balances. cellar.depositIntoPosition(address(usdcCLR), 1000e6, address(this)); // $1000 @@ -407,4 +403,53 @@ contract CellarTest is Test { "Should not have counted withdrawals from holdings and position as yield." ); } + + event Accrual(uint256 platformFees, uint256 performanceFees); + + function testAccrueUsesHighWatermark() external { + // Initialize position balances. + cellar.depositIntoPosition(address(usdcCLR), 1000e6, address(this)); // $1000 + cellar.depositIntoPosition(address(wethCLR), 1e18, address(this)); // $2000 + cellar.depositIntoPosition(address(wbtcCLR), 1e8, address(this)); // $30,000 + + // Simulate gains. + simulateGains(address(usdcCLR), 500e6); // $500 + simulateGains(address(wethCLR), 0.5e18); // $1000 + simulateGains(address(wbtcCLR), 0.5e8); // $15,000 + + cellar.accrue(); + + assertApproxEqAbs( + cellar.convertToAssets(cellar.balanceOf(address(cellar))), + 1650e6, + 1, // May be off by 1 due to rounding. + "Should have minted performance fees to cellar for gains." + ); + + // Simulate losing all previous gains. + simulateLoss(address(usdcCLR), 500e6); // -$500 + simulateLoss(address(wethCLR), 0.5e18); // -$1000 + simulateLoss(address(wbtcCLR), 0.5e8); // -$15,000 + + uint256 performanceFeesBefore = cellar.balanceOf(address(cellar)); + + cellar.accrue(); + + assertEq( + cellar.balanceOf(address(cellar)), + performanceFeesBefore, + "Should have minted no performance fees for losses." + ); + + // Simulate recovering previous gains. + simulateGains(address(usdcCLR), 500e6); // $500 + simulateGains(address(wethCLR), 0.5e18); // $1000 + simulateGains(address(wbtcCLR), 0.5e8); // $15,000 + + assertEq( + cellar.balanceOf(address(cellar)), + performanceFeesBefore, + "Should have minted no performance fees for no net gains." + ); + } } From c7705a6274306bd53dd0625a10455cf889264eab Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Mon, 27 Jun 2022 15:01:45 -0700 Subject: [PATCH 28/49] feat(Cellar): add ability to withdraw from all positions in proportion --- src/base/Cellar.sol | 96 ++++++++++++++++++++++++++++++++++++++++----- test/Cellar.t.sol | 61 ++++++++++++++-------------- 2 files changed, 117 insertions(+), 40 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 51d03af1..163c8fe0 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -10,6 +10,7 @@ import { Registry, SwapRouter, PriceRouter } from "../Registry.sol"; import { IGravity } from "../interfaces/IGravity.sol"; import { AddressArray } from "src/utils/AddressArray.sol"; import { Math } from "../utils/Math.sol"; +import { console } from "@forge-std/Test.sol"; // TODO: Delete. import "../Errors.sol"; @@ -441,7 +442,7 @@ contract Cellar is ERC4626, Ownable, Multicall { event PulledFromPosition(address indexed position, uint256 amount); - function withdrawFromPositions( + function withdrawFromPositionsInOrder( uint256 assets, address receiver, address owner @@ -497,17 +498,16 @@ contract Cellar is ERC4626, Ownable, Multicall { receivedAssets = new ERC20[](numOfReceivedAssets); amountsOut = new uint256[](numOfReceivedAssets); - uint256 j; - for (uint256 i; i < amountsReceived.length; i++) { - if (amountsReceived[i] == 0) continue; + for (uint256 i = amountsReceived.length; i > 0; i--) { + if (amountsReceived[i - 1] == 0) continue; - ERC20 positionAsset = positionAssets[i]; - receivedAssets[j] = positionAsset; - amountsOut[j] = amountsReceived[i]; - j++; + ERC20 positionAsset = positionAssets[i - 1]; + receivedAssets[numOfReceivedAssets - 1] = positionAsset; + amountsOut[numOfReceivedAssets - 1] = amountsReceived[i - 1]; + numOfReceivedAssets--; // Transfer withdrawn assets to the receiver. - positionAsset.safeTransfer(receiver, amountsReceived[i]); + positionAsset.safeTransfer(receiver, amountsReceived[i - 1]); } emit WithdrawFromPositions(msg.sender, receiver, owner, receivedAssets, amountsOut, shares); @@ -561,6 +561,84 @@ contract Cellar is ERC4626, Ownable, Multicall { } } + // TODO: DRY this up. + + function withdrawFromPositionsInProportion( + uint256 assets, + address receiver, + address owner + ) + external + returns ( + uint256 shares, + ERC20[] memory receivedAssets, + uint256[] memory amountsOut + ) + { + // Get data efficiently. + ( + uint256 _totalAssets, + , + ERC4626[] memory _positions, + ERC20[] memory positionAssets, + uint256[] memory positionBalances + ) = _getData(); + + // Get the amount of share needed to redeem. + shares = _previewWithdraw(assets, _totalAssets); + + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + } + + // Saves SLOADs during looping. + uint256 totalShares = totalSupply; + + _burn(owner, shares); + + uint256[] memory amountsReceived = new uint256[](_positions.length); + uint256 numOfReceivedAssets; + + // Withdraw assets from positions in proportion to shares redeemed. + for (uint256 i; i < _positions.length; i++) { + // Move on to next position if this one is empty. + if (positionBalances[i] == 0) continue; + + // Get the amount of assets to withdraw from this position based on proportion to shares redeemed. + uint256 amount = positionBalances[i].mulDivDown(shares, totalShares); + + // Update position balance. + getPositionData[address(_positions[i])].highWatermark -= amount.toInt256(); + + amountsReceived[i] = amount; + numOfReceivedAssets++; + + // Withdraw from position. + _positions[i].withdraw(amount, address(this), address(this)); + + emit PulledFromPosition(address(_positions[i]), amount); + } + + receivedAssets = new ERC20[](numOfReceivedAssets); + amountsOut = new uint256[](numOfReceivedAssets); + + for (uint256 i = amountsReceived.length; i > 0; i--) { + if (amountsReceived[i - 1] == 0) continue; + + ERC20 positionAsset = positionAssets[i - 1]; + receivedAssets[numOfReceivedAssets - 1] = positionAsset; + amountsOut[numOfReceivedAssets - 1] = amountsReceived[i - 1]; + numOfReceivedAssets--; + + // Transfer withdrawn assets to the receiver. + positionAsset.safeTransfer(receiver, amountsReceived[i - 1]); + } + + emit WithdrawFromPositions(msg.sender, receiver, owner, receivedAssets, amountsOut, shares); + } + // ========================================= ACCOUNTING LOGIC ========================================= /** diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 3e6736b0..c8fee6bb 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -148,58 +148,57 @@ contract CellarTest is Test { assertEq(USDC.balanceOf(address(this)), assets, "Should have withdrawn assets to user."); } - function testWithdrawFromPositions() external { - cellar.depositIntoPosition(address(wethCLR), 1e18); + function testWithdrawFromPositionsInOrder() external { + cellar.depositIntoPosition(address(wethCLR), 1e18); // $2000 + cellar.depositIntoPosition(address(wbtcCLR), 1e8); // $30,000 - assertEq(cellar.totalAssets(), 2000e6, "Should have updated total assets with assets deposited."); + assertEq(cellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); // Mint shares to user to redeem. - deal(address(cellar), address(this), cellar.previewWithdraw(1000e6)); + deal(address(cellar), address(this), cellar.previewWithdraw(32_000e6)); // Withdraw from position. - (uint256 shares, ERC20[] memory receivedAssets, uint256[] memory amountsOut) = cellar.withdrawFromPositions( - 1000e6, - address(this), - address(this) - ); + (uint256 shares, ERC20[] memory receivedAssets, uint256[] memory amountsOut) = cellar + .withdrawFromPositionsInOrder(32_000e6, address(this), address(this)); assertEq(cellar.balanceOf(address(this)), 0, "Should have redeemed all shares."); - assertEq(shares, 1000e18, "Should returned all redeemed shares."); - assertEq(receivedAssets.length, 1, "Should have received one asset."); - assertEq(amountsOut.length, 1, "Should have gotten out one amount."); + assertEq(shares, 32_000e18, "Should returned all redeemed shares."); + assertEq(receivedAssets.length, 2, "Should have received two assets."); + assertEq(amountsOut.length, 2, "Should have gotten out two amount."); assertEq(address(receivedAssets[0]), address(WETH), "Should have received WETH."); - assertEq(amountsOut[0], 0.5e18, "Should have gotten out 0.5 WETH."); - assertEq(WETH.balanceOf(address(this)), 0.5e18, "Should have transferred position balance to user."); - assertEq(cellar.totalAssets(), 1000e6, "Should have updated cellar total assets."); + assertEq(address(receivedAssets[1]), address(WBTC), "Should have received WBTC."); + assertEq(amountsOut[0], 1e18, "Should have gotten out 1 WETH."); + assertEq(amountsOut[1], 1e8, "Should have gotten out 1 WBTC."); + assertEq(WETH.balanceOf(address(this)), 1e18, "Should have transferred position balance to user."); + assertEq(WBTC.balanceOf(address(this)), 1e8, "Should have transferred position balance to user."); + assertEq(cellar.totalAssets(), 0, "Should have emptied cellar."); } - function testWithdrawFromPositionsCompletely() external { - cellar.depositIntoPosition(address(wethCLR), 1e18); - cellar.depositIntoPosition(address(wbtcCLR), 1e8); + function testWithdrawFromPositionsInProportion() external { + cellar.depositIntoPosition(address(wethCLR), 1e18); // $2000 + cellar.depositIntoPosition(address(wbtcCLR), 1e8); // $30,000 assertEq(cellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); + assertEq(cellar.totalSupply(), 32_000e18); // Mint shares to user to redeem. - deal(address(cellar), address(this), cellar.previewWithdraw(32_000e6)); + deal(address(cellar), address(this), cellar.previewWithdraw(16_000e6)); // Withdraw from position. - (uint256 shares, ERC20[] memory receivedAssets, uint256[] memory amountsOut) = cellar.withdrawFromPositions( - 32_000e6, - address(this), - address(this) - ); + (uint256 shares, ERC20[] memory receivedAssets, uint256[] memory amountsOut) = cellar + .withdrawFromPositionsInProportion(16_000e6, address(this), address(this)); assertEq(cellar.balanceOf(address(this)), 0, "Should have redeemed all shares."); - assertEq(shares, 32_000e18, "Should returned all redeemed shares."); + assertEq(shares, 16_000e18, "Should returned all redeemed shares."); assertEq(receivedAssets.length, 2, "Should have received two assets."); assertEq(amountsOut.length, 2, "Should have gotten out two amount."); assertEq(address(receivedAssets[0]), address(WETH), "Should have received WETH."); assertEq(address(receivedAssets[1]), address(WBTC), "Should have received WBTC."); - assertEq(amountsOut[0], 1e18, "Should have gotten out 1 WETH."); - assertEq(amountsOut[1], 1e8, "Should have gotten out 1 WBTC."); - assertEq(WETH.balanceOf(address(this)), 1e18, "Should have transferred position balance to user."); - assertEq(WBTC.balanceOf(address(this)), 1e8, "Should have transferred position balance to user."); - assertEq(cellar.totalAssets(), 0, "Should have emptied cellar."); + assertEq(amountsOut[0], 0.5e18, "Should have gotten out 0.5 WETH."); + assertEq(amountsOut[1], 0.5e8, "Should have gotten out 0.5 WBTC."); + assertEq(WETH.balanceOf(address(this)), 0.5e18, "Should have transferred position balance to user."); + assertEq(WBTC.balanceOf(address(this)), 0.5e8, "Should have transferred position balance to user."); + assertEq(cellar.totalAssets(), 16_000e6, "Should have half of assets remaining in cellar."); } // ========================================== REBALANCE TEST ========================================== @@ -394,7 +393,7 @@ contract CellarTest is Test { assertEq(cellar.balanceOf(address(cellar)), 0, "Should not have counted withdrawals from position as yield."); // Withdraw assets from holding pool and USDC cellar position. - cellar.withdrawFromPositions(assets, address(this), address(this)); + cellar.withdrawFromPositionsInOrder(assets, address(this), address(this)); cellar.accrue(); assertEq( From a1d7c25433538ca461b7acf1612935a3286fd5b4 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Mon, 27 Jun 2022 16:30:48 -0700 Subject: [PATCH 29/49] feat(Cellar): use PositionLib to allow for positions in ERC20, ERC4626, and other Cellars --- src/base/Cellar.sol | 76 ++++++++++++++++++---------------------- src/base/PositionLib.sol | 67 ++++++++++++++++++++++------------- test/Cellar.t.sol | 8 ++--- 3 files changed, 81 insertions(+), 70 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 51d03af1..a445bfd1 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -10,6 +10,7 @@ import { Registry, SwapRouter, PriceRouter } from "../Registry.sol"; import { IGravity } from "../interfaces/IGravity.sol"; import { AddressArray } from "src/utils/AddressArray.sol"; import { Math } from "../utils/Math.sol"; +import { PositionLib, PositionType } from "src/base/PositionLib.sol"; import "../Errors.sol"; @@ -19,6 +20,7 @@ contract Cellar is ERC4626, Ownable, Multicall { using SafeCast for uint256; using SafeCast for int256; using Math for uint256; + using PositionLib for address; // ========================================= MULTI-POSITION CONFIG ========================================= @@ -53,12 +55,6 @@ contract Cellar is ERC4626, Ownable, Multicall { */ event PositionSwapped(address indexed newPosition1, address indexed newPosition2, uint256 index1, uint256 index2); - enum PositionType { - ERC20, - ERC4626, - Cellar - } - // TODO: pack struct struct PositionData { PositionType positionType; @@ -110,7 +106,7 @@ contract Cellar is ERC4626, Ownable, Multicall { address position = positions[index]; // Only remove position if it is empty. - uint256 positionBalance = ERC4626(position).balanceOf(address(this)); + uint256 positionBalance = position.balanceOf(getPositionData[position].positionType, address(this)); if (positionBalance > 0) revert USR_PositionNotEmpty(position, positionBalance); // Remove position at the given index. @@ -130,7 +126,7 @@ contract Cellar is ERC4626, Ownable, Multicall { address position = positions[index]; // Only remove position if it is empty. - uint256 positionBalance = ERC4626(position).balanceOf(address(this)); + uint256 positionBalance = position.balanceOf(getPositionData[position].positionType, address(this)); if (positionBalance > 0) revert USR_PositionNotEmpty(position, positionBalance); // Remove last position. @@ -145,7 +141,7 @@ contract Cellar is ERC4626, Ownable, Multicall { address oldPosition = positions[index]; // Only remove position if it is empty. - uint256 positionBalance = ERC4626(oldPosition).balanceOf(address(this)); + uint256 positionBalance = oldPosition.balanceOf(getPositionData[oldPosition].positionType, address(this)); if (positionBalance > 0) revert USR_PositionNotEmpty(oldPosition, positionBalance); // Replace old position with new position. @@ -183,7 +179,7 @@ contract Cellar is ERC4626, Ownable, Multicall { isTrusted[position] = true; // Set max approval to deposit into position if it is ERC4626. - ERC4626(position).asset().safeApprove(position, type(uint256).max); + position.asset(getPositionData[position].positionType).safeApprove(position, type(uint256).max); emit TrustChanged(position, true); } @@ -196,7 +192,7 @@ contract Cellar is ERC4626, Ownable, Multicall { positions.remove(position); // Remove approval for position. - ERC4626(position).asset().safeApprove(position, 0); + position.asset(getPositionData[position].positionType).safeApprove(position, 0); // NOTE: After position has been removed, SP should be notified on the UI that the position // can no longer be used and to exit the position or rebalance its assets into another @@ -406,8 +402,8 @@ contract Cellar is ERC4626, Ownable, Multicall { isTrusted[position] = true; isPositionUsed[position] = true; - - ERC4626(position).asset().safeApprove(position, type(uint256).max); + //TODO need to set the position type before this line! + position.asset(getPositionData[position].positionType).safeApprove(position, type(uint256).max); } // Initialize last accrual timestamp to time that cellar was created, otherwise the first @@ -471,7 +467,7 @@ contract Cellar is ERC4626, Ownable, Multicall { ( uint256 _totalAssets, , - ERC4626[] memory _positions, + address[] memory _positions, ERC20[] memory positionAssets, uint256[] memory positionBalances ) = _getData(); @@ -516,7 +512,7 @@ contract Cellar is ERC4626, Ownable, Multicall { function _pullFromPositions( uint256 assets, - ERC4626[] memory _positions, + address[] memory _positions, ERC20[] memory positionAssets, uint256[] memory positionBalances ) internal returns (uint256[] memory amountsReceived, uint256 numOfReceivedAssets) { @@ -549,12 +545,12 @@ contract Cellar is ERC4626, Ownable, Multicall { numOfReceivedAssets++; // Update position balance. - getPositionData[address(_positions[i])].highWatermark -= amount.toInt256(); + getPositionData[_positions[i]].highWatermark -= amount.toInt256(); // Withdraw from position. - _positions[i].withdraw(amount, address(this), address(this)); + _positions[i].withdraw(getPositionData[_positions[i]].positionType, amount, address(this), address(this)); - emit PulledFromPosition(address(_positions[i]), amount); + emit PulledFromPosition(_positions[i], amount); // Stop if no more assets to withdraw. if (assets == 0) break; @@ -573,10 +569,8 @@ contract Cellar is ERC4626, Ownable, Multicall { uint256[] memory balances = new uint256[](numOfPositions); for (uint256 i; i < numOfPositions; i++) { - ERC4626 position = ERC4626(positions[i]); - - positionAssets[i] = position.asset(); - balances[i] = position.maxWithdraw(address(this)); + positionAssets[i] = positions[i].asset(getPositionData[positions[i]].positionType); + balances[i] = positions[i].maxWithdraw(getPositionData[positions[i]].positionType, address(this)); } assets = registry.priceRouter().getValues(positionAssets, balances, asset) + totalHoldings(); @@ -681,7 +675,7 @@ contract Cellar is ERC4626, Ownable, Multicall { ( uint256 _totalAssets, , - ERC4626[] memory _positions, + address[] memory _positions, ERC20[] memory positionAssets, uint256[] memory positionBalances ) = _getData(); @@ -693,7 +687,7 @@ contract Cellar is ERC4626, Ownable, Multicall { ERC20 denominationAsset = asset; for (uint256 i; i < _positions.length; i++) { - PositionData storage positionData = getPositionData[address(_positions[i])]; + PositionData storage positionData = getPositionData[_positions[i]]; // Get the current position balance. uint256 balanceThisAccrual = positionBalances[i]; @@ -741,36 +735,36 @@ contract Cellar is ERC4626, Ownable, Multicall { * @param assetsFrom amount of assets to move from the from position */ function rebalance( - ERC4626 fromPosition, - ERC4626 toPosition, + address fromPosition, + address toPosition, uint256 assetsFrom, SwapRouter.Exchange exchange, bytes calldata params ) external onlyOwner returns (uint256 assetsTo) { // Withdraw from position, if not the rebalancing from the holding pool. - if (address(fromPosition) != address(this)) { + if (fromPosition != address(this)) { // Without this, withdrawals from this position would be counted as losses during the // next fee accrual. - getPositionData[address(fromPosition)].highWatermark -= assetsFrom.toInt256(); + getPositionData[fromPosition].highWatermark -= assetsFrom.toInt256(); - fromPosition.withdraw(assetsFrom, address(this), address(this)); + fromPosition.withdraw(getPositionData[fromPosition].positionType, assetsFrom, address(this), address(this)); } // Swap to the asset of the other position if necessary. - ERC20 fromAsset = fromPosition.asset(); - ERC20 toAsset = toPosition.asset(); + ERC20 fromAsset = fromPosition.asset(getPositionData[fromPosition].positionType); + ERC20 toAsset = toPosition.asset(getPositionData[toPosition].positionType); assetsTo = fromAsset != toAsset ? _swap(fromAsset, assetsFrom, exchange, params) : assetsFrom; // Deposit to position, if not the rebalancing to the holding pool - if (address(toPosition) != address(this)) { + if (toPosition != address(this)) { // Check that position being rebalanced to is currently being used. - if (!isPositionUsed[address(toPosition)]) revert USR_InvalidPosition(address(toPosition)); + if (!isPositionUsed[toPosition]) revert USR_InvalidPosition(address(toPosition)); // Without this, deposits to this position would be counted as yield during the next fee // accrual. - getPositionData[address(toPosition)].highWatermark += assetsTo.toInt256(); + getPositionData[toPosition].highWatermark += assetsTo.toInt256(); - toPosition.deposit(assetsTo, address(this)); + toPosition.deposit(getPositionData[toPosition].positionType, assetsTo, address(this)); } } @@ -889,23 +883,21 @@ contract Cellar is ERC4626, Ownable, Multicall { returns ( uint256 _totalAssets, uint256 _totalHoldings, - ERC4626[] memory _positions, + address[] memory _positions, ERC20[] memory positionAssets, uint256[] memory positionBalances ) { uint256 len = positions.length; - _positions = new ERC4626[](len); + _positions = new address[](len); positionAssets = new ERC20[](len); positionBalances = new uint256[](len); for (uint256 i; i < len; i++) { - ERC4626 position = ERC4626(positions[i]); - - _positions[i] = position; - positionAssets[i] = position.asset(); - positionBalances[i] = position.maxWithdraw(address(this)); + _positions[i] = positions[i]; + positionAssets[i] = positions[i].asset(getPositionData[positions[i]].positionType); + positionBalances[i] = positions[i].maxWithdraw(getPositionData[positions[i]].positionType, address(this)); } _totalHoldings = totalHoldings(); diff --git a/src/base/PositionLib.sol b/src/base/PositionLib.sol index bde7ba11..126f2213 100644 --- a/src/base/PositionLib.sol +++ b/src/base/PositionLib.sol @@ -4,60 +4,79 @@ pragma solidity 0.8.15; import { ERC4626, ERC20 } from "./ERC4626.sol"; import { Cellar } from "./Cellar.sol"; -//TODO move PositionType struct into here? +enum PositionType { + ERC20, + ERC4626, + Cellar +} + library PositionLib { - function asset(ERC4626 vault, Cellar.PositionType positionType) internal view returns (ERC20) { - if (positionType == Cellar.PositionType.ERC4626 || positionType == Cellar.PositionType.Cellar) { - return vault.asset(); - } else if (positionType == Cellar.PositionType.ERC20) { - return ERC20(address(vault)); + function asset(address position, PositionType positionType) internal view returns (ERC20) { + if (positionType == PositionType.ERC4626 || positionType == PositionType.Cellar) { + return ERC4626(position).asset(); + } else if (positionType == PositionType.ERC20) { + return ERC20(position); } else { revert("Unsupported Position Type"); } } function maxWithdraw( - ERC4626 vault, - Cellar.PositionType positionType, + address position, + PositionType positionType, + address holder + ) internal view returns (uint256) { + if (positionType == PositionType.ERC4626 || positionType == PositionType.Cellar) { + return ERC4626(position).maxWithdraw(holder); + } else if (positionType == PositionType.ERC20) { + return ERC20(position).balanceOf(holder); + } else { + revert("Unsupported Position Type"); + } + } + + function balanceOf( + address position, + PositionType positionType, address holder ) internal view returns (uint256) { - if (positionType == Cellar.PositionType.ERC4626 || positionType == Cellar.PositionType.Cellar) { - return vault.maxWithdraw(holder); - } else if (positionType == Cellar.PositionType.ERC20) { - return ERC20(address(vault)).balanceOf(holder); + if (positionType == PositionType.ERC4626 || positionType == PositionType.Cellar) { + return ERC4626(position).balanceOf(holder); + } else if (positionType == PositionType.ERC20) { + return ERC20(position).balanceOf(holder); } else { revert("Unsupported Position Type"); } } function withdraw( - ERC4626 vault, - Cellar.PositionType positionType, + address position, + PositionType positionType, uint256 assets, address receiver, address owner ) internal returns (uint256 shares) { - if (positionType == Cellar.PositionType.ERC4626 || positionType == Cellar.PositionType.Cellar) { - shares = vault.withdraw(assets, receiver, owner); - } else if (positionType == Cellar.PositionType.ERC20) { + if (positionType == PositionType.ERC4626 || positionType == PositionType.Cellar) { + shares = ERC4626(position).withdraw(assets, receiver, owner); + } else if (positionType == PositionType.ERC20) { //since we dont need to actually do anything just return how many tokens the cellar has? Assumes the amount of tokens the cellar has is equal to the amount of shares it has - shares = ERC20(address(vault)).balanceOf(owner); + shares = ERC20(position).balanceOf(owner); } else { revert("Unsupported Position Type"); } } function deposit( - ERC4626 vault, - Cellar.PositionType positionType, + address position, + PositionType positionType, uint256 assets, address receiver ) internal returns (uint256 shares) { - if (positionType == Cellar.PositionType.ERC4626 || positionType == Cellar.PositionType.Cellar) { - shares = vault.deposit(assets, receiver); - } else if (positionType == Cellar.PositionType.ERC20) { + if (positionType == PositionType.ERC4626 || positionType == PositionType.Cellar) { + shares = ERC4626(position).deposit(assets, receiver); + } else if (positionType == PositionType.ERC20) { //since we dont need to actually do anything just return how many tokens the cellar has? Assumes the amount of tokens the cellar has is equal to the amount of shares it has - shares = ERC20(address(vault)).balanceOf(receiver); + shares = ERC20(position).balanceOf(receiver); } else { revert("Unsupported Position Type"); } diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 51061e6a..861242f6 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -289,8 +289,8 @@ contract CellarTest is Test { // Deposit assets from holding pool to USDC cellar position cellar.rebalance( - ERC4626(address(cellar)), - ERC4626(address(usdcCLR)), + address(cellar), + address(usdcCLR), assets, SwapRouter.Exchange.UNIV2, // Does not matter, no swap is involved. abi.encode(0) // Does not matter, no swap is involved. @@ -301,8 +301,8 @@ contract CellarTest is Test { // Withdraw some assets from USDC cellar position to holding position. cellar.rebalance( - ERC4626(address(usdcCLR)), - ERC4626(address(cellar)), + address(usdcCLR), + address(cellar), assets / 2, SwapRouter.Exchange.UNIV2, // Does not matter, no swap is involved. abi.encode(0) // Does not matter, no swap is involved. From 7b5466e2dc89ee99cacdd7f93a53a913b9bb1d59 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Tue, 28 Jun 2022 12:41:26 -0700 Subject: [PATCH 30/49] refactor(Cellar): rewrite withdraw logic, reimplement holding pool as a position --- src/Errors.sol | 3 +- src/base/Cellar.sol | 409 ++++++++++++++++++++++----------------- src/mocks/MockCellar.sol | 3 +- 3 files changed, 236 insertions(+), 179 deletions(-) diff --git a/src/Errors.sol b/src/Errors.sol index 001e2fc0..c26fd889 100644 --- a/src/Errors.sol +++ b/src/Errors.sol @@ -158,7 +158,8 @@ error USR_IncompatiblePosition(address incompatibleAsset, address expectedAsset) error USR_PositionAlreadyUsed(address position); /** - * @notice Attempted an action on a position that is not being used by the cellar. + * @notice Attempted an action on a position that is not being used by the cellar but must be for + * the operation to succeed. * @param position address of the invalid position */ error USR_InvalidPosition(address position); diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 163c8fe0..11c225d9 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -21,7 +21,7 @@ contract Cellar is ERC4626, Ownable, Multicall { using SafeCast for int256; using Math for uint256; - // ========================================= MULTI-POSITION CONFIG ========================================= + // ========================================= POSITIONS CONFIG ========================================= /** * @notice Emitted when a position is added. @@ -205,6 +205,47 @@ contract Cellar is ERC4626, Ownable, Multicall { emit TrustChanged(position, false); } + // ============================================ WITHDRAW CONFIG ============================================ + + event WithdrawTypeChanged(WithdrawType oldType, WithdrawType newType); + + enum WithdrawType { + Orderly, + Proportional + } + + WithdrawType public withdrawType; + + function setWithdrawType(WithdrawType newWithdrawType) external onlyOwner { + emit WithdrawTypeChanged(withdrawType, newWithdrawType); + + withdrawType = newWithdrawType; + } + + // ============================================ HOLDINGS CONFIG ============================================ + + event HoldingPositionChanged(address indexed oldPosition, address indexed newPosition); + + /** + * @notice The "default" position which uses the same asset as the cellar. It is the position + * deposited assets will automatically go into (perhaps while waiting to be rebalanced + * to other positions) and commonly the first position withdrawn assets will be pulled + * from if using orderly withdraws. + * @dev MUST accept the same asset as the cellar's `asset`. MUST be a position present in + * `positions`. Should be a static (eg. just holding) or lossless (eg. lending on Aave) + * position. Should not be expensive to move assets in or out of as this will occur + * frequently. It is highly recommended to choose a "simple" holding position. + */ + address public holdingPosition; + + function setHoldingPosition(address newHoldingPosition) external onlyOwner { + if (!isPositionUsed[newHoldingPosition]) revert USR_InvalidPosition(newHoldingPosition); + + emit HoldingPositionChanged(holdingPosition, newHoldingPosition); + + holdingPosition = newHoldingPosition; + } + // ============================================ ACCRUAL STORAGE ============================================ /** @@ -394,10 +435,13 @@ contract Cellar is ERC4626, Ownable, Multicall { Registry _registry, ERC20 _asset, address[] memory _positions, + address _holdingPosition, string memory _name, string memory _symbol ) ERC4626(_asset, _name, _symbol, 18) Ownable() { registry = _registry; + + // Initialize positions. positions = _positions; for (uint256 i; i < _positions.length; i++) { @@ -411,6 +455,11 @@ contract Cellar is ERC4626, Ownable, Multicall { ERC4626(position).asset().safeApprove(position, type(uint256).max); } + // Initialize holding position. + if (!isPositionUsed[_holdingPosition]) revert USR_InvalidPosition(_holdingPosition); + + holdingPosition = _holdingPosition; + // Initialize last accrual timestamp to time that cellar was created, otherwise the first // `accrue` will take platform fees from 1970 to the time it is called. lastAccrual = uint64(block.timestamp); @@ -421,6 +470,8 @@ contract Cellar is ERC4626, Ownable, Multicall { // =========================================== CORE LOGIC =========================================== + event PulledFromPosition(address indexed position, uint256 amount); + function beforeDeposit( uint256 assets, uint256, @@ -430,70 +481,71 @@ contract Cellar is ERC4626, Ownable, Multicall { if (assets > maxAssets) revert USR_DepositRestricted(assets, maxAssets); } - // TODO: move to ICellar once done - event WithdrawFromPositions( - address indexed caller, - address indexed receiver, - address indexed owner, - ERC20[] receivedAssets, - uint256[] amountsOut, - uint256 shares - ); + function afterDeposit( + uint256 assets, + uint256, + address + ) internal override { + // TODO: Refactor once support for different position types is implemented. + ERC4626(holdingPosition).deposit(assets, address(this)); + } - event PulledFromPosition(address indexed position, uint256 amount); + function withdraw( + uint256 assets, + address receiver, + address owner + ) public override returns (uint256 shares) { + (shares, , ) = withdrawFromPositions(assets, receiver, owner); + } - function withdrawFromPositionsInOrder( + function redeem( + uint256 shares, + address receiver, + address owner + ) public override returns (uint256 assets) { + (assets, , ) = redeemFromPositions(shares, receiver, owner); + } + + function withdrawFromPositions( uint256 assets, address receiver, address owner ) - external + public returns ( uint256 shares, ERC20[] memory receivedAssets, uint256[] memory amountsOut ) { - // Only withdraw if not enough assets in the holding pool. - if (totalHoldings() >= assets) { - receivedAssets = new ERC20[](1); - amountsOut = new uint256[](1); - - receivedAssets[0] = asset; - amountsOut[0] = assets; - - shares = withdraw(assets, receiver, owner); - } else { - // Would be more efficient to store `totalHoldings` to avoid calling twice, but will - // cause stack errors. - assets -= totalHoldings(); - - // Get data efficiently. - ( - uint256 _totalAssets, - , - ERC4626[] memory _positions, - ERC20[] memory positionAssets, - uint256[] memory positionBalances - ) = _getData(); - - // Get the amount of share needed to redeem. - shares = _previewWithdraw(assets, _totalAssets); - - if (msg.sender != owner) { - uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. - - if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; - } + // Get data efficiently. + ( + uint256 _totalAssets, // Store totalHoldings and pass into _withdrawInOrder if no stack errors. + ERC4626[] memory _positions, + ERC20[] memory positionAssets, + uint256[] memory positionBalances + ) = _getData(); + + // No need to check for rounding error, `previewWithdraw` rounds up. + shares = _previewWithdraw(assets, _totalAssets); - _burn(owner, shares); + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; + } + + uint256 totalShares = totalSupply; - (uint256[] memory amountsReceived, uint256 numOfReceivedAssets) = _pullFromPositions( - assets, - _positions, - positionAssets, - positionBalances - ); + _burn(owner, shares); + + emit Withdraw(msg.sender, receiver, owner, assets, shares); + + // Scope to avoid stack errors. + { + (uint256[] memory amountsReceived, uint256 numOfReceivedAssets) = withdrawType == WithdrawType.Orderly + ? _withdrawInOrder(assets, receiver, _positions, positionAssets, positionBalances) + : _withdrawInProportion(shares, totalShares, receiver, _positions, positionBalances); receivedAssets = new ERC20[](numOfReceivedAssets); amountsOut = new uint256[](numOfReceivedAssets); @@ -505,17 +557,68 @@ contract Cellar is ERC4626, Ownable, Multicall { receivedAssets[numOfReceivedAssets - 1] = positionAsset; amountsOut[numOfReceivedAssets - 1] = amountsReceived[i - 1]; numOfReceivedAssets--; - - // Transfer withdrawn assets to the receiver. - positionAsset.safeTransfer(receiver, amountsReceived[i - 1]); } + } + } + + function redeemFromPositions( + uint256 shares, + address receiver, + address owner + ) + public + returns ( + uint256 assets, + ERC20[] memory receivedAssets, + uint256[] memory amountsOut + ) + { + // Get data efficiently. + ( + uint256 _totalAssets, // Store totalHoldings and pass into _withdrawInOrder if no stack errors. + ERC4626[] memory _positions, + ERC20[] memory positionAssets, + uint256[] memory positionBalances + ) = _getData(); - emit WithdrawFromPositions(msg.sender, receiver, owner, receivedAssets, amountsOut, shares); + if (msg.sender != owner) { + uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. + + if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; } + + // Check for rounding error since we round down in previewRedeem. + require((assets = _convertToAssets(shares, _totalAssets)) != 0, "ZERO_ASSETS"); + + uint256 totalShares = totalSupply; + + _burn(owner, shares); + + // Scope to avoid stack errors. + { + (uint256[] memory amountsReceived, uint256 numOfReceivedAssets) = withdrawType == WithdrawType.Orderly + ? _withdrawInOrder(assets, receiver, _positions, positionAssets, positionBalances) + : _withdrawInProportion(shares, totalShares, receiver, _positions, positionBalances); + + receivedAssets = new ERC20[](numOfReceivedAssets); + amountsOut = new uint256[](numOfReceivedAssets); + + for (uint256 i = amountsReceived.length; i > 0; i--) { + if (amountsReceived[i - 1] == 0) continue; + + ERC20 positionAsset = positionAssets[i - 1]; + receivedAssets[numOfReceivedAssets - 1] = positionAsset; + amountsOut[numOfReceivedAssets - 1] = amountsReceived[i - 1]; + numOfReceivedAssets--; + } + } + + emit Withdraw(msg.sender, receiver, owner, assets, shares); } - function _pullFromPositions( + function _withdrawInOrder( uint256 assets, + address receiver, ERC4626[] memory _positions, ERC20[] memory positionAssets, uint256[] memory positionBalances @@ -529,20 +632,23 @@ contract Cellar is ERC4626, Ownable, Multicall { // Move on to next position if this one is empty. if (positionBalances[i] == 0) continue; + // TODO: Check for optimization. uint256 onePositionAsset = 10**positionAssets[i].decimals(); - uint256 positionAssetToAssetExchangeRate = priceRouter.getExchangeRate(positionAssets[i], asset); + uint256 exchangeRate = priceRouter.getExchangeRate(positionAssets[i], asset); // Denominate position balance in cellar's asset. - uint256 totalPositionBalanceInAssets = positionBalances[i].mulDivDown( - positionAssetToAssetExchangeRate, - onePositionAsset - ); + uint256 totalPositionBalanceInAssets = positionBalances[i].mulDivDown(exchangeRate, onePositionAsset); // We want to pull as much as we can from this position, but no more than needed. uint256 amount; - (amount, assets) = totalPositionBalanceInAssets > assets - ? (assets.mulDivDown(onePositionAsset, positionAssetToAssetExchangeRate), 0) - : (positionBalances[i], assets - totalPositionBalanceInAssets); + + if (totalPositionBalanceInAssets > assets) { + amount = assets.mulDivDown(onePositionAsset, exchangeRate); + assets = 0; + } else { + amount = positionBalances[i]; + assets = assets - totalPositionBalanceInAssets; + } // Return the amount that will be received and increment number of received assets. amountsReceived[i] = amount; @@ -551,8 +657,8 @@ contract Cellar is ERC4626, Ownable, Multicall { // Update position balance. getPositionData[address(_positions[i])].highWatermark -= amount.toInt256(); - // Withdraw from position. - _positions[i].withdraw(amount, address(this), address(this)); + // Withdraw from position to receiver. + _positions[i].withdraw(amount, receiver, address(this)); emit PulledFromPosition(address(_positions[i]), amount); @@ -561,82 +667,41 @@ contract Cellar is ERC4626, Ownable, Multicall { } } - // TODO: DRY this up. - - function withdrawFromPositionsInProportion( - uint256 assets, + function _withdrawInProportion( + uint256 shares, + uint256 totalShares, address receiver, - address owner - ) - external - returns ( - uint256 shares, - ERC20[] memory receivedAssets, - uint256[] memory amountsOut - ) - { - // Get data efficiently. - ( - uint256 _totalAssets, - , - ERC4626[] memory _positions, - ERC20[] memory positionAssets, - uint256[] memory positionBalances - ) = _getData(); - - // Get the amount of share needed to redeem. - shares = _previewWithdraw(assets, _totalAssets); - - if (msg.sender != owner) { - uint256 allowed = allowance[owner][msg.sender]; // Saves gas for limited approvals. - - if (allowed != type(uint256).max) allowance[owner][msg.sender] = allowed - shares; - } - - // Saves SLOADs during looping. - uint256 totalShares = totalSupply; - - _burn(owner, shares); - - uint256[] memory amountsReceived = new uint256[](_positions.length); - uint256 numOfReceivedAssets; + ERC4626[] memory _positions, + uint256[] memory positionBalances + ) internal returns (uint256[] memory amountsReceived, uint256 numOfReceivedAssets) { + amountsReceived = new uint256[](_positions.length); // Withdraw assets from positions in proportion to shares redeemed. for (uint256 i; i < _positions.length; i++) { + ERC4626 position = _positions[i]; + uint256 positionBalance = positionBalances[i]; + // Move on to next position if this one is empty. - if (positionBalances[i] == 0) continue; + if (positionBalance == 0) continue; // Get the amount of assets to withdraw from this position based on proportion to shares redeemed. - uint256 amount = positionBalances[i].mulDivDown(shares, totalShares); + uint256 amount = positionBalance.mulDivDown(shares, totalShares); // Update position balance. - getPositionData[address(_positions[i])].highWatermark -= amount.toInt256(); + getPositionData[address(position)].highWatermark -= amount.toInt256(); + // Return the amount that will be received and increment number of received assets. amountsReceived[i] = amount; numOfReceivedAssets++; - // Withdraw from position. - _positions[i].withdraw(amount, address(this), address(this)); - - emit PulledFromPosition(address(_positions[i]), amount); - } - - receivedAssets = new ERC20[](numOfReceivedAssets); - amountsOut = new uint256[](numOfReceivedAssets); - - for (uint256 i = amountsReceived.length; i > 0; i--) { - if (amountsReceived[i - 1] == 0) continue; + // Update position balance. + getPositionData[address(_positions[i])].highWatermark -= amount.toInt256(); - ERC20 positionAsset = positionAssets[i - 1]; - receivedAssets[numOfReceivedAssets - 1] = positionAsset; - amountsOut[numOfReceivedAssets - 1] = amountsReceived[i - 1]; - numOfReceivedAssets--; + // Withdraw from position to receiver. + position.withdraw(amount, receiver, address(this)); - // Transfer withdrawn assets to the receiver. - positionAsset.safeTransfer(receiver, amountsReceived[i - 1]); + emit PulledFromPosition(address(position), amount); } - - emit WithdrawFromPositions(msg.sender, receiver, owner, receivedAssets, amountsOut, shares); } // ========================================= ACCOUNTING LOGIC ========================================= @@ -657,14 +722,7 @@ contract Cellar is ERC4626, Ownable, Multicall { balances[i] = position.maxWithdraw(address(this)); } - assets = registry.priceRouter().getValues(positionAssets, balances, asset) + totalHoldings(); - } - - /** - * @notice The total amount of assets in holding position. - */ - function totalHoldings() public view returns (uint256) { - return asset.balanceOf(address(this)); + assets = registry.priceRouter().getValues(positionAssets, balances, asset); } /** @@ -739,6 +797,33 @@ contract Cellar is ERC4626, Ownable, Multicall { shares = totalShares == 0 ? assetsNormalized : assetsNormalized.mulDivUp(totalShares, totalAssetsNormalized); } + function _getData() + internal + view + returns ( + uint256 _totalAssets, + ERC4626[] memory _positions, + ERC20[] memory positionAssets, + uint256[] memory positionBalances + ) + { + uint256 len = positions.length; + + _positions = new ERC4626[](len); + positionAssets = new ERC20[](len); + positionBalances = new uint256[](len); + + for (uint256 i; i < len; i++) { + ERC4626 position = ERC4626(positions[i]); + + _positions[i] = position; + positionAssets[i] = position.asset(); + positionBalances[i] = position.maxWithdraw(address(this)); + } + + _totalAssets = registry.priceRouter().getValues(positionAssets, positionBalances, asset); + } + // =========================================== ACCRUAL LOGIC =========================================== /** @@ -758,7 +843,6 @@ contract Cellar is ERC4626, Ownable, Multicall { // Get data efficiently. ( uint256 _totalAssets, - , ERC4626[] memory _positions, ERC20[] memory positionAssets, uint256[] memory positionBalances @@ -809,6 +893,19 @@ contract Cellar is ERC4626, Ownable, Multicall { emit Accrual(platformFees, performanceFees); } + function _convertToFees(uint256 assets, uint256 exchangeRate) internal view returns (uint256 fees) { + // Convert amount of assets to take as fees to shares. + uint256 feesInShares = assets * exchangeRate; + + // Saves an SLOAD. + uint256 totalShares = totalSupply; + + // Get the amount of fees to mint. Without this, the value of fees minted would be slightly + // diluted because total shares increased while total assets did not. This counteracts that. + uint256 denominator = totalShares - feesInShares; + fees = denominator > 0 ? feesInShares.mulDivUp(totalShares, denominator) : 0; + } + // =========================================== POSITION LOGIC =========================================== /** @@ -961,35 +1058,6 @@ contract Cellar is ERC4626, Ownable, Multicall { // ========================================== HELPER FUNCTIONS ========================================== - function _getData() - internal - view - returns ( - uint256 _totalAssets, - uint256 _totalHoldings, - ERC4626[] memory _positions, - ERC20[] memory positionAssets, - uint256[] memory positionBalances - ) - { - uint256 len = positions.length; - - _positions = new ERC4626[](len); - positionAssets = new ERC20[](len); - positionBalances = new uint256[](len); - - for (uint256 i; i < len; i++) { - ERC4626 position = ERC4626(positions[i]); - - _positions[i] = position; - positionAssets[i] = position.asset(); - positionBalances[i] = position.maxWithdraw(address(this)); - } - - _totalHoldings = totalHoldings(); - _totalAssets = registry.priceRouter().getValues(positionAssets, positionBalances, asset) + _totalHoldings; - } - function _swap( ERC20 assetIn, uint256 amountIn, @@ -1013,17 +1081,4 @@ contract Cellar is ERC4626, Ownable, Multicall { // TODO: consider replacing with revert statement require(assetIn.balanceOf(address(this)) == expectedAssetsInAfter, "INCORRECT_PARAMS_AMOUNT"); } - - function _convertToFees(uint256 assets, uint256 exchangeRate) internal view returns (uint256 fees) { - // Convert amount of assets to take as fees to shares. - uint256 feesInShares = assets * exchangeRate; - - // Saves an SLOAD. - uint256 totalShares = totalSupply; - - // Get the amount of fees to mint. Without this, the value of fees minted would be slightly - // diluted because total shares increased while total assets did not. This counteracts that. - uint256 denominator = totalShares - feesInShares; - fees = denominator > 0 ? feesInShares.mulDivUp(totalShares, denominator) : 0; - } } diff --git a/src/mocks/MockCellar.sol b/src/mocks/MockCellar.sol index c6a31eb6..7442fa9e 100644 --- a/src/mocks/MockCellar.sol +++ b/src/mocks/MockCellar.sol @@ -12,9 +12,10 @@ contract MockCellar is Cellar, Test { Registry _registry, ERC20 _asset, address[] memory _positions, + address _holdingPosition, string memory _name, string memory _symbol - ) Cellar(_registry, _asset, _positions, _name, _symbol) {} + ) Cellar(_registry, _asset, _positions, _holdingPosition, _name, _symbol) {} function depositIntoPosition( address position, From fc3f95702b18dcd4ee4f9163a522eaf978e9e69c Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Tue, 28 Jun 2022 14:32:41 -0700 Subject: [PATCH 31/49] refactor(PositionLib): revise library functions --- src/base/Cellar.sol | 61 ++++++++++++++++++++-------------------- src/base/PositionLib.sol | 61 +++++++++++++++------------------------- src/mocks/MockCellar.sol | 35 +++++++++++++---------- test/Cellar.t.sol | 17 +++++++++-- 4 files changed, 88 insertions(+), 86 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index a445bfd1..8ff55dde 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -16,11 +16,11 @@ import "../Errors.sol"; contract Cellar is ERC4626, Ownable, Multicall { using AddressArray for address[]; + using PositionLib for address; using SafeTransferLib for ERC20; using SafeCast for uint256; using SafeCast for int256; using Math for uint256; - using PositionLib for address; // ========================================= MULTI-POSITION CONFIG ========================================= @@ -174,12 +174,12 @@ contract Cellar is ERC4626, Ownable, Multicall { mapping(address => bool) public isTrusted; - function trustPosition(address position) external onlyOwner { + function trustPosition(address position, PositionType positionType) external onlyOwner { // Trust position. isTrusted[position] = true; - // Set max approval to deposit into position if it is ERC4626. - position.asset(getPositionData[position].positionType).safeApprove(position, type(uint256).max); + // Set position type. + getPositionData[position].positionType = positionType; emit TrustChanged(position, true); } @@ -191,9 +191,6 @@ contract Cellar is ERC4626, Ownable, Multicall { // Remove position from the list of positions if it is present. positions.remove(position); - // Remove approval for position. - position.asset(getPositionData[position].positionType).safeApprove(position, 0); - // NOTE: After position has been removed, SP should be notified on the UI that the position // can no longer be used and to exit the position or rebalance its assets into another // position ASAP. @@ -389,6 +386,7 @@ contract Cellar is ERC4626, Ownable, Multicall { Registry _registry, ERC20 _asset, address[] memory _positions, + PositionType[] memory _positionTypes, string memory _name, string memory _symbol ) ERC4626(_asset, _name, _symbol, 18) Ownable() { @@ -402,8 +400,7 @@ contract Cellar is ERC4626, Ownable, Multicall { isTrusted[position] = true; isPositionUsed[position] = true; - //TODO need to set the position type before this line! - position.asset(getPositionData[position].positionType).safeApprove(position, type(uint256).max); + getPositionData[position].positionType = _positionTypes[i]; } // Initialize last accrual timestamp to time that cellar was created, otherwise the first @@ -544,11 +541,13 @@ contract Cellar is ERC4626, Ownable, Multicall { amountsReceived[i] = amount; numOfReceivedAssets++; + PositionData storage positionData = getPositionData[_positions[i]]; + // Update position balance. - getPositionData[_positions[i]].highWatermark -= amount.toInt256(); + positionData.highWatermark -= amount.toInt256(); // Withdraw from position. - _positions[i].withdraw(getPositionData[_positions[i]].positionType, amount, address(this), address(this)); + _positions[i].withdraw(positionData.positionType, amount, address(this)); emit PulledFromPosition(_positions[i], amount); @@ -570,7 +569,7 @@ contract Cellar is ERC4626, Ownable, Multicall { for (uint256 i; i < numOfPositions; i++) { positionAssets[i] = positions[i].asset(getPositionData[positions[i]].positionType); - balances[i] = positions[i].maxWithdraw(getPositionData[positions[i]].positionType, address(this)); + balances[i] = positions[i].balanceOf(getPositionData[positions[i]].positionType, address(this)); } assets = registry.priceRouter().getValues(positionAssets, balances, asset) + totalHoldings(); @@ -741,31 +740,31 @@ contract Cellar is ERC4626, Ownable, Multicall { SwapRouter.Exchange exchange, bytes calldata params ) external onlyOwner returns (uint256 assetsTo) { - // Withdraw from position, if not the rebalancing from the holding pool. - if (fromPosition != address(this)) { - // Without this, withdrawals from this position would be counted as losses during the - // next fee accrual. - getPositionData[fromPosition].highWatermark -= assetsFrom.toInt256(); + // Get position data. + PositionData storage fromPositionData = getPositionData[fromPosition]; + PositionData storage toPositionData = getPositionData[toPosition]; - fromPosition.withdraw(getPositionData[fromPosition].positionType, assetsFrom, address(this), address(this)); - } + // Without this, withdrawals from this position would be counted as losses during the + // next fee accrual. + fromPositionData.highWatermark -= assetsFrom.toInt256(); + + // Withdraw from position. + fromPosition.withdraw(fromPositionData.positionType, assetsFrom, address(this)); // Swap to the asset of the other position if necessary. - ERC20 fromAsset = fromPosition.asset(getPositionData[fromPosition].positionType); - ERC20 toAsset = toPosition.asset(getPositionData[toPosition].positionType); + ERC20 fromAsset = fromPosition.asset(fromPositionData.positionType); + ERC20 toAsset = toPosition.asset(toPositionData.positionType); assetsTo = fromAsset != toAsset ? _swap(fromAsset, assetsFrom, exchange, params) : assetsFrom; - // Deposit to position, if not the rebalancing to the holding pool - if (toPosition != address(this)) { - // Check that position being rebalanced to is currently being used. - if (!isPositionUsed[toPosition]) revert USR_InvalidPosition(address(toPosition)); + // Check that position being rebalanced to is currently being used. + if (!isPositionUsed[toPosition]) revert USR_InvalidPosition(address(toPosition)); - // Without this, deposits to this position would be counted as yield during the next fee - // accrual. - getPositionData[toPosition].highWatermark += assetsTo.toInt256(); + // Without this, deposits to this position would be counted as yield during the next fee + // accrual. + toPositionData.highWatermark += assetsTo.toInt256(); - toPosition.deposit(getPositionData[toPosition].positionType, assetsTo, address(this)); - } + // Deposit into position. + toPosition.deposit(toPositionData.positionType, assetsTo); } // ============================================ LIMITS LOGIC ============================================ @@ -897,7 +896,7 @@ contract Cellar is ERC4626, Ownable, Multicall { for (uint256 i; i < len; i++) { _positions[i] = positions[i]; positionAssets[i] = positions[i].asset(getPositionData[positions[i]].positionType); - positionBalances[i] = positions[i].maxWithdraw(getPositionData[positions[i]].positionType, address(this)); + positionBalances[i] = positions[i].balanceOf(getPositionData[positions[i]].positionType, address(this)); } _totalHoldings = totalHoldings(); diff --git a/src/base/PositionLib.sol b/src/base/PositionLib.sol index 126f2213..34ab0eab 100644 --- a/src/base/PositionLib.sol +++ b/src/base/PositionLib.sol @@ -2,7 +2,9 @@ pragma solidity 0.8.15; import { ERC4626, ERC20 } from "./ERC4626.sol"; -import { Cellar } from "./Cellar.sol"; +import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; + +error USR_InvalidPositionType(); enum PositionType { ERC20, @@ -11,74 +13,57 @@ enum PositionType { } library PositionLib { + using SafeTransferLib for ERC20; + function asset(address position, PositionType positionType) internal view returns (ERC20) { if (positionType == PositionType.ERC4626 || positionType == PositionType.Cellar) { return ERC4626(position).asset(); } else if (positionType == PositionType.ERC20) { return ERC20(position); } else { - revert("Unsupported Position Type"); - } - } - - function maxWithdraw( - address position, - PositionType positionType, - address holder - ) internal view returns (uint256) { - if (positionType == PositionType.ERC4626 || positionType == PositionType.Cellar) { - return ERC4626(position).maxWithdraw(holder); - } else if (positionType == PositionType.ERC20) { - return ERC20(position).balanceOf(holder); - } else { - revert("Unsupported Position Type"); + revert USR_InvalidPositionType(); } } function balanceOf( address position, PositionType positionType, - address holder - ) internal view returns (uint256) { + address owner + ) internal view returns (uint256 balance) { if (positionType == PositionType.ERC4626 || positionType == PositionType.Cellar) { - return ERC4626(position).balanceOf(holder); + balance = ERC4626(position).maxWithdraw(owner); } else if (positionType == PositionType.ERC20) { - return ERC20(position).balanceOf(holder); + balance = ERC20(position).balanceOf(owner); } else { - revert("Unsupported Position Type"); + revert USR_InvalidPositionType(); } } - function withdraw( + function deposit( address position, PositionType positionType, - uint256 assets, - address receiver, - address owner - ) internal returns (uint256 shares) { + uint256 assets + ) internal { if (positionType == PositionType.ERC4626 || positionType == PositionType.Cellar) { - shares = ERC4626(position).withdraw(assets, receiver, owner); - } else if (positionType == PositionType.ERC20) { - //since we dont need to actually do anything just return how many tokens the cellar has? Assumes the amount of tokens the cellar has is equal to the amount of shares it has - shares = ERC20(position).balanceOf(owner); - } else { - revert("Unsupported Position Type"); + ERC4626(position).asset().safeApprove(position, assets); + ERC4626(position).deposit(assets, address(this)); + } else if (positionType != PositionType.ERC20) { + revert USR_InvalidPositionType(); } } - function deposit( + function withdraw( address position, PositionType positionType, uint256 assets, address receiver - ) internal returns (uint256 shares) { + ) internal { if (positionType == PositionType.ERC4626 || positionType == PositionType.Cellar) { - shares = ERC4626(position).deposit(assets, receiver); + ERC4626(position).withdraw(assets, receiver, address(this)); } else if (positionType == PositionType.ERC20) { - //since we dont need to actually do anything just return how many tokens the cellar has? Assumes the amount of tokens the cellar has is equal to the amount of shares it has - shares = ERC20(position).balanceOf(receiver); + if (receiver != address(this)) ERC20(position).safeTransfer(receiver, assets); } else { - revert("Unsupported Position Type"); + revert USR_InvalidPositionType(); } } } diff --git a/src/mocks/MockCellar.sol b/src/mocks/MockCellar.sol index c6a31eb6..57a60530 100644 --- a/src/mocks/MockCellar.sol +++ b/src/mocks/MockCellar.sol @@ -1,10 +1,11 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.15; -import { Cellar, Registry, ERC4626, ERC20, SafeCast } from "src/base/Cellar.sol"; +import { Cellar, Registry, ERC4626, ERC20, SafeCast, PositionLib, PositionType } from "src/base/Cellar.sol"; import { Test, console } from "@forge-std/Test.sol"; contract MockCellar is Cellar, Test { + using PositionLib for address; using SafeCast for uint256; using SafeCast for int256; @@ -12,36 +13,40 @@ contract MockCellar is Cellar, Test { Registry _registry, ERC20 _asset, address[] memory _positions, + PositionType[] memory _positionTypes, string memory _name, string memory _symbol - ) Cellar(_registry, _asset, _positions, _name, _symbol) {} + ) Cellar(_registry, _asset, _positions, _positionTypes, _name, _symbol) {} function depositIntoPosition( address position, uint256 amount, address mintSharesTo ) external returns (uint256 shares) { - uint256 amountInAssets = registry.priceRouter().getValue(ERC4626(position).asset(), amount, asset); - shares = previewDeposit(amountInAssets); - - deal(address(ERC4626(position).asset()), address(this), amount); + shares = _depositIntoPosition(position, amount); - getPositionData[position].highWatermark += amount.toInt256(); + _mint(mintSharesTo, shares); + } - ERC4626(position).deposit(amount, address(this)); + function depositIntoPosition(address position, uint256 amount) external returns (uint256 shares) { + shares = _depositIntoPosition(position, amount); - _mint(mintSharesTo, shares); + totalSupply += shares; } - function depositIntoPosition(address position, uint256 amount) public returns (uint256 shares) { - uint256 amountInAssets = registry.priceRouter().getValue(ERC4626(position).asset(), amount, asset); + function _depositIntoPosition(address position, uint256 amount) internal returns (uint256 shares) { + PositionData storage positionData = getPositionData[position]; + PositionType positionType = positionData.positionType; + + ERC20 positionAsset = position.asset(positionType); + + uint256 amountInAssets = registry.priceRouter().getValue(positionAsset, amount, asset); shares = previewDeposit(amountInAssets); - deal(address(ERC4626(position).asset()), address(this), amount); + deal(address(positionAsset), address(this), amount); - getPositionData[position].highWatermark += amount.toInt256(); - totalSupply += shares; + positionData.highWatermark += amount.toInt256(); - ERC4626(position).deposit(amount, address(this)); + position.deposit(positionType, amount); } } diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 861242f6..c5b33434 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.15; -import { MockCellar, ERC4626, ERC20 } from "src/mocks/MockCellar.sol"; +import { MockCellar, ERC4626, ERC20, PositionType } from "src/mocks/MockCellar.sol"; import { Registry, PriceRouter, SwapRouter, IGravity } from "src/Registry.sol"; import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; import { IUniswapV2Router, IUniswapV3Router } from "src/modules/swap-router/SwapRouter.sol"; @@ -81,7 +81,19 @@ contract CellarTest is Test { positions[1] = address(wethCLR); positions[2] = address(wbtcCLR); - cellar = new MockCellar(registry, USDC, positions, "Multiposition Cellar LP Token", "multiposition-CLR"); + PositionType[] memory positionTypes = new PositionType[](3); + positionTypes[0] = PositionType.ERC4626; + positionTypes[1] = PositionType.ERC4626; + positionTypes[2] = PositionType.ERC4626; + + cellar = new MockCellar( + registry, + USDC, + positions, + positionTypes, + "Multiposition Cellar LP Token", + "multiposition-CLR" + ); vm.label(address(cellar), "cellar"); // Transfer ownership to this contract for testing. @@ -152,6 +164,7 @@ contract CellarTest is Test { cellar.depositIntoPosition(address(wethCLR), 1e18); assertEq(cellar.totalAssets(), 2000e6, "Should have updated total assets with assets deposited."); + assertEq(cellar.totalSupply(), 2000e18, "Should have updated total shares."); // Mint shares to user to redeem. deal(address(cellar), address(this), cellar.previewWithdraw(1000e6)); From c71a9c55111ac2c7e38d6337b8680cf0ae963697 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Tue, 28 Jun 2022 15:41:19 -0700 Subject: [PATCH 32/49] fix(SwapRouter): from address is now checked to ensure attackers can't use it to steal users funds --- src/modules/swap-router/SwapRouter.sol | 27 ++++++++++------- test/SwapRouter.t.sol | 41 +++++++++++++++++++++----- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/modules/swap-router/SwapRouter.sol b/src/modules/swap-router/SwapRouter.sol index 94f31264..cdf2f9ff 100644 --- a/src/modules/swap-router/SwapRouter.sol +++ b/src/modules/swap-router/SwapRouter.sol @@ -54,6 +54,7 @@ contract SwapRouter { * @return amountOut amount of tokens received from the swap */ function swap(Exchange exchange, bytes memory swapData) public returns (uint256 amountOut) { + swapData = abi.encode(msg.sender, swapData); // Route swap call to appropriate function using selector. (bool success, bytes memory result) = address(this).call( abi.encodeWithSelector(getExchangeSelector[exchange], swapData) @@ -103,9 +104,15 @@ contract SwapRouter { * @return amountOut amount of tokens received from the swap */ function swapWithUniV2(bytes memory swapData) public returns (uint256 amountOut) { - (address[] memory path, uint256 assets, uint256 assetsOutMin, address recipient, address from) = abi.decode( + address from; + (from, swapData) = abi.decode(swapData, (address, bytes)); + if (msg.sender != address(this)) { + //if called externally, then check from == msg.sender + require(from == msg.sender, "Restricted from input"); + } + (address[] memory path, uint256 assets, uint256 assetsOutMin, address recipient) = abi.decode( swapData, - (address[], uint256, uint256, address, address) + (address[], uint256, uint256, address) ); // Transfer assets to this contract to swap. @@ -138,14 +145,14 @@ contract SwapRouter { * @return amountOut amount of tokens received from the swap */ function swapWithUniV3(bytes memory swapData) public returns (uint256 amountOut) { - ( - address[] memory path, - uint24[] memory poolFees, - uint256 assets, - uint256 assetsOutMin, - address recipient, - address from - ) = abi.decode(swapData, (address[], uint24[], uint256, uint256, address, address)); + address from; + (from, swapData) = abi.decode(swapData, (address, bytes)); + if (msg.sender != address(this)) { + //if called externally, then check from == msg.sender + require(from == msg.sender, "Restricted from input"); + } + (address[] memory path, uint24[] memory poolFees, uint256 assets, uint256 assetsOutMin, address recipient) = abi + .decode(swapData, (address[], uint24[], uint256, uint256, address)); // Transfer assets to this contract to swap. ERC20 assetIn = ERC20(path[0]); diff --git a/test/SwapRouter.t.sol b/test/SwapRouter.t.sol index 3544ee95..cadc62e1 100644 --- a/test/SwapRouter.t.sol +++ b/test/SwapRouter.t.sol @@ -48,7 +48,7 @@ contract SwapRouterTest is Test { // Test swap. deal(address(DAI), sender, assets, true); DAI.approve(address(swapRouter), assets); - bytes memory swapData = abi.encode(path, assets, 0, reciever, sender); + bytes memory swapData = abi.encode(path, assets, 0, reciever); uint256 out = swapRouter.swap(SwapRouter.Exchange.UNIV2, swapData); assertTrue(DAI.balanceOf(sender) == 0, "DAI Balance of sender should be 0"); @@ -71,7 +71,7 @@ contract SwapRouterTest is Test { // Test swap. deal(address(DAI), sender, assets, true); DAI.approve(address(swapRouter), assets); - bytes memory swapData = abi.encode(path, assets, 0, reciever, sender); + bytes memory swapData = abi.encode(path, assets, 0, reciever); uint256 out = swapRouter.swap(SwapRouter.Exchange.UNIV2, swapData); assertTrue(DAI.balanceOf(sender) == 0, "DAI Balance of sender should be 0"); @@ -97,7 +97,7 @@ contract SwapRouterTest is Test { // Test swap. deal(address(DAI), sender, assets, true); DAI.approve(address(swapRouter), assets); - bytes memory swapData = abi.encode(path, poolFees, assets, 0, reciever, sender); + bytes memory swapData = abi.encode(path, poolFees, assets, 0, reciever); uint256 out = swapRouter.swap(SwapRouter.Exchange.UNIV3, swapData); assertTrue(DAI.balanceOf(sender) == 0, "DAI Balance of sender should be 0"); @@ -125,7 +125,7 @@ contract SwapRouterTest is Test { // Test swap. deal(address(DAI), sender, assets, true); DAI.approve(address(swapRouter), assets); - bytes memory swapData = abi.encode(path, poolFees, assets, 0, reciever, sender); + bytes memory swapData = abi.encode(path, poolFees, assets, 0, reciever); uint256 out = swapRouter.swap(SwapRouter.Exchange.UNIV3, swapData); assertTrue(DAI.balanceOf(sender) == 0, "DAI Balance of sender should be 0"); @@ -147,7 +147,7 @@ contract SwapRouterTest is Test { // Test swap. deal(address(DAI), sender, 2 * assets, true); DAI.approve(address(swapRouter), 2 * assets); - bytes memory swapData = abi.encode(path, assets, 0, reciever, sender); + bytes memory swapData = abi.encode(path, assets, 0, reciever); SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](2); exchanges[0] = SwapRouter.Exchange.UNIV2; @@ -183,7 +183,7 @@ contract SwapRouterTest is Test { // Test swap. deal(address(DAI), sender, 3 * assets, true); DAI.approve(address(swapRouter), 3 * assets); - bytes memory swapData = abi.encode(path, assets, 0, reciever, sender); + bytes memory swapData = abi.encode(path, assets, 0, reciever); SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](3); exchanges[0] = SwapRouter.Exchange.UNIV2; @@ -197,7 +197,7 @@ contract SwapRouterTest is Test { // Alter swap data to work with UniV3 uint24[] memory poolFees = new uint24[](1); poolFees[0] = 3000; - swapData = abi.encode(path, poolFees, assets, 0, reciever, sender); + swapData = abi.encode(path, poolFees, assets, 0, reciever); multiSwapData[2] = swapData; uint256[] memory amountsOut = swapRouter.multiSwap(exchanges, multiSwapData); @@ -210,4 +210,31 @@ contract SwapRouterTest is Test { assertTrue(WETH.balanceOf(reciever) > 0, "WETH Balance of Reciever should be greater than 0"); assertEq(sum, WETH.balanceOf(reciever), "Amount Out should equal WETH Balance of reciever"); } + + function testFromCheck() external { + // Specify the swap path. + address[] memory path = new address[](2); + path[0] = address(DAI); + path[1] = address(WETH); + + uint256 assets = 1e18; + + // Test swap. + deal(address(DAI), sender, assets, true); + DAI.approve(address(swapRouter), assets); + bytes memory swapData = abi.encode(path, assets, 0, reciever); + bytes memory attackerData = abi.encode(address(0xAAAA), swapData); + vm.expectRevert(bytes("Restricted from input")); + swapRouter.swapWithUniV2(attackerData); + vm.expectRevert(bytes("Restricted from input")); //make sure the UNIV3 swap reverts too + swapRouter.swapWithUniV3(attackerData); + vm.expectRevert(); + swapRouter.swapWithUniV2(swapData); //caller does not properly format swapData + bytes memory goodData = abi.encode(sender, swapData); + swapRouter.swapWithUniV2(goodData); + + //not really a security concern just want to confirm that improperly formatted swap data reverts + vm.expectRevert(bytes("Swap reverted.")); + swapRouter.swap(SwapRouter.Exchange.UNIV2, goodData); //swap data already has sender encoded + } } From 5f53365f7134508dd97cbae78bd2df17cdcab1aa Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Wed, 29 Jun 2022 15:17:14 -0700 Subject: [PATCH 33/49] refactor(CellarRouter): refactor cellar router to use the swap router --- src/CellarRouter.sol | 202 +++++++++---------------------- src/Registry.sol | 1 + src/interfaces/ICellarRouter.sol | 31 +++-- test/CellarRouter.t.sol | 5 +- 4 files changed, 77 insertions(+), 162 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index 6b84350e..a29ab543 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -7,6 +7,8 @@ import { Cellar } from "./base/Cellar.sol"; import { IUniswapV3Router } from "./interfaces/IUniswapV3Router.sol"; import { IUniswapV2Router02 as IUniswapV2Router } from "./interfaces/IUniswapV2Router02.sol"; import { ICellarRouter } from "./interfaces/ICellarRouter.sol"; +import { Registry } from "src/Registry.sol"; +import { SwapRouter } from "src/modules/swap-router/SwapRouter.sol"; import "./Errors.sol"; @@ -24,13 +26,23 @@ contract CellarRouter is ICellarRouter { */ IUniswapV2Router public immutable uniswapV2Router; // 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D + /** + * @notice Registry contract + */ + Registry public immutable registry; //TODO set registry + /** * @param _uniswapV3Router Uniswap V3 swap router address * @param _uniswapV2Router Uniswap V2 swap router address */ - constructor(IUniswapV3Router _uniswapV3Router, IUniswapV2Router _uniswapV2Router) { + constructor( + IUniswapV3Router _uniswapV3Router, + IUniswapV2Router _uniswapV2Router, + Registry _registry + ) { uniswapV3Router = _uniswapV3Router; uniswapV2Router = _uniswapV2Router; + registry = _registry; } // ======================================= DEPOSIT OPERATIONS ======================================= @@ -75,31 +87,31 @@ contract CellarRouter is ICellarRouter { * fee tiers of each pool used for each swap. The current possible pool fee tiers for * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap * V2, leave pool fees empty to use Uniswap V2 for swap. - * @param cellar address of the cellar to deposit into - * @param path array of [token1, token2, token3] that specifies the swap path on Sushiswap - * @param poolFees amount out of 1e4 (eg. 10000 == 1%) that represents the fee tier to use for each swap + * @param cellar address of the cellar + * @param exchange ENUM representing what exchange to make the swap at + * Refer to src/SwapRouter.sol for list of available options + * @param swapData bytes variable containing all the data needed to make a swap * @param assets amount of assets to deposit - * @param assetsOutMin minimum amount of assets received from swap - * @param receiver address receiving the shares + * @param receiver address to recieve the cellar shares + * @param assetIn ERC20 asset caller wants to swap and deposit with * @return shares amount of shares minted */ function depositAndSwapIntoCellar( Cellar cellar, - address[] calldata path, - uint24[] calldata poolFees, + SwapRouter.Exchange exchange, + bytes calldata swapData, uint256 assets, - uint256 assetsOutMin, - address receiver + address receiver, + ERC20 assetIn ) public returns (uint256 shares) { // Retrieve the asset being swapped and asset of cellar. ERC20 asset = cellar.asset(); - ERC20 assetIn = ERC20(path[0]); // Transfer assets from the user to the router. assetIn.safeTransferFrom(msg.sender, address(this), assets); - // Check whether a swap is necessary. If not, skip swap and deposit into cellar directly. - if (assetIn != asset) assets = _swap(path, poolFees, assets, assetsOutMin); + // Swap assets into desired token + assets = registry.swapRouter().swap(exchange, swapData); // Approve the cellar to spend assets. asset.safeApprove(address(cellar), assets); @@ -116,34 +128,32 @@ contract CellarRouter is ICellarRouter { * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap * V2, leave pool fees empty to use Uniswap V2 for swap. * @param cellar address of the cellar to deposit into - * @param path array of [token1, token2, token3] that specifies the swap path on Sushiswap - * @param poolFees amount out of 1e4 (eg. 10000 == 1%) that represents the fee tier to use for each swap + * @param exchange ENUM representing what exchange to make the swap at + * Refer to src/SwapRouter.sol for list of available options + * @param swapData bytes variable containing all the data needed to make a swap * @param assets amount of assets to deposit - * @param assetsOutMin minimum amount of assets received from swap - * @param receiver address receiving the shares + * @param assetIn ERC20 asset caller wants to swap and deposit with + * @param reciever address to recieve the cellar shares * @param deadline timestamp after which permit is invalid * @param signature a valid secp256k1 signature * @return shares amount of shares minted */ function depositAndSwapIntoCellarWithPermit( Cellar cellar, - address[] calldata path, - uint24[] calldata poolFees, + SwapRouter.Exchange exchange, + bytes calldata swapData, uint256 assets, - uint256 assetsOutMin, - address receiver, + ERC20 assetIn, + address reciever, uint256 deadline, bytes memory signature ) external returns (uint256 shares) { - // Retrieve the asset being swapped. - ERC20 assetIn = ERC20(path[0]); - // Approve for router to burn user shares via permit. (uint8 v, bytes32 r, bytes32 s) = _splitSignature(signature); assetIn.permit(msg.sender, address(this), assets, deadline, v, r, s); // Deposit assets into the cellar using a swap if necessary. - shares = depositAndSwapIntoCellar(cellar, path, poolFees, assets, assetsOutMin, receiver); + shares = depositAndSwapIntoCellar(cellar, exchange, swapData, assets, reciever, assetIn); } // ======================================= WITHDRAW OPERATIONS ======================================= @@ -153,38 +163,25 @@ contract CellarRouter is ICellarRouter { * withdrawn asset is not already. * @dev Permission is required from caller for router to burn shares. Please make sure that * caller has approved the router to spend their shares. - * @dev If using Uniswap V3 for swap, must specify the pool fee tier to use for each swap. For - * example, if there are "n" addresses in path, there should be "n-1" values specifying the - * fee tiers of each pool used for each swap. The current possible pool fee tiers for - * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap - * V2, leave pool fees empty to use Uniswap V2 for swap. * @param cellar address of the cellar - * @param path array of [token1, token2, token3] that specifies the swap path on swap - * @param poolFees amount out of 1e4 (eg. 10000 == 1%) that represents the fee tier to use for each swap + * @param exchange ENUM representing what exchange to make the swap at + * Refer to src/SwapRouter.sol for list of available options + * @param swapData bytes variable containing all the data needed to make a swap + * reciever address should be the callers address * @param assets amount of assets to withdraw - * @param assetsOutMin minimum amount of assets received from swap - * @param receiver address receiving the assets * @return shares amount of shares burned */ function withdrawAndSwapFromCellar( Cellar cellar, - address[] calldata path, - uint24[] calldata poolFees, - uint256 assets, - uint256 assetsOutMin, - address receiver + SwapRouter.Exchange exchange, + bytes calldata swapData, + uint256 assets ) public returns (uint256 shares) { - ERC20 asset = cellar.asset(); - ERC20 assetOut = ERC20(path[path.length - 1]); - // Withdraw assets from the cellar. shares = cellar.withdraw(assets, address(this), msg.sender); - // Check whether a swap is necessary. If not, skip swap and transfer withdrawn assets to receiver. - if (assetOut != asset) assets = _swap(path, poolFees, assets, assetsOutMin); - - // Transfer assets from the router to the receiver. - assetOut.safeTransfer(receiver, assets); + // Swap assets into desired token + registry.swapRouter().swap(exchange, swapData); } /** @@ -196,22 +193,19 @@ contract CellarRouter is ICellarRouter { * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap * V2, leave pool fees empty to use Uniswap V2 for swap. * @param cellar address of the cellar - * @param path array of [token1, token2, token3] that specifies the swap path on swap - * @param poolFees amount out of 1e4 (eg. 10000 == 1%) that represents the fee tier to use for each swap + * @param exchange ENUM representing what exchange to make the swap at + * Refer to src/SwapRouter.sol for list of available options + * @param swapData bytes variable containing all the data needed to make a swap * @param assets amount of assets to withdraw - * @param assetsOutMin minimum amount of assets received from swap - * @param receiver address receiving the assets * @param deadline timestamp after which permit is invalid * @param signature a valid secp256k1 signature * @return shares amount of shares burned */ function withdrawAndSwapFromCellarWithPermit( Cellar cellar, - address[] calldata path, - uint24[] calldata poolFees, + SwapRouter.Exchange exchange, + bytes calldata swapData, uint256 assets, - uint256 assetsOutMin, - address receiver, uint256 deadline, bytes memory signature ) external returns (uint256 shares) { @@ -220,7 +214,7 @@ contract CellarRouter is ICellarRouter { cellar.permit(msg.sender, address(this), assets, deadline, v, r, s); // Withdraw assets from the cellar and swap to another asset if necessary. - shares = withdrawAndSwapFromCellar(cellar, path, poolFees, assets, assetsOutMin, receiver); + shares = withdrawAndSwapFromCellar(cellar, exchange, swapData, assets); } /** @@ -232,40 +226,25 @@ contract CellarRouter is ICellarRouter { * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap * V2, leave pool fees empty to use Uniswap V2 for swap. * @param cellar address of the cellar - * @param paths array of arrays of [token1, token2, token3] that specifies the swap path on swap, - * if paths[i].length is 1, then no swap will be made - * @param poolFees array of arrays of amounts out of 1e4 (eg. 10000 == 1%) that represents the fee tier to use for each swap + * @param exchange ENUM representing what exchange to make the swap at + * Refer to src/SwapRouter.sol for list of available options + * @param swapData bytes variable containing all the data needed to make a swap + * reciever address should be the callers address * @param assets amount of assets to withdraw - * @param assetsIn array of amounts in for each swap, allows caller to swap the same asset at multiple different exchanges - * @param assetsOutMins array of minimum amounts of assets received from swaps - * @param receiver address receiving the assets * @return shares amount of shares burned */ function withdrawFromPositionsIntoSingleAsset( Cellar cellar, - address[][] calldata paths, - uint24[][] calldata poolFees, - uint256 assets, - uint256[] calldata assetsIn, - uint256[] calldata assetsOutMins, - address receiver + SwapRouter.Exchange[] calldata exchange, + bytes[] calldata swapData, + uint256 assets ) public returns (uint256 shares) { - ERC20 assetOut = ERC20(paths[0][paths[0].length - 1]); // get the last asset from the first path - require(paths.length == assetsOutMins.length, "Array length mismatch"); - // `paths.length` was stored in a memory variable, but was removed because stack too deep. + //TODO Brian add the balanceOf checks to make sure nothing is left in the router // Withdraw assets from the cellar. ERC20[] memory receivedAssets; - //uint256[] memory amountsOut; (shares, receivedAssets, ) = cellar.withdrawFromPositions(assets, address(this), msg.sender); - assets = assetOut.balanceOf(address(this)); //zero out for use in for loop - for (uint256 i = 0; i < paths.length; i++) { - if (paths[i].length == 1) continue; //no need to swap - assets += _swap(paths[i], poolFees[i], assetsIn[i], assetsOutMins[i]); - } - - // Transfer assets from the router to the receiver. - assetOut.safeTransfer(receiver, assets); + registry.swapRouter().multiSwap(exchange, swapData); } // ========================================= HELPER FUNCTIONS ========================================= @@ -300,67 +279,4 @@ contract CellarRouter is ICellarRouter { v := byte(0, mload(add(signature, 96))) } } - - /** - * @notice Perform a swap using Uniswap. - * @dev If using Uniswap V3 for swap, must specify the pool fee tier to use for each swap. For - * example, if there are "n" addresses in path, there should be "n-1" values specifying the - * fee tiers of each pool used for each swap. The current possible pool fee tiers for - * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap - * V2, leave pool fees empty to use Uniswap V2 for swap. - * @param path array of [token1, token2, token3] that specifies the swap path on swap - * @param poolFees amount out of 1e4 (eg. 10000 == 1%) that represents the fee tier to use for each swap - * @param assets amount of assets to withdraw - * @param assetsOutMin minimum amount of assets received from swap - * @return assetsOut amount of assets received after swap - */ - function _swap( - address[] calldata path, - uint24[] calldata poolFees, - uint256 assets, - uint256 assetsOutMin - ) internal returns (uint256 assetsOut) { - // Retrieve the asset being swapped. - ERC20 assetIn = ERC20(path[0]); - - // Check whether to use Uniswap V2 or Uniswap V3 for swap. - if (poolFees.length == 0) { - // If no pool fees are specified, use Uniswap V2 for swap. - - // Approve assets to be swapped through the router. - assetIn.safeApprove(address(uniswapV2Router), assets); - - // Execute the swap. - uint256[] memory amountsOut = uniswapV2Router.swapExactTokensForTokens( - assets, - assetsOutMin, - path, - address(this), - block.timestamp + 60 - ); - - assetsOut = amountsOut[amountsOut.length - 1]; - } else { - // If pool fees are specified, use Uniswap V3 for swap. - - // Approve assets to be swapped through the router. - assetIn.safeApprove(address(uniswapV3Router), assets); - - // Encode swap parameters. - bytes memory encodePackedPath = abi.encodePacked(address(assetIn)); - for (uint256 i = 1; i < path.length; i++) - encodePackedPath = abi.encodePacked(encodePackedPath, poolFees[i - 1], path[i]); - - // Execute the swap. - assetsOut = uniswapV3Router.exactInput( - IUniswapV3Router.ExactInputParams({ - path: encodePackedPath, - recipient: address(this), - deadline: block.timestamp + 60, - amountIn: assets, - amountOutMinimum: assetsOutMin - }) - ); - } - } } diff --git a/src/Registry.sol b/src/Registry.sol index a5122c44..37f2bb99 100644 --- a/src/Registry.sol +++ b/src/Registry.sol @@ -10,6 +10,7 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol"; // TODO: configure defaults // TODO: add natspec // TODO: add events +//TODO should this be deployed first with no constructor args? contract Registry is Ownable { SwapRouter public swapRouter; diff --git a/src/interfaces/ICellarRouter.sol b/src/interfaces/ICellarRouter.sol index 02bb9ccd..be63ff5c 100644 --- a/src/interfaces/ICellarRouter.sol +++ b/src/interfaces/ICellarRouter.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.15; import { ERC20 } from "@solmate/tokens/ERC20.sol"; import { Cellar } from "src/base/Cellar.sol"; +import { SwapRouter } from "src/modules/swap-router/SwapRouter.sol"; interface ICellarRouter { // ======================================= ROUTER OPERATIONS ======================================= @@ -17,40 +18,36 @@ interface ICellarRouter { function depositAndSwapIntoCellar( Cellar cellar, - address[] calldata path, - uint24[] calldata poolFees, + SwapRouter.Exchange exchange, + bytes calldata swapData, uint256 assets, - uint256 assetsOutMin, - address receiver + address receiver, + ERC20 assetIn ) external returns (uint256 shares); function depositAndSwapIntoCellarWithPermit( Cellar cellar, - address[] calldata path, - uint24[] calldata poolFees, + SwapRouter.Exchange exchange, + bytes calldata swapData, uint256 assets, - uint256 assetsOutMin, - address receiver, + ERC20 assetIn, + address reciever, uint256 deadline, bytes memory signature ) external returns (uint256 shares); function withdrawAndSwapFromCellar( Cellar cellar, - address[] calldata path, - uint24[] calldata poolFees, - uint256 assets, - uint256 assetsOutMin, - address receiver + SwapRouter.Exchange exchange, + bytes calldata swapData, + uint256 assets ) external returns (uint256 shares); function withdrawAndSwapFromCellarWithPermit( Cellar cellar, - address[] calldata path, - uint24[] calldata poolFees, + SwapRouter.Exchange exchange, + bytes calldata swapData, uint256 assets, - uint256 assetsOutMin, - address receiver, uint256 deadline, bytes memory signature ) external returns (uint256 shares); diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index 75101b26..fa650fd1 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -66,8 +66,6 @@ contract CellarRouterTest is Test { priceRouter = new MockPriceRouter(); exchange = new MockExchange(priceRouter); - router = new CellarRouter(IUniswapV3Router(address(exchange)), IUniswapV2Router(address(exchange))); - forkedRouter = new CellarRouter(IUniswapV3Router(uniV3Router), IUniswapV2Router(uniV2Router)); swapRouter = new SwapRouter(IUniswapV2Router(address(exchange)), IUniswapV3Router(address(exchange))); gravity = new MockGravity(); @@ -77,6 +75,9 @@ contract CellarRouterTest is Test { IGravity(address(gravity)) ); + router = new CellarRouter(IUniswapV3Router(address(exchange)), IUniswapV2Router(address(exchange)), registry); + forkedRouter = new CellarRouter(IUniswapV3Router(uniV3Router), IUniswapV2Router(uniV2Router), registry); + ABC = new MockERC20("ABC", 18); XYZ = new MockERC20("XYZ", 18); From 95c955816d256a36f2aad49663c01bd1cc199d52 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Thu, 30 Jun 2022 12:00:52 -0700 Subject: [PATCH 34/49] refactor(CellarRouter): allow withdrawing and swapping into multiple assets --- src/CellarRouter.sol | 78 ++++++++++++++++++++++++++--------------- test/CellarRouter.t.sol | 47 ++++++++++++------------- 2 files changed, 73 insertions(+), 52 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index 6b84350e..446be215 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -3,17 +3,22 @@ pragma solidity 0.8.15; import { ERC20 } from "@solmate/tokens/ERC20.sol"; import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; -import { Cellar } from "./base/Cellar.sol"; +import { Registry } from "src/Registry.sol"; +import { Cellar } from "src/base/Cellar.sol"; import { IUniswapV3Router } from "./interfaces/IUniswapV3Router.sol"; import { IUniswapV2Router02 as IUniswapV2Router } from "./interfaces/IUniswapV2Router02.sol"; import { ICellarRouter } from "./interfaces/ICellarRouter.sol"; import "./Errors.sol"; +// TODO: Fix comments (some of them still reference Sushiswap). +// TODO: Rewrite natspec comments to be more clear. + contract CellarRouter is ICellarRouter { using SafeTransferLib for ERC20; // ========================================== CONSTRUCTOR ========================================== + /** * @notice Uniswap V3 swap router contract. Used for swapping if pool fees are specified. */ @@ -44,7 +49,7 @@ contract CellarRouter is ICellarRouter { * @param signature a valid secp256k1 signature * @return shares amount of shares minted */ - function depositIntoCellarWithPermit( + function depositWithPermit( Cellar cellar, uint256 assets, address receiver, @@ -83,7 +88,7 @@ contract CellarRouter is ICellarRouter { * @param receiver address receiving the shares * @return shares amount of shares minted */ - function depositAndSwapIntoCellar( + function depositAndSwap( Cellar cellar, address[] calldata path, uint24[] calldata poolFees, @@ -99,7 +104,7 @@ contract CellarRouter is ICellarRouter { assetIn.safeTransferFrom(msg.sender, address(this), assets); // Check whether a swap is necessary. If not, skip swap and deposit into cellar directly. - if (assetIn != asset) assets = _swap(path, poolFees, assets, assetsOutMin); + if (assetIn != asset) assets = _swap(path, poolFees, assets, assetsOutMin, address(this)); // Approve the cellar to spend assets. asset.safeApprove(address(cellar), assets); @@ -125,7 +130,7 @@ contract CellarRouter is ICellarRouter { * @param signature a valid secp256k1 signature * @return shares amount of shares minted */ - function depositAndSwapIntoCellarWithPermit( + function depositAndSwapWithPermit( Cellar cellar, address[] calldata path, uint24[] calldata poolFees, @@ -166,7 +171,7 @@ contract CellarRouter is ICellarRouter { * @param receiver address receiving the assets * @return shares amount of shares burned */ - function withdrawAndSwapFromCellar( + function withdrawAndSwap( Cellar cellar, address[] calldata path, uint24[] calldata poolFees, @@ -181,7 +186,7 @@ contract CellarRouter is ICellarRouter { shares = cellar.withdraw(assets, address(this), msg.sender); // Check whether a swap is necessary. If not, skip swap and transfer withdrawn assets to receiver. - if (assetOut != asset) assets = _swap(path, poolFees, assets, assetsOutMin); + if (assetOut != asset) assets = _swap(path, poolFees, assets, assetsOutMin, address(this)); // Transfer assets from the router to the receiver. assetOut.safeTransfer(receiver, assets); @@ -205,7 +210,7 @@ contract CellarRouter is ICellarRouter { * @param signature a valid secp256k1 signature * @return shares amount of shares burned */ - function withdrawAndSwapFromCellarWithPermit( + function withdrawAndSwapWithPermit( Cellar cellar, address[] calldata path, uint24[] calldata poolFees, @@ -220,11 +225,11 @@ contract CellarRouter is ICellarRouter { cellar.permit(msg.sender, address(this), assets, deadline, v, r, s); // Withdraw assets from the cellar and swap to another asset if necessary. - shares = withdrawAndSwapFromCellar(cellar, path, poolFees, assets, assetsOutMin, receiver); + shares = withdrawAndSwap(cellar, path, poolFees, assets, assetsOutMin, receiver); } /** - * @notice Withdraws from a multi assset cellar and then performs swaps to another desired asset, if the + * @notice Withdraws from a multi assset cellar and then performs swaps to a single desired asset, if the * withdrawn asset is not already. * @dev If using Uniswap V3 for swap, must specify the pool fee tier to use for each swap. For * example, if there are "n" addresses in path, there should be "n-1" values specifying the @@ -232,16 +237,16 @@ contract CellarRouter is ICellarRouter { * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap * V2, leave pool fees empty to use Uniswap V2 for swap. * @param cellar address of the cellar - * @param paths array of arrays of [token1, token2, token3] that specifies the swap path on swap, - * if paths[i].length is 1, then no swap will be made - * @param poolFees array of arrays of amounts out of 1e4 (eg. 10000 == 1%) that represents the fee tier to use for each swap + * @param paths array of arrays of [token1, token2, token3] that specifies the swap path on swap + * @param poolFees array of arrays of amounts out of 1e4 (eg. 10000 == 1%) that represents the + * fee tier to use for each swap * @param assets amount of assets to withdraw * @param assetsIn array of amounts in for each swap, allows caller to swap the same asset at multiple different exchanges * @param assetsOutMins array of minimum amounts of assets received from swaps * @param receiver address receiving the assets * @return shares amount of shares burned */ - function withdrawFromPositionsIntoSingleAsset( + function withdrawFromPositionsAndSwap( Cellar cellar, address[][] calldata paths, uint24[][] calldata poolFees, @@ -250,22 +255,24 @@ contract CellarRouter is ICellarRouter { uint256[] calldata assetsOutMins, address receiver ) public returns (uint256 shares) { - ERC20 assetOut = ERC20(paths[0][paths[0].length - 1]); // get the last asset from the first path require(paths.length == assetsOutMins.length, "Array length mismatch"); - // `paths.length` was stored in a memory variable, but was removed because stack too deep. - // Withdraw assets from the cellar. + ERC20[] memory receivedAssets; - //uint256[] memory amountsOut; - (shares, receivedAssets, ) = cellar.withdrawFromPositions(assets, address(this), msg.sender); + uint256[] memory amountsOut; + (shares, receivedAssets, amountsOut) = cellar.withdrawFromPositions(assets, address(this), msg.sender); - assets = assetOut.balanceOf(address(this)); //zero out for use in for loop - for (uint256 i = 0; i < paths.length; i++) { - if (paths[i].length == 1) continue; //no need to swap - assets += _swap(paths[i], poolFees[i], assetsIn[i], assetsOutMins[i]); - } + uint256[] memory balancesBefore = _getBalancesBefore(receivedAssets, amountsOut); - // Transfer assets from the router to the receiver. - assetOut.safeTransfer(receiver, assets); + for (uint256 i; i < paths.length; i++) _swap(paths[i], poolFees[i], assetsIn[i], assetsOutMins[i], receiver); + + for (uint256 i; i < receivedAssets.length; i++) { + uint256 balanceBefore = balancesBefore[i]; + uint256 balanceAfter = receivedAssets[i].balanceOf(address(this)); + + if (balanceAfter != balanceBefore) { + receivedAssets[i].transfer(receiver, balanceAfter - balanceBefore); + } + } } // ========================================= HELPER FUNCTIONS ========================================= @@ -301,6 +308,20 @@ contract CellarRouter is ICellarRouter { } } + function _getBalancesBefore(ERC20[] memory assets, uint256[] memory amountsReceived) + internal + view + returns (uint256[] memory balancesBefore) + { + balancesBefore = new uint256[](assets.length); + + for (uint256 i; i < assets.length; i++) { + ERC20 asset = assets[i]; + + balancesBefore[i] = asset.balanceOf(address(this)) - amountsReceived[i]; + } + } + /** * @notice Perform a swap using Uniswap. * @dev If using Uniswap V3 for swap, must specify the pool fee tier to use for each swap. For @@ -318,7 +339,8 @@ contract CellarRouter is ICellarRouter { address[] calldata path, uint24[] calldata poolFees, uint256 assets, - uint256 assetsOutMin + uint256 assetsOutMin, + address receiver ) internal returns (uint256 assetsOut) { // Retrieve the asset being swapped. ERC20 assetIn = ERC20(path[0]); @@ -335,7 +357,7 @@ contract CellarRouter is ICellarRouter { assets, assetsOutMin, path, - address(this), + receiver, block.timestamp + 60 ); diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index 75101b26..cd7c00f4 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -34,23 +34,20 @@ contract CellarRouterTest is Test { MockERC4626 private forkedCellar; CellarRouter private forkedRouter; - bytes32 private constant PERMIT_TYPEHASH = - keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); - uint256 private constant privateKey = 0xBEEF; - address private owner = vm.addr(privateKey); + address private immutable owner = vm.addr(0xBEEF); // Mainnet contracts: address private constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; address private constant uniV2Router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; - ERC20 private DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + ERC20 private constant DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); - ERC20 private USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + ERC20 private constant USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); MockERC4626 private usdcCLR; - ERC20 private WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + ERC20 private constant WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); MockERC4626 private wethCLR; - ERC20 private WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); + ERC20 private constant WBTC = ERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); MockERC4626 private wbtcCLR; function setUp() public { @@ -66,6 +63,8 @@ contract CellarRouterTest is Test { priceRouter = new MockPriceRouter(); exchange = new MockExchange(priceRouter); + // TODO: Remove forked router and related tests, these tests should be in swap router. + router = new CellarRouter(IUniswapV3Router(address(exchange)), IUniswapV2Router(address(exchange))); forkedRouter = new CellarRouter(IUniswapV3Router(uniV3Router), IUniswapV2Router(uniV2Router)); swapRouter = new SwapRouter(IUniswapV2Router(address(exchange)), IUniswapV3Router(address(exchange))); @@ -255,6 +254,8 @@ contract CellarRouterTest is Test { // ======================================= WITHDRAW TESTS ======================================= + // TODO: Add test ensuring that no assets remain after withdrawal. + function testWithdrawAndSwapFromCellar(uint256 assets) external { assets = bound(assets, 1e18, type(uint72).max); @@ -334,7 +335,7 @@ contract CellarRouterTest is Test { assetsIn[1] = 1e8; multiCellar.approve(address(router), type(uint256).max); - router.withdrawFromPositionsIntoSingleAsset( + router.withdrawAndSwapFromCellarPositionsInOrder( multiCellar, paths, poolFees, @@ -360,26 +361,23 @@ contract CellarRouterTest is Test { deal(address(multiCellar), address(this), multiCellar.previewWithdraw(32_000e6)); //create paths - address[][] memory paths = new address[][](2); - paths[0] = new address[](1); - paths[0][0] = address(WETH); - paths[1] = new address[](2); - paths[1][0] = address(WBTC); - paths[1][1] = address(WETH); - uint24[][] memory poolFees = new uint24[][](2); + address[][] memory paths = new address[][](1); + paths[0] = new address[](2); + paths[0][0] = address(WBTC); + paths[0][1] = address(WETH); + + uint24[][] memory poolFees = new uint24[][](1); poolFees[0] = new uint24[](0); - poolFees[1] = new uint24[](0); - uint256 assets = 32_000e6; - uint256[] memory minOuts = new uint256[](2); + + uint256[] memory minOuts = new uint256[](1); minOuts[0] = 0; - minOuts[1] = 0; - uint256[] memory assetsIn = new uint256[](2); + uint256[] memory assetsIn = new uint256[](1); assetsIn[0] = 1e18; - assetsIn[1] = 1e8; + uint256 assets = 32_000e6; multiCellar.approve(address(router), type(uint256).max); - router.withdrawFromPositionsIntoSingleAsset( + router.withdrawAndSwapFromCellarPositionsInOrder( multiCellar, paths, poolFees, @@ -414,6 +412,7 @@ contract CellarRouterTest is Test { paths[3] = new address[](2); paths[3][0] = address(WBTC); paths[3][1] = address(USDC); + uint24[][] memory poolFees = new uint24[][](4); poolFees[0] = new uint24[](0); poolFees[1] = new uint24[](0); @@ -433,7 +432,7 @@ contract CellarRouterTest is Test { assetsIn[3] = 0.5e8; multiCellar.approve(address(router), type(uint256).max); - router.withdrawFromPositionsIntoSingleAsset( + router.withdrawAndSwapFromCellarPositionsInOrder( multiCellar, paths, poolFees, From 528a37d1718975990be4d69291ba631887776dd7 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Thu, 30 Jun 2022 12:43:25 -0700 Subject: [PATCH 35/49] fix(CellarRouter): missed approve calls before swaps, so tests were failing --- src/CellarRouter.sol | 12 +++- test/CellarRouter.t.sol | 118 +++++++++++++++++++++++----------------- 2 files changed, 79 insertions(+), 51 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index a29ab543..8ad234df 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -111,6 +111,7 @@ contract CellarRouter is ICellarRouter { assetIn.safeTransferFrom(msg.sender, address(this), assets); // Swap assets into desired token + assetIn.safeApprove(address(registry.swapRouter()), assets); assets = registry.swapRouter().swap(exchange, swapData); // Approve the cellar to spend assets. @@ -181,6 +182,7 @@ contract CellarRouter is ICellarRouter { shares = cellar.withdraw(assets, address(this), msg.sender); // Swap assets into desired token + cellar.asset().safeApprove(address(registry.swapRouter()), assets); registry.swapRouter().swap(exchange, swapData); } @@ -242,9 +244,17 @@ contract CellarRouter is ICellarRouter { //TODO Brian add the balanceOf checks to make sure nothing is left in the router // Withdraw assets from the cellar. ERC20[] memory receivedAssets; - (shares, receivedAssets, ) = cellar.withdrawFromPositions(assets, address(this), msg.sender); + uint256[] memory assetsOut; + (shares, receivedAssets, assetsOut) = cellar.withdrawFromPositions(assets, address(this), msg.sender); + + //need to approve the swaprouter to spend the cellar router assets + for (uint256 i = 0; i < assetsOut.length; i++) { + receivedAssets[i].safeApprove(address(registry.swapRouter()), assetsOut[i]); + } registry.swapRouter().multiSwap(exchange, swapData); + + //So if we aren't summing everthing together, then transferring out at the end, we need to handle the edge case where the user wants one of the assets they get from the cellar and isn't making a swap } // ========================================= HELPER FUNCTIONS ========================================= diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index fa650fd1..884c1128 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -26,6 +26,7 @@ contract CellarRouterTest is Test { MockGravity private gravity; Registry private registry; SwapRouter private swapRouter; + SwapRouter private realSwapRouter; MockERC4626 private cellar; MockCellar private multiCellar; //cellar with multiple assets @@ -67,6 +68,7 @@ contract CellarRouterTest is Test { exchange = new MockExchange(priceRouter); swapRouter = new SwapRouter(IUniswapV2Router(address(exchange)), IUniswapV3Router(address(exchange))); + realSwapRouter = new SwapRouter(IUniswapV2Router(uniV2Router), IUniswapV3Router(uniV3Router)); gravity = new MockGravity(); registry = new Registry( @@ -76,7 +78,7 @@ contract CellarRouterTest is Test { ); router = new CellarRouter(IUniswapV3Router(address(exchange)), IUniswapV2Router(address(exchange)), registry); - forkedRouter = new CellarRouter(IUniswapV3Router(uniV3Router), IUniswapV2Router(uniV2Router), registry); + //forkedRouter = new CellarRouter(IUniswapV3Router(uniV3Router), IUniswapV2Router(uniV2Router), registry); ABC = new MockERC20("ABC", 18); XYZ = new MockERC20("XYZ", 18); @@ -141,7 +143,15 @@ contract CellarRouterTest is Test { vm.startPrank(owner); XYZ.approve(address(router), assets); XYZ.mint(owner, assets); - uint256 shares = router.depositAndSwapIntoCellar(Cellar(address(cellar)), path, poolFees, assets, 0, owner); + bytes memory swapData = abi.encode(path, assets, 0, address(router)); + uint256 shares = router.depositAndSwapIntoCellar( + Cellar(address(cellar)), + SwapRouter.Exchange.UNIV2, + swapData, + assets, + owner, + XYZ + ); vm.stopPrank(); // Assets received by the cellar will be different from the amount of assets a user attempted @@ -163,6 +173,8 @@ contract CellarRouterTest is Test { // Ignore if not on mainnet. if (block.chainid != 1) return; + registry.setSwapRouter(realSwapRouter); // use the real swap router for this test + assets = bound(assets, 1e18, type(uint112).max); // Specify the swap path. @@ -170,20 +182,18 @@ contract CellarRouterTest is Test { path[0] = address(DAI); path[1] = address(WETH); - // Specify the pool fee tiers to use for each swap (none). - uint24[] memory poolFees; - // Test deposit and swap. vm.startPrank(owner); deal(address(DAI), owner, assets, true); - DAI.approve(address(forkedRouter), assets); - uint256 shares = forkedRouter.depositAndSwapIntoCellar( + DAI.approve(address(router), assets); + bytes memory swapData = abi.encode(path, assets, 0, address(router)); + uint256 shares = router.depositAndSwapIntoCellar( Cellar(address(forkedCellar)), - path, - poolFees, + SwapRouter.Exchange.UNIV2, + swapData, assets, - 0, - owner + owner, + DAI ); vm.stopPrank(); @@ -210,6 +220,8 @@ contract CellarRouterTest is Test { // Ignore if not on mainnet. if (block.chainid != 1) return; + registry.setSwapRouter(realSwapRouter); // use the real swap router for this test + assets = bound(assets, 1e18, type(uint112).max); // Specify the swap path. @@ -224,14 +236,15 @@ contract CellarRouterTest is Test { // Test deposit and swap. vm.startPrank(owner); deal(address(DAI), owner, assets, true); - DAI.approve(address(forkedRouter), assets); - uint256 shares = forkedRouter.depositAndSwapIntoCellar( + DAI.approve(address(router), assets); + bytes memory swapData = abi.encode(path, poolFees, assets, 0, address(router)); + uint256 shares = router.depositAndSwapIntoCellar( Cellar(address(forkedCellar)), - path, - poolFees, + SwapRouter.Exchange.UNIV3, + swapData, assets, - 0, - owner + owner, + DAI ); vm.stopPrank(); @@ -274,7 +287,15 @@ contract CellarRouterTest is Test { vm.startPrank(owner); XYZ.approve(address(router), assets); XYZ.mint(owner, assets); - router.depositAndSwapIntoCellar(Cellar(address(cellar)), path, poolFees, assets, 0, owner); + bytes memory swapData = abi.encode(path, assets, 0, address(router)); + router.depositAndSwapIntoCellar( + Cellar(address(cellar)), + SwapRouter.Exchange.UNIV2, + swapData, + assets, + owner, + XYZ + ); // Assets received by the cellar will be different from the amount of assets a user attempted // to deposit due to slippage swaps. @@ -285,13 +306,12 @@ contract CellarRouterTest is Test { // Test withdraw and swap. cellar.approve(address(router), assetsReceivedAfterDeposit); + swapData = abi.encode(path, assetsReceivedAfterDeposit, 0, owner); uint256 sharesRedeemed = router.withdrawAndSwapFromCellar( Cellar(address(cellar)), - path, - poolFees, - assetsReceivedAfterDeposit, - 0, - owner + SwapRouter.Exchange.UNIV2, + swapData, + assetsReceivedAfterDeposit ); vm.stopPrank(); @@ -335,15 +355,13 @@ contract CellarRouterTest is Test { assetsIn[1] = 1e8; multiCellar.approve(address(router), type(uint256).max); - router.withdrawFromPositionsIntoSingleAsset( - multiCellar, - paths, - poolFees, - assets, - assetsIn, - minOuts, - address(this) - ); + SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](2); + exchanges[0] = SwapRouter.Exchange.UNIV2; + exchanges[1] = SwapRouter.Exchange.UNIV2; + bytes[] memory swapData = new bytes[](2); + swapData[0] = abi.encode(paths[0], 1e18, 0, address(this)); + swapData[1] = abi.encode(paths[1], 1e8, 0, address(this)); + router.withdrawFromPositionsIntoSingleAsset(multiCellar, exchanges, swapData, assets); assertEq(USDC.balanceOf(address(this)), 30_400e6, "Did not recieve expected assets"); } @@ -380,15 +398,13 @@ contract CellarRouterTest is Test { assetsIn[1] = 1e8; multiCellar.approve(address(router), type(uint256).max); - router.withdrawFromPositionsIntoSingleAsset( - multiCellar, - paths, - poolFees, - assets, - assetsIn, - minOuts, - address(this) - ); + SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](2); + exchanges[0] = SwapRouter.Exchange.UNIV2; + exchanges[1] = SwapRouter.Exchange.UNIV2; + bytes[] memory swapData = new bytes[](2); + swapData[0] = abi.encode(paths[0], 1e18, 0, address(this)); + swapData[1] = abi.encode(paths[1], 1e8, 0, address(this)); + router.withdrawFromPositionsIntoSingleAsset(multiCellar, exchanges, swapData, assets); assertEq(WETH.balanceOf(address(this)), 15.25e18, "Did not recieve expected assets"); } @@ -434,15 +450,17 @@ contract CellarRouterTest is Test { assetsIn[3] = 0.5e8; multiCellar.approve(address(router), type(uint256).max); - router.withdrawFromPositionsIntoSingleAsset( - multiCellar, - paths, - poolFees, - assets, - assetsIn, - minOuts, - address(this) - ); + SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](4); + exchanges[0] = SwapRouter.Exchange.UNIV2; + exchanges[1] = SwapRouter.Exchange.UNIV2; + exchanges[2] = SwapRouter.Exchange.UNIV2; + exchanges[3] = SwapRouter.Exchange.UNIV2; + bytes[] memory swapData = new bytes[](4); + swapData[0] = abi.encode(paths[0], 0.5e18, 0, address(this)); + swapData[1] = abi.encode(paths[1], 0.5e8, 0, address(this)); + swapData[2] = abi.encode(paths[2], 0.5e18, 0, address(this)); + swapData[3] = abi.encode(paths[3], 0.5e8, 0, address(this)); + router.withdrawFromPositionsIntoSingleAsset(multiCellar, exchanges, swapData, assets); assertEq(USDC.balanceOf(address(this)), 30_400e6, "Did not recieve expected assets"); } From cc62459547402c279fd0a3af292e0149f7c807d4 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Thu, 30 Jun 2022 15:40:11 -0700 Subject: [PATCH 36/49] refactory(CellarRouter): use swap router in cellar router --- src/CellarRouter.sol | 29 ++++++--- src/interfaces/ICellarRouter.sol | 16 ++--- src/modules/swap-router/SwapRouter.sol | 3 +- test/CellarRouter.t.sol | 82 ++++++++++++-------------- 4 files changed, 69 insertions(+), 61 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index 38453d7f..49898ee5 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -99,6 +99,7 @@ contract CellarRouter is ICellarRouter { * @param swapData bytes variable containing all the data needed to make a swap * @param assets amount of assets to deposit * @param receiver address to recieve the cellar shares + * @param assetIn ERC20 token used to deposit * @return shares amount of shares minted */ function depositAndSwap( @@ -106,7 +107,8 @@ contract CellarRouter is ICellarRouter { SwapRouter.Exchange exchange, bytes calldata swapData, uint256 assets, - address receiver + address receiver, + ERC20 assetIn ) public returns (uint256 shares) { // Retrieve the asset being swapped and asset of cellar. ERC20 asset = cellar.asset(); @@ -158,7 +160,7 @@ contract CellarRouter is ICellarRouter { assetIn.permit(msg.sender, address(this), assets, deadline, v, r, s); // Deposit assets into the cellar using a swap if necessary. - shares = depositAndSwapIntoCellar(cellar, exchange, swapData, assets, reciever, assetIn); + shares = depositAndSwap(cellar, exchange, swapData, assets, reciever, assetIn); } // ======================================= WITHDRAW OPERATIONS ======================================= @@ -176,13 +178,15 @@ contract CellarRouter is ICellarRouter { * @param swapData bytes variable containing all the data needed to make a swap * reciever address should be the callers address * @param assets amount of assets to withdraw + * @param receiver the address swapped tokens are sent to * @return shares amount of shares burned */ function withdrawAndSwap( Cellar cellar, SwapRouter.Exchange exchange, bytes calldata swapData, - uint256 assets + uint256 assets, + address receiver ) public returns (uint256 shares) { // Withdraw assets from the cellar. shares = cellar.withdraw(assets, address(this), msg.sender); @@ -207,6 +211,7 @@ contract CellarRouter is ICellarRouter { * @param assets amount of assets to withdraw * @param deadline timestamp after which permit is invalid * @param signature a valid secp256k1 signature + * @param receiver the address swapped tokens are sent to * @return shares amount of shares burned */ function withdrawAndSwapWithPermit( @@ -215,14 +220,15 @@ contract CellarRouter is ICellarRouter { bytes calldata swapData, uint256 assets, uint256 deadline, - bytes memory signature + bytes memory signature, + address receiver ) external returns (uint256 shares) { // Approve for router to burn user shares via permit. (uint8 v, bytes32 r, bytes32 s) = _splitSignature(signature); cellar.permit(msg.sender, address(this), assets, deadline, v, r, s); // Withdraw assets from the cellar and swap to another asset if necessary. - shares = withdrawAndSwap(cellar, exchange, swapData, assets); + shares = withdrawAndSwap(cellar, exchange, swapData, assets, receiver); } /** @@ -239,16 +245,16 @@ contract CellarRouter is ICellarRouter { * @param swapData bytes variable containing all the data needed to make a swap * reciever address should be the callers address * @param assets amount of assets to withdraw + * @param receiver the address swapped tokens are sent to * @return shares amount of shares burned */ function withdrawFromPositionsAndSwap( Cellar cellar, SwapRouter.Exchange[] calldata exchange, bytes[] calldata swapData, - uint256 assets + uint256 assets, + address receiver ) public returns (uint256 shares) { - require(paths.length == assetsOutMins.length, "Array length mismatch"); - ERC20[] memory receivedAssets; uint256[] memory amountsOut; (shares, receivedAssets, amountsOut) = cellar.withdrawFromPositions(assets, address(this), msg.sender); @@ -258,8 +264,15 @@ contract CellarRouter is ICellarRouter { bytes[] memory data = new bytes[](swapData.length); for (uint256 i; i < swapData.length; i++) data[i] = abi.encodeCall(SwapRouter.swap, (exchange[i], swapData[i])); + for (uint256 i; i < receivedAssets.length; i++) + receivedAssets[i].safeApprove(address(registry.swapRouter()), amountsOut[i]); + registry.swapRouter().multicall(data); + //zero out approval in case it wasn't used + for (uint256 i; i < receivedAssets.length; i++) + receivedAssets[i].safeApprove(address(registry.swapRouter()), 0); + for (uint256 i; i < receivedAssets.length; i++) { uint256 balanceBefore = balancesBefore[i]; uint256 balanceAfter = receivedAssets[i].balanceOf(address(this)); diff --git a/src/interfaces/ICellarRouter.sol b/src/interfaces/ICellarRouter.sol index be63ff5c..28c73763 100644 --- a/src/interfaces/ICellarRouter.sol +++ b/src/interfaces/ICellarRouter.sol @@ -8,7 +8,7 @@ import { SwapRouter } from "src/modules/swap-router/SwapRouter.sol"; interface ICellarRouter { // ======================================= ROUTER OPERATIONS ======================================= - function depositIntoCellarWithPermit( + function depositWithPermit( Cellar cellar, uint256 assets, address receiver, @@ -16,7 +16,7 @@ interface ICellarRouter { bytes memory signature ) external returns (uint256 shares); - function depositAndSwapIntoCellar( + function depositAndSwap( Cellar cellar, SwapRouter.Exchange exchange, bytes calldata swapData, @@ -25,7 +25,7 @@ interface ICellarRouter { ERC20 assetIn ) external returns (uint256 shares); - function depositAndSwapIntoCellarWithPermit( + function depositAndSwapWithPermit( Cellar cellar, SwapRouter.Exchange exchange, bytes calldata swapData, @@ -36,19 +36,21 @@ interface ICellarRouter { bytes memory signature ) external returns (uint256 shares); - function withdrawAndSwapFromCellar( + function withdrawAndSwap( Cellar cellar, SwapRouter.Exchange exchange, bytes calldata swapData, - uint256 assets + uint256 assets, + address receiver ) external returns (uint256 shares); - function withdrawAndSwapFromCellarWithPermit( + function withdrawAndSwapWithPermit( Cellar cellar, SwapRouter.Exchange exchange, bytes calldata swapData, uint256 assets, uint256 deadline, - bytes memory signature + bytes memory signature, + address receiver ) external returns (uint256 shares); } diff --git a/src/modules/swap-router/SwapRouter.sol b/src/modules/swap-router/SwapRouter.sol index cdf2f9ff..1cfe93c3 100644 --- a/src/modules/swap-router/SwapRouter.sol +++ b/src/modules/swap-router/SwapRouter.sol @@ -5,8 +5,9 @@ import { ERC20 } from "@solmate/tokens/ERC20.sol"; import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; import { IUniswapV2Router02 as IUniswapV2Router } from "src/interfaces/IUniswapV2Router02.sol"; import { IUniswapV3Router } from "src/interfaces/IUniswapV3Router.sol"; +import { Multicall } from "src/base/Multicall.sol"; -contract SwapRouter { +contract SwapRouter is Multicall { using SafeTransferLib for ERC20; /** @notice Planned additions diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index 167231e8..43172b39 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -97,12 +97,28 @@ contract CellarRouterTest is Test { cellar = new MockERC4626(ERC20(address(ABC)), "ABC Cellar", "abcCLR", 18); forkedCellar = new MockERC4626(ERC20(address(WETH)), "WETH Cellar", "WETHCLR", 18); // For mainnet fork test. - address[] memory positions = new address[](3); - positions[0] = address(usdcCLR); - positions[1] = address(wethCLR); - positions[2] = address(wbtcCLR); - - multiCellar = new MockCellar(registry, USDC, positions, "Multiposition Cellar LP Token", "multiposition-CLR"); + address[] memory positions = new address[](4); + positions[0] = address(USDC); + positions[1] = address(usdcCLR); + positions[2] = address(wethCLR); + positions[3] = address(wbtcCLR); + + Cellar.PositionType[] memory positionTypes = new Cellar.PositionType[](4); + positionTypes[0] = Cellar.PositionType.ERC20; + positionTypes[1] = Cellar.PositionType.ERC4626; + positionTypes[2] = Cellar.PositionType.ERC4626; + positionTypes[3] = Cellar.PositionType.ERC4626; + + multiCellar = new MockCellar( + registry, + USDC, + positions, + positionTypes, + address(USDC), + Cellar.WithdrawType.Orderly, + "Multiposition Cellar LP Token", + "multiposition-CLR" + ); vm.label(address(cellar), "cellar"); // Transfer ownership to this contract for testing. @@ -133,15 +149,12 @@ contract CellarRouterTest is Test { path[0] = address(XYZ); path[1] = address(ABC); - // Specify the pool fee tiers to use for each swap (none). - uint24[] memory poolFees; - // Test deposit and swap. vm.startPrank(owner); XYZ.approve(address(router), assets); XYZ.mint(owner, assets); bytes memory swapData = abi.encode(path, assets, 0, address(router)); - uint256 shares = router.depositAndSwapIntoCellar( + uint256 shares = router.depositAndSwap( Cellar(address(cellar)), SwapRouter.Exchange.UNIV2, swapData, @@ -184,7 +197,7 @@ contract CellarRouterTest is Test { deal(address(DAI), owner, assets, true); DAI.approve(address(router), assets); bytes memory swapData = abi.encode(path, assets, 0, address(router)); - uint256 shares = router.depositAndSwapIntoCellar( + uint256 shares = router.depositAndSwap( Cellar(address(forkedCellar)), SwapRouter.Exchange.UNIV2, swapData, @@ -235,7 +248,7 @@ contract CellarRouterTest is Test { deal(address(DAI), owner, assets, true); DAI.approve(address(router), assets); bytes memory swapData = abi.encode(path, poolFees, assets, 0, address(router)); - uint256 shares = router.depositAndSwapIntoCellar( + uint256 shares = router.depositAndSwap( Cellar(address(forkedCellar)), SwapRouter.Exchange.UNIV3, swapData, @@ -279,22 +292,12 @@ contract CellarRouterTest is Test { path[0] = address(XYZ); path[1] = address(ABC); - // Specify the pool fee tiers to use for each swap (none). - uint24[] memory poolFees; - // Deposit and swap vm.startPrank(owner); XYZ.approve(address(router), assets); XYZ.mint(owner, assets); bytes memory swapData = abi.encode(path, assets, 0, address(router)); - router.depositAndSwapIntoCellar( - Cellar(address(cellar)), - SwapRouter.Exchange.UNIV2, - swapData, - assets, - owner, - XYZ - ); + router.depositAndSwap(Cellar(address(cellar)), SwapRouter.Exchange.UNIV2, swapData, assets, owner, XYZ); // Assets received by the cellar will be different from the amount of assets a user attempted // to deposit due to slippage swaps. @@ -306,11 +309,12 @@ contract CellarRouterTest is Test { // Test withdraw and swap. cellar.approve(address(router), assetsReceivedAfterDeposit); swapData = abi.encode(path, assetsReceivedAfterDeposit, 0, owner); - uint256 sharesRedeemed = router.withdrawAndSwapFromCellar( + uint256 sharesRedeemed = router.withdrawAndSwap( Cellar(address(cellar)), SwapRouter.Exchange.UNIV2, swapData, - assetsReceivedAfterDeposit + assetsReceivedAfterDeposit, + owner ); vm.stopPrank(); @@ -327,7 +331,6 @@ contract CellarRouterTest is Test { function testWithdrawFromPositionsIntoSingleAssetWTwoSwaps() external { multiCellar.depositIntoPosition(address(wethCLR), 1e18); multiCellar.depositIntoPosition(address(wbtcCLR), 1e8); - assertEq(multiCellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); // Mint shares to user to redeem. @@ -341,9 +344,7 @@ contract CellarRouterTest is Test { paths[1] = new address[](2); paths[1][0] = address(WBTC); paths[1][1] = address(USDC); - uint24[][] memory poolFees = new uint24[][](2); - poolFees[0] = new uint24[](0); - poolFees[1] = new uint24[](0); + uint256 assets = 32_000e6; uint256[] memory minOuts = new uint256[](2); minOuts[0] = 0; @@ -360,7 +361,8 @@ contract CellarRouterTest is Test { bytes[] memory swapData = new bytes[](2); swapData[0] = abi.encode(paths[0], 1e18, 0, address(this)); swapData[1] = abi.encode(paths[1], 1e8, 0, address(this)); - router.withdrawFromPositionsIntoSingleAsset(multiCellar, exchanges, swapData, assets); + + router.withdrawFromPositionsAndSwap(multiCellar, exchanges, swapData, assets, address(this)); assertEq(USDC.balanceOf(address(this)), 30_400e6, "Did not recieve expected assets"); } @@ -383,9 +385,6 @@ contract CellarRouterTest is Test { paths[0][0] = address(WBTC); paths[0][1] = address(WETH); - uint24[][] memory poolFees = new uint24[][](1); - poolFees[0] = new uint24[](0); - uint256[] memory minOuts = new uint256[](1); minOuts[0] = 0; @@ -394,13 +393,11 @@ contract CellarRouterTest is Test { uint256 assets = 32_000e6; multiCellar.approve(address(router), type(uint256).max); - SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](2); + SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](1); exchanges[0] = SwapRouter.Exchange.UNIV2; - exchanges[1] = SwapRouter.Exchange.UNIV2; - bytes[] memory swapData = new bytes[](2); - swapData[0] = abi.encode(paths[0], 1e18, 0, address(this)); - swapData[1] = abi.encode(paths[1], 1e8, 0, address(this)); - router.withdrawFromPositionsIntoSingleAsset(multiCellar, exchanges, swapData, assets); + bytes[] memory swapData = new bytes[](1); + swapData[0] = abi.encode(paths[0], 1e8, 0, address(this)); + router.withdrawFromPositionsAndSwap(multiCellar, exchanges, swapData, assets, address(this)); assertEq(WETH.balanceOf(address(this)), 15.25e18, "Did not recieve expected assets"); } @@ -428,11 +425,6 @@ contract CellarRouterTest is Test { paths[3][0] = address(WBTC); paths[3][1] = address(USDC); - uint24[][] memory poolFees = new uint24[][](4); - poolFees[0] = new uint24[](0); - poolFees[1] = new uint24[](0); - poolFees[2] = new uint24[](0); - poolFees[3] = new uint24[](0); uint256 assets = 32_000e6; uint256[] memory minOuts = new uint256[](4); minOuts[0] = 0; @@ -457,7 +449,7 @@ contract CellarRouterTest is Test { swapData[1] = abi.encode(paths[1], 0.5e8, 0, address(this)); swapData[2] = abi.encode(paths[2], 0.5e18, 0, address(this)); swapData[3] = abi.encode(paths[3], 0.5e8, 0, address(this)); - router.withdrawFromPositionsIntoSingleAsset(multiCellar, exchanges, swapData, assets); + router.withdrawFromPositionsAndSwap(multiCellar, exchanges, swapData, assets, address(this)); assertEq(USDC.balanceOf(address(this)), 30_400e6, "Did not recieve expected assets"); } From d64a11a076c38e5c76041db2e68c4e4c321c6f54 Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Fri, 1 Jul 2022 13:38:18 -0700 Subject: [PATCH 37/49] refactor(Cellar): change cellar swap function to work with new swap router --- src/base/Cellar.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 7bde2389..e4f2e3ef 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -926,7 +926,7 @@ contract Cellar is ERC4626, Ownable, Multicall { // Swap to the asset of the other position if necessary. ERC20 fromAsset = _assetOf(fromPosition); ERC20 toAsset = _assetOf(toPosition); - assetsTo = fromAsset != toAsset ? _swap(fromAsset, assetsFrom, exchange, params) : assetsFrom; + assetsTo = fromAsset != toAsset ? _swap(fromAsset, assetsFrom, exchange, params, address(this)) : assetsFrom; // Deposit into position. _depositTo(toPosition, assetsTo); @@ -1100,7 +1100,8 @@ contract Cellar is ERC4626, Ownable, Multicall { ERC20 assetIn, uint256 amountIn, SwapRouter.Exchange exchange, - bytes calldata params + bytes calldata params, + address recipient ) internal returns (uint256 amountOut) { // Store the expected amount of the asset in that we expect to have after the swap. uint256 expectedAssetsInAfter = assetIn.balanceOf(address(this)) - amountIn; @@ -1112,7 +1113,7 @@ contract Cellar is ERC4626, Ownable, Multicall { assetIn.safeApprove(address(swapRouter), amountIn); // Perform swap. - amountOut = swapRouter.swap(exchange, params); + amountOut = swapRouter.swap(exchange, params, recipient); // Check that the amount of assets swapped is what is expected. Will revert if the `params` // specified a different amount of assets to swap then `amountIn`. From efb0ce08c8274bfab4b41bfc9693e4938f25dbbc Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Tue, 5 Jul 2022 14:54:00 -0700 Subject: [PATCH 38/49] chore: remove old TODOs --- src/CellarRouter.sol | 2 +- test/Cellar.t.sol | 2 -- test/CellarRouter.t.sol | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index 54e427ed..fa17e324 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -33,7 +33,7 @@ contract CellarRouter is ICellarRouter { /** * @notice Registry contract */ - Registry public immutable registry; // TODO: set registry + Registry public immutable registry; /** * @dev Owner will be set to the Gravity Bridge, which relays instructions from the Steward diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index dcd5d88b..477ba09a 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -13,8 +13,6 @@ import { MockGravity } from "src/mocks/MockGravity.sol"; import { Test, console } from "@forge-std/Test.sol"; import { Math } from "src/utils/Math.sol"; -// TODO: Add test for proportional withdraw type. - contract CellarTest is Test { using SafeTransferLib for ERC20; using Math for uint256; diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index 60d13fda..1815ad81 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -219,7 +219,6 @@ contract CellarRouterTest is Test { assertEq(DAI.balanceOf(owner), 0, "Should have deposited assets from user."); } - // TODO: make external function testDepositAndSwapUsingUniswapV3OnMainnet(uint256 assets) external { // Ignore if not on mainnet. if (block.chainid != 1) return; From 53a549a0161207175cd88cace1deb6ebce3825f1 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Tue, 5 Jul 2022 16:50:33 -0700 Subject: [PATCH 39/49] refactor(Cellar): remove withdrawFromPositions function --- src/base/Cellar.sol | 108 +++++++++---------------------------- src/mocks/MockCellar.sol | 5 +- src/utils/AddressArray.sol | 12 +++++ test/Cellar.t.sol | 63 +++++++++++----------- 4 files changed, 71 insertions(+), 117 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index ab14c7d7..a3fd2a48 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -10,11 +10,13 @@ import { Registry, SwapRouter, PriceRouter } from "../Registry.sol"; import { IGravity } from "../interfaces/IGravity.sol"; import { AddressArray } from "src/utils/AddressArray.sol"; import { Math } from "../utils/Math.sol"; +import { console } from "@forge-std/Test.sol"; // TODO: Delete. import "../Errors.sol"; contract Cellar is ERC4626, Ownable, Multicall { using AddressArray for address[]; + using AddressArray for ERC20[]; using SafeTransferLib for ERC20; using SafeCast for uint256; using SafeCast for int256; @@ -501,29 +503,6 @@ contract Cellar is ERC4626, Ownable, Multicall { address receiver, address owner ) public override returns (uint256 shares) { - (shares, , ) = withdrawFromPositions(assets, receiver, owner); - } - - function redeem( - uint256 shares, - address receiver, - address owner - ) public override returns (uint256 assets) { - (assets, , ) = redeemFromPositions(shares, receiver, owner); - } - - function withdrawFromPositions( - uint256 assets, - address receiver, - address owner - ) - public - returns ( - uint256 shares, - ERC20[] memory receivedAssets, - uint256[] memory amountsOut - ) - { // Get data efficiently. ( uint256 _totalAssets, // Store totalHoldings and pass into _withdrawInOrder if no stack errors. @@ -547,38 +526,16 @@ contract Cellar is ERC4626, Ownable, Multicall { emit Withdraw(msg.sender, receiver, owner, assets, shares); - // Scope to avoid stack errors. - { - (uint256[] memory amountsReceived, uint256 numOfReceivedAssets) = withdrawType == WithdrawType.Orderly - ? _withdrawInOrder(assets, receiver, _positions, positionAssets, positionBalances) - : _withdrawInProportion(shares, totalShares, receiver, _positions, positionBalances); - - receivedAssets = new ERC20[](numOfReceivedAssets); - amountsOut = new uint256[](numOfReceivedAssets); - - for (uint256 i = amountsReceived.length; i > 0; i--) { - if (amountsReceived[i - 1] == 0) continue; - - ERC20 positionAsset = positionAssets[i - 1]; - receivedAssets[numOfReceivedAssets - 1] = positionAsset; - amountsOut[numOfReceivedAssets - 1] = amountsReceived[i - 1]; - numOfReceivedAssets--; - } - } + withdrawType == WithdrawType.Orderly + ? _withdrawInOrder(assets, receiver, _positions, positionAssets, positionBalances) + : _withdrawInProportion(shares, totalShares, receiver, _positions, positionBalances); } - function redeemFromPositions( + function redeem( uint256 shares, address receiver, address owner - ) - public - returns ( - uint256 assets, - ERC20[] memory receivedAssets, - uint256[] memory amountsOut - ) - { + ) public override returns (uint256 assets) { // Get data efficiently. ( uint256 _totalAssets, // Store totalHoldings and pass into _withdrawInOrder if no stack errors. @@ -600,26 +557,11 @@ contract Cellar is ERC4626, Ownable, Multicall { _burn(owner, shares); - // Scope to avoid stack errors. - { - (uint256[] memory amountsReceived, uint256 numOfReceivedAssets) = withdrawType == WithdrawType.Orderly - ? _withdrawInOrder(assets, receiver, _positions, positionAssets, positionBalances) - : _withdrawInProportion(shares, totalShares, receiver, _positions, positionBalances); - - receivedAssets = new ERC20[](numOfReceivedAssets); - amountsOut = new uint256[](numOfReceivedAssets); - - for (uint256 i = amountsReceived.length; i > 0; i--) { - if (amountsReceived[i - 1] == 0) continue; - - ERC20 positionAsset = positionAssets[i - 1]; - receivedAssets[numOfReceivedAssets - 1] = positionAsset; - amountsOut[numOfReceivedAssets - 1] = amountsReceived[i - 1]; - numOfReceivedAssets--; - } - } - emit Withdraw(msg.sender, receiver, owner, assets, shares); + + withdrawType == WithdrawType.Orderly + ? _withdrawInOrder(assets, receiver, _positions, positionAssets, positionBalances) + : _withdrawInProportion(shares, totalShares, receiver, _positions, positionBalances); } function _withdrawInOrder( @@ -628,17 +570,14 @@ contract Cellar is ERC4626, Ownable, Multicall { address[] memory _positions, ERC20[] memory positionAssets, uint256[] memory positionBalances - ) internal returns (uint256[] memory amountsReceived, uint256 numOfReceivedAssets) { - amountsReceived = new uint256[](_positions.length); - + ) internal { // Get the price router. - PriceRouter priceRouter = registry.priceRouter(); + PriceRouter priceRouter = PriceRouter(registry.getAddress(2)); for (uint256 i; ; i++) { // Move on to next position if this one is empty. if (positionBalances[i] == 0) continue; - // TODO: Check for optimization. uint256 onePositionAsset = 10**positionAssets[i].decimals(); uint256 exchangeRate = priceRouter.getExchangeRate(positionAssets[i], asset); @@ -658,7 +597,6 @@ contract Cellar is ERC4626, Ownable, Multicall { // Return the amount that will be received and increment number of received assets. amountsReceived[i] = amount; - numOfReceivedAssets++; // Withdraw from position. _withdrawFrom(_positions[i], amount, receiver); @@ -676,7 +614,7 @@ contract Cellar is ERC4626, Ownable, Multicall { address receiver, address[] memory _positions, uint256[] memory positionBalances - ) internal returns (uint256[] memory amountsReceived, uint256 numOfReceivedAssets) { + ) internal { amountsReceived = new uint256[](_positions.length); // Withdraw assets from positions in proportion to shares redeemed. @@ -692,7 +630,6 @@ contract Cellar is ERC4626, Ownable, Multicall { // Return the amount that will be received and increment number of received assets. amountsReceived[i] = amount; - numOfReceivedAssets++; // Withdraw from position to receiver. _withdrawFrom(position, amount, receiver); @@ -718,7 +655,8 @@ contract Cellar is ERC4626, Ownable, Multicall { balances[i] = _balanceOf(position); } - assets = registry.priceRouter().getValues(positionAssets, balances, asset); + PriceRouter priceRouter = PriceRouter(registry.getAddress(2)); + assets = priceRouter.getValues(positionAssets, balances, asset); } /** @@ -817,7 +755,8 @@ contract Cellar is ERC4626, Ownable, Multicall { positionBalances[i] = _balanceOf(position); } - _totalAssets = registry.priceRouter().getValues(positionAssets, positionBalances, asset); + PriceRouter priceRouter = PriceRouter(registry.getAddress(2)); + _totalAssets = priceRouter.getValues(positionAssets, positionBalances, asset); } // =========================================== ACCRUAL LOGIC =========================================== @@ -834,7 +773,7 @@ contract Cellar is ERC4626, Ownable, Multicall { */ function accrue() public { // Get the latest address of the price router. - PriceRouter priceRouter = registry.priceRouter(); + PriceRouter priceRouter = PriceRouter(registry.getAddress(2)); // Get data efficiently. ( @@ -927,7 +866,7 @@ contract Cellar is ERC4626, Ownable, Multicall { // Swap to the asset of the other position if necessary. ERC20 fromAsset = _assetOf(fromPosition); ERC20 toAsset = _assetOf(toPosition); - assetsTo = fromAsset != toAsset ? _swap(fromAsset, assetsFrom, exchange, params) : assetsFrom; + assetsTo = fromAsset != toAsset ? _swap(fromAsset, assetsFrom, exchange, params, address(this)) : assetsFrom; // Deposit into position. _depositTo(toPosition, assetsTo); @@ -1075,19 +1014,20 @@ contract Cellar is ERC4626, Ownable, Multicall { ERC20 assetIn, uint256 amountIn, SwapRouter.Exchange exchange, - bytes calldata params + bytes calldata params, + address receiver ) internal returns (uint256 amountOut) { // Store the expected amount of the asset in that we expect to have after the swap. uint256 expectedAssetsInAfter = assetIn.balanceOf(address(this)) - amountIn; // Get the address of the latest swap router. - SwapRouter swapRouter = registry.swapRouter(); + SwapRouter swapRouter = SwapRouter(registry.getAddress(1)); // Approve swap router to swap assets. assetIn.safeApprove(address(swapRouter), amountIn); // Perform swap. - amountOut = swapRouter.swap(exchange, params); + amountOut = swapRouter.swap(exchange, params, receiver); // Check that the amount of assets swapped is what is expected. Will revert if the `params` // specified a different amount of assets to swap then `amountIn`. diff --git a/src/mocks/MockCellar.sol b/src/mocks/MockCellar.sol index 4fd4a13e..3b3f36e3 100644 --- a/src/mocks/MockCellar.sol +++ b/src/mocks/MockCellar.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.15; -import { Cellar, Registry, ERC4626, ERC20, SafeCast } from "src/base/Cellar.sol"; +import { Cellar, Registry, PriceRouter, ERC4626, ERC20, SafeCast } from "src/base/Cellar.sol"; import { Test, console } from "@forge-std/Test.sol"; contract MockCellar is Cellar, Test { @@ -38,7 +38,8 @@ contract MockCellar is Cellar, Test { function _depositIntoPosition(address position, uint256 amount) internal returns (uint256 shares) { ERC20 positionAsset = _assetOf(position); - uint256 amountInAssets = registry.priceRouter().getValue(positionAsset, amount, asset); + PriceRouter priceRouter = PriceRouter(registry.getAddress(2)); + uint256 amountInAssets = priceRouter.getValue(positionAsset, amount, asset); shares = previewDeposit(amountInAssets); deal(address(positionAsset), address(this), amount); diff --git a/src/utils/AddressArray.sol b/src/utils/AddressArray.sol index 43c22c31..4ce65a78 100644 --- a/src/utils/AddressArray.sol +++ b/src/utils/AddressArray.sol @@ -1,12 +1,16 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.15; +import { ERC20 } from "@solmate/tokens/ERC20.sol"; + // TODO: add natspec /** * @notice A library to extend the address array data type. */ library AddressArray { + // =========================================== ADDRESS STORAGE =========================================== + function add( address[] storage array, uint256 index, @@ -54,4 +58,12 @@ library AddressArray { return false; } + + // =========================================== ERC20 MEMORY =========================================== + + function contains(ERC20[] memory array, ERC20 value) internal pure returns (bool, uint256) { + for (uint256 i; i < array.length; i++) if (value == array[i]) return (true, i); + + return (false, type(uint256).max); + } } diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index b87c0c39..bf98a018 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -54,9 +54,11 @@ contract CellarTest is Test { gravity = new MockGravity(); registry = new Registry( - SwapRouter(address(swapRouter)), - PriceRouter(address(priceRouter)), - IGravity(address(gravity)) + // Set this contract to the Gravity Bridge for testing to give the permissions usually + // given to the Gravity Bridge to this contract. + address(this), + address(swapRouter), + address(priceRouter) ); // Setup exchange rates: @@ -102,10 +104,6 @@ contract CellarTest is Test { ); vm.label(address(cellar), "cellar"); - // Transfer ownership to this contract for testing. - vm.prank(address(registry.gravityBridge())); - cellar.transferOwnership(address(this)); - // Mint enough liquidity to swap router for swaps. deal(address(USDC), address(exchange), type(uint224).max); deal(address(WETH), address(exchange), type(uint224).max); @@ -166,7 +164,7 @@ contract CellarTest is Test { assertEq(USDC.balanceOf(address(this)), assets, "Should have withdrawn assets to user."); } - function testWithdrawFromPositionsInOrder() external { + function testWithdrawInOrder() external { cellar.depositIntoPosition(address(wethCLR), 1e18); // $2000 cellar.depositIntoPosition(address(wbtcCLR), 1e8); // $30,000 @@ -176,26 +174,16 @@ contract CellarTest is Test { deal(address(cellar), address(this), cellar.previewWithdraw(32_000e6)); // Withdraw from position. - (uint256 shares, ERC20[] memory receivedAssets, uint256[] memory amountsOut) = cellar.withdrawFromPositions( - 32_000e6, - address(this), - address(this) - ); + uint256 shares = cellar.withdraw(32_000e6, address(this), address(this)); assertEq(cellar.balanceOf(address(this)), 0, "Should have redeemed all shares."); assertEq(shares, 32_000e18, "Should returned all redeemed shares."); - assertEq(receivedAssets.length, 2, "Should have received two assets."); - assertEq(amountsOut.length, 2, "Should have gotten out two amount."); - assertEq(address(receivedAssets[0]), address(WETH), "Should have received WETH."); - assertEq(address(receivedAssets[1]), address(WBTC), "Should have received WBTC."); - assertEq(amountsOut[0], 1e18, "Should have gotten out 1 WETH."); - assertEq(amountsOut[1], 1e8, "Should have gotten out 1 WBTC."); assertEq(WETH.balanceOf(address(this)), 1e18, "Should have transferred position balance to user."); assertEq(WBTC.balanceOf(address(this)), 1e8, "Should have transferred position balance to user."); assertEq(cellar.totalAssets(), 0, "Should have emptied cellar."); } - function testWithdrawFromPositionsInProportion() external { + function testWithdrawInProportion() external { cellar.depositIntoPosition(address(wethCLR), 1e18); // $2000 cellar.depositIntoPosition(address(wbtcCLR), 1e8); // $30,000 @@ -207,25 +195,38 @@ contract CellarTest is Test { // Withdraw from position. cellar.setWithdrawType(Cellar.WithdrawType.Proportional); - (uint256 shares, ERC20[] memory receivedAssets, uint256[] memory amountsOut) = cellar.withdrawFromPositions( - 16_000e6, - address(this), - address(this) - ); + uint256 shares = cellar.withdraw(16_000e6, address(this), address(this)); assertEq(cellar.balanceOf(address(this)), 0, "Should have redeemed all shares."); assertEq(shares, 16_000e18, "Should returned all redeemed shares."); - assertEq(receivedAssets.length, 2, "Should have received two assets."); - assertEq(amountsOut.length, 2, "Should have gotten out two amount."); - assertEq(address(receivedAssets[0]), address(WETH), "Should have received WETH."); - assertEq(address(receivedAssets[1]), address(WBTC), "Should have received WBTC."); - assertEq(amountsOut[0], 0.5e18, "Should have gotten out 0.5 WETH."); - assertEq(amountsOut[1], 0.5e8, "Should have gotten out 0.5 WBTC."); assertEq(WETH.balanceOf(address(this)), 0.5e18, "Should have transferred position balance to user."); assertEq(WBTC.balanceOf(address(this)), 0.5e8, "Should have transferred position balance to user."); assertEq(cellar.totalAssets(), 16_000e6, "Should have half of assets remaining in cellar."); } + function testWithdrawWithDuplicateReceivedAssets() external { + MockERC4626 wethVault = new MockERC4626(WETH, "WETH Vault LP Token", "WETH-VLT", 18); + cellar.trustPosition(address(wethVault), Cellar.PositionType.ERC4626); + cellar.pushPosition(address(wethVault)); + + cellar.depositIntoPosition(address(wethCLR), 1e18); // $2000 + cellar.depositIntoPosition(address(wethVault), 0.5e18); // $1000 + + assertEq(cellar.totalAssets(), 3000e6, "Should have updated total assets with assets deposited."); + assertEq(cellar.totalSupply(), 3000e18); + + // Mint shares to user to redeem. + deal(address(cellar), address(this), cellar.previewWithdraw(3000e6)); + + // Withdraw from position. + uint256 shares = cellar.withdraw(3000e6, address(this), address(this)); + + assertEq(cellar.balanceOf(address(this)), 0, "Should have redeemed all shares."); + assertEq(shares, 3000e18, "Should returned all redeemed shares."); + assertEq(WETH.balanceOf(address(this)), 1.5e18, "Should have transferred position balance to user."); + assertEq(cellar.totalAssets(), 0, "Should have no assets remaining in cellar."); + } + // ========================================== REBALANCE TEST ========================================== // TODO: Test rebalancing to invalid position. From d276d466156fcabcc76af9d998a67c7833011bbd Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Wed, 6 Jul 2022 12:21:06 -0700 Subject: [PATCH 40/49] style(Swap/PriceRouter): add in natspec comments and titles --- src/modules/price-router/PriceRouter.sol | 22 ++++++++++++++++++++-- src/modules/swap-router/SwapRouter.sol | 7 +++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/modules/price-router/PriceRouter.sol b/src/modules/price-router/PriceRouter.sol index 00dc7c55..9f92ce67 100644 --- a/src/modules/price-router/PriceRouter.sol +++ b/src/modules/price-router/PriceRouter.sol @@ -9,8 +9,12 @@ import { Math } from "src/utils/Math.sol"; import "src/Errors.sol"; -// TODO: Add test cases that there are no ERC20 operations performed on remapped assets. - +/** + * @title Sommeliet Price Router + * @notice Provides a universal interface allowing Sommelier contracts to retrieve secure pricing data + * from Chainlink and arbitrary adaptors + * @author crispymangoes, Brian Le + */ contract PriceRouter is Ownable, ChainlinkPriceFeedAdaptor { using SafeTransferLib for ERC20; using Math for uint256; @@ -183,6 +187,14 @@ contract PriceRouter is Ownable, ChainlinkPriceFeedAdaptor { // =========================================== HELPER FUNCTIONS =========================================== + /** + * @notice Gets the exchange rate between a base and a quote asset + * @param baseAsset the asset to convert into quoteAsset + * @param quoteAsset the asset base asset is converted into + * @return exchangeRate baseAsset/quoteAsset + * if base is ETH and quote is USD + * would return ETH/USD + */ function _getExchangeRate( ERC20 baseAsset, ERC20 quoteAsset, @@ -191,6 +203,12 @@ contract PriceRouter is Ownable, ChainlinkPriceFeedAdaptor { exchangeRate = _getValueInUSD(baseAsset).mulDivDown(10**quoteAssetDecimals, _getValueInUSD(quoteAsset)); } + /** + * @notice Gets the valuation of some asset in USD + * @dev USD valuation has 8 decimals + * @param asset the asset to get the value of in USD + * @return value the value of asset in USD + */ function _getValueInUSD(ERC20 asset) internal view returns (uint256 value) { AssetData storage assetData = getAssetData[asset]; diff --git a/src/modules/swap-router/SwapRouter.sol b/src/modules/swap-router/SwapRouter.sol index 8e5cea92..91d11956 100644 --- a/src/modules/swap-router/SwapRouter.sol +++ b/src/modules/swap-router/SwapRouter.sol @@ -7,6 +7,13 @@ import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; import { IUniswapV2Router02 as IUniswapV2Router } from "src/interfaces/IUniswapV2Router02.sol"; import { IUniswapV3Router } from "src/interfaces/IUniswapV3Router.sol"; +/** + * @title Sommeliet Swap Router + * @notice Provides a universal interface allowing Sommelier contracts to interact with multiple + * @dev Perform multiple swaps using Multicall + * different exchanges to perform swaps + * @author crispymangoes, Brian Le + */ contract SwapRouter is Multicall { using SafeTransferLib for ERC20; From 6777b676f60cbb7f895add231c904eb8d82e3820 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Wed, 6 Jul 2022 13:00:34 -0700 Subject: [PATCH 41/49] refactor(CellarRouter): combine withdrawAndSwap with withdrawFromAndSwap --- src/CellarRouter.sol | 126 +++++++++------------ src/interfaces/ICellarRouter.sol | 10 +- test/CellarRouter.t.sol | 188 +++++-------------------------- 3 files changed, 89 insertions(+), 235 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index fa17e324..b3214ad0 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.15; import { ERC20 } from "@solmate/tokens/ERC20.sol"; +import { ERC4626 } from "@solmate/mixins/ERC4626.sol"; import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; import { Registry } from "src/Registry.sol"; import { Cellar } from "src/base/Cellar.sol"; @@ -171,28 +172,54 @@ contract CellarRouter is ICellarRouter { * @dev Permission is required from caller for router to burn shares. Please make sure that * caller has approved the router to spend their shares. * @param cellar address of the cellar - * @param exchange ENUM representing what exchange to make the swap at - * Refer to src/SwapRouter.sol for list of available options - * @param swapData bytes variable containing all the data needed to make a swap - * receiver address should be the callers address + * @param exchanges enums representing what exchange to make the swap at, + * refer to src/SwapRouter.sol for list of available options + * @param swapDatas bytes variable containing all the data needed to make a swap + * receiver address should be the callers address * @param assets amount of assets to withdraw * @param receiver the address swapped tokens are sent to * @return shares amount of shares burned */ function withdrawAndSwap( Cellar cellar, - SwapRouter.Exchange exchange, - bytes calldata swapData, + SwapRouter.Exchange[] calldata exchanges, + bytes[] calldata swapDatas, uint256 assets, address receiver ) public returns (uint256 shares) { - // Withdraw assets from the cellar. + // Withdraw from the cellar. May potentially receive multiple assets shares = cellar.withdraw(assets, address(this), msg.sender); - // Swap assets into desired token. + // Get the address of the swap router. SwapRouter swapRouter = SwapRouter(registry.getAddress(1)); - cellar.asset().safeApprove(address(swapRouter), assets); - swapRouter.swap(exchange, swapData, receiver); + + // Get all the assets that could potentially have been received. + ERC20[] memory positionAssets = _getPositionAssets(cellar); + + if (swapDatas.length != 0) { + // Encode data used to perform swap. + bytes[] memory data = new bytes[](swapDatas.length); + for (uint256 i; i < swapDatas.length; i++) + data[i] = abi.encodeCall(SwapRouter.swap, (exchanges[i], swapDatas[i], receiver)); + + // Approve swap router to swap each asset. + for (uint256 i; i < positionAssets.length; i++) + positionAssets[i].safeApprove(address(swapRouter), type(uint256).max); + + // Execute swap(s). + swapRouter.multicall(data); + } + + for (uint256 i; i < positionAssets.length; i++) { + ERC20 asset = positionAssets[i]; + + // Reset approvals. + asset.safeApprove(address(swapRouter), 0); + + // Transfer remaining unswapped balances to receiver. + uint256 remainingBalance = asset.balanceOf(address(this)); + if (remainingBalance != 0) asset.transfer(receiver, remainingBalance); + } } /** @@ -204,9 +231,9 @@ contract CellarRouter is ICellarRouter { * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap * V2, leave pool fees empty to use Uniswap V2 for swap. * @param cellar address of the cellar - * @param exchange ENUM representing what exchange to make the swap at + * @param exchanges enum representing what exchange to make the swap at * Refer to src/SwapRouter.sol for list of available options - * @param swapData bytes variable containing all the data needed to make a swap + * @param swapDatas bytes variable containing all the data needed to make a swap * @param assets amount of assets to withdraw * @param deadline timestamp after which permit is invalid * @param signature a valid secp256k1 signature @@ -215,8 +242,8 @@ contract CellarRouter is ICellarRouter { */ function withdrawAndSwapWithPermit( Cellar cellar, - SwapRouter.Exchange exchange, - bytes calldata swapData, + SwapRouter.Exchange[] calldata exchanges, + bytes[] calldata swapDatas, uint256 assets, uint256 deadline, bytes memory signature, @@ -227,63 +254,7 @@ contract CellarRouter is ICellarRouter { cellar.permit(msg.sender, address(this), assets, deadline, v, r, s); // Withdraw assets from the cellar and swap to another asset if necessary. - shares = withdrawAndSwap(cellar, exchange, swapData, assets, receiver); - } - - /** - * @notice Withdraws from a multi assset cellar and then performs swaps to a single desired asset, if the - * withdrawn asset is not already. - * @dev If using Uniswap V3 for swap, must specify the pool fee tier to use for each swap. For - * example, if there are "n" addresses in path, there should be "n-1" values specifying the - * fee tiers of each pool used for each swap. The current possible pool fee tiers for - * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap - * V2, leave pool fees empty to use Uniswap V2 for swap. - * @param cellar address of the cellar - * @param exchange ENUM representing what exchange to make the swap at - * Refer to src/SwapRouter.sol for list of available options - * @param swapData bytes variable containing all the data needed to make a swap - * receiver address should be the callers address - * @param assets amount of assets to withdraw - * @param receiver the address swapped tokens are sent to - * @return shares amount of shares burned - */ - function withdrawFromPositionsAndSwap( - Cellar cellar, - SwapRouter.Exchange[] calldata exchange, - bytes[] calldata swapData, - uint256 assets, - address receiver - ) public returns (uint256 shares) { - ERC20[] memory receivedAssets; - uint256[] memory amountsOut; - (shares, receivedAssets, amountsOut) = cellar.withdrawFromPositions(assets, address(this), msg.sender); - - uint256[] memory balancesBefore = _getBalancesBefore(receivedAssets, amountsOut); - - SwapRouter swapRouter = SwapRouter(registry.getAddress(1)); - - bytes[] memory data = new bytes[](swapData.length); - for (uint256 i; i < swapData.length; i++) - data[i] = abi.encodeCall(SwapRouter.swap, (exchange[i], swapData[i], receiver)); - - for (uint256 i; i < receivedAssets.length; i++) - receivedAssets[i].safeApprove(address(swapRouter), amountsOut[i]); - - swapRouter.multicall(data); - - for (uint256 i; i < receivedAssets.length; i++) { - ERC20 receivedAsset = receivedAssets[i]; - - // Remove approvals in case it wasn't used. - receivedAsset.safeApprove(address(swapRouter), 0); - - uint256 balanceBefore = balancesBefore[i]; - uint256 balanceAfter = receivedAsset.balanceOf(address(this)); - - if (balanceAfter != balanceBefore) { - receivedAsset.transfer(receiver, balanceAfter - balanceBefore); - } - } + shares = withdrawAndSwap(cellar, exchanges, swapDatas, assets, receiver); } // ========================================= HELPER FUNCTIONS ========================================= @@ -332,4 +303,17 @@ contract CellarRouter is ICellarRouter { balancesBefore[i] = asset.balanceOf(address(this)) - amountsReceived[i]; } } + + function _getPositionAssets(Cellar cellar) internal view returns (ERC20[] memory assets) { + address[] memory positions = cellar.getPositions(); + + assets = new ERC20[](positions.length); + + for (uint256 i; i < positions.length; i++) { + address position = positions[i]; + (Cellar.PositionType positionType, ) = cellar.getPositionData(position); + + assets[i] = positionType == Cellar.PositionType.ERC20 ? ERC20(position) : ERC4626(position).asset(); + } + } } diff --git a/src/interfaces/ICellarRouter.sol b/src/interfaces/ICellarRouter.sol index 28c73763..706ada4f 100644 --- a/src/interfaces/ICellarRouter.sol +++ b/src/interfaces/ICellarRouter.sol @@ -31,23 +31,23 @@ interface ICellarRouter { bytes calldata swapData, uint256 assets, ERC20 assetIn, - address reciever, + address receiver, uint256 deadline, bytes memory signature ) external returns (uint256 shares); function withdrawAndSwap( Cellar cellar, - SwapRouter.Exchange exchange, - bytes calldata swapData, + SwapRouter.Exchange[] calldata exchanges, + bytes[] calldata swapDatas, uint256 assets, address receiver ) external returns (uint256 shares); function withdrawAndSwapWithPermit( Cellar cellar, - SwapRouter.Exchange exchange, - bytes calldata swapData, + SwapRouter.Exchange[] calldata exchanges, + bytes[] calldata swapDatas, uint256 assets, uint256 deadline, bytes memory signature, diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index 1815ad81..a09b5051 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -17,6 +17,7 @@ import { MockGravity } from "src/mocks/MockGravity.sol"; import { Test, console } from "@forge-std/Test.sol"; import { Math } from "src/utils/Math.sol"; +// solhint-disable-next-line max-states-count contract CellarRouterTest is Test { using Math for uint256; @@ -34,7 +35,6 @@ contract CellarRouterTest is Test { CellarRouter private router; MockERC4626 private forkedCellar; - CellarRouter private forkedRouter; address private immutable owner = vm.addr(0xBEEF); @@ -67,9 +67,14 @@ contract CellarRouterTest is Test { swapRouter = new SwapRouter(IUniswapV2Router(address(exchange)), IUniswapV3Router(address(exchange))); realSwapRouter = new SwapRouter(IUniswapV2Router(uniV2Router), IUniswapV3Router(uniV3Router)); - gravity = new MockGravity(); - registry = new Registry(address(gravity), address(swapRouter), address(priceRouter)); + registry = new Registry( + // Set this contract to the Gravity Bridge for testing to give the permissions usually + // given to the Gravity Bridge to this contract. + address(this), + address(swapRouter), + address(priceRouter) + ); router = new CellarRouter(IUniswapV3Router(address(exchange)), IUniswapV2Router(address(exchange)), registry); //forkedRouter = new CellarRouter(IUniswapV3Router(uniV3Router), IUniswapV2Router(uniV2Router), registry); @@ -272,178 +277,43 @@ contract CellarRouterTest is Test { // ======================================= WITHDRAW TESTS ======================================= - // TODO: Add test ensuring that no assets remain after withdrawal. - - function testWithdrawAndSwapFromCellar(uint256 assets) external { - assets = bound(assets, 1e18, type(uint72).max); - - // Mint liquidity for swap. - ABC.mint(address(exchange), 2 * assets); - - // Specify the swap path. - address[] memory path = new address[](2); - path[0] = address(XYZ); - path[1] = address(ABC); - - // Deposit and swap - vm.startPrank(owner); - XYZ.approve(address(router), assets); - XYZ.mint(owner, assets); - bytes memory swapData = abi.encode(path, assets, 0); - router.depositAndSwap(Cellar(address(cellar)), SwapRouter.Exchange.UNIV2, swapData, assets, owner, XYZ); - - // Assets received by the cellar will be different from the amount of assets a user attempted - // to deposit due to slippage swaps. - uint256 assetsReceivedAfterDeposit = exchange.quote(assets, path); - - // Reverse the swap path. - (path[0], path[1]) = (path[1], path[0]); - - // Test withdraw and swap. - cellar.approve(address(router), assetsReceivedAfterDeposit); - swapData = abi.encode(path, assetsReceivedAfterDeposit, 0, owner); - uint256 sharesRedeemed = router.withdrawAndSwap( - Cellar(address(cellar)), - SwapRouter.Exchange.UNIV2, - swapData, - assetsReceivedAfterDeposit, - owner - ); - vm.stopPrank(); - - uint256 assetsReceivedAfterWithdraw = exchange.quote(assetsReceivedAfterDeposit, path); - - // Run test. - assertEq(sharesRedeemed, assetsReceivedAfterDeposit, "Should have 1:1 exchange rate."); - assertEq(cellar.totalSupply(), 0, "Should have updated total supply with shares minted."); - assertEq(cellar.totalAssets(), 0, "Should have updated total assets into account the withdrawn assets."); - assertEq(cellar.balanceOf(owner), 0, "Should have updated user's share balance."); - assertEq(XYZ.balanceOf(owner), assetsReceivedAfterWithdraw, "Should have withdrawn assets to the user."); - } - - function testWithdrawFromPositionsIntoSingleAssetWTwoSwaps() external { + function testWithdrawAndSwap() external { multiCellar.depositIntoPosition(address(wethCLR), 1e18); multiCellar.depositIntoPosition(address(wbtcCLR), 1e8); + assertEq(multiCellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); // Mint shares to user to redeem. deal(address(multiCellar), address(this), multiCellar.previewWithdraw(32_000e6)); - //create paths - address[][] memory paths = new address[][](2); - paths[0] = new address[](2); - paths[0][0] = address(WETH); - paths[0][1] = address(USDC); - paths[1] = new address[](2); - paths[1][0] = address(WBTC); - paths[1][1] = address(USDC); - - uint256 assets = 32_000e6; - uint256[] memory minOuts = new uint256[](2); - minOuts[0] = 0; - minOuts[1] = 0; - - uint256[] memory assetsIn = new uint256[](2); - assetsIn[0] = 1e18; - assetsIn[1] = 1e8; - - multiCellar.approve(address(router), type(uint256).max); + // Encode swaps. + // Swap 1: 0.2 WETH -> WBTC. + // Swap 2: 0.8 WETH -> USDC. SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](2); exchanges[0] = SwapRouter.Exchange.UNIV2; exchanges[1] = SwapRouter.Exchange.UNIV2; - bytes[] memory swapData = new bytes[](2); - swapData[0] = abi.encode(paths[0], 1e18, 0); - swapData[1] = abi.encode(paths[1], 1e8, 0); - - router.withdrawFromPositionsAndSwap(multiCellar, exchanges, swapData, assets, address(this)); - - assertEq(USDC.balanceOf(address(this)), 30_400e6, "Did not recieve expected assets"); - } - - /** - * @notice if the asset wanted is an asset given, then it should just be added to the output with no swaps needed - */ - function testWithdrawFromPositionsIntoSingleAssetWOneSwap() external { - multiCellar.depositIntoPosition(address(wethCLR), 1e18); - multiCellar.depositIntoPosition(address(wbtcCLR), 1e8); - - assertEq(multiCellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); - - // Mint shares to user to redeem. - deal(address(multiCellar), address(this), multiCellar.previewWithdraw(32_000e6)); - - //create paths - address[][] memory paths = new address[][](1); - paths[0] = new address[](2); - paths[0][0] = address(WBTC); - paths[0][1] = address(WETH); - - uint256[] memory minOuts = new uint256[](1); - minOuts[0] = 0; - - uint256[] memory assetsIn = new uint256[](1); - assetsIn[0] = 1e18; - uint256 assets = 32_000e6; - multiCellar.approve(address(router), type(uint256).max); - SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](1); - exchanges[0] = SwapRouter.Exchange.UNIV2; - bytes[] memory swapData = new bytes[](1); - swapData[0] = abi.encode(paths[0], 1e8, 0, address(this)); - router.withdrawFromPositionsAndSwap(multiCellar, exchanges, swapData, assets, address(this)); - assertEq(WETH.balanceOf(address(this)), 15.25e18, "Did not recieve expected assets"); - } - - function testWithdrawFromPositionsIntoSingleAssetWFourSwaps() external { - multiCellar.depositIntoPosition(address(wethCLR), 1e18); - multiCellar.depositIntoPosition(address(wbtcCLR), 1e8); - - assertEq(multiCellar.totalAssets(), 32_000e6, "Should have updated total assets with assets deposited."); - - // Mint shares to user to redeem. - deal(address(multiCellar), address(this), multiCellar.previewWithdraw(32_000e6)); - - //create paths - address[][] memory paths = new address[][](4); + address[][] memory paths = new address[][](2); paths[0] = new address[](2); paths[0][0] = address(WETH); - paths[0][1] = address(USDC); + paths[0][1] = address(WBTC); + paths[1] = new address[](2); - paths[1][0] = address(WBTC); + paths[1][0] = address(WETH); paths[1][1] = address(USDC); - paths[2] = new address[](2); - paths[2][0] = address(WETH); - paths[2][1] = address(USDC); - paths[3] = new address[](2); - paths[3][0] = address(WBTC); - paths[3][1] = address(USDC); - - uint256 assets = 32_000e6; - uint256[] memory minOuts = new uint256[](4); - minOuts[0] = 0; - minOuts[1] = 0; - minOuts[2] = 0; - minOuts[3] = 0; - - uint256[] memory assetsIn = new uint256[](4); - assetsIn[0] = 0.5e18; - assetsIn[1] = 0.5e8; - assetsIn[2] = 0.5e18; - assetsIn[3] = 0.5e8; + + bytes[] memory swapData = new bytes[](2); + swapData[0] = abi.encode(paths[0], 0.2e18, 0); + swapData[1] = abi.encode(paths[1], 0.8e18, 0); multiCellar.approve(address(router), type(uint256).max); - SwapRouter.Exchange[] memory exchanges = new SwapRouter.Exchange[](4); - exchanges[0] = SwapRouter.Exchange.UNIV2; - exchanges[1] = SwapRouter.Exchange.UNIV2; - exchanges[2] = SwapRouter.Exchange.UNIV2; - exchanges[3] = SwapRouter.Exchange.UNIV2; - bytes[] memory swapData = new bytes[](4); - swapData[0] = abi.encode(paths[0], 0.5e18, 0, address(this)); - swapData[1] = abi.encode(paths[1], 0.5e8, 0, address(this)); - swapData[2] = abi.encode(paths[2], 0.5e18, 0, address(this)); - swapData[3] = abi.encode(paths[3], 0.5e8, 0, address(this)); - router.withdrawFromPositionsAndSwap(multiCellar, exchanges, swapData, assets, address(this)); - - assertEq(USDC.balanceOf(address(this)), 30_400e6, "Did not recieve expected assets"); + router.withdrawAndSwap(multiCellar, exchanges, swapData, 32_000e6, address(this)); + + assertEq(WETH.balanceOf(address(this)), 0, "Should receive no WETH."); + assertGt(WBTC.balanceOf(address(this)), 0, "Should receive WBTC"); + assertGt(USDC.balanceOf(address(this)), 0, "Should receive USDC"); + assertEq(WETH.allowance(address(router), address(swapRouter)), 0, "Should have no WETH allowances."); + assertEq(WBTC.allowance(address(router), address(swapRouter)), 0, "Should have no WBTC allowances."); + assertEq(USDC.allowance(address(router), address(swapRouter)), 0, "Should have no USDC allowances."); } } From ce243bbbd001599ddb7f7d715aaabcc75e49067f Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Wed, 6 Jul 2022 13:12:21 -0700 Subject: [PATCH 42/49] docs: fix typos in contract natspec --- src/modules/price-router/PriceRouter.sol | 6 +++--- src/modules/swap-router/SwapRouter.sol | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/modules/price-router/PriceRouter.sol b/src/modules/price-router/PriceRouter.sol index 9f92ce67..b8cf5169 100644 --- a/src/modules/price-router/PriceRouter.sol +++ b/src/modules/price-router/PriceRouter.sol @@ -10,9 +10,9 @@ import { Math } from "src/utils/Math.sol"; import "src/Errors.sol"; /** - * @title Sommeliet Price Router - * @notice Provides a universal interface allowing Sommelier contracts to retrieve secure pricing data - * from Chainlink and arbitrary adaptors + * @title Price Router + * @notice Provides a universal interface allowing Sommelier contracts to retrieve secure pricing + * data from Chainlink and arbitrary adaptors. * @author crispymangoes, Brian Le */ contract PriceRouter is Ownable, ChainlinkPriceFeedAdaptor { diff --git a/src/modules/swap-router/SwapRouter.sol b/src/modules/swap-router/SwapRouter.sol index 78986ba9..7dcdaec5 100644 --- a/src/modules/swap-router/SwapRouter.sol +++ b/src/modules/swap-router/SwapRouter.sol @@ -9,10 +9,10 @@ import { IUniswapV3Router } from "src/interfaces/IUniswapV3Router.sol"; import { Multicall } from "src/base/Multicall.sol"; /** - * @title Sommeliet Swap Router + * @title Sommelier Price Router * @notice Provides a universal interface allowing Sommelier contracts to interact with multiple - * @dev Perform multiple swaps using Multicall - * different exchanges to perform swaps + * different exchanges to perform swaps. + * @dev Perform multiple swaps using Multicall. * @author crispymangoes, Brian Le */ contract SwapRouter is Multicall { From 2b1940e4208654fde6b9bd3ada546b3327146240 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Wed, 6 Jul 2022 13:38:56 -0700 Subject: [PATCH 43/49] docs: fix natspec in PriceRouter and SwapRouter --- src/modules/price-router/PriceRouter.sol | 19 +++--- src/modules/swap-router/SwapRouter.sol | 77 ++++++++++++------------ 2 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/modules/price-router/PriceRouter.sol b/src/modules/price-router/PriceRouter.sol index b8cf5169..15971b82 100644 --- a/src/modules/price-router/PriceRouter.sol +++ b/src/modules/price-router/PriceRouter.sol @@ -22,8 +22,10 @@ contract PriceRouter is Ownable, ChainlinkPriceFeedAdaptor { // =========================================== ASSETS CONFIG =========================================== /** - * @param minPrice is the minimum price in USD for the asset before reverting - * @param maxPrice is the maximum price in USD for the asset before reverting + * @param remap address of asset to get pricing data for instead if a price feed is not + * available (eg. ETH for WETH), set to `address(0)` for no remapping + * @param minPrice minimum price in USD for the asset before reverting + * @param maxPrice maximum price in USD for the asset before reverting * @param heartbeat maximum allowed time that can pass with no update before price data is considered stale * @param isSupported whether this asset is supported by the platform or not */ @@ -31,20 +33,23 @@ contract PriceRouter is Ownable, ChainlinkPriceFeedAdaptor { ERC20 remap; uint256 minPrice; uint256 maxPrice; - uint96 heartBeat; //maximum allowed time to pass with no update + uint96 heartBeat; bool isSupported; } + /** + * @notice Get the asset data for a given asset. + */ mapping(ERC20 => AssetData) public getAssetData; uint96 public constant DEFAULT_HEART_BEAT = 1 days; - // ======================================= Adaptor OPERATIONS ======================================= + // ======================================= ADAPTOR OPERATIONS ======================================= /** * @notice Add an asset for the price router to support. * @param asset address of asset to support on the platform - * @param remap address of asset to use pricing data for instead if a price feed is not + * @param remap address of asset to get pricing data for instead if a price feed is not * available (eg. ETH for WETH), set to `address(0)` for no remapping * @param minPrice minimum price in USD with 8 decimals for the asset before reverting, * set to `0` to use Chainlink's default @@ -191,9 +196,7 @@ contract PriceRouter is Ownable, ChainlinkPriceFeedAdaptor { * @notice Gets the exchange rate between a base and a quote asset * @param baseAsset the asset to convert into quoteAsset * @param quoteAsset the asset base asset is converted into - * @return exchangeRate baseAsset/quoteAsset - * if base is ETH and quote is USD - * would return ETH/USD + * @return exchangeRate value of base asset in terms of quote asset */ function _getExchangeRate( ERC20 baseAsset, diff --git a/src/modules/swap-router/SwapRouter.sol b/src/modules/swap-router/SwapRouter.sol index 7dcdaec5..0a2b6c83 100644 --- a/src/modules/swap-router/SwapRouter.sol +++ b/src/modules/swap-router/SwapRouter.sol @@ -18,32 +18,35 @@ import { Multicall } from "src/base/Multicall.sol"; contract SwapRouter is Multicall { using SafeTransferLib for ERC20; - /** @notice Planned additions - BALANCERV2, - CURVE, - ONEINCH - */ + /** + * @param UNIV2 Uniswap V2 + * @param UNIV3 Uniswap V3 + */ enum Exchange { UNIV2, UNIV3 } + /** + * @notice Get the selector of the function to call in order to perform swap with a given exchange. + */ mapping(Exchange => bytes4) public getExchangeSelector; // ========================================== CONSTRUCTOR ========================================== /** - * @notice Uniswap V2 swap router contract. Used for swapping if pool fees are not specified. + * @notice Uniswap V2 swap router contract. */ IUniswapV2Router public immutable uniswapV2Router; // 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D /** - * @notice Uniswap V3 swap router contract. Used for swapping if pool fees are specified. + * @notice Uniswap V3 swap router contract. */ IUniswapV3Router public immutable uniswapV3Router; // 0xE592427A0AEce92De3Edee1F18E0157C05861564 /** - * + * @param _uniswapV2Router address of the Uniswap V2 swap router contract + * @param _uniswapV3Router address of the Uniswap V3 swap router contract */ constructor(IUniswapV2Router _uniswapV2Router, IUniswapV3Router _uniswapV3Router) { // Set up all exchanges. @@ -57,20 +60,20 @@ contract SwapRouter is Multicall { // ======================================= SWAP OPERATIONS ======================================= /** - * @notice Route swap calls to the appropriate exchanges. + * @notice Perform a swap using a supported exchange. * @param exchange value dictating which exchange to use to make the swap * @param swapData encoded data used for the swap - * @param recipient address to send the swapped tokens to - * @return amountOut amount of tokens received from the swap + * @param receiver address to send the received assets to + * @return amountOut amount of assets received from the swap */ function swap( Exchange exchange, bytes memory swapData, - address recipient + address receiver ) external returns (uint256 amountOut) { // Route swap call to appropriate function using selector. (bool success, bytes memory result) = address(this).delegatecall( - abi.encodeWithSelector(getExchangeSelector[exchange], swapData, recipient) + abi.encodeWithSelector(getExchangeSelector[exchange], swapData, receiver) ); if (!success) { @@ -90,33 +93,33 @@ contract SwapRouter is Multicall { } /** - * @notice Allows caller to make swaps using the UniswapV2 Exchange. + * @notice Perform a swap using Uniswap V2. * @param swapData bytes variable storing the following swap information: * address[] path: array of addresses dictating what swap path to follow - * uint256 assets: the amount of path[0] you want to swap with - * uint256 assetsOutMin: the minimum amount of path[path.length - 1] tokens you want from the swap - * @param recipient address to send the swapped tokens to - * @return amountOut amount of tokens received from the swap + * uint256 amount: amount of the first asset in the path to swap + * uint256 amountOutMin: the minimum amount of the last asset in the path to receive + * @param receiver address to send the received assets to + * @return amountOut amount of assets received from the swap */ - function swapWithUniV2(bytes memory swapData, address recipient) public returns (uint256 amountOut) { - (address[] memory path, uint256 assets, uint256 assetsOutMin) = abi.decode( + function swapWithUniV2(bytes memory swapData, address receiver) public returns (uint256 amountOut) { + (address[] memory path, uint256 amount, uint256 amountOutMin) = abi.decode( swapData, (address[], uint256, uint256) ); // Transfer assets to this contract to swap. ERC20 assetIn = ERC20(path[0]); - assetIn.safeTransferFrom(msg.sender, address(this), assets); + assetIn.safeTransferFrom(msg.sender, address(this), amount); // Approve assets to be swapped through the router. - assetIn.safeApprove(address(uniswapV2Router), assets); + assetIn.safeApprove(address(uniswapV2Router), amount); // Execute the swap. uint256[] memory amountsOut = uniswapV2Router.swapExactTokensForTokens( - assets, - assetsOutMin, + amount, + amountOutMin, path, - recipient, + receiver, block.timestamp + 60 ); @@ -124,27 +127,27 @@ contract SwapRouter is Multicall { } /** - * @notice Allows caller to make swaps using the UniswapV3 Exchange. + * @notice Perform a swap using Uniswap V3. * @param swapData bytes variable storing the following swap information * address[] path: array of addresses dictating what swap path to follow * uint24[] poolFees: array of pool fees dictating what swap pools to use - * uint256 assets: the amount of path[0] you want to swap with - * uint256 assetsOutMin: the minimum amount of path[path.length - 1] tokens you want from the swap - * @param recipient address to send the swapped tokens to - * @return amountOut amount of tokens received from the swap + * uint256 amount: amount of the first asset in the path to swap + * uint256 amountOutMin: the minimum amount of the last asset in the path to receive + * @param receiver address to send the received assets to + * @return amountOut amount of assets received from the swap */ - function swapWithUniV3(bytes memory swapData, address recipient) public returns (uint256 amountOut) { - (address[] memory path, uint24[] memory poolFees, uint256 assets, uint256 assetsOutMin) = abi.decode( + function swapWithUniV3(bytes memory swapData, address receiver) public returns (uint256 amountOut) { + (address[] memory path, uint24[] memory poolFees, uint256 amount, uint256 amountOutMin) = abi.decode( swapData, (address[], uint24[], uint256, uint256) ); // Transfer assets to this contract to swap. ERC20 assetIn = ERC20(path[0]); - assetIn.safeTransferFrom(msg.sender, address(this), assets); + assetIn.safeTransferFrom(msg.sender, address(this), amount); // Approve assets to be swapped through the router. - assetIn.safeApprove(address(uniswapV3Router), assets); + assetIn.safeApprove(address(uniswapV3Router), amount); // Encode swap parameters. bytes memory encodePackedPath = abi.encodePacked(address(assetIn)); @@ -155,10 +158,10 @@ contract SwapRouter is Multicall { amountOut = uniswapV3Router.exactInput( IUniswapV3Router.ExactInputParams({ path: encodePackedPath, - recipient: recipient, + recipient: receiver, deadline: block.timestamp + 60, - amountIn: assets, - amountOutMinimum: assetsOutMin + amountIn: amount, + amountOutMinimum: amountOutMin }) ); } From b2b2c5f028e409150fc8ab6f20b9124c75629470 Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Wed, 6 Jul 2022 15:01:33 -0700 Subject: [PATCH 44/49] docs: fix natspec title of PriceRouter and SwapRouter --- src/modules/price-router/PriceRouter.sol | 2 +- src/modules/swap-router/SwapRouter.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/price-router/PriceRouter.sol b/src/modules/price-router/PriceRouter.sol index 15971b82..9e505244 100644 --- a/src/modules/price-router/PriceRouter.sol +++ b/src/modules/price-router/PriceRouter.sol @@ -10,7 +10,7 @@ import { Math } from "src/utils/Math.sol"; import "src/Errors.sol"; /** - * @title Price Router + * @title Sommelier Price Router * @notice Provides a universal interface allowing Sommelier contracts to retrieve secure pricing * data from Chainlink and arbitrary adaptors. * @author crispymangoes, Brian Le diff --git a/src/modules/swap-router/SwapRouter.sol b/src/modules/swap-router/SwapRouter.sol index 0a2b6c83..5e0b33b4 100644 --- a/src/modules/swap-router/SwapRouter.sol +++ b/src/modules/swap-router/SwapRouter.sol @@ -9,7 +9,7 @@ import { IUniswapV3Router } from "src/interfaces/IUniswapV3Router.sol"; import { Multicall } from "src/base/Multicall.sol"; /** - * @title Sommelier Price Router + * @title Sommelier Swap Router * @notice Provides a universal interface allowing Sommelier contracts to interact with multiple * different exchanges to perform swaps. * @dev Perform multiple swaps using Multicall. From b37cf19e6fa37baf77f85585d0bfdf899f05b0ad Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Wed, 6 Jul 2022 15:02:43 -0700 Subject: [PATCH 45/49] docs(Cellar): add natspec --- src/base/Cellar.sol | 194 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 183 insertions(+), 11 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 0e076233..0297de3e 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -15,6 +15,11 @@ import { Math } from "../utils/Math.sol"; import "../Errors.sol"; +/** + * @title Sommelier Cellar + * @notice A composable ERC4626 that can use a set of other ERC4626 or ERC20 positions to earn yield. + * @author crispymangoes, Brian Le + */ contract Cellar is ERC4626, Ownable, Multicall { using AddressArray for address[]; using AddressArray for ERC20[]; @@ -56,28 +61,60 @@ contract Cellar is ERC4626, Ownable, Multicall { */ event PositionSwapped(address indexed newPosition1, address indexed newPosition2, uint256 index1, uint256 index2); + /** + * @notice Value specifying the interface a position uses. + * @param ERC20 an ERC20 token + * @param ERC4626 an ERC4626 vault + * @param Cellar a cellar + */ enum PositionType { ERC20, ERC4626, Cellar } - // TODO: pack struct + /** + * @notice Data related to a position. + * @param positionType value specifying the interface a position uses + * @param highWatermark amount representing the balance this position needs to exceed during the + * next accrual to receive performance fees + */ struct PositionData { PositionType positionType; int256 highWatermark; } + /** + * @notice Addresses of the positions current used by the cellar. + */ address[] public positions; + /** + * @notice Tell whether a position is currently used. + * @param position address of position to check + * @return isUsed boolean specifying whether position is currently used + */ mapping(address => bool) public isPositionUsed; + /** + * @notice Get the data related to a position. + * @param position address of position to get data for + * @return positionData data related to the position + */ mapping(address => PositionData) public getPositionData; + /** + * @notice Get the addresses of the positions current used by the cellar. + */ function getPositions() external view returns (address[] memory) { return positions; } + /** + * @notice Insert a trusted position to the list of positions used by the cellar at a given index. + * @param index index at which to insert the position + * @param position address of position to add + */ function addPosition(uint256 index, address position) external onlyOwner whenNotShutdown { if (!isTrusted[position]) revert USR_UntrustedPosition(position); @@ -92,8 +129,10 @@ contract Cellar is ERC4626, Ownable, Multicall { } /** + * @notice Push a trusted position to the end of the list of positions used by the cellar. * @dev If you know you are going to add a position to the end of the array, this is more * efficient then `addPosition`. + * @param position address of position to add */ function pushPosition(address position) external onlyOwner whenNotShutdown { if (!isTrusted[position]) revert USR_UntrustedPosition(position); @@ -108,6 +147,10 @@ contract Cellar is ERC4626, Ownable, Multicall { emit PositionAdded(position, positions.length - 1); } + /** + * @notice Remove the position at a given index from the list of positions used by the cellar. + * @param index index at which to remove the position + */ function removePosition(uint256 index) external onlyOwner { // Get position being removed. address position = positions[index]; @@ -124,6 +167,7 @@ contract Cellar is ERC4626, Ownable, Multicall { } /** + * @notice Remove the last position in the list of positions used by the cellar. * @dev If you know you are going to remove a position from the end of the array, this is more * efficient then `removePosition`. */ @@ -143,7 +187,12 @@ contract Cellar is ERC4626, Ownable, Multicall { emit PositionRemoved(position, index); } - function replacePosition(address newPosition, uint256 index) external onlyOwner whenNotShutdown { + /** + * @notice Replace a position at a given index with a new position. + * @param index index at which to replace the position + * @param newPosition address of position to replace with + */ + function replacePosition(uint256 index, address newPosition) external onlyOwner whenNotShutdown { // Store the old position before its replaced. address oldPosition = positions[index]; @@ -159,6 +208,11 @@ contract Cellar is ERC4626, Ownable, Multicall { emit PositionReplaced(oldPosition, newPosition, index); } + /** + * @notice Swap the positions at two given indexes. + * @param index1 index of first position to swap + * @param index2 index of second position to swap + */ function swapPositions(uint256 index1, uint256 index2) external onlyOwner { // Get the new positions that will be at each index. address newPosition1 = positions[index2]; @@ -179,8 +233,18 @@ contract Cellar is ERC4626, Ownable, Multicall { */ event TrustChanged(address indexed position, bool isTrusted); + /** + * @notice Tell whether a position is trusted. + * @param position address of position to check + * @return isTrusted boolean specifying whether position is trusted + */ mapping(address => bool) public isTrusted; + /** + * @notice Trust a position to be used by the cellar. + * @param position address of position to trust + * @param positionType value specifying the interface the position uses + */ function trustPosition(address position, PositionType positionType) external onlyOwner { // Trust position. isTrusted[position] = true; @@ -191,6 +255,10 @@ contract Cellar is ERC4626, Ownable, Multicall { emit TrustChanged(position, true); } + /** + * @notice Distrust a position to prevent it from being used by the cellar. + * @param position address of position to distrust + */ function distrustPosition(address position) external onlyOwner { // Distrust position. isTrusted[position] = false; @@ -199,22 +267,43 @@ contract Cellar is ERC4626, Ownable, Multicall { positions.remove(position); // NOTE: After position has been removed, SP should be notified on the UI that the position - // can no longer be used and to exit the position or rebalance its assets into another - // position ASAP. + // can no longer be used and to exit the position or rebalance its assets into another + // position ASAP. emit TrustChanged(position, false); } // ============================================ WITHDRAW CONFIG ============================================ + /** + * @notice Emitted when withdraw type configuration is changed. + * @param oldType previous withdraw type + * @param newType new withdraw type + */ event WithdrawTypeChanged(WithdrawType oldType, WithdrawType newType); + /** + * @notice The withdraw type to use for the cellar. + * @param ORDERLY use `positions` in specify the order in which assets are withdrawn (eg. + * `positions[0]` is withdrawn from first), least impactful position (position + * that will have its core positions impacted the least by having funds removed) + * should be first and most impactful position should be last + * @param PROPORTIONAL pull assets from each position proportionally when withdrawing, used if + * trying to maintain a specific ratio + */ enum WithdrawType { - Orderly, - Proportional + ORDERLY, + PROPORTIONAL } + /** + * @notice The withdraw type to used by the cellar. + */ WithdrawType public withdrawType; + /** + * @notice Set the withdraw type used by the cellar. + * @param newWithdrawType value of the new withdraw type to use + */ function setWithdrawType(WithdrawType newWithdrawType) external onlyOwner { emit WithdrawTypeChanged(withdrawType, newWithdrawType); @@ -223,6 +312,11 @@ contract Cellar is ERC4626, Ownable, Multicall { // ============================================ HOLDINGS CONFIG ============================================ + /** + * @notice Emitted when the holdings position is changed. + * @param oldPosition address of the old holdings position + * @param newPosition address of the new holdings position + */ event HoldingPositionChanged(address indexed oldPosition, address indexed newPosition); /** @@ -237,6 +331,10 @@ contract Cellar is ERC4626, Ownable, Multicall { */ address public holdingPosition; + /** + * @notice Set the holding position used by the cellar. + * @param newHoldingPosition address of the new holding position to use + */ function setHoldingPosition(address newHoldingPosition) external onlyOwner { if (!isPositionUsed[newHoldingPosition]) revert USR_InvalidPosition(newHoldingPosition); @@ -252,6 +350,7 @@ contract Cellar is ERC4626, Ownable, Multicall { /** * @notice Timestamp of when the last accrual occurred. + * @dev Used for determining the amount of platform fees that can be taken during an accrual period. */ uint64 public lastAccrual; @@ -420,8 +519,9 @@ contract Cellar is ERC4626, Ownable, Multicall { // =========================================== CONSTRUCTOR =========================================== - // TODO: since registry address should never change, consider hardcoding the address once - // registry is finalized and making this a constant + /** + * @notice Address of the platform's registry contract. Used to get the latest address of modules. + */ Registry public immutable registry; /** @@ -429,7 +529,12 @@ contract Cellar is ERC4626, Ownable, Multicall { * module to the cellars. * https://github.com/PeggyJV/steward * https://github.com/cosmos/gravity-bridge/blob/main/solidity/contracts/Gravity.sol + * @param _registry address of the platform's registry contract * @param _asset address of underlying token used for the for accounting, depositing, and withdrawing + * @param _positions addresses of the positions to initialize the cellar with + * @param _positionTypes types of each positions used + * @param _holdingPosition address of the position to use as the holding position + * @param _withdrawType withdraw type to use for the cellar * @param _name name of this cellar's share token * @param _name symbol of this cellar's share token */ @@ -499,6 +604,19 @@ contract Cellar is ERC4626, Ownable, Multicall { _depositTo(holdingPosition, assets); } + /** + * @notice Withdraw assets from the cellar by redeeming shares. + * @dev Unlike conventional ERC4626 contracts, this may not always return one asset to the receiver. + * Since there are no swaps involved in this function, the receiver may receive multiple + * assets. The value of all the assets returned will be equal to the amount defined by + * `assets` denominated in the `asset` of the cellar (eg. if `asset` is USDC and `assets` + * is 1000, then the receiver will receive $1000 worth of assets in either one or many + * tokens). + * @param assets equivalent value of the assets withdrawn, denominated in the cellar's asset + * @param receiver address that will receive withdrawn assets + * @param owner address that owns the shares being redeemed + * @return shares amount of shares redeemed + */ function withdraw( uint256 assets, address receiver, @@ -527,11 +645,24 @@ contract Cellar is ERC4626, Ownable, Multicall { emit Withdraw(msg.sender, receiver, owner, assets, shares); - withdrawType == WithdrawType.Orderly + withdrawType == WithdrawType.ORDERLY ? _withdrawInOrder(assets, receiver, _positions, positionAssets, positionBalances) : _withdrawInProportion(shares, totalShares, receiver, _positions, positionBalances); } + /** + * @notice Redeem shares to withdraw assets from the cellar. + * @dev Unlike conventional ERC4626 contracts, this may not always return one asset to the receiver. + * Since there are no swaps involved in this function, the receiver may receive multiple + * assets. The value of all the assets returned will be equal to the amount defined by + * `assets` denominated in the `asset` of the cellar (eg. if `asset` is USDC and `assets` + * is 1000, then the receiver will receive $1000 worth of assets in either one or many + * tokens). + * @param shares amount of shares to redeem + * @param receiver address that will receive withdrawn assets + * @param owner address that owns the shares being redeemed + * @return assets equivalent value of the assets withdrawn, denominated in the cellar's asset + */ function redeem( uint256 shares, address receiver, @@ -560,11 +691,15 @@ contract Cellar is ERC4626, Ownable, Multicall { emit Withdraw(msg.sender, receiver, owner, assets, shares); - withdrawType == WithdrawType.Orderly + withdrawType == WithdrawType.ORDERLY ? _withdrawInOrder(assets, receiver, _positions, positionAssets, positionBalances) : _withdrawInProportion(shares, totalShares, receiver, _positions, positionBalances); } + /** + * @dev Withdraw from positions in the order defined by `positions`. Used if the withdraw type + * is `ORDERLY`. + */ function _withdrawInOrder( uint256 assets, address receiver, @@ -606,6 +741,10 @@ contract Cellar is ERC4626, Ownable, Multicall { } } + /** + * @dev Withdraw from each position proportional to that of shares redeemed. Used if the + * withdraw type is `PROPORTIONAL`. + */ function _withdrawInProportion( uint256 shares, uint256 totalShares, @@ -688,6 +827,9 @@ contract Cellar is ERC4626, Ownable, Multicall { shares = _previewWithdraw(assets, totalAssets()); } + /** + * @dev Used to more efficiently convert amount of shares to assets using a stored `totalAssets` value. + */ function _convertToAssets(uint256 shares, uint256 _totalAssets) internal view returns (uint256 assets) { uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. uint8 assetDecimals = asset.decimals(); @@ -697,6 +839,9 @@ contract Cellar is ERC4626, Ownable, Multicall { assets = assets.changeDecimals(18, assetDecimals); } + /** + * @dev Used to more efficiently convert amount of assets to shares using a stored `totalAssets` value. + */ function _convertToShares(uint256 assets, uint256 _totalAssets) internal view returns (uint256 shares) { uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. uint8 assetDecimals = asset.decimals(); @@ -706,6 +851,9 @@ contract Cellar is ERC4626, Ownable, Multicall { shares = totalShares == 0 ? assetsNormalized : assetsNormalized.mulDivDown(totalShares, totalAssetsNormalized); } + /** + * @dev Used to more efficiently simulate minting shares using a stored `totalAssets` value. + */ function _previewMint(uint256 shares, uint256 _totalAssets) internal view returns (uint256 assets) { uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. uint8 assetDecimals = asset.decimals(); @@ -715,6 +863,9 @@ contract Cellar is ERC4626, Ownable, Multicall { assets = assets.changeDecimals(18, assetDecimals); } + /** + * @dev Used to more efficiently simulate withdrawing assets using a stored `totalAssets` value. + */ function _previewWithdraw(uint256 assets, uint256 _totalAssets) internal view returns (uint256 shares) { uint256 totalShares = totalSupply; // Saves an extra SLOAD if totalSupply is non-zero. uint8 assetDecimals = asset.decimals(); @@ -724,6 +875,10 @@ contract Cellar is ERC4626, Ownable, Multicall { shares = totalShares == 0 ? assetsNormalized : assetsNormalized.mulDivUp(totalShares, totalAssetsNormalized); } + /** + * @dev Used to efficiently get and store accounting information to avoid having to expensively + * recompute it. + */ function _getData() internal view @@ -821,6 +976,9 @@ contract Cellar is ERC4626, Ownable, Multicall { emit Accrual(platformFees, performanceFees); } + /** + * @dev Calculate the amount of fees to mint such that value of fees after minting is not diluted. + */ function _convertToFees(uint256 assets, uint256 exchangeRate) internal view returns (uint256 fees) { // Convert amount of assets to take as fees to shares. uint256 feesInShares = assets * exchangeRate; @@ -948,6 +1106,9 @@ contract Cellar is ERC4626, Ownable, Multicall { // ========================================== HELPER FUNCTIONS ========================================== + /** + * @dev Deposit into a position according to its position type and update related state. + */ function _depositTo(address position, uint256 assets) internal { PositionData storage positionData = getPositionData[position]; PositionType positionType = positionData.positionType; @@ -963,6 +1124,9 @@ contract Cellar is ERC4626, Ownable, Multicall { } } + /** + * @dev Withdraw from a position according to its position type and update related state. + */ function _withdrawFrom( address position, uint256 assets, @@ -983,6 +1147,9 @@ contract Cellar is ERC4626, Ownable, Multicall { } } + /** + * @dev Get the balance of a position according to its position type. + */ function _balanceOf(address position) internal view returns (uint256) { PositionType positionType = getPositionData[position].positionType; @@ -993,6 +1160,9 @@ contract Cellar is ERC4626, Ownable, Multicall { } } + /** + * @dev Get the asset of a position according to its position type. + */ function _assetOf(address position) internal view returns (ERC20) { PositionType positionType = getPositionData[position].positionType; @@ -1003,6 +1173,9 @@ contract Cellar is ERC4626, Ownable, Multicall { } } + /** + * @dev Perform a swap using the swap router and check that it behaves as expected. + */ function _swap( ERC20 assetIn, uint256 amountIn, @@ -1024,7 +1197,6 @@ contract Cellar is ERC4626, Ownable, Multicall { // Check that the amount of assets swapped is what is expected. Will revert if the `params` // specified a different amount of assets to swap then `amountIn`. - // TODO: consider replacing with revert statement require(assetIn.balanceOf(address(this)) == expectedAssetsInAfter, "INCORRECT_PARAMS_AMOUNT"); } } From bcb8f9f6dc045ef7db4be63ac43575a22941d29a Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Wed, 6 Jul 2022 15:54:38 -0700 Subject: [PATCH 46/49] tests(Cellar): fix build errors --- src/base/Cellar.sol | 6 ------ test/Cellar.t.sol | 4 ++-- test/CellarRouter.t.sol | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 0297de3e..8b68ddc4 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -91,15 +91,11 @@ contract Cellar is ERC4626, Ownable, Multicall { /** * @notice Tell whether a position is currently used. - * @param position address of position to check - * @return isUsed boolean specifying whether position is currently used */ mapping(address => bool) public isPositionUsed; /** * @notice Get the data related to a position. - * @param position address of position to get data for - * @return positionData data related to the position */ mapping(address => PositionData) public getPositionData; @@ -235,8 +231,6 @@ contract Cellar is ERC4626, Ownable, Multicall { /** * @notice Tell whether a position is trusted. - * @param position address of position to check - * @return isTrusted boolean specifying whether position is trusted */ mapping(address => bool) public isTrusted; diff --git a/test/Cellar.t.sol b/test/Cellar.t.sol index 63a5823a..6012f45f 100644 --- a/test/Cellar.t.sol +++ b/test/Cellar.t.sol @@ -96,7 +96,7 @@ contract CellarTest is Test { positions, positionTypes, address(USDC), - Cellar.WithdrawType.Orderly, + Cellar.WithdrawType.ORDERLY, "Multiposition Cellar LP Token", "multiposition-CLR" ); @@ -192,7 +192,7 @@ contract CellarTest is Test { deal(address(cellar), address(this), cellar.previewWithdraw(16_000e6)); // Withdraw from position. - cellar.setWithdrawType(Cellar.WithdrawType.Proportional); + cellar.setWithdrawType(Cellar.WithdrawType.PROPORTIONAL); uint256 shares = cellar.withdraw(16_000e6, address(this), address(this)); assertEq(cellar.balanceOf(address(this)), 0, "Should have redeemed all shares."); diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index a09b5051..c9f6f0fe 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -117,7 +117,7 @@ contract CellarRouterTest is Test { positions, positionTypes, address(USDC), - Cellar.WithdrawType.Orderly, + Cellar.WithdrawType.ORDERLY, "Multiposition Cellar LP Token", "multiposition-CLR" ); From 87aa5e339d1fff37472a3005489c57cb4970652b Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Wed, 6 Jul 2022 15:58:24 -0700 Subject: [PATCH 47/49] docs(CellarRouter): fix natspec, and add natspec to cellar router --- src/CellarRouter.sol | 101 +++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 56 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index b3214ad0..052d1700 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -15,42 +15,26 @@ import "./Errors.sol"; // TODO: Fix comments (some of them still reference Sushiswap). // TODO: Rewrite natspec comments to be more clear. - +/** + * @title Sommelier Cellar Router + * @notice Allows for better user experience when on-boarding/off-boarding from cellars + * by combining together deposit/withdraw TXs with appropriate swaps + * @author Brian Le + */ contract CellarRouter is ICellarRouter { using SafeTransferLib for ERC20; // ========================================== CONSTRUCTOR ========================================== - /** - * @notice Uniswap V3 swap router contract. Used for swapping if pool fees are specified. - */ - IUniswapV3Router public immutable uniswapV3Router; // 0xE592427A0AEce92De3Edee1F18E0157C05861564 - - /** - * @notice Uniswap V2 swap router contract. Used for swapping if pool fees are not specified. - */ - IUniswapV2Router public immutable uniswapV2Router; // 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D - /** * @notice Registry contract */ Registry public immutable registry; /** - * @dev Owner will be set to the Gravity Bridge, which relays instructions from the Steward - * module to the cellars. - * https://github.com/PeggyJV/steward - * https://github.com/cosmos/gravity-bridge/blob/main/solidity/contracts/Gravity.sol - * @param _uniswapV3Router Uniswap V3 swap router address - * @param _uniswapV2Router Uniswap V2 swap router address + * @param _registry Registry contract used to get most current swap router */ - constructor( - IUniswapV3Router _uniswapV3Router, - IUniswapV2Router _uniswapV2Router, - Registry _registry - ) { - uniswapV3Router = _uniswapV3Router; - uniswapV2Router = _uniswapV2Router; + constructor(Registry _registry) { registry = _registry; } @@ -90,19 +74,17 @@ contract CellarRouter is ICellarRouter { } /** - * @notice Deposit into a cellar by first performing a swap to the cellar's current asset if necessary. - * @dev If using Uniswap V3 for swap, must specify the pool fee tier to use for each swap. For - * example, if there are "n" addresses in path, there should be "n-1" values specifying the - * fee tiers of each pool used for each swap. The current possible pool fee tiers for - * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap - * V2, leave pool fees empty to use Uniswap V2 for swap. + * @notice Deposit into a cellar by first performing a swap to the cellar's current asset. + * @dev Uses the swap router to perform the swap * @param cellar address of the cellar * @param exchange ENUM representing what exchange to make the swap at - * Refer to src/SwapRouter.sol for list of available options + * Refer to src/modules/swap-router/SwapRouter.sol for list of available options * @param swapData bytes variable containing all the data needed to make a swap - * @param assets amount of assets to deposit + * Composition is based off what exchange is chosen for the swap refer to + * src/modules/swap-router/SwapRouter.sol to see what data should be encoded into swapData + * @param assets amount of assets to swap, must match initial swap asset in swapData * @param receiver address to recieve the cellar shares - * @param assetIn ERC20 token used to deposit + * @param assetIn ERC20 token used to swap for deposit token * @return shares amount of shares minted */ function depositAndSwap( @@ -129,17 +111,15 @@ contract CellarRouter is ICellarRouter { } /** - * @notice Deposit into a cellar by first performing a swap to the cellar's current asset if necessary. - * @dev If using Uniswap V3 for swap, must specify the pool fee tier to use for each swap. For - * example, if there are "n" addresses in path, there should be "n-1" values specifying the - * fee tiers of each pool used for each swap. The current possible pool fee tiers for - * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap - * V2, leave pool fees empty to use Uniswap V2 for swap. + * @notice Deposit into a cellar by first performing a swap to the cellar's current asset. + * @dev Uses the swap router to perform the swap * @param cellar address of the cellar to deposit into * @param exchange ENUM representing what exchange to make the swap at - * Refer to src/SwapRouter.sol for list of available options + * Refer to src/modules/swap-router/SwapRouter.sol for list of available options * @param swapData bytes variable containing all the data needed to make a swap - * @param assets amount of assets to deposit + * Composition is based off what exchange is chosen for the swap refer to + * src/modules/swap-router/SwapRouter.sol to see what data should be encoded into swapData + * @param assets amount of assets to swap, must match initial swap asset in swapData * @param assetIn ERC20 asset caller wants to swap and deposit with * @param receiver address to recieve the cellar shares * @param deadline timestamp after which permit is invalid @@ -167,15 +147,15 @@ contract CellarRouter is ICellarRouter { // ======================================= WITHDRAW OPERATIONS ======================================= /** - * @notice Withdraws from a cellar and then performs a swap to another desired asset, if the - * withdrawn asset is not already. + * @notice Withdraws from a cellar and then performs swap(s) to another desired asset. * @dev Permission is required from caller for router to burn shares. Please make sure that * caller has approved the router to spend their shares. * @param cellar address of the cellar * @param exchanges enums representing what exchange to make the swap at, - * refer to src/SwapRouter.sol for list of available options - * @param swapDatas bytes variable containing all the data needed to make a swap - * receiver address should be the callers address + * refer to src/modules/swap-router/SwapRouter.sol for list of available options + * @param swapDatas bytes array variable containing all the data needed to make multiple swaps + * Composition is based off what exchange is chosen for the swap refer to + * src/modules/swap-router/SwapRouter.sol to see what data should be encoded into swapDatas[i] * @param assets amount of assets to withdraw * @param receiver the address swapped tokens are sent to * @return shares amount of shares burned @@ -223,17 +203,15 @@ contract CellarRouter is ICellarRouter { } /** - * @notice Withdraws from a cellar and then performs a swap to another desired asset, if the - * withdrawn asset is not already, using permit. - * @dev If using Uniswap V3 for swap, must specify the pool fee tier to use for each swap. For - * example, if there are "n" addresses in path, there should be "n-1" values specifying the - * fee tiers of each pool used for each swap. The current possible pool fee tiers for - * Uniswap V3 are 0.01% (100), 0.05% (500), 0.3% (3000), and 1% (10000). If using Uniswap - * V2, leave pool fees empty to use Uniswap V2 for swap. + * @notice Withdraws from a cellar and then performs swap(s) to another desired asset, using permit. + * @dev Permission is required from caller for router to burn shares. Please make sure that + * caller has approved the router to spend their shares. * @param cellar address of the cellar - * @param exchanges enum representing what exchange to make the swap at - * Refer to src/SwapRouter.sol for list of available options - * @param swapDatas bytes variable containing all the data needed to make a swap + * @param exchanges enums representing what exchange to make the swap at, + * refer to src/modules/swap-router/SwapRouter.sol for list of available options + * @param swapDatas bytes array variable containing all the data needed to make multiple swaps + * Composition is based off what exchange is chosen for the swap refer to + * src/modules/swap-router/SwapRouter.sol to see what data should be encoded into swapDatas[i] * @param assets amount of assets to withdraw * @param deadline timestamp after which permit is invalid * @param signature a valid secp256k1 signature @@ -290,6 +268,12 @@ contract CellarRouter is ICellarRouter { } } + /** + * @notice Used to determine the amounts of assets Router had using current balances and amountsReceived. + * @param assets array of ERC20 tokens to query the balances of + * @param amountsRecevied the amount of each assets received + * @return balancesBefore array of balances before amounts were received + */ function _getBalancesBefore(ERC20[] memory assets, uint256[] memory amountsReceived) internal view @@ -304,6 +288,11 @@ contract CellarRouter is ICellarRouter { } } + /** + * @notice Find what assets a cellar's positions uses. + * @param cellar address of the cellar + * @return assets array of assets that make up cellar's positions + */ function _getPositionAssets(Cellar cellar) internal view returns (ERC20[] memory assets) { address[] memory positions = cellar.getPositions(); From 057d0f183b40677b3fa306f4718aa0f580085c1d Mon Sep 17 00:00:00 2001 From: crispymangoes Date: Wed, 6 Jul 2022 16:06:03 -0700 Subject: [PATCH 48/49] docs(CellarRouter): fix natspec in cellar router --- src/CellarRouter.sol | 2 +- test/CellarRouter.t.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index 052d1700..8afc6df2 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -271,7 +271,7 @@ contract CellarRouter is ICellarRouter { /** * @notice Used to determine the amounts of assets Router had using current balances and amountsReceived. * @param assets array of ERC20 tokens to query the balances of - * @param amountsRecevied the amount of each assets received + * @param amountsReceived the amount of each assets received * @return balancesBefore array of balances before amounts were received */ function _getBalancesBefore(ERC20[] memory assets, uint256[] memory amountsReceived) diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index a09b5051..40f5d316 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -76,7 +76,7 @@ contract CellarRouterTest is Test { address(priceRouter) ); - router = new CellarRouter(IUniswapV3Router(address(exchange)), IUniswapV2Router(address(exchange)), registry); + router = new CellarRouter(registry); //forkedRouter = new CellarRouter(IUniswapV3Router(uniV3Router), IUniswapV2Router(uniV2Router), registry); ABC = new MockERC20("ABC", 18); @@ -117,7 +117,7 @@ contract CellarRouterTest is Test { positions, positionTypes, address(USDC), - Cellar.WithdrawType.Orderly, + Cellar.WithdrawType.ORDERLY, "Multiposition Cellar LP Token", "multiposition-CLR" ); From 2dcd6d06750526fedafcae7288910c955e7cc08c Mon Sep 17 00:00:00 2001 From: brianle83 <0xble@pm.me> Date: Thu, 7 Jul 2022 10:34:07 -0700 Subject: [PATCH 49/49] docs(CellarRouter): revise contract natspec --- src/CellarRouter.sol | 50 +++++++++++++++++++------------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index 8afc6df2..727b2d0d 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -13,12 +13,10 @@ import { SwapRouter } from "src/modules/swap-router/SwapRouter.sol"; import "./Errors.sol"; -// TODO: Fix comments (some of them still reference Sushiswap). -// TODO: Rewrite natspec comments to be more clear. /** * @title Sommelier Cellar Router - * @notice Allows for better user experience when on-boarding/off-boarding from cellars - * by combining together deposit/withdraw TXs with appropriate swaps + * @notice Enables depositing/withdrawing from cellars using permits and swapping from/to different + * assets before/after depositing/withdrawing. * @author Brian Le */ contract CellarRouter is ICellarRouter { @@ -27,12 +25,12 @@ contract CellarRouter is ICellarRouter { // ========================================== CONSTRUCTOR ========================================== /** - * @notice Registry contract + * @notice Registry contract used to get most current swap router. */ Registry public immutable registry; /** - * @param _registry Registry contract used to get most current swap router + * @param _registry address of the registry contract */ constructor(Registry _registry) { registry = _registry; @@ -77,13 +75,12 @@ contract CellarRouter is ICellarRouter { * @notice Deposit into a cellar by first performing a swap to the cellar's current asset. * @dev Uses the swap router to perform the swap * @param cellar address of the cellar - * @param exchange ENUM representing what exchange to make the swap at - * Refer to src/modules/swap-router/SwapRouter.sol for list of available options - * @param swapData bytes variable containing all the data needed to make a swap - * Composition is based off what exchange is chosen for the swap refer to - * src/modules/swap-router/SwapRouter.sol to see what data should be encoded into swapData + * @param exchange value representing what exchange to make the swap at, refer to + * `SwapRouter.sol` for list of available options + * @param swapData bytes variable containing all the data needed to make a swap, refer to + * `SwapRouter.sol` to see what parameters need to be encoded for each exchange * @param assets amount of assets to swap, must match initial swap asset in swapData - * @param receiver address to recieve the cellar shares + * @param receiver address to receive the cellar shares * @param assetIn ERC20 token used to swap for deposit token * @return shares amount of shares minted */ @@ -114,14 +111,13 @@ contract CellarRouter is ICellarRouter { * @notice Deposit into a cellar by first performing a swap to the cellar's current asset. * @dev Uses the swap router to perform the swap * @param cellar address of the cellar to deposit into - * @param exchange ENUM representing what exchange to make the swap at - * Refer to src/modules/swap-router/SwapRouter.sol for list of available options - * @param swapData bytes variable containing all the data needed to make a swap - * Composition is based off what exchange is chosen for the swap refer to - * src/modules/swap-router/SwapRouter.sol to see what data should be encoded into swapData + * @param exchange value representing what exchange to make the swap at, refer to + * `SwapRouter.sol` for list of available options + * @param swapData bytes variable containing all the data needed to make a swap, refer to + * `SwapRouter.sol` to see what parameters need to be encoded for each exchange * @param assets amount of assets to swap, must match initial swap asset in swapData * @param assetIn ERC20 asset caller wants to swap and deposit with - * @param receiver address to recieve the cellar shares + * @param receiver address to receive the cellar shares * @param deadline timestamp after which permit is invalid * @param signature a valid secp256k1 signature * @return shares amount of shares minted @@ -151,11 +147,10 @@ contract CellarRouter is ICellarRouter { * @dev Permission is required from caller for router to burn shares. Please make sure that * caller has approved the router to spend their shares. * @param cellar address of the cellar - * @param exchanges enums representing what exchange to make the swap at, - * refer to src/modules/swap-router/SwapRouter.sol for list of available options - * @param swapDatas bytes array variable containing all the data needed to make multiple swaps - * Composition is based off what exchange is chosen for the swap refer to - * src/modules/swap-router/SwapRouter.sol to see what data should be encoded into swapDatas[i] + * @param exchanges value representing what exchange to make the swap at, refer to + * `SwapRouter.sol` for list of available options + * @param swapDatas bytes variable containing all the data needed to make a swap, refer to + * `SwapRouter.sol` to see what parameters need to be encoded for each exchange * @param assets amount of assets to withdraw * @param receiver the address swapped tokens are sent to * @return shares amount of shares burned @@ -207,11 +202,10 @@ contract CellarRouter is ICellarRouter { * @dev Permission is required from caller for router to burn shares. Please make sure that * caller has approved the router to spend their shares. * @param cellar address of the cellar - * @param exchanges enums representing what exchange to make the swap at, - * refer to src/modules/swap-router/SwapRouter.sol for list of available options - * @param swapDatas bytes array variable containing all the data needed to make multiple swaps - * Composition is based off what exchange is chosen for the swap refer to - * src/modules/swap-router/SwapRouter.sol to see what data should be encoded into swapDatas[i] + * @param exchanges value representing what exchange to make the swap at, refer to + * `SwapRouter.sol` for list of available options + * @param swapDatas bytes variable containing all the data needed to make a swap, refer to + * `SwapRouter.sol` to see what parameters need to be encoded for each exchange * @param assets amount of assets to withdraw * @param deadline timestamp after which permit is invalid * @param signature a valid secp256k1 signature