Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .github/workflows/foundry-gas-diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
# due to non-deterministic fuzzing, but keep it not always deterministic
POLYGON_RPC_URL: ${{ secrets.POLYGON_RPC_URL }}
AVALANCHE_RPC_URL: ${{ secrets.AVALANCHE_RPC_URL }}
ETHEREUM_RPC_URL: ${{ secrets.ETHEREUM_RPC_URL }}
FOUNDRY_FUZZ_SEED: 0x${{ github.event.pull_request.base.sha || github.sha }}

- name: Compare gas reports
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ jobs:
env:
POLYGON_RPC_URL: ${{ secrets.POLYGON_RPC_URL }}
AVALANCHE_RPC_URL: ${{ secrets.AVALANCHE_RPC_URL }}
ETHEREUM_RPC_URL: ${{ secrets.ETHEREUM_RPC_URL }}
run: forge test -vvv
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ Some of the tests rely on an RPC connection for forking network state. Make sure
```
POLYGON_RPC_URL=[Your favourite Polygon RPC URL]
AVALANCHE_RPC_URL=[Your favourite Avalanche RPC URL]
ETHEREUM_RPC_URL=[Your favourite Ethereum RPC URL]
```

The fork tests all use Polygon, except tests for claiming Aave rewards, which use Avalanche.
The fork tests all use Polygon, except tests for claiming Aave rewards, which use Avalanche, and Merkl rewards, which use Ethereum.

This test suite also includes a16z's [ERC-4626 Property Tests](https://a16zcrypto.com/generalized-property-tests-for-erc4626-vaults/), which are in the `ATokenVaultProperties.t.sol` file. These tests do not use a forked network state but rather use mock contracts, found in the `test/mocks` folder.

Expand Down
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ runs = 256
gas_reports = ["ATokenVault"]
isolate = true
ignored_warnings_from = ["lib/", "node_modules/"] # Ignore warnings from dependencies
evm_version = 'cancun'

[fuzz]
max_test_rejects = 65536
Expand Down
75 changes: 75 additions & 0 deletions src/ATokenVaultMerklRewardClaimer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// SPDX-License-Identifier: UNLICENSED
// All Rights Reserved © AaveCo

pragma solidity ^0.8.10;

import {IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPoolAddressesProvider.sol";
import {IERC20} from "@openzeppelin/interfaces/IERC20.sol";

import {ATokenVault} from "./ATokenVault.sol";
import {IATokenVaultMerklRewardClaimer} from "./interfaces/IATokenVaultMerklRewardClaimer.sol";

interface IMerklDistributor {
function claim(
address[] calldata users,
address[] calldata tokens,
uint256[] calldata amounts,
bytes32[][] calldata proofs
) external;
}

/**
* @title ATokenVaultMerklRewardClaimer
* @author Aave Protocol
* @notice A contract that allows the owner to claim Merkl rewards for the ATokenVault
*/
contract ATokenVaultMerklRewardClaimer is ATokenVault, IATokenVaultMerklRewardClaimer {
/**
* @dev Constructor.
* @param underlying The underlying ERC20 asset which can be supplied to Aave
* @param referralCode The Aave referral code to use for deposits from this vault
* @param poolAddressesProvider The address of the Aave v3 Pool Addresses Provider
*/
constructor(address underlying, uint16 referralCode, IPoolAddressesProvider poolAddressesProvider)
ATokenVault(underlying, referralCode, poolAddressesProvider)
{}

/// @inheritdoc IATokenVaultMerklRewardClaimer
function getMerklDistributor() external view override returns (address) {
return _s.merklDistributor;
}

/// @inheritdoc IATokenVaultMerklRewardClaimer
function setMerklDistributor(address merklDistributor) external override onlyOwner {
require(merklDistributor != address(0), "ZERO_ADDRESS_NOT_VALID");
address currentMerklDistributor = _s.merklDistributor;
_s.merklDistributor = merklDistributor;
emit MerklDistributorUpdated(currentMerklDistributor, merklDistributor);
}

/// @inheritdoc IATokenVaultMerklRewardClaimer
function claimMerklRewards(address[] calldata rewardTokens, uint256[] calldata amounts, bytes32[][] calldata proofs)
public
override
onlyOwner
{
require(_s.merklDistributor != address(0), "MERKL_DISTRIBUTOR_NOT_SET");

address[] memory users = new address[](rewardTokens.length);
for (uint256 i = 0; i < rewardTokens.length; i++) {
// users represent depositors into Aave which is this contract
users[i] = address(this);
}

// The claim function does not return a list of tokens and amounts actually received.
// It is possible for rewards to be in aTokens, the underlying asset or some other token.
// If necessary the owner can use IATokenVault.emergencyRescue(...) to rescue the non-aToken rewards.
IMerklDistributor(_s.merklDistributor).claim(users, rewardTokens, amounts, proofs);
// Do not attempt to accrue yield as it can be delegated to subsequent calls to this contract.
// We do not need to accrue before claiming because new shares are not granted anywhere (rewards are socialized across all current share holders).
// We do not need to accrue after claiming because any subsequent call will trigger an accrual before state updates
// and preview functions read the balance of aTokens on the vault at runtime.

emit MerklRewardsClaimed(rewardTokens, amounts);
}
}
4 changes: 3 additions & 1 deletion src/ATokenVaultStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ abstract contract ATokenVaultStorage {
uint40 __deprecated_gap;
// as a fraction of 1e18
uint64 fee;
// Merkl distributor contract address called to claim Merkl rewards
address merklDistributor;
// Reserved storage space to allow for layout changes in the future
uint256[50] __gap;
uint256[49] __gap;
}

Storage internal _s;
Expand Down
54 changes: 54 additions & 0 deletions src/interfaces/IATokenVaultMerklRewardClaimer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-License-Identifier: UNLICENSED
// All Rights Reserved © AaveCo

pragma solidity ^0.8.10;

/**
* @title IATokenVaultMerklRewardClaimer
* @author Aave Protocol
*
* @notice Defines the basic interface of the ATokenVaultMerklRewardClaimer
*/
interface IATokenVaultMerklRewardClaimer {
/**
* @dev Emitted when Merkl rewards are claimed by the vault contract
* @dev The token addresses do not always match the actual tokens received by the vault contract after rewards are claimed
* @dev The amounts do not always match the actual amounts received by the vault contract after rewards are claimed
* @param tokens Addresses of the ERC-20 reward tokens claimed (the tokens passed as params to the Merkl distributor contract)
* @param amounts Amounts of the reward tokens claimed for each token (the amounts passed as params to the Merkl distributor contract)
*/
event MerklRewardsClaimed(address[] tokens, uint256[] amounts);

/**
* @dev Emitted when the Merkl distributor address is updated
* @param oldMerklDistributor The old address of the Merkl distributor contract
* @param newMerklDistributor The new address of the Merkl distributor contract
*/
event MerklDistributorUpdated(address indexed oldMerklDistributor, address indexed newMerklDistributor);

/**
* @notice Getter for the contract address called to claim Merkl rewards
* @return Address of the Merkl distributor contract
*/
function getMerklDistributor() external view returns (address);

/**
* @notice Sets the Merkl distributor address for the vault uses to claim Merkl rewards.
* @dev Only callable by the owner
* @param merklDistributor Address of the new Merkl distributor contract
*/
function setMerklDistributor(address merklDistributor) external;

/**
* @notice Claims Merkl rewards earned by deposits from this contract through the Merkl distributor contract
* @dev Only callable by the owner
* @dev Merkl distributor address must be set
* @dev The IMerklDistributor.claim(...) function does not return a list of tokens and amounts the users actually receive
* @dev The order of the tokens, amounts, and proofs must align with eachother
* @param rewardTokens Addresses of the ERC-20 reward tokens to claim (the tokens passed as params to the Merkl distributor contract)
* @param amounts Amounts of the reward tokens to claim for each token
* @param proofs Merkl proof passed to the Merkl distributor contract
*/
function claimMerklRewards(address[] calldata rewardTokens, uint256[] calldata amounts, bytes32[][] calldata proofs)
external;
}
28 changes: 28 additions & 0 deletions test/ATokenVaultBaseTest.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPoolAddressesPro
import {TransparentUpgradeableProxy} from "@openzeppelin/proxy/transparent/TransparentUpgradeableProxy.sol";

import {ATokenVault, MathUpgradeable} from "../src/ATokenVault.sol";
import {ATokenVaultMerklRewardClaimer} from "../src/ATokenVaultMerklRewardClaimer.sol";

contract ATokenVaultBaseTest is Test {
using SafeERC20Upgradeable for IERC20Upgradeable;
Expand Down Expand Up @@ -143,4 +144,31 @@ contract ATokenVaultBaseTest is Test {

vault = ATokenVault(address(proxy));
}

function _deployATokenVaultMerklRewardClaimer(address underlying, address addressesProvider) internal {
_deployATokenVaultMerklRewardClaimer(underlying, addressesProvider, 10e18);
}

function _deployATokenVaultMerklRewardClaimer(address underlying, address addressesProvider, uint256 _initialLockDeposit) internal {
initialLockDeposit = _initialLockDeposit;
vault = new ATokenVaultMerklRewardClaimer(underlying, referralCode, IPoolAddressesProvider(addressesProvider));

bytes memory data = abi.encodeWithSelector(
ATokenVault.initialize.selector,
OWNER,
fee,
SHARE_NAME,
SHARE_SYMBOL,
_initialLockDeposit
);

deal(underlying, address(this), _initialLockDeposit);
address proxyAddr = computeCreateAddress(address(this), vm.getNonce(address(this)) + 1);

IERC20Upgradeable(underlying).safeApprove(address(proxyAddr), _initialLockDeposit);

TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(address(vault), PROXY_ADMIN, data);

vault = ATokenVaultMerklRewardClaimer(address(proxy));
}
}
Loading
Loading