Skip to content

Commit b128d4e

Browse files
feat: Immutable aToken Vault (#104)
* feat: Immutable aToken Vault added * test: aToken Vault tests adapted to new Immutable Vault * misc: Correct comment * feat: Simplified the approach by overriding _disableInitializers * test: Removal of unused stuff * feat: Mark functions as virtual & remove unnecessary super._disableInitializers call * chore: Audit report added for Immutable aToken Vault
1 parent 01e3f45 commit b128d4e

11 files changed

+4254
-0
lines changed
476 KB
Binary file not shown.

src/ImmutableATokenVault.sol

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
// All Rights Reserved © AaveCo
3+
4+
pragma solidity ^0.8.10;
5+
6+
import {SafeERC20Upgradeable} from "@openzeppelin-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
7+
import {IERC20Upgradeable} from "@openzeppelin-upgradeable/interfaces/IERC20Upgradeable.sol";
8+
import {IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPoolAddressesProvider.sol";
9+
import {ATokenVault} from "./ATokenVault.sol";
10+
11+
/**
12+
* @title ImmutableATokenVault
13+
* @author Aave Labs
14+
* @notice An immutable ERC-4626 vault for Aave V3, with support to add a fee on yield earned.
15+
*/
16+
contract ImmutableATokenVault is ATokenVault {
17+
using SafeERC20Upgradeable for IERC20Upgradeable;
18+
19+
/**
20+
* @dev Constructor.
21+
* @param underlying The underlying ERC-20 asset.
22+
* @param referralCode The Aave referral code to use for deposits from this vault.
23+
* @param poolAddressesProvider The address of the Aave v3 Pool Addresses Provider.
24+
* @param owner The owner of the vault to set.
25+
* @param initialFee The initial fee to set, expressed in wad, where 1e18 is 100%.
26+
* @param shareName The name to set for this vault's shares.
27+
* @param shareSymbol The symbol to set for this vault's shares.
28+
* @param initialLockDeposit The initial amount of underlying assets to deposit. Required to prevent a frontrunning
29+
* attack (in underlying tokens). Note that care should be taken to provide a non-trivial amount, but this depends
30+
* on the underlying asset's decimals.
31+
*/
32+
constructor(
33+
address underlying,
34+
uint16 referralCode,
35+
IPoolAddressesProvider poolAddressesProvider,
36+
address owner,
37+
uint256 initialFee,
38+
string memory shareName,
39+
string memory shareSymbol,
40+
uint256 initialLockDeposit
41+
) ATokenVault(underlying, referralCode, poolAddressesProvider) {
42+
_initialize(
43+
underlying,
44+
owner,
45+
initialFee,
46+
shareName,
47+
shareSymbol,
48+
initialLockDeposit
49+
);
50+
}
51+
52+
/**
53+
* @dev Initializes the contract given that the base contract, ATokenVault, uses upgradable contracts.
54+
*/
55+
function _initialize(
56+
address underlying,
57+
address owner,
58+
uint256 initialFee,
59+
string memory shareName,
60+
string memory shareSymbol,
61+
uint256 initialLockDeposit
62+
) internal virtual initializer {
63+
require(owner != address(0), "ZERO_ADDRESS_NOT_VALID");
64+
require(initialLockDeposit != 0, "ZERO_INITIAL_LOCK_DEPOSIT");
65+
_transferOwnership(owner);
66+
__ERC4626_init(IERC20Upgradeable(underlying));
67+
__ERC20_init(shareName, shareSymbol);
68+
__EIP712_init(shareName, "1");
69+
_setFee(initialFee);
70+
IERC20Upgradeable(underlying).safeApprove(address(AAVE_POOL), type(uint256).max);
71+
_handleDeposit(initialLockDeposit, address(this), msg.sender, false);
72+
}
73+
74+
/**
75+
* @dev Overrides the base contract's `_disableInitializers` function to do nothing.
76+
* This turns the `_disableInitializers` call in ATokenVault's constructor ineffective,
77+
* allowing initialization at the ImmutableATokenVault's constructor.
78+
*/
79+
function _disableInitializers() internal virtual override { }
80+
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.10;
4+
5+
import "forge-std/Test.sol";
6+
import {IERC20Upgradeable} from "@openzeppelin-upgradeable/interfaces/IERC20Upgradeable.sol";
7+
import {SafeERC20Upgradeable} from "@openzeppelin-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
8+
import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol";
9+
import {IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPoolAddressesProvider.sol";
10+
11+
import {MathUpgradeable} from "../src/ATokenVault.sol";
12+
import {ImmutableATokenVault} from "../src/ImmutableATokenVault.sol";
13+
14+
contract ImmutableATokenVaultBaseTest is Test {
15+
using SafeERC20Upgradeable for IERC20Upgradeable;
16+
using MathUpgradeable for uint256;
17+
18+
// Fork tests using Polygon for Aave v3
19+
address constant POLYGON_DAI = 0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063;
20+
address constant POLYGON_ADAI = 0x82E64f49Ed5EC1bC6e43DAD4FC8Af9bb3A2312EE;
21+
address constant POLYGON_AAVE_POOL = 0x794a61358D6845594F94dc1DB02A252b5b4814aD;
22+
address constant POLYGON_POOL_ADDRESSES_PROVIDER = 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb;
23+
address constant POLYGON_REWARDS_CONTROLLER = 0x929EC64c34a17401F460460D4B9390518E5B473e;
24+
address constant POLYGON_DATA_PROVIDER = 0x69FA688f1Dc47d4B5d8029D5a35FB7a548310654;
25+
26+
// Fork tests using Avalanche for Aave v3
27+
address constant AVALANCHE_USDC = 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E;
28+
address constant AVALANCHE_AUSDC = 0x625E7708f30cA75bfd92586e17077590C60eb4cD;
29+
address constant AVALANCHE_WAVAX = 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7;
30+
address constant AVALANCHE_POOL_ADDRESSES_PROVIDER = 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb;
31+
address constant AVALANCHE_REWARDS_CONTROLLER = 0x929EC64c34a17401F460460D4B9390518E5B473e;
32+
33+
uint256 constant SCALE = 1e18;
34+
uint256 constant ONE = 1e18;
35+
uint256 constant TEN = 10e18;
36+
uint256 constant HUNDRED = 100e18;
37+
uint256 constant ONE_PERCENT = 0.01e18;
38+
uint256 constant ONE_AND_HALF_PERCENT = 0.015e18;
39+
uint256 constant ONE_BPS = 0.0001e18;
40+
41+
uint256 constant OWNER_PRIV_KEY = 11111;
42+
uint256 constant ALICE_PRIV_KEY = 12345;
43+
uint256 constant BOB_PRIV_KEY = 54321;
44+
uint256 constant CHAD_PRIV_KEY = 98765;
45+
46+
address OWNER = vm.addr(OWNER_PRIV_KEY);
47+
address ALICE = vm.addr(ALICE_PRIV_KEY);
48+
address BOB = vm.addr(BOB_PRIV_KEY);
49+
address CHAD = vm.addr(CHAD_PRIV_KEY);
50+
51+
string constant SHARE_NAME = "Wrapped aDAI";
52+
string constant SHARE_SYMBOL = "waDAI";
53+
54+
uint256 fee = 0.2e18; // 20%
55+
uint16 referralCode = 4546;
56+
57+
ImmutableATokenVault vault;
58+
address vaultAssetAddress; // aDAI, must be set in every setUp
59+
uint256 initialLockDeposit; // Must be set in every setUp
60+
61+
// Initializer Errors
62+
bytes constant ERR_INITIALIZED = bytes("Initializable: contract is already initialized");
63+
64+
// Ownable Errors
65+
bytes constant ERR_NOT_OWNER = bytes("Ownable: caller is not the owner");
66+
67+
// Meta Tx Errors
68+
bytes constant ERR_INVALID_SIGNER = bytes("INVALID_SIGNER");
69+
bytes constant ERR_PERMIT_DEADLINE_EXPIRED = bytes("PERMIT_DEADLINE_EXPIRED");
70+
bytes constant ERR_SIG_INVALID = bytes("SIG_INVALID");
71+
bytes constant ERR_SIG_EXPIRED = bytes("SIG_EXPIRED");
72+
73+
// Vault Errors
74+
bytes constant ERR_ZERO_ADDRESS_NOT_VALID = bytes("ZERO_ADDRESS_NOT_VALID");
75+
bytes constant ERR_ZERO_INITIAL_DEPOSIT = bytes("ZERO_INITIAL_LOCK_DEPOSIT");
76+
bytes constant ERR_ZERO_ASSETS = bytes("ZERO_ASSETS");
77+
bytes constant ERR_ZERO_SHARES = bytes("ZERO_SHARES");
78+
bytes constant ERR_TRANSFER_FROM_FAILED = bytes("TRANSFER_FROM_FAILED");
79+
bytes constant ERR_CANNOT_RESCUE_ATOKEN = bytes("CANNOT_RESCUE_ATOKEN");
80+
bytes constant ERR_FEE_TOO_HIGH = bytes("FEE_TOO_HIGH");
81+
bytes constant ERR_ASSET_NOT_SUPPORTED = bytes("ASSET_NOT_SUPPORTED");
82+
bytes constant ERR_INSUFFICIENT_FEES = bytes("INSUFFICIENT_FEES");
83+
bytes constant ERR_CANNOT_CLAIM_TO_ZERO_ADDRESS = bytes("CANNOT_CLAIM_TO_ZERO_ADDRESS");
84+
bytes constant SAFE_TRANSFER_ARITHMETIC = bytes("NH{q");
85+
bytes constant ERR_DEPOSIT_EXCEEDS_MAX = bytes("DEPOSIT_EXCEEDS_MAX");
86+
bytes constant ERR_MINT_EXCEEDS_MAX = bytes("MINT_EXCEEDS_MAX");
87+
bytes constant ERR_WITHDRAW_EXCEEDS_MAX = bytes("WITHDRAW_EXCEEDS_MAX");
88+
bytes constant ERR_REDEEM_EXCEEDS_MAX = bytes("REDEEM_EXCEEDS_MAX");
89+
90+
// ERC4626 Events
91+
event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares);
92+
event Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares);
93+
94+
// ATokenVault Events
95+
event FeeUpdated(uint256 indexed oldFee, uint256 indexed newFee);
96+
event FeesWithdrawn(address indexed to, uint256 indexed amount, uint256 newVaultBalance, uint256 newTotalFeesAccrued);
97+
event YieldAccrued(uint256 accruedYield, uint256 newFeesFromYield, uint256 newVaultBalance);
98+
event RewardsClaimed(address indexed to, address[] rewardsList, uint256[] claimedAmounts);
99+
event EmergencyRescue(address indexed token, address indexed to, uint256 amount);
100+
101+
function setUp() public virtual {}
102+
103+
// For debug purposes
104+
function _logVaultBalances(address user, string memory label) internal view {
105+
console.log("\n", label);
106+
console.log("ERC20 Assets\t\t\t", ERC20(vaultAssetAddress).balanceOf(address(vault)));
107+
console.log("totalAssets()\t\t\t", vault.totalAssets());
108+
console.log("lastVaultBalance()\t\t", vault.getLastVaultBalance());
109+
console.log("User Withdrawable\t\t", vault.maxWithdraw(user));
110+
console.log("claimable fees\t\t", vault.getClaimableFees());
111+
console.log("current time\t\t\t", block.timestamp);
112+
}
113+
114+
function _deploy(address underlying, address addressesProvider) internal {
115+
_deploy(underlying, addressesProvider, 10e18);
116+
}
117+
118+
function _deploy(address underlying, address addressesProvider, uint256 _initialLockDeposit) internal {
119+
initialLockDeposit = _initialLockDeposit;
120+
deal(underlying, address(this), _initialLockDeposit);
121+
address vaultAddress = computeCreateAddress(address(this), vm.getNonce(address(this)) + 1);
122+
IERC20Upgradeable(underlying).safeApprove(vaultAddress, _initialLockDeposit);
123+
124+
vault = new ImmutableATokenVault(
125+
underlying,
126+
referralCode,
127+
IPoolAddressesProvider(addressesProvider),
128+
OWNER,
129+
fee,
130+
SHARE_NAME,
131+
SHARE_SYMBOL,
132+
_initialLockDeposit
133+
);
134+
}
135+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.10;
3+
4+
import {MathUpgradeable} from "@openzeppelin-upgradeable/utils/math/MathUpgradeable.sol";
5+
import {IPool} from "@aave-v3-core/interfaces/IPool.sol";
6+
import {IACLManager} from "@aave-v3-core/interfaces/IACLManager.sol";
7+
import {ImmutableATokenVaultForkBaseTest} from "./ImmutableATokenVaultForkBaseTest.t.sol";
8+
9+
contract ImmutableATokenVaultEdge is ImmutableATokenVaultForkBaseTest {
10+
using MathUpgradeable for uint256;
11+
12+
// This test demonstrates a problematic scenario if the initial deposit is too little.
13+
function testLowInitialDepositLock() public {
14+
_deploy(POLYGON_DAI, POLYGON_POOL_ADDRESSES_PROVIDER, 1);
15+
16+
_transferFromUser(OWNER, 2);
17+
18+
_depositFromUser(ALICE, 201);
19+
assertEq(vault.balanceOf(ALICE), 67);
20+
21+
_depositFromUser(BOB, 200);
22+
assertEq(vault.balanceOf(BOB), 66);
23+
24+
_transferFromUser(OWNER, 8);
25+
26+
_redeemFromUser(ALICE, 67);
27+
assertEq(vault.balanceOf(ALICE), 0);
28+
29+
vm.prank(BOB);
30+
vault.redeem(66, BOB, BOB);
31+
assertEq(vault.balanceOf(BOB), 0);
32+
}
33+
34+
function testCalculationClaimableFees() public {
35+
/**
36+
* This test ensure the calculation of fees is always correct and up-to-date, no matter
37+
* the action and duration (if same block).
38+
* - ATokens sent to the vault are considered as yield.
39+
* - Yield can be increased more than once in same block.
40+
*/
41+
42+
// Remove initial lock deposit
43+
vm.startPrank(address(vault));
44+
vault.redeem(vault.balanceOf(address(vault)), address(1), address(vault));
45+
vm.stopPrank();
46+
47+
// Bob deposits 1000 tokens, no new yield
48+
uint256 oldYield = aDai.balanceOf(address(vault));
49+
uint256 oldFees = vault.getClaimableFees();
50+
_depositFromUser(BOB, 1000);
51+
assertEq(oldYield + 1000, aDai.balanceOf(address(vault)), "unexpected yield after deposit");
52+
assertEq(oldFees, vault.getClaimableFees(), "unexpected fees after deposit");
53+
oldYield = aDai.balanceOf(address(vault));
54+
oldFees = vault.getClaimableFees();
55+
56+
// User deposit and redeem, no new yield
57+
_depositFromUser(address(0x222), 1_000_000e18);
58+
_withdrawFromUser(address(0x222), 0);
59+
assertEq(vault.balanceOf(address(0x222)), 0);
60+
assertEq(oldYield, aDai.balanceOf(address(vault)), "unexpected yield after user");
61+
assertEq(oldFees, vault.getClaimableFees(), "unexpected fees after user");
62+
oldYield = aDai.balanceOf(address(vault));
63+
oldFees = vault.getClaimableFees();
64+
65+
// Gift to the vault
66+
_accrueYieldInVault(100);
67+
68+
// Increase yield in the Aave Protocol by accumulating yield to Portals
69+
uint256 currentFees = vault.getClaimableFees();
70+
uint256 currentIndex = IPool(POLYGON_AAVE_POOL).getReserveNormalizedIncome(address(dai));
71+
uint256 currentBalance = vault.ATOKEN().balanceOf(address(vault));
72+
// Using portals
73+
address BRIDGE = address(0xb0b0);
74+
uint256 bridgeMint = 40_000_000e18;
75+
// Mock call `isBridge`
76+
address aclManagerAddress = vault.AAVE_POOL().ADDRESSES_PROVIDER().getACLManager();
77+
IACLManager aclManager = IACLManager(aclManagerAddress);
78+
vm.mockCall(address(aclManagerAddress), abi.encodeWithSelector(aclManager.isBridge.selector, BRIDGE), abi.encode(true));
79+
vm.startPrank(BRIDGE);
80+
deal(address(dai), BRIDGE, bridgeMint);
81+
dai.approve(POLYGON_AAVE_POOL, bridgeMint);
82+
IPool(POLYGON_AAVE_POOL).backUnbacked(address(dai), 0, bridgeMint);
83+
vm.stopPrank();
84+
85+
uint256 newIndex = IPool(POLYGON_AAVE_POOL).getReserveNormalizedIncome(address(dai));
86+
uint256 newBalance = vault.ATOKEN().balanceOf(address(vault));
87+
assertLt(currentIndex, newIndex, "unexpected change in index");
88+
assertLt(currentBalance, newBalance, "unexpected change in balance");
89+
uint256 newFees = (newBalance - currentBalance).mulDiv(vault.getFee(), SCALE, MathUpgradeable.Rounding.Down);
90+
// 1 wei imprecision due to AToken nature
91+
assertApproxEqAbs(vault.getClaimableFees(), currentFees + newFees, 1, "claimable fees are not up to date");
92+
currentFees = vault.getClaimableFees();
93+
94+
// Same fees as block before
95+
vm.warp(block.timestamp + 1);
96+
assertEq(vault.getClaimableFees(), currentFees, "claimable fees are not up to date");
97+
}
98+
}

0 commit comments

Comments
 (0)