Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
80 changes: 80 additions & 0 deletions src/ImmutableATokenVault.sol
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 { }
}
135 changes: 135 additions & 0 deletions test/ImmutableATokenVaultBaseTest.t.sol
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
);
}
}
98 changes: 98 additions & 0 deletions test/ImmutableATokenVaultEdge.t.sol
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");
}
}
Loading
Loading