Skip to content

Commit 8d1699b

Browse files
authored
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
1 parent 60c1c95 commit 8d1699b

File tree

8 files changed

+260
-6
lines changed

8 files changed

+260
-6
lines changed

.gitmodules

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[submodule "lib/forge-std"]
22
path = lib/forge-std
33
url = https://github.com/foundry-rs/forge-std
4+
[submodule "lib/utils"]
5+
path = lib/utils
6+
url = https://github.com/AngleProtocol/utils

contracts/BaseRouter.sol

+18-3
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,11 @@ abstract contract BaseRouter is Initializable, IDepositWithReferral {
155155
);
156156

157157
/// @notice Deploys the router contract on a chain
158-
function initializeRouter(address _core, address _uniswapRouter, address _oneInch) public initializer {
158+
function initializeRouter(
159+
address _core,
160+
address _uniswapRouter,
161+
address _oneInch
162+
) public initializer {
159163
if (_core == address(0)) revert ZeroAddress();
160164
core = ICoreBorrow(_core);
161165
uniswapV3Router = IUniswapV3Router(_uniswapRouter);
@@ -258,6 +262,7 @@ abstract contract BaseRouter is Initializable, IDepositWithReferral {
258262
data[i],
259263
(IERC20, IERC4626, uint256, address, uint256)
260264
);
265+
if (amount == type(uint256).max) amount = token.balanceOf(address(this));
261266
_changeAllowance(token, address(savingsRate), type(uint256).max);
262267
_deposit4626(savingsRate, amount, to, minSharesOut);
263268
} else if (actions[i] == ActionType.deposit4626Referral) {
@@ -269,13 +274,15 @@ abstract contract BaseRouter is Initializable, IDepositWithReferral {
269274
uint256 minSharesOut,
270275
address referrer
271276
) = abi.decode(data[i], (IERC20, IERC4626, uint256, address, uint256, address));
277+
if (amount == type(uint256).max) amount = token.balanceOf(address(this));
272278
_changeAllowance(token, address(savingsRate), type(uint256).max);
273279
_deposit4626Referral(savingsRate, amount, to, minSharesOut, referrer);
274280
} else if (actions[i] == ActionType.redeem4626) {
275281
(IERC4626 savingsRate, uint256 shares, address to, uint256 minAmountOut) = abi.decode(
276282
data[i],
277283
(IERC4626, uint256, address, uint256)
278284
);
285+
if (shares == type(uint256).max) shares = savingsRate.balanceOf(msg.sender);
279286
_redeem4626(savingsRate, shares, to, minAmountOut);
280287
} else if (actions[i] == ActionType.withdraw4626) {
281288
(IERC4626 savingsRate, uint256 amount, address to, uint256 maxSharesOut) = abi.decode(
@@ -406,7 +413,11 @@ abstract contract BaseRouter is Initializable, IDepositWithReferral {
406413
/// @param tokenOut Token to sweep
407414
/// @param minAmountOut Minimum amount of tokens to recover
408415
/// @param to Address to which tokens should be sent
409-
function _sweep(address tokenOut, uint256 minAmountOut, address to) internal virtual {
416+
function _sweep(
417+
address tokenOut,
418+
uint256 minAmountOut,
419+
address to
420+
) internal virtual {
410421
uint256 balanceToken = IERC20(tokenOut).balanceOf(address(this));
411422
_slippageCheck(balanceToken, minAmountOut);
412423
if (balanceToken != 0) {
@@ -577,7 +588,11 @@ abstract contract BaseRouter is Initializable, IDepositWithReferral {
577588
/// @param token Address of the token to change allowance
578589
/// @param spender Address to change the allowance of
579590
/// @param amount Amount allowed
580-
function _changeAllowance(IERC20 token, address spender, uint256 amount) internal {
591+
function _changeAllowance(
592+
IERC20 token,
593+
address spender,
594+
uint256 amount
595+
) internal {
581596
uint256 currentAllowance = token.allowance(address(this), spender);
582597
// In case `currentAllowance < type(uint256).max / 2` and we want to increase it:
583598
// Do nothing (to handle tokens that need reapprovals to 0 and save gas)

foundry.toml

+15-1
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,25 @@ runs = 500
2121
runs = 500
2222

2323
[rpc_endpoints]
24+
arbitrum = "${ETH_NODE_URI_ARBITRUM}"
2425
mainnet = "${ETH_NODE_URI_MAINNET}"
2526
polygon = "${ETH_NODE_URI_POLYGON}"
2627
goerli = "${ETH_NODE_URI_GOERLI}"
28+
optimism = "${ETH_NODE_URI_OPTIMISM}"
29+
avalanche = "${ETH_NODE_URI_AVALANCHE}"
30+
base = "${ETH_NODE_URI_BASE}"
31+
linea = "${ETH_NODE_URI_LINEA}"
32+
celo = "${ETH_NODE_URI_CELO}"
33+
gnosis = "${ETH_NODE_URI_GNOSIS}"
2734

2835
[etherscan]
36+
arbitrum = { key = "${ARBITRUM_ETHERSCAN_API_KEY}" }
2937
mainnet = { key = "${MAINNET_ETHERSCAN_API_KEY}" }
3038
polygon = { key = "${POLYGON_ETHERSCAN_API_KEY}" }
31-
goerli = { key = "${GOERLI_ETHERSCAN_API_KEY}" }
39+
goerli = { key = "${GOERLI_ETHERSCAN_API_KEY}" }
40+
optimism = { key = "${OPTIMISM_ETHERSCAN_API_KEY}" }
41+
avalanche = { key = "${AVALANCHE_ETHERSCAN_API_KEY}" }
42+
base = { key = "${BASE_ETHERSCAN_API_KEY}", url = "https://api.basescan.org/api" }
43+
linea = { key = "${LINEA_ETHERSCAN_API_KEY}"}
44+
celo = { key = "${CELO_ETHERSCAN_API_KEY}", url = "https://api.celoscan.io/api" }
45+
gnosis = { key = "${GNOSIS_ETHERSCAN_API_KEY}" , url = "https://api.gnosisscan.io/api"}

lib/forge-std

Submodule forge-std updated 64 files

lib/utils

Submodule utils added at e64751f

remappings.txt

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
@uniswap/=node_modules/@uniswap/
55
ds-test/=lib/forge-std/lib/ds-test/src/
66
forge-std/=lib/forge-std/src/
7+
utils/=lib/utils/

scripts/foundry/UpgradeRouter.s.sol

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.17;
3+
4+
import "forge-std/Script.sol";
5+
import "utils/src/CommonUtils.sol";
6+
import { AngleRouterMainnet } from "contracts/implementations/mainnet/AngleRouterMainnet.sol";
7+
import { AngleRouterArbitrum } from "contracts/implementations/arbitrum/AngleRouterArbitrum.sol";
8+
import { AngleRouterOptimism } from "contracts/implementations/optimism/AngleRouterOptimism.sol";
9+
import { AngleRouterAvalanche } from "contracts/implementations/avalanche/AngleRouterAvalanche.sol";
10+
import { AngleRouterBase } from "contracts/implementations/base/AngleRouterBase.sol";
11+
import { AngleRouterCelo } from "contracts/implementations/celo/AngleRouterCelo.sol";
12+
import { AngleRouterGnosis } from "contracts/implementations/gnosis/AngleRouterGnosis.sol";
13+
import { AngleRouterLinea } from "contracts/implementations/linea/AngleRouterLinea.sol";
14+
import { AngleRouterPolygon } from "contracts/implementations/polygon/AngleRouterPolygon.sol";
15+
16+
contract UpgradeRouterScript is Script, CommonUtils {
17+
function run() public {
18+
uint256 chainId = vm.envUint("CHAIN_ID");
19+
20+
uint256 deployerPrivateKey = vm.deriveKey(vm.envString("MNEMONIC_MAINNET"), 0);
21+
address deployer = vm.addr(deployerPrivateKey);
22+
console.log("Address: %s", deployer);
23+
vm.startBroadcast(deployerPrivateKey);
24+
25+
address routerImpl;
26+
if (chainId == CHAIN_ETHEREUM) {
27+
routerImpl = address(new AngleRouterMainnet());
28+
} else if (chainId == CHAIN_ARBITRUM) {
29+
routerImpl = address(new AngleRouterArbitrum());
30+
} else if (chainId == CHAIN_OPTIMISM) {
31+
routerImpl = address(new AngleRouterOptimism());
32+
} else if (chainId == CHAIN_AVALANCHE) {
33+
routerImpl = address(new AngleRouterAvalanche());
34+
} else if (chainId == CHAIN_BASE) {
35+
routerImpl = address(new AngleRouterBase());
36+
} else if (chainId == CHAIN_CELO) {
37+
routerImpl = address(new AngleRouterCelo());
38+
} else if (chainId == CHAIN_GNOSIS) {
39+
routerImpl = address(new AngleRouterGnosis());
40+
} else if (chainId == CHAIN_LINEA) {
41+
routerImpl = address(new AngleRouterLinea());
42+
} else if (chainId == CHAIN_POLYGON) {
43+
routerImpl = address(new AngleRouterPolygon());
44+
}
45+
46+
console.log("Deployed router implementation at address: %s", routerImpl);
47+
48+
vm.stopBroadcast();
49+
}
50+
}

test/foundry/AngeRouterMainnet.t.sol

+171-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,12 @@ contract AngleRouterMainnetTest is BaseTest {
101101
assertEq(token.balanceOf(address(to)), 0);
102102
}
103103

104-
function testMint4626ForgotFunds(uint256 initShares, uint256 shares, uint256 maxAmount, uint256 gainOrLoss) public {
104+
function testMint4626ForgotFunds(
105+
uint256 initShares,
106+
uint256 shares,
107+
uint256 maxAmount,
108+
uint256 gainOrLoss
109+
) public {
105110
address to = address(router);
106111
uint256 balanceUsers = BASE_TOKENS * 1 ether;
107112
deal(address(token), address(_alice), balanceUsers);
@@ -189,6 +194,52 @@ contract AngleRouterMainnetTest is BaseTest {
189194
assertEq(token.balanceOf(address(to)), 0);
190195
}
191196

197+
function testDeposit4626MaxBalance(
198+
uint256 initShares,
199+
uint256 amount,
200+
uint256 minSharesOut,
201+
uint256 gainOrLoss
202+
) public {
203+
address to = address(router);
204+
205+
uint256 balanceUsers = BASE_TOKENS * 1 ether;
206+
deal(address(token), address(_alice), balanceUsers);
207+
208+
_randomizeSavingsRate(gainOrLoss, initShares);
209+
210+
amount = bound(amount, 0, balanceUsers);
211+
uint256 previewDeposit = savingsRate.previewDeposit(amount);
212+
213+
PermitType[] memory paramsPermit = new PermitType[](0);
214+
ActionType[] memory actionType = new ActionType[](2);
215+
bytes[] memory data = new bytes[](2);
216+
217+
actionType[0] = ActionType.transfer;
218+
data[0] = abi.encode(token, router, amount);
219+
actionType[1] = ActionType.deposit4626;
220+
data[1] = abi.encode(token, savingsRate, type(uint256).max, to, minSharesOut);
221+
222+
uint256 mintedShares = savingsRate.convertToShares(amount);
223+
224+
vm.startPrank(_alice);
225+
token.approve(address(router), type(uint256).max);
226+
// as this is a mock vault, previewMint is exactly what is needed to mint
227+
if (previewDeposit < minSharesOut) {
228+
vm.expectRevert(BaseRouter.TooSmallAmountOut.selector);
229+
router.mixer(paramsPermit, actionType, data);
230+
return;
231+
} else {
232+
router.mixer(paramsPermit, actionType, data);
233+
}
234+
vm.stopPrank();
235+
236+
assertEq(savingsRate.balanceOf(address(to)), previewDeposit);
237+
assertEq(savingsRate.balanceOf(address(to)), mintedShares);
238+
239+
assertEq(token.balanceOf(address(router)), 0);
240+
assertEq(token.balanceOf(address(_alice)), balanceUsers - amount);
241+
}
242+
192243
function testDeposit4626ForgotFunds(
193244
uint256 initShares,
194245
uint256 amount,
@@ -232,6 +283,53 @@ contract AngleRouterMainnetTest is BaseTest {
232283
assertEq(token.balanceOf(address(_alice)), balanceUsers - amount);
233284
}
234285

286+
function testDepositReferral4626MaxBalance(
287+
uint256 initShares,
288+
uint256 amount,
289+
uint256 minSharesOut,
290+
uint256 gainOrLoss,
291+
address referrer
292+
) public {
293+
address to = address(router);
294+
295+
uint256 balanceUsers = BASE_TOKENS * 1 ether;
296+
deal(address(token), address(_alice), balanceUsers);
297+
298+
_randomizeSavingsRate(gainOrLoss, initShares);
299+
300+
amount = bound(amount, 0, balanceUsers);
301+
uint256 previewDeposit = savingsRate.previewDeposit(amount);
302+
303+
PermitType[] memory paramsPermit = new PermitType[](0);
304+
ActionType[] memory actionType = new ActionType[](2);
305+
bytes[] memory data = new bytes[](2);
306+
307+
actionType[0] = ActionType.transfer;
308+
data[0] = abi.encode(token, router, amount);
309+
actionType[1] = ActionType.deposit4626Referral;
310+
data[1] = abi.encode(token, savingsRate, type(uint256).max, to, minSharesOut, referrer);
311+
312+
uint256 mintedShares = savingsRate.convertToShares(amount);
313+
314+
vm.startPrank(_alice);
315+
token.approve(address(router), type(uint256).max);
316+
// as this is a mock vault, previewMint is exactly what is needed to mint
317+
if (previewDeposit < minSharesOut) {
318+
vm.expectRevert(BaseRouter.TooSmallAmountOut.selector);
319+
router.mixer(paramsPermit, actionType, data);
320+
return;
321+
} else {
322+
router.mixer(paramsPermit, actionType, data);
323+
}
324+
vm.stopPrank();
325+
326+
assertEq(savingsRate.balanceOf(address(to)), previewDeposit);
327+
assertEq(savingsRate.balanceOf(address(to)), mintedShares);
328+
329+
assertEq(token.balanceOf(address(router)), 0);
330+
assertEq(token.balanceOf(address(_alice)), balanceUsers - amount);
331+
}
332+
235333
function testRedeem4626GoodPractice(
236334
uint256 initShares,
237335
uint256 aliceAmount,
@@ -387,6 +485,78 @@ contract AngleRouterMainnetTest is BaseTest {
387485
assertEq(token.balanceOf(address(_alice)), balanceUsers - aliceAmount);
388486
}
389487

488+
function testRedeem4626MaxBalance(
489+
uint256 initShares,
490+
uint256 aliceAmount,
491+
uint256 minAmount,
492+
uint256 gainOrLoss,
493+
uint256 gainOrLoss2
494+
) public {
495+
uint256 balanceUsers = BASE_TOKENS * 1 ether;
496+
deal(address(token), address(_alice), balanceUsers);
497+
498+
_randomizeSavingsRate(gainOrLoss, initShares);
499+
500+
aliceAmount = bound(aliceAmount, 0, balanceUsers);
501+
uint256 previewDeposit = savingsRate.previewDeposit(aliceAmount);
502+
// otherwise there could be overflows
503+
vm.assume(previewDeposit < type(uint256).max / BASE_PARAMS);
504+
505+
uint256 previewRedeem;
506+
{
507+
// do a first deposit
508+
PermitType[] memory paramsPermit = new PermitType[](0);
509+
ActionType[] memory actionType = new ActionType[](2);
510+
bytes[] memory data = new bytes[](2);
511+
512+
actionType[0] = ActionType.transfer;
513+
data[0] = abi.encode(token, router, aliceAmount);
514+
actionType[1] = ActionType.deposit4626;
515+
data[1] = abi.encode(token, savingsRate, aliceAmount, _alice, previewDeposit);
516+
517+
vm.startPrank(_alice);
518+
token.approve(address(router), type(uint256).max);
519+
router.mixer(paramsPermit, actionType, data);
520+
vm.stopPrank();
521+
522+
assertEq(savingsRate.balanceOf(address(router)), 0);
523+
assertEq(savingsRate.balanceOf(address(_alice)), previewDeposit);
524+
assertEq(token.balanceOf(address(router)), 0);
525+
assertEq(token.balanceOf(address(_alice)), balanceUsers - aliceAmount);
526+
527+
// make the savings rate have a loss / gain
528+
gainOrLoss2 = bound(gainOrLoss2, 1, 1 ether * 1 ether);
529+
deal(address(token), address(savingsRate), gainOrLoss2);
530+
531+
// then redeem
532+
uint256 sharesToBurn = savingsRate.balanceOf(_alice);
533+
534+
actionType = new ActionType[](1);
535+
data = new bytes[](1);
536+
537+
actionType[0] = ActionType.redeem4626;
538+
data[0] = abi.encode(savingsRate, type(uint256).max, address(router), minAmount);
539+
540+
previewRedeem = savingsRate.previewRedeem(sharesToBurn);
541+
vm.startPrank(_alice);
542+
savingsRate.approve(address(router), type(uint256).max);
543+
// as this is a mock vault, previewRedeem is exactly what should be received
544+
if (previewRedeem < minAmount) {
545+
vm.expectRevert(BaseRouter.TooSmallAmountOut.selector);
546+
router.mixer(paramsPermit, actionType, data);
547+
return;
548+
} else {
549+
router.mixer(paramsPermit, actionType, data);
550+
}
551+
vm.stopPrank();
552+
assertEq(savingsRate.balanceOf(address(_alice)), previewDeposit - sharesToBurn);
553+
}
554+
555+
assertEq(savingsRate.balanceOf(address(router)), 0);
556+
assertEq(token.balanceOf(address(router)), previewRedeem);
557+
assertEq(token.balanceOf(address(_alice)), balanceUsers - aliceAmount);
558+
}
559+
390560
function testWithdraw4626GoodPractice(
391561
uint256 initShares,
392562
uint256 aliceAmount,

0 commit comments

Comments
 (0)