diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f38975b3..bf9b55ee 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,7 +34,7 @@ jobs: - name: Run Forge tests run: | - forge test -vvv + forge test -vvv --fork-url https://eth-mainnet.alchemyapi.io/v2/${{ secrets.ALCHEMY_API_KEY }} id: forge-test hardhat: diff --git a/README.md b/README.md index c834f877..44acb381 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ forge test npx hardhat test ``` +Run tests on forked chains with Foundry: + +```bash +forge test --fork-url ALCHEMY_API_KEY +``` + Run tests for both Hardhat and Foundry: ```bash diff --git a/foundry.toml b/foundry.toml index 5bd3ce8a..f727078d 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,8 +2,4 @@ # Sets the concrete solc version to use # This overrides the `auto_detect_solc` value solc_version = '0.8.13' -auto_detect_solc = false - -[ci] -# Perform extreme fuzzing on CI runs -fuzz_runs = 100_000 \ No newline at end of file +auto_detect_solc = false \ No newline at end of file diff --git a/src/CellarRouter.sol b/src/CellarRouter.sol index 59f42f80..cb34452f 100644 --- a/src/CellarRouter.sol +++ b/src/CellarRouter.sol @@ -87,7 +87,7 @@ contract CellarRouter is ICellarRouter { function depositAndSwapIntoCellar( ERC4626 cellar, address[] calldata path, - uint256[] calldata poolFees, + uint24[] calldata poolFees, uint256 assets, uint256 assetsOutMin, address receiver @@ -131,7 +131,7 @@ contract CellarRouter is ICellarRouter { function depositAndSwapIntoCellarWithPermit( ERC4626 cellar, address[] calldata path, - uint256[] calldata poolFees, + uint24[] calldata poolFees, uint256 assets, uint256 assetsOutMin, address receiver, @@ -173,7 +173,7 @@ contract CellarRouter is ICellarRouter { function withdrawAndSwapFromCellar( ERC4626 cellar, address[] calldata path, - uint256[] calldata poolFees, + uint24[] calldata poolFees, uint256 assets, uint256 assetsOutMin, address receiver @@ -214,7 +214,7 @@ contract CellarRouter is ICellarRouter { function withdrawAndSwapFromCellarWithPermit( ERC4626 cellar, address[] calldata path, - uint256[] calldata poolFees, + uint24[] calldata poolFees, uint256 assets, uint256 assetsOutMin, address receiver, @@ -246,7 +246,7 @@ contract CellarRouter is ICellarRouter { */ function _swap( address[] calldata path, - uint256[] calldata poolFees, + uint24[] calldata poolFees, uint256 assets, uint256 assetsOutMin ) internal returns (uint256 assetsOut) { diff --git a/src/SwapRouter.sol b/src/SwapRouter.sol index 95b09678..89171748 100644 --- a/src/SwapRouter.sol +++ b/src/SwapRouter.sol @@ -3,7 +3,6 @@ pragma solidity 0.8.13; import { ERC20 } from "@solmate/tokens/ERC20.sol"; import { SafeTransferLib } from "@solmate/utils/SafeTransferLib.sol"; -import { IAggregationRouterV4 as AggregationRouterV4 } from "./interfaces/IAggregationRouterV4.sol"; import { IUniswapV2Router02 as UniswapV2Router } from "./interfaces/IUniswapV2Router02.sol"; import { IUniswapV3Router as UniswapV3Router } from "./interfaces/IUniswapV3Router.sol"; @@ -34,20 +33,10 @@ contract SwapRouter { UniswapV3Router public immutable uniswapV3Router; // 0xE592427A0AEce92De3Edee1F18E0157C05861564 /** - * @notice 1Inch Dex Aggregation Router + * */ - AggregationRouterV4 public immutable aggRouterV4; // 0x1111111254fb6c44bAC0beD2854e76F90643097d - - /** - * @param _aggRouterV4 1 Inch Router Address - */ - constructor( - AggregationRouterV4 _aggRouterV4, - UniswapV2Router _uniswapV2Router, - UniswapV3Router _uniswapV3Router - ) { + constructor(UniswapV2Router _uniswapV2Router, UniswapV3Router _uniswapV3Router) { //set up all exchanges - aggRouterV4 = _aggRouterV4; uniswapV2Router = _uniswapV2Router; uniswapV3Router = _uniswapV3Router; diff --git a/src/base/Cellar.sol b/src/base/Cellar.sol index 6c76e525..50aa34de 100644 --- a/src/base/Cellar.sol +++ b/src/base/Cellar.sol @@ -50,39 +50,36 @@ contract Cellar is ERC4626, Ownable, Multicall { */ event PositionSwapped(address indexed newPosition1, address indexed newPosition2, uint256 index1, uint256 index2); - enum PositionType { - ERC20, - ERC4626 - } - + // TODO: pack struct struct PositionData { - PositionType positionType; bool isLossless; uint256 balance; - address[] pathToAsset; // Left empty if position either is (if ERC20) or uses (if ERC4626) same asset as cellar. } address[] public positions; + mapping(address => bool) public isPositionUsed; + mapping(address => PositionData) public getPositionData; function getPositions() external view returns (address[] memory) { return positions; } - function isPositionUsed(address position) public view returns (bool) { - return positions.contains(position); - } + function addPosition(uint256 index, address position) external onlyOwner whenNotShutdown { + if (!isTrusted[position]) revert USR_UntrustedPosition(position); - function isPositionUsingSameAsset(address position) public view returns (bool) { - return getPositionData[position].pathToAsset.length == 0; - } + // Check if position is already being used. + if (isPositionUsed[position]) revert USR_PositionAlreadyUsed(position); - function addPosition(uint256 index, address position) public onlyOwner whenNotShutdown { - if (!isTrusted[position]) revert USR_UntrustedPosition(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; emit PositionAdded(position, index); } @@ -91,27 +88,34 @@ contract Cellar is ERC4626, Ownable, Multicall { * @dev If you know you are going to add a position to the end of the array, this is more * efficient then `addPosition`. */ - function pushPosition(address position) public onlyOwner whenNotShutdown { + function pushPosition(address position) external onlyOwner whenNotShutdown { if (!isTrusted[position]) revert USR_UntrustedPosition(position); // Check if position is already being used. - if (isPositionUsed(position)) revert USR_PositionAlreadyUsed(position); + 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; emit PositionAdded(position, positions.length - 1); } - function removePosition(uint256 index) public onlyOwner { + function removePosition(uint256 index) external onlyOwner { // Get position being removed. address position = positions[index]; + // Only remove position if it is empty. + if (ERC4626(position).balanceOf(address(this)) > 0) revert USR_PositionNotEmpty(position); + // Remove position at the given index. positions.remove(index); - - // Pull any assets that were in the removed position to the holding pool. - _emptyPosition(position); + isPositionUsed[position] = false; emit PositionRemoved(position, index); } @@ -120,34 +124,37 @@ contract Cellar is ERC4626, Ownable, Multicall { * @dev If you know you are going to remove a position from the end of the array, this is more * efficient then `removePosition`. */ - function popPosition() public onlyOwner { + function popPosition() external onlyOwner { // Get the index of the last position and last position itself. uint256 index = positions.length - 1; address position = positions[index]; + // Only remove position if it is empty. + if (ERC4626(position).balanceOf(address(this)) > 0) revert USR_PositionNotEmpty(position); + // Remove last position. positions.pop(); - - // Pull any assets that were in the removed position to the holding pool. - _emptyPosition(position); + isPositionUsed[position] = false; emit PositionRemoved(position, index); } - function replacePosition(address newPosition, uint256 index) public onlyOwner whenNotShutdown { + function replacePosition(address newPosition, uint256 index) external onlyOwner whenNotShutdown { // Store the old position before its replaced. address oldPosition = positions[index]; + // Only remove position if it is empty. + if (ERC4626(oldPosition).balanceOf(address(this)) > 0) revert USR_PositionNotEmpty(oldPosition); + // Replace old position with new position. positions[index] = newPosition; - - // Pull any assets that were in the old position to the holding pool. - _emptyPosition(oldPosition); + isPositionUsed[oldPosition] = false; + isPositionUsed[newPosition] = true; emit PositionReplaced(oldPosition, newPosition, index); } - function swapPositions(uint256 index1, uint256 index2) public onlyOwner { + function swapPositions(uint256 index1, uint256 index2) external onlyOwner { // Get the new positions that will be at each index. address newPosition1 = positions[index2]; address newPosition2 = positions[index1]; @@ -169,35 +176,32 @@ contract Cellar is ERC4626, Ownable, Multicall { mapping(address => bool) public isTrusted; - function trustPosition( - address position, - PositionType positionType, - bool isLossless - ) public onlyOwner { + function trustPosition(address position, bool isLossless) external onlyOwner { // Trust position. isTrusted[position] = true; - // Set position type and lossless flag. - PositionData storage positionData = getPositionData[position]; - positionData.positionType = positionType; - positionData.isLossless = isLossless; + // Set position's lossless flag. + getPositionData[position].isLossless = isLossless; // Set max approval to deposit into position if it is ERC4626. - if (positionType == PositionType.ERC4626) ERC4626(position).asset().safeApprove(position, type(uint256).max); + ERC4626(position).asset().safeApprove(position, type(uint256).max); emit TrustChanged(position, true); } - function distrustPosition(address position) public onlyOwner { + function distrustPosition(address position) external onlyOwner { // Distrust position. isTrusted[position] = false; // Remove position from the list of positions if it is present. positions.remove(position); - // Pull any assets that were in the removed position to the holding pool. - _emptyPosition(position); + // Remove approval for position. + ERC4626(position).asset().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. emit TrustChanged(position, false); } @@ -244,19 +248,6 @@ contract Cellar is ERC4626, Ownable, Multicall { accrualPeriod = newAccrualPeriod; } - // ============================================ 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; - } - // ========================================= FEES CONFIG ========================================= /** @@ -420,6 +411,19 @@ 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 @@ -458,6 +462,62 @@ 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( + uint256 assets, + uint256, + address, + address + ) internal override { + uint256 totalAssetsInHolding = totalHoldings(); + + // Only withdraw if not enough assets in the holding pool. + if (assets > totalAssetsInHolding) { + uint256 totalAssetsInCellar = totalAssets(); + + // The amounts needed to cover this withdraw and reach the target holdings percentage. + uint256 assetsMissingForWithdraw = assets - totalAssetsInHolding; + uint256 assetsMissingForTargetHoldings = (totalAssetsInCellar - assets).mulWadDown(targetHoldingsPercent); + + // Pull enough to cover the withdraw and reach the target holdings percentage. + uint256 assetsleftToWithdraw = assetsMissingForWithdraw + assetsMissingForTargetHoldings; + + uint256 newTotalLosslessBalance = totalLosslessBalance; + + for (uint256 i; ; i++) { + ERC4626 position = ERC4626(positions[i]); + + uint256 totalPositionAssets = position.maxWithdraw(address(this)); + + // Move on to next position if this one is empty. + if (totalPositionAssets == 0) continue; + + // We want to pull as much as we can from this position, but no more than needed. + uint256 assetsWithdrawn = Math.min(totalPositionAssets, assetsleftToWithdraw); + + PositionData storage positionData = getPositionData[address(position)]; + + if (positionData.isLossless) newTotalLosslessBalance -= assetsWithdrawn; + + // Without this the next accrual would count this withdrawal as a loss. + positionData.balance -= assetsWithdrawn; + + // Update the assets left to withdraw. + assetsleftToWithdraw -= assetsWithdrawn; + + // Pull from this position. + position.withdraw(assetsWithdrawn, address(this), address(this)); + + if (assetsleftToWithdraw == 0) break; + } + + totalLosslessBalance = newTotalLosslessBalance; + } + } + // ========================================= ACCOUNTING LOGIC ========================================= function denominateInAsset(address token, uint256 amount) public view returns (uint256 assets) {} @@ -526,12 +586,10 @@ contract Cellar is ERC4626, Ownable, Multicall { uint256 newTotalLosslessBalance; for (uint256 i; i < positions.length; i++) { - address position = positions[i]; - PositionData storage positionData = getPositionData[position]; + ERC4626 position = ERC4626(positions[i]); + PositionData storage positionData = getPositionData[address(position)]; - uint256 balanceThisAccrual = positionData.positionType == PositionType.ERC4626 - ? ERC4626(position).maxWithdraw(address(this)) - : ERC20(position).balanceOf(address(this)); + uint256 balanceThisAccrual = position.maxWithdraw(address(this)); // Check whether position is lossless. if (positionData.isLossless) { @@ -571,14 +629,135 @@ contract Cellar is ERC4626, Ownable, Multicall { lastAccrual = uint32(block.timestamp); - totalLosslessBalance = newTotalLosslessBalance; + totalLosslessBalance = uint240(newTotalLosslessBalance); emit Accrual(platformFees, performanceFees); } // =========================================== POSITION LOGIC =========================================== - // TODO: add enterPosition, exitPosition, and rebalance functions + // 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 + */ + function enterPosition(address position, uint256 assets) public onlyOwner { + // Check that position is a valid position. + if (!isPositionUsed[position]) revert USR_InvalidPosition(position); + + PositionData storage positionData = getPositionData[address(position)]; + + if (positionData.isLossless) totalLosslessBalance += assets; + + 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 + */ + 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)); + } + + /** + * @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 + */ + function rebalance( + address fromPosition, + address toPosition, + uint256 assets + ) external onlyOwner { + // 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]; + + // Update tracked balance of both positions. + fromPositionData.balance -= assets; + toPositionData.balance += assets; + + // Update total lossless balance. + uint256 newTotalLosslessBalance = totalLosslessBalance; + + 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)); + } // ============================================ LIMITS LOGIC ============================================ @@ -667,50 +846,15 @@ contract Cellar is ERC4626, Ownable, Multicall { ERC20 token, address to, uint256 amount - ) public onlyOwner { + ) external onlyOwner { // Prevent sweeping of assets managed by the cellar and shares minted to the cellar as fees. if (token == asset || token == this) revert USR_ProtectedAsset(address(token)); + for (uint256 i; i < positions.length; i++) + if (address(token) == address(positions[i])) revert USR_ProtectedAsset(address(token)); // Transfer out tokens in this cellar that shouldn't be here. token.safeTransfer(to, amount); emit Sweep(address(token), to, amount); } - - // ======================================== HELPER FUNCTIONS ======================================== - - function _emptyPosition(address position) internal { - 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)); - } } diff --git a/src/interfaces/IAggregationExecutor.sol b/src/interfaces/IAggregationExecutor.sol deleted file mode 100644 index eb6d63c2..00000000 --- a/src/interfaces/IAggregationExecutor.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; - -/// @title Interface for making arbitrary calls during swap -interface IAggregationExecutor { - /// @notice Make calls on `msgSender` with specified data - function callBytes(address msgSender, bytes calldata data) external payable; // 0x2636f7f8 -} diff --git a/src/interfaces/IAggregationRouterV4.sol b/src/interfaces/IAggregationRouterV4.sol deleted file mode 100644 index 3fbf6486..00000000 --- a/src/interfaces/IAggregationRouterV4.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.13; - -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import { IAggregationExecutor } from "./IAggregationExecutor.sol"; - -interface IAggregationRouterV4 { - // ===================== Structs ====================== - struct SwapDescription { - IERC20 srcToken; - IERC20 dstToken; - address payable srcReceiver; - address payable dstReceiver; - uint256 amount; - uint256 minReturnAmount; - uint256 flags; - bytes permit; - } - - // ======================================= ROUTER OPERATIONS ======================================= - - function swap( - IAggregationExecutor caller, - SwapDescription calldata desc, - bytes calldata data - ) - external - payable - returns ( - uint256 returnAmount, - uint256 spentAmount, - uint256 gasLeft - ); -} diff --git a/src/interfaces/ICellarRouter.sol b/src/interfaces/ICellarRouter.sol index 7664d4aa..b8ada7d6 100644 --- a/src/interfaces/ICellarRouter.sol +++ b/src/interfaces/ICellarRouter.sol @@ -20,7 +20,7 @@ interface ICellarRouter { function depositAndSwapIntoCellar( ERC4626 cellar, address[] calldata path, - uint256[] calldata poolFees, + uint24[] calldata poolFees, uint256 assets, uint256 assetsOutMin, address receiver @@ -29,7 +29,7 @@ interface ICellarRouter { function depositAndSwapIntoCellarWithPermit( ERC4626 cellar, address[] calldata path, - uint256[] calldata poolFees, + uint24[] calldata poolFees, uint256 assets, uint256 assetsOutMin, address receiver, diff --git a/test/CellarRouter.t.sol b/test/CellarRouter.t.sol index 75e5b58b..9dc52282 100644 --- a/test/CellarRouter.t.sol +++ b/test/CellarRouter.t.sol @@ -10,7 +10,7 @@ import { MockERC20 } from "src/mocks/MockERC20.sol"; import { MockERC4626 } from "src/mocks/MockERC4626.sol"; import { MockSwapRouter } from "src/mocks/MockSwapRouter.sol"; -import { Test } from "@forge-std/Test.sol"; +import { Test, console } from "@forge-std/Test.sol"; import { Math } from "src/utils/Math.sol"; contract CellarRouterTest is Test { @@ -23,25 +23,32 @@ contract CellarRouterTest is Test { MockERC4626 private cellar; CellarRouter private router; + 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); + // Mainnet contracts: + address private constant uniV3Router = 0xE592427A0AEce92De3Edee1F18E0157C05861564; + address private constant uniV2Router = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + ERC20 private WETH = ERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + ERC20 private DAI = ERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F); + function setUp() public { swapRouter = new MockSwapRouter(); router = new CellarRouter(UniswapV3Router(address(swapRouter)), UniswapV2Router(address(swapRouter))); + forkedRouter = new CellarRouter(UniswapV3Router(uniV3Router), UniswapV2Router(uniV2Router)); ABC = new MockERC20("ABC", 18); XYZ = new MockERC20("XYZ", 18); - // Setup exchange rates: - swapRouter.setExchangeRate(address(ABC), address(XYZ), 1e18); - swapRouter.setExchangeRate(address(XYZ), address(ABC), 1e18); - - // Set up a cellar: + // 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. } // ======================================= DEPOSIT TESTS ======================================= @@ -98,7 +105,7 @@ contract CellarRouterTest is Test { path[1] = address(ABC); // Specify the pool fee tiers to use for each swap (none). - uint256[] memory poolFees; + uint24[] memory poolFees; // Test deposit and swap. vm.startPrank(owner); @@ -145,7 +152,7 @@ contract CellarRouterTest is Test { path[1] = address(ABC); // Specify the pool fee tiers to use for each swap (none). - uint256[] memory poolFees; + uint24[] memory poolFees; // Test deposit and swap with permit. vm.startPrank(owner); @@ -179,6 +186,103 @@ contract CellarRouterTest is Test { assertEq(ABC.balanceOf(owner), 0, "Should have deposited assets from user."); } + function testDepositAndSwapIntoCellarUsingUniswapV2OnMainnet(uint256 assets) external { + // Ignore if not on mainnet. + if (block.chainid != 1) return; + + assets = bound(assets, 1e18, type(uint112).max); + + // Specify the swap path. + address[] memory path = new address[](2); + 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( + ERC4626(address(forkedCellar)), + path, + poolFees, + assets, + 0, + owner + ); + vm.stopPrank(); + + // Assets received by the cellar will be equal to WETH currently in forked cellar because no + // other deposits have been made. + uint256 assetsReceived = WETH.balanceOf(address(forkedCellar)); + + // Run test. + assertEq(shares, assetsReceived, "Should have 1:1 exchange rate for initial deposit."); + assertEq(forkedCellar.previewWithdraw(assetsReceived), shares, "Withdrawing assets should burn shares given."); + assertEq(forkedCellar.previewDeposit(assetsReceived), shares, "Depositing assets should mint shares given."); + assertEq(forkedCellar.totalSupply(), shares, "Should have updated total supply with shares minted."); + assertEq(forkedCellar.totalAssets(), assetsReceived, "Should have updated total assets with assets deposited."); + assertEq(forkedCellar.balanceOf(owner), shares, "Should have updated user's share balance."); + assertEq( + forkedCellar.convertToAssets(forkedCellar.balanceOf(owner)), + assetsReceived, + "Should return all user's assets." + ); + assertEq(DAI.balanceOf(owner), 0, "Should have deposited assets from user."); + } + + function testDepositAndSwapIntoCellarUsingUniswapV3OnMainnet(uint256 assets) external { + // Ignore if not on mainnet. + if (block.chainid != 1) return; + + assets = bound(assets, 1e18, type(uint112).max); + + // Specify the swap path. + address[] memory path = new address[](2); + path[0] = address(DAI); + path[1] = address(WETH); + + // Specify the pool fee tiers to use for each swap, 0.3% for DAI <-> WETH. + uint24[] memory poolFees = new uint24[](1); + poolFees[0] = 3000; + + // Test deposit and swap. + vm.startPrank(owner); + deal(address(DAI), owner, assets, true); + DAI.approve(address(forkedRouter), assets); + uint256 shares = forkedRouter.depositAndSwapIntoCellar( + ERC4626(address(forkedCellar)), + path, + poolFees, + assets, + 0, + owner + ); + vm.stopPrank(); + + // Assets received by the cellar will be equal to WETH currently in forked cellar because no + // other deposits have been made. + uint256 assetsReceived = WETH.balanceOf(address(forkedCellar)); + + // Run test. + assertEq(shares, assetsReceived, "Should have 1:1 exchange rate for initial deposit."); + assertEq(forkedCellar.previewWithdraw(assetsReceived), shares, "Withdrawing assets should burn shares given."); + assertEq(forkedCellar.previewDeposit(assetsReceived), shares, "Depositing assets should mint shares given."); + assertEq(forkedCellar.totalSupply(), shares, "Should have updated total supply with shares minted."); + assertEq(forkedCellar.totalAssets(), assetsReceived, "Should have updated total assets with assets deposited."); + assertEq(forkedCellar.balanceOf(owner), shares, "Should have updated user's share balance."); + assertEq( + forkedCellar.convertToAssets(forkedCellar.balanceOf(owner)), + assetsReceived, + "Should return all user's assets." + ); + assertEq(DAI.balanceOf(owner), 0, "Should have deposited assets from user."); + } + + // ======================================= WITHDRAW TESTS ======================================= + function testWithdrawAndSwapFromCellar(uint256 assets) external { assets = bound(assets, 1e18, type(uint72).max); @@ -191,7 +295,7 @@ contract CellarRouterTest is Test { path[1] = address(ABC); // Specify the pool fee tiers to use for each swap (none). - uint256[] memory poolFees; + uint24[] memory poolFees; // Deposit and swap vm.startPrank(owner); @@ -228,8 +332,6 @@ contract CellarRouterTest is Test { assertEq(XYZ.balanceOf(owner), assetsReceivedAfterWithdraw, "Should have withdrawn assets to the user."); } - // ======================================= WITHDRAW TESTS ======================================= - function testWithdrawAndSwapFromCellarWithPermit(uint256 assets) external { assets = bound(assets, 1e18, type(uint72).max); @@ -242,7 +344,7 @@ contract CellarRouterTest is Test { path[1] = address(ABC); // Specify the pool fee tiers to use for each swap (none). - uint256[] memory poolFees; + uint24[] memory poolFees; // Deposit and swap vm.startPrank(owner); diff --git a/test/CellarStaking.test.ts b/test/CellarStaking.test.ts index 1776bce0..6593464e 100644 --- a/test/CellarStaking.test.ts +++ b/test/CellarStaking.test.ts @@ -1467,9 +1467,9 @@ describe("CellarStaking", () => { await stakingUser.stake(ether("1"), lockDay); // Schedule initialTokenAmount / 2 new rewards. Should fail because in total we need 3 * initialTokenAmount / 2 rewards - await expect( - stakingDist.notifyRewardAmount(initialTokenAmount.div(2)) - ).to.be.revertedWith("STATE_RewardsNotFunded"); + await expect(stakingDist.notifyRewardAmount(initialTokenAmount.div(2))).to.be.revertedWith( + "STATE_RewardsNotFunded", + ); }); it("should update and extend existing schedule", async () => {