-
Notifications
You must be signed in to change notification settings - Fork 66
feat: Immutable aToken Vault #104
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a9f49e2
feat: Immutable aToken Vault added
donosonaumczuk fc0b167
test: aToken Vault tests adapted to new Immutable Vault
donosonaumczuk 92e3bc8
misc: Correct comment
donosonaumczuk 5ed6735
feat: Simplified the approach by overriding _disableInitializers
donosonaumczuk 86bff4c
test: Removal of unused stuff
donosonaumczuk 44e25a1
feat: Mark functions as virtual & remove unnecessary super._disableIn…
donosonaumczuk 8b2ec05
chore: Audit report added for Immutable aToken Vault
donosonaumczuk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| // SPDX-License-Identifier: UNLICENSED | ||
| // All Rights Reserved © AaveCo | ||
|
|
||
| pragma solidity ^0.8.10; | ||
|
|
||
| import {SafeERC20Upgradeable} from "@openzeppelin-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; | ||
| import {IERC20Upgradeable} from "@openzeppelin-upgradeable/interfaces/IERC20Upgradeable.sol"; | ||
| import {IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPoolAddressesProvider.sol"; | ||
| import {ATokenVault} from "./ATokenVault.sol"; | ||
|
|
||
| /** | ||
| * @title ImmutableATokenVault | ||
| * @author Aave Labs | ||
| * @notice An immutable ERC-4626 vault for Aave V3, with support to add a fee on yield earned. | ||
| */ | ||
| contract ImmutableATokenVault is ATokenVault { | ||
| using SafeERC20Upgradeable for IERC20Upgradeable; | ||
|
|
||
| /** | ||
| * @dev Constructor. | ||
| * @param underlying The underlying ERC-20 asset. | ||
| * @param referralCode The Aave referral code to use for deposits from this vault. | ||
| * @param poolAddressesProvider The address of the Aave v3 Pool Addresses Provider. | ||
| * @param owner The owner of the vault to set. | ||
| * @param initialFee The initial fee to set, expressed in wad, where 1e18 is 100%. | ||
| * @param shareName The name to set for this vault's shares. | ||
| * @param shareSymbol The symbol to set for this vault's shares. | ||
| * @param initialLockDeposit The initial amount of underlying assets to deposit. Required to prevent a frontrunning | ||
| * attack (in underlying tokens). Note that care should be taken to provide a non-trivial amount, but this depends | ||
| * on the underlying asset's decimals. | ||
| */ | ||
| constructor( | ||
| address underlying, | ||
| uint16 referralCode, | ||
| IPoolAddressesProvider poolAddressesProvider, | ||
| address owner, | ||
| uint256 initialFee, | ||
| string memory shareName, | ||
| string memory shareSymbol, | ||
| uint256 initialLockDeposit | ||
| ) ATokenVault(underlying, referralCode, poolAddressesProvider) { | ||
| _initialize( | ||
| underlying, | ||
| owner, | ||
| initialFee, | ||
| shareName, | ||
| shareSymbol, | ||
| initialLockDeposit | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Initializes the contract given that the base contract, ATokenVault, uses upgradable contracts. | ||
| */ | ||
| function _initialize( | ||
| address underlying, | ||
| address owner, | ||
| uint256 initialFee, | ||
| string memory shareName, | ||
| string memory shareSymbol, | ||
| uint256 initialLockDeposit | ||
| ) internal virtual initializer { | ||
| require(owner != address(0), "ZERO_ADDRESS_NOT_VALID"); | ||
| require(initialLockDeposit != 0, "ZERO_INITIAL_LOCK_DEPOSIT"); | ||
| _transferOwnership(owner); | ||
| __ERC4626_init(IERC20Upgradeable(underlying)); | ||
| __ERC20_init(shareName, shareSymbol); | ||
| __EIP712_init(shareName, "1"); | ||
| _setFee(initialFee); | ||
| IERC20Upgradeable(underlying).safeApprove(address(AAVE_POOL), type(uint256).max); | ||
| _handleDeposit(initialLockDeposit, address(this), msg.sender, false); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Overrides the base contract's `_disableInitializers` function to do nothing. | ||
| * This turns the `_disableInitializers` call in ATokenVault's constructor ineffective, | ||
| * allowing initialization at the ImmutableATokenVault's constructor. | ||
| */ | ||
| function _disableInitializers() internal virtual override { } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| pragma solidity ^0.8.10; | ||
|
|
||
| import "forge-std/Test.sol"; | ||
| import {IERC20Upgradeable} from "@openzeppelin-upgradeable/interfaces/IERC20Upgradeable.sol"; | ||
| import {SafeERC20Upgradeable} from "@openzeppelin-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol"; | ||
| import {ERC20} from "@openzeppelin/token/ERC20/ERC20.sol"; | ||
| import {IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPoolAddressesProvider.sol"; | ||
|
|
||
| import {MathUpgradeable} from "../src/ATokenVault.sol"; | ||
| import {ImmutableATokenVault} from "../src/ImmutableATokenVault.sol"; | ||
|
|
||
| contract ImmutableATokenVaultBaseTest is Test { | ||
| using SafeERC20Upgradeable for IERC20Upgradeable; | ||
| using MathUpgradeable for uint256; | ||
|
|
||
| // Fork tests using Polygon for Aave v3 | ||
| address constant POLYGON_DAI = 0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063; | ||
| address constant POLYGON_ADAI = 0x82E64f49Ed5EC1bC6e43DAD4FC8Af9bb3A2312EE; | ||
| address constant POLYGON_AAVE_POOL = 0x794a61358D6845594F94dc1DB02A252b5b4814aD; | ||
| address constant POLYGON_POOL_ADDRESSES_PROVIDER = 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb; | ||
| address constant POLYGON_REWARDS_CONTROLLER = 0x929EC64c34a17401F460460D4B9390518E5B473e; | ||
| address constant POLYGON_DATA_PROVIDER = 0x69FA688f1Dc47d4B5d8029D5a35FB7a548310654; | ||
|
|
||
| // Fork tests using Avalanche for Aave v3 | ||
| address constant AVALANCHE_USDC = 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E; | ||
| address constant AVALANCHE_AUSDC = 0x625E7708f30cA75bfd92586e17077590C60eb4cD; | ||
| address constant AVALANCHE_WAVAX = 0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7; | ||
| address constant AVALANCHE_POOL_ADDRESSES_PROVIDER = 0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb; | ||
| address constant AVALANCHE_REWARDS_CONTROLLER = 0x929EC64c34a17401F460460D4B9390518E5B473e; | ||
|
|
||
| uint256 constant SCALE = 1e18; | ||
| uint256 constant ONE = 1e18; | ||
| uint256 constant TEN = 10e18; | ||
| uint256 constant HUNDRED = 100e18; | ||
| uint256 constant ONE_PERCENT = 0.01e18; | ||
| uint256 constant ONE_AND_HALF_PERCENT = 0.015e18; | ||
| uint256 constant ONE_BPS = 0.0001e18; | ||
|
|
||
| uint256 constant OWNER_PRIV_KEY = 11111; | ||
| uint256 constant ALICE_PRIV_KEY = 12345; | ||
| uint256 constant BOB_PRIV_KEY = 54321; | ||
| uint256 constant CHAD_PRIV_KEY = 98765; | ||
|
|
||
| address OWNER = vm.addr(OWNER_PRIV_KEY); | ||
| address ALICE = vm.addr(ALICE_PRIV_KEY); | ||
| address BOB = vm.addr(BOB_PRIV_KEY); | ||
| address CHAD = vm.addr(CHAD_PRIV_KEY); | ||
|
|
||
| string constant SHARE_NAME = "Wrapped aDAI"; | ||
| string constant SHARE_SYMBOL = "waDAI"; | ||
|
|
||
| uint256 fee = 0.2e18; // 20% | ||
| uint16 referralCode = 4546; | ||
|
|
||
| ImmutableATokenVault vault; | ||
| address vaultAssetAddress; // aDAI, must be set in every setUp | ||
| uint256 initialLockDeposit; // Must be set in every setUp | ||
|
|
||
| // Initializer Errors | ||
| bytes constant ERR_INITIALIZED = bytes("Initializable: contract is already initialized"); | ||
|
|
||
| // Ownable Errors | ||
| bytes constant ERR_NOT_OWNER = bytes("Ownable: caller is not the owner"); | ||
|
|
||
| // Meta Tx Errors | ||
| bytes constant ERR_INVALID_SIGNER = bytes("INVALID_SIGNER"); | ||
| bytes constant ERR_PERMIT_DEADLINE_EXPIRED = bytes("PERMIT_DEADLINE_EXPIRED"); | ||
| bytes constant ERR_SIG_INVALID = bytes("SIG_INVALID"); | ||
| bytes constant ERR_SIG_EXPIRED = bytes("SIG_EXPIRED"); | ||
|
|
||
| // Vault Errors | ||
| bytes constant ERR_ZERO_ADDRESS_NOT_VALID = bytes("ZERO_ADDRESS_NOT_VALID"); | ||
| bytes constant ERR_ZERO_INITIAL_DEPOSIT = bytes("ZERO_INITIAL_LOCK_DEPOSIT"); | ||
| bytes constant ERR_ZERO_ASSETS = bytes("ZERO_ASSETS"); | ||
| bytes constant ERR_ZERO_SHARES = bytes("ZERO_SHARES"); | ||
| bytes constant ERR_TRANSFER_FROM_FAILED = bytes("TRANSFER_FROM_FAILED"); | ||
| bytes constant ERR_CANNOT_RESCUE_ATOKEN = bytes("CANNOT_RESCUE_ATOKEN"); | ||
| bytes constant ERR_FEE_TOO_HIGH = bytes("FEE_TOO_HIGH"); | ||
| bytes constant ERR_ASSET_NOT_SUPPORTED = bytes("ASSET_NOT_SUPPORTED"); | ||
| bytes constant ERR_INSUFFICIENT_FEES = bytes("INSUFFICIENT_FEES"); | ||
| bytes constant ERR_CANNOT_CLAIM_TO_ZERO_ADDRESS = bytes("CANNOT_CLAIM_TO_ZERO_ADDRESS"); | ||
| bytes constant SAFE_TRANSFER_ARITHMETIC = bytes("NH{q"); | ||
| bytes constant ERR_DEPOSIT_EXCEEDS_MAX = bytes("DEPOSIT_EXCEEDS_MAX"); | ||
| bytes constant ERR_MINT_EXCEEDS_MAX = bytes("MINT_EXCEEDS_MAX"); | ||
| bytes constant ERR_WITHDRAW_EXCEEDS_MAX = bytes("WITHDRAW_EXCEEDS_MAX"); | ||
| bytes constant ERR_REDEEM_EXCEEDS_MAX = bytes("REDEEM_EXCEEDS_MAX"); | ||
|
|
||
| // ERC4626 Events | ||
| event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); | ||
| event Withdraw(address indexed sender, address indexed receiver, address indexed owner, uint256 assets, uint256 shares); | ||
|
|
||
| // ATokenVault Events | ||
| event FeeUpdated(uint256 indexed oldFee, uint256 indexed newFee); | ||
| event FeesWithdrawn(address indexed to, uint256 indexed amount, uint256 newVaultBalance, uint256 newTotalFeesAccrued); | ||
| event YieldAccrued(uint256 accruedYield, uint256 newFeesFromYield, uint256 newVaultBalance); | ||
| event RewardsClaimed(address indexed to, address[] rewardsList, uint256[] claimedAmounts); | ||
| event EmergencyRescue(address indexed token, address indexed to, uint256 amount); | ||
|
|
||
| function setUp() public virtual {} | ||
|
|
||
| // For debug purposes | ||
| function _logVaultBalances(address user, string memory label) internal view { | ||
| console.log("\n", label); | ||
| console.log("ERC20 Assets\t\t\t", ERC20(vaultAssetAddress).balanceOf(address(vault))); | ||
| console.log("totalAssets()\t\t\t", vault.totalAssets()); | ||
| console.log("lastVaultBalance()\t\t", vault.getLastVaultBalance()); | ||
| console.log("User Withdrawable\t\t", vault.maxWithdraw(user)); | ||
| console.log("claimable fees\t\t", vault.getClaimableFees()); | ||
| console.log("current time\t\t\t", block.timestamp); | ||
| } | ||
|
|
||
| function _deploy(address underlying, address addressesProvider) internal { | ||
| _deploy(underlying, addressesProvider, 10e18); | ||
| } | ||
|
|
||
| function _deploy(address underlying, address addressesProvider, uint256 _initialLockDeposit) internal { | ||
| initialLockDeposit = _initialLockDeposit; | ||
| deal(underlying, address(this), _initialLockDeposit); | ||
| address vaultAddress = computeCreateAddress(address(this), vm.getNonce(address(this)) + 1); | ||
| IERC20Upgradeable(underlying).safeApprove(vaultAddress, _initialLockDeposit); | ||
|
|
||
| vault = new ImmutableATokenVault( | ||
| underlying, | ||
| referralCode, | ||
| IPoolAddressesProvider(addressesProvider), | ||
| OWNER, | ||
| fee, | ||
| SHARE_NAME, | ||
| SHARE_SYMBOL, | ||
| _initialLockDeposit | ||
| ); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.10; | ||
|
|
||
| import {MathUpgradeable} from "@openzeppelin-upgradeable/utils/math/MathUpgradeable.sol"; | ||
| import {IPool} from "@aave-v3-core/interfaces/IPool.sol"; | ||
| import {IACLManager} from "@aave-v3-core/interfaces/IACLManager.sol"; | ||
| import {ImmutableATokenVaultForkBaseTest} from "./ImmutableATokenVaultForkBaseTest.t.sol"; | ||
|
|
||
| contract ImmutableATokenVaultEdge is ImmutableATokenVaultForkBaseTest { | ||
| using MathUpgradeable for uint256; | ||
|
|
||
| // This test demonstrates a problematic scenario if the initial deposit is too little. | ||
| function testLowInitialDepositLock() public { | ||
| _deploy(POLYGON_DAI, POLYGON_POOL_ADDRESSES_PROVIDER, 1); | ||
|
|
||
| _transferFromUser(OWNER, 2); | ||
|
|
||
| _depositFromUser(ALICE, 201); | ||
| assertEq(vault.balanceOf(ALICE), 67); | ||
|
|
||
| _depositFromUser(BOB, 200); | ||
| assertEq(vault.balanceOf(BOB), 66); | ||
|
|
||
| _transferFromUser(OWNER, 8); | ||
|
|
||
| _redeemFromUser(ALICE, 67); | ||
| assertEq(vault.balanceOf(ALICE), 0); | ||
|
|
||
| vm.prank(BOB); | ||
| vault.redeem(66, BOB, BOB); | ||
| assertEq(vault.balanceOf(BOB), 0); | ||
| } | ||
|
|
||
| function testCalculationClaimableFees() public { | ||
| /** | ||
| * This test ensure the calculation of fees is always correct and up-to-date, no matter | ||
| * the action and duration (if same block). | ||
| * - ATokens sent to the vault are considered as yield. | ||
| * - Yield can be increased more than once in same block. | ||
| */ | ||
|
|
||
| // Remove initial lock deposit | ||
| vm.startPrank(address(vault)); | ||
| vault.redeem(vault.balanceOf(address(vault)), address(1), address(vault)); | ||
| vm.stopPrank(); | ||
|
|
||
| // Bob deposits 1000 tokens, no new yield | ||
| uint256 oldYield = aDai.balanceOf(address(vault)); | ||
| uint256 oldFees = vault.getClaimableFees(); | ||
| _depositFromUser(BOB, 1000); | ||
| assertEq(oldYield + 1000, aDai.balanceOf(address(vault)), "unexpected yield after deposit"); | ||
| assertEq(oldFees, vault.getClaimableFees(), "unexpected fees after deposit"); | ||
| oldYield = aDai.balanceOf(address(vault)); | ||
| oldFees = vault.getClaimableFees(); | ||
|
|
||
| // User deposit and redeem, no new yield | ||
| _depositFromUser(address(0x222), 1_000_000e18); | ||
| _withdrawFromUser(address(0x222), 0); | ||
| assertEq(vault.balanceOf(address(0x222)), 0); | ||
| assertEq(oldYield, aDai.balanceOf(address(vault)), "unexpected yield after user"); | ||
| assertEq(oldFees, vault.getClaimableFees(), "unexpected fees after user"); | ||
| oldYield = aDai.balanceOf(address(vault)); | ||
| oldFees = vault.getClaimableFees(); | ||
|
|
||
| // Gift to the vault | ||
| _accrueYieldInVault(100); | ||
|
|
||
| // Increase yield in the Aave Protocol by accumulating yield to Portals | ||
| uint256 currentFees = vault.getClaimableFees(); | ||
| uint256 currentIndex = IPool(POLYGON_AAVE_POOL).getReserveNormalizedIncome(address(dai)); | ||
| uint256 currentBalance = vault.ATOKEN().balanceOf(address(vault)); | ||
| // Using portals | ||
| address BRIDGE = address(0xb0b0); | ||
| uint256 bridgeMint = 40_000_000e18; | ||
| // Mock call `isBridge` | ||
| address aclManagerAddress = vault.AAVE_POOL().ADDRESSES_PROVIDER().getACLManager(); | ||
| IACLManager aclManager = IACLManager(aclManagerAddress); | ||
| vm.mockCall(address(aclManagerAddress), abi.encodeWithSelector(aclManager.isBridge.selector, BRIDGE), abi.encode(true)); | ||
| vm.startPrank(BRIDGE); | ||
| deal(address(dai), BRIDGE, bridgeMint); | ||
| dai.approve(POLYGON_AAVE_POOL, bridgeMint); | ||
| IPool(POLYGON_AAVE_POOL).backUnbacked(address(dai), 0, bridgeMint); | ||
| vm.stopPrank(); | ||
|
|
||
| uint256 newIndex = IPool(POLYGON_AAVE_POOL).getReserveNormalizedIncome(address(dai)); | ||
| uint256 newBalance = vault.ATOKEN().balanceOf(address(vault)); | ||
| assertLt(currentIndex, newIndex, "unexpected change in index"); | ||
| assertLt(currentBalance, newBalance, "unexpected change in balance"); | ||
| uint256 newFees = (newBalance - currentBalance).mulDiv(vault.getFee(), SCALE, MathUpgradeable.Rounding.Down); | ||
| // 1 wei imprecision due to AToken nature | ||
| assertApproxEqAbs(vault.getClaimableFees(), currentFees + newFees, 1, "claimable fees are not up to date"); | ||
| currentFees = vault.getClaimableFees(); | ||
|
|
||
| // Same fees as block before | ||
| vm.warp(block.timestamp + 1); | ||
| assertEq(vault.getClaimableFees(), currentFees, "claimable fees are not up to date"); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.