Skip to content

Commit

Permalink
Feat/deposit-max-balance-savings (#26)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
0xtekgrinder authored Aug 9, 2024
1 parent 60c1c95 commit 8d1699b
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 6 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -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
21 changes: 18 additions & 3 deletions contracts/BaseRouter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -269,13 +274,15 @@ 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) {
(IERC4626 savingsRate, uint256 shares, address to, uint256 minAmountOut) = abi.decode(
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(
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 15 additions & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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}" }
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"}
2 changes: 1 addition & 1 deletion lib/forge-std
Submodule forge-std updated 64 files
+1 −0 .gitattributes
+128 −0 .github/workflows/ci.yml
+31 −0 .github/workflows/sync.yml
+0 −27 .github/workflows/tests.yml
+1 −1 .gitignore
+0 −3 .gitmodules
+1 −1 LICENSE-APACHE
+1 −1 LICENSE-MIT
+9 −5 README.md
+19 −0 foundry.toml
+0 −1 lib/ds-test
+16 −0 package.json
+635 −0 scripts/vm.py
+35 −0 src/Base.sol
+24 −41 src/Script.sol
+669 −0 src/StdAssertions.sol
+259 −0 src/StdChains.sol
+817 −0 src/StdCheats.sol
+15 −0 src/StdError.sol
+122 −0 src/StdInvariant.sol
+122 −61 src/StdJson.sol
+43 −0 src/StdMath.sol
+473 −0 src/StdStorage.sol
+333 −0 src/StdStyle.sol
+179 −0 src/StdToml.sol
+226 −0 src/StdUtils.sol
+29 −1,134 src/Test.sol
+1,865 −218 src/Vm.sol
+401 −382 src/console.sol
+1 −1,535 src/console2.sol
+105 −0 src/interfaces/IERC1155.sol
+12 −0 src/interfaces/IERC165.sol
+43 −0 src/interfaces/IERC20.sol
+190 −0 src/interfaces/IERC4626.sol
+164 −0 src/interfaces/IERC721.sol
+73 −0 src/interfaces/IMulticall3.sol
+234 −0 src/mocks/MockERC20.sol
+231 −0 src/mocks/MockERC721.sol
+13,937 −0 src/safeconsole.sol
+0 −20 src/test/Script.t.sol
+0 −602 src/test/StdAssertions.t.sol
+0 −282 src/test/StdCheats.t.sol
+0 −200 src/test/StdMath.t.sol
+0 −321 src/test/StdStorage.t.sol
+145 −0 test/StdAssertions.t.sol
+226 −0 test/StdChains.t.sol
+618 −0 test/StdCheats.t.sol
+14 −18 test/StdError.t.sol
+49 −0 test/StdJson.t.sol
+212 −0 test/StdMath.t.sol
+471 −0 test/StdStorage.t.sol
+110 −0 test/StdStyle.t.sol
+49 −0 test/StdToml.t.sol
+342 −0 test/StdUtils.t.sol
+15 −0 test/Vm.t.sol
+10 −0 test/compilation/CompilationScript.sol
+10 −0 test/compilation/CompilationScriptBase.sol
+10 −0 test/compilation/CompilationTest.sol
+10 −0 test/compilation/CompilationTestBase.sol
+0 −0 test/fixtures/broadcast.log.json
+8 −0 test/fixtures/test.json
+6 −0 test/fixtures/test.toml
+441 −0 test/mocks/MockERC20.t.sol
+721 −0 test/mocks/MockERC721.t.sol
1 change: 1 addition & 0 deletions lib/utils
Submodule utils added at e64751
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/
50 changes: 50 additions & 0 deletions scripts/foundry/UpgradeRouter.s.sol
Original file line number Diff line number Diff line change
@@ -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();
}
}
172 changes: 171 additions & 1 deletion test/foundry/AngeRouterMainnet.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 8d1699b

Please sign in to comment.