From 8d1699b23dcaa06388865e653a90e012e5882fe2 Mon Sep 17 00:00:00 2001 From: 0xtekgrinder <72015889+0xtekgrinder@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:44:37 +0200 Subject: [PATCH] Feat/deposit-max-balance-savings (#26) * feat: deposit whole balance into vault if max amount provided * tests: add test with max balance * feat: max amount for mint, withdraw and redeem * tests: max balance * feat: remove mint with max balance * tests: add referral deposit test and remove max mint one * feat: remove useless console inside BaserRouter * feat: upgradeRouter script * style: prettier script * feat: broadcast inside script * chore: new rpc endpoints and etherscan keys --- .gitmodules | 3 + contracts/BaseRouter.sol | 21 +++- foundry.toml | 16 ++- lib/forge-std | 2 +- lib/utils | 1 + remappings.txt | 1 + scripts/foundry/UpgradeRouter.s.sol | 50 ++++++++ test/foundry/AngeRouterMainnet.t.sol | 172 ++++++++++++++++++++++++++- 8 files changed, 260 insertions(+), 6 deletions(-) create mode 160000 lib/utils create mode 100644 scripts/foundry/UpgradeRouter.s.sol diff --git a/.gitmodules b/.gitmodules index 888d42d..74640c6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "lib/utils"] + path = lib/utils + url = https://github.com/AngleProtocol/utils diff --git a/contracts/BaseRouter.sol b/contracts/BaseRouter.sol index 3d6b414..2d4fde3 100644 --- a/contracts/BaseRouter.sol +++ b/contracts/BaseRouter.sol @@ -155,7 +155,11 @@ abstract contract BaseRouter is Initializable, IDepositWithReferral { ); /// @notice Deploys the router contract on a chain - function initializeRouter(address _core, address _uniswapRouter, address _oneInch) public initializer { + function initializeRouter( + address _core, + address _uniswapRouter, + address _oneInch + ) public initializer { if (_core == address(0)) revert ZeroAddress(); core = ICoreBorrow(_core); uniswapV3Router = IUniswapV3Router(_uniswapRouter); @@ -258,6 +262,7 @@ abstract contract BaseRouter is Initializable, IDepositWithReferral { data[i], (IERC20, IERC4626, uint256, address, uint256) ); + if (amount == type(uint256).max) amount = token.balanceOf(address(this)); _changeAllowance(token, address(savingsRate), type(uint256).max); _deposit4626(savingsRate, amount, to, minSharesOut); } else if (actions[i] == ActionType.deposit4626Referral) { @@ -269,6 +274,7 @@ abstract contract BaseRouter is Initializable, IDepositWithReferral { uint256 minSharesOut, address referrer ) = abi.decode(data[i], (IERC20, IERC4626, uint256, address, uint256, address)); + if (amount == type(uint256).max) amount = token.balanceOf(address(this)); _changeAllowance(token, address(savingsRate), type(uint256).max); _deposit4626Referral(savingsRate, amount, to, minSharesOut, referrer); } else if (actions[i] == ActionType.redeem4626) { @@ -276,6 +282,7 @@ abstract contract BaseRouter is Initializable, IDepositWithReferral { data[i], (IERC4626, uint256, address, uint256) ); + if (shares == type(uint256).max) shares = savingsRate.balanceOf(msg.sender); _redeem4626(savingsRate, shares, to, minAmountOut); } else if (actions[i] == ActionType.withdraw4626) { (IERC4626 savingsRate, uint256 amount, address to, uint256 maxSharesOut) = abi.decode( @@ -406,7 +413,11 @@ abstract contract BaseRouter is Initializable, IDepositWithReferral { /// @param tokenOut Token to sweep /// @param minAmountOut Minimum amount of tokens to recover /// @param to Address to which tokens should be sent - function _sweep(address tokenOut, uint256 minAmountOut, address to) internal virtual { + function _sweep( + address tokenOut, + uint256 minAmountOut, + address to + ) internal virtual { uint256 balanceToken = IERC20(tokenOut).balanceOf(address(this)); _slippageCheck(balanceToken, minAmountOut); if (balanceToken != 0) { @@ -577,7 +588,11 @@ abstract contract BaseRouter is Initializable, IDepositWithReferral { /// @param token Address of the token to change allowance /// @param spender Address to change the allowance of /// @param amount Amount allowed - function _changeAllowance(IERC20 token, address spender, uint256 amount) internal { + function _changeAllowance( + IERC20 token, + address spender, + uint256 amount + ) internal { uint256 currentAllowance = token.allowance(address(this), spender); // In case `currentAllowance < type(uint256).max / 2` and we want to increase it: // Do nothing (to handle tokens that need reapprovals to 0 and save gas) diff --git a/foundry.toml b/foundry.toml index 07b6fa2..b637294 100644 --- a/foundry.toml +++ b/foundry.toml @@ -21,11 +21,25 @@ runs = 500 runs = 500 [rpc_endpoints] +arbitrum = "${ETH_NODE_URI_ARBITRUM}" mainnet = "${ETH_NODE_URI_MAINNET}" polygon = "${ETH_NODE_URI_POLYGON}" goerli = "${ETH_NODE_URI_GOERLI}" +optimism = "${ETH_NODE_URI_OPTIMISM}" +avalanche = "${ETH_NODE_URI_AVALANCHE}" +base = "${ETH_NODE_URI_BASE}" +linea = "${ETH_NODE_URI_LINEA}" +celo = "${ETH_NODE_URI_CELO}" +gnosis = "${ETH_NODE_URI_GNOSIS}" [etherscan] +arbitrum = { key = "${ARBITRUM_ETHERSCAN_API_KEY}" } mainnet = { key = "${MAINNET_ETHERSCAN_API_KEY}" } polygon = { key = "${POLYGON_ETHERSCAN_API_KEY}" } -goerli = { key = "${GOERLI_ETHERSCAN_API_KEY}" } \ No newline at end of file +goerli = { key = "${GOERLI_ETHERSCAN_API_KEY}" } +optimism = { key = "${OPTIMISM_ETHERSCAN_API_KEY}" } +avalanche = { key = "${AVALANCHE_ETHERSCAN_API_KEY}" } +base = { key = "${BASE_ETHERSCAN_API_KEY}", url = "https://api.basescan.org/api" } +linea = { key = "${LINEA_ETHERSCAN_API_KEY}"} +celo = { key = "${CELO_ETHERSCAN_API_KEY}", url = "https://api.celoscan.io/api" } +gnosis = { key = "${GNOSIS_ETHERSCAN_API_KEY}" , url = "https://api.gnosisscan.io/api"} diff --git a/lib/forge-std b/lib/forge-std index 26413f2..1714bee 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 26413f24cbbd4b4fb5c855d542ef9721790c1e7f +Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d diff --git a/lib/utils b/lib/utils new file mode 160000 index 0000000..e64751f --- /dev/null +++ b/lib/utils @@ -0,0 +1 @@ +Subproject commit e64751fc263a2f3a7f52563da3c010a13195cb6b diff --git a/remappings.txt b/remappings.txt index c3b565e..362914c 100644 --- a/remappings.txt +++ b/remappings.txt @@ -4,3 +4,4 @@ @uniswap/=node_modules/@uniswap/ ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ +utils/=lib/utils/ \ No newline at end of file diff --git a/scripts/foundry/UpgradeRouter.s.sol b/scripts/foundry/UpgradeRouter.s.sol new file mode 100644 index 0000000..c96bdc7 --- /dev/null +++ b/scripts/foundry/UpgradeRouter.s.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.17; + +import "forge-std/Script.sol"; +import "utils/src/CommonUtils.sol"; +import { AngleRouterMainnet } from "contracts/implementations/mainnet/AngleRouterMainnet.sol"; +import { AngleRouterArbitrum } from "contracts/implementations/arbitrum/AngleRouterArbitrum.sol"; +import { AngleRouterOptimism } from "contracts/implementations/optimism/AngleRouterOptimism.sol"; +import { AngleRouterAvalanche } from "contracts/implementations/avalanche/AngleRouterAvalanche.sol"; +import { AngleRouterBase } from "contracts/implementations/base/AngleRouterBase.sol"; +import { AngleRouterCelo } from "contracts/implementations/celo/AngleRouterCelo.sol"; +import { AngleRouterGnosis } from "contracts/implementations/gnosis/AngleRouterGnosis.sol"; +import { AngleRouterLinea } from "contracts/implementations/linea/AngleRouterLinea.sol"; +import { AngleRouterPolygon } from "contracts/implementations/polygon/AngleRouterPolygon.sol"; + +contract UpgradeRouterScript is Script, CommonUtils { + function run() public { + uint256 chainId = vm.envUint("CHAIN_ID"); + + uint256 deployerPrivateKey = vm.deriveKey(vm.envString("MNEMONIC_MAINNET"), 0); + address deployer = vm.addr(deployerPrivateKey); + console.log("Address: %s", deployer); + vm.startBroadcast(deployerPrivateKey); + + address routerImpl; + if (chainId == CHAIN_ETHEREUM) { + routerImpl = address(new AngleRouterMainnet()); + } else if (chainId == CHAIN_ARBITRUM) { + routerImpl = address(new AngleRouterArbitrum()); + } else if (chainId == CHAIN_OPTIMISM) { + routerImpl = address(new AngleRouterOptimism()); + } else if (chainId == CHAIN_AVALANCHE) { + routerImpl = address(new AngleRouterAvalanche()); + } else if (chainId == CHAIN_BASE) { + routerImpl = address(new AngleRouterBase()); + } else if (chainId == CHAIN_CELO) { + routerImpl = address(new AngleRouterCelo()); + } else if (chainId == CHAIN_GNOSIS) { + routerImpl = address(new AngleRouterGnosis()); + } else if (chainId == CHAIN_LINEA) { + routerImpl = address(new AngleRouterLinea()); + } else if (chainId == CHAIN_POLYGON) { + routerImpl = address(new AngleRouterPolygon()); + } + + console.log("Deployed router implementation at address: %s", routerImpl); + + vm.stopBroadcast(); + } +} diff --git a/test/foundry/AngeRouterMainnet.t.sol b/test/foundry/AngeRouterMainnet.t.sol index 02ae28a..8b63d6f 100644 --- a/test/foundry/AngeRouterMainnet.t.sol +++ b/test/foundry/AngeRouterMainnet.t.sol @@ -101,7 +101,12 @@ contract AngleRouterMainnetTest is BaseTest { assertEq(token.balanceOf(address(to)), 0); } - function testMint4626ForgotFunds(uint256 initShares, uint256 shares, uint256 maxAmount, uint256 gainOrLoss) public { + function testMint4626ForgotFunds( + uint256 initShares, + uint256 shares, + uint256 maxAmount, + uint256 gainOrLoss + ) public { address to = address(router); uint256 balanceUsers = BASE_TOKENS * 1 ether; deal(address(token), address(_alice), balanceUsers); @@ -189,6 +194,52 @@ contract AngleRouterMainnetTest is BaseTest { assertEq(token.balanceOf(address(to)), 0); } + function testDeposit4626MaxBalance( + uint256 initShares, + uint256 amount, + uint256 minSharesOut, + uint256 gainOrLoss + ) public { + address to = address(router); + + uint256 balanceUsers = BASE_TOKENS * 1 ether; + deal(address(token), address(_alice), balanceUsers); + + _randomizeSavingsRate(gainOrLoss, initShares); + + amount = bound(amount, 0, balanceUsers); + uint256 previewDeposit = savingsRate.previewDeposit(amount); + + PermitType[] memory paramsPermit = new PermitType[](0); + ActionType[] memory actionType = new ActionType[](2); + bytes[] memory data = new bytes[](2); + + actionType[0] = ActionType.transfer; + data[0] = abi.encode(token, router, amount); + actionType[1] = ActionType.deposit4626; + data[1] = abi.encode(token, savingsRate, type(uint256).max, to, minSharesOut); + + uint256 mintedShares = savingsRate.convertToShares(amount); + + vm.startPrank(_alice); + token.approve(address(router), type(uint256).max); + // as this is a mock vault, previewMint is exactly what is needed to mint + if (previewDeposit < minSharesOut) { + vm.expectRevert(BaseRouter.TooSmallAmountOut.selector); + router.mixer(paramsPermit, actionType, data); + return; + } else { + router.mixer(paramsPermit, actionType, data); + } + vm.stopPrank(); + + assertEq(savingsRate.balanceOf(address(to)), previewDeposit); + assertEq(savingsRate.balanceOf(address(to)), mintedShares); + + assertEq(token.balanceOf(address(router)), 0); + assertEq(token.balanceOf(address(_alice)), balanceUsers - amount); + } + function testDeposit4626ForgotFunds( uint256 initShares, uint256 amount, @@ -232,6 +283,53 @@ contract AngleRouterMainnetTest is BaseTest { assertEq(token.balanceOf(address(_alice)), balanceUsers - amount); } + function testDepositReferral4626MaxBalance( + uint256 initShares, + uint256 amount, + uint256 minSharesOut, + uint256 gainOrLoss, + address referrer + ) public { + address to = address(router); + + uint256 balanceUsers = BASE_TOKENS * 1 ether; + deal(address(token), address(_alice), balanceUsers); + + _randomizeSavingsRate(gainOrLoss, initShares); + + amount = bound(amount, 0, balanceUsers); + uint256 previewDeposit = savingsRate.previewDeposit(amount); + + PermitType[] memory paramsPermit = new PermitType[](0); + ActionType[] memory actionType = new ActionType[](2); + bytes[] memory data = new bytes[](2); + + actionType[0] = ActionType.transfer; + data[0] = abi.encode(token, router, amount); + actionType[1] = ActionType.deposit4626Referral; + data[1] = abi.encode(token, savingsRate, type(uint256).max, to, minSharesOut, referrer); + + uint256 mintedShares = savingsRate.convertToShares(amount); + + vm.startPrank(_alice); + token.approve(address(router), type(uint256).max); + // as this is a mock vault, previewMint is exactly what is needed to mint + if (previewDeposit < minSharesOut) { + vm.expectRevert(BaseRouter.TooSmallAmountOut.selector); + router.mixer(paramsPermit, actionType, data); + return; + } else { + router.mixer(paramsPermit, actionType, data); + } + vm.stopPrank(); + + assertEq(savingsRate.balanceOf(address(to)), previewDeposit); + assertEq(savingsRate.balanceOf(address(to)), mintedShares); + + assertEq(token.balanceOf(address(router)), 0); + assertEq(token.balanceOf(address(_alice)), balanceUsers - amount); + } + function testRedeem4626GoodPractice( uint256 initShares, uint256 aliceAmount, @@ -387,6 +485,78 @@ contract AngleRouterMainnetTest is BaseTest { assertEq(token.balanceOf(address(_alice)), balanceUsers - aliceAmount); } + function testRedeem4626MaxBalance( + uint256 initShares, + uint256 aliceAmount, + uint256 minAmount, + uint256 gainOrLoss, + uint256 gainOrLoss2 + ) public { + uint256 balanceUsers = BASE_TOKENS * 1 ether; + deal(address(token), address(_alice), balanceUsers); + + _randomizeSavingsRate(gainOrLoss, initShares); + + aliceAmount = bound(aliceAmount, 0, balanceUsers); + uint256 previewDeposit = savingsRate.previewDeposit(aliceAmount); + // otherwise there could be overflows + vm.assume(previewDeposit < type(uint256).max / BASE_PARAMS); + + uint256 previewRedeem; + { + // do a first deposit + PermitType[] memory paramsPermit = new PermitType[](0); + ActionType[] memory actionType = new ActionType[](2); + bytes[] memory data = new bytes[](2); + + actionType[0] = ActionType.transfer; + data[0] = abi.encode(token, router, aliceAmount); + actionType[1] = ActionType.deposit4626; + data[1] = abi.encode(token, savingsRate, aliceAmount, _alice, previewDeposit); + + vm.startPrank(_alice); + token.approve(address(router), type(uint256).max); + router.mixer(paramsPermit, actionType, data); + vm.stopPrank(); + + assertEq(savingsRate.balanceOf(address(router)), 0); + assertEq(savingsRate.balanceOf(address(_alice)), previewDeposit); + assertEq(token.balanceOf(address(router)), 0); + assertEq(token.balanceOf(address(_alice)), balanceUsers - aliceAmount); + + // make the savings rate have a loss / gain + gainOrLoss2 = bound(gainOrLoss2, 1, 1 ether * 1 ether); + deal(address(token), address(savingsRate), gainOrLoss2); + + // then redeem + uint256 sharesToBurn = savingsRate.balanceOf(_alice); + + actionType = new ActionType[](1); + data = new bytes[](1); + + actionType[0] = ActionType.redeem4626; + data[0] = abi.encode(savingsRate, type(uint256).max, address(router), minAmount); + + previewRedeem = savingsRate.previewRedeem(sharesToBurn); + vm.startPrank(_alice); + savingsRate.approve(address(router), type(uint256).max); + // as this is a mock vault, previewRedeem is exactly what should be received + if (previewRedeem < minAmount) { + vm.expectRevert(BaseRouter.TooSmallAmountOut.selector); + router.mixer(paramsPermit, actionType, data); + return; + } else { + router.mixer(paramsPermit, actionType, data); + } + vm.stopPrank(); + assertEq(savingsRate.balanceOf(address(_alice)), previewDeposit - sharesToBurn); + } + + assertEq(savingsRate.balanceOf(address(router)), 0); + assertEq(token.balanceOf(address(router)), previewRedeem); + assertEq(token.balanceOf(address(_alice)), balanceUsers - aliceAmount); + } + function testWithdraw4626GoodPractice( uint256 initShares, uint256 aliceAmount,