Skip to content

Commit 6069218

Browse files
committed
feat: add new contract to support merkl reward claiming
- inherits from ATokenVault - updates storage struct to store merkl distributor contract (consumes the first of 50 gap slots) - expose functions to set/get distributor (onlyOwner) - expose function to claim rewards from merkl distributor (onlyOwner) - add test that forks Ethereum mainnet to claim rewards
1 parent 497d18d commit 6069218

File tree

10 files changed

+371
-2
lines changed

10 files changed

+371
-2
lines changed

.github/workflows/foundry-gas-diff.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ jobs:
3535
# due to non-deterministic fuzzing, but keep it not always deterministic
3636
POLYGON_RPC_URL: ${{ secrets.POLYGON_RPC_URL }}
3737
AVALANCHE_RPC_URL: ${{ secrets.AVALANCHE_RPC_URL }}
38+
ETHEREUM_RPC_URL: ${{ secrets.ETHEREUM_RPC_URL }}
3839
FOUNDRY_FUZZ_SEED: 0x${{ github.event.pull_request.base.sha || github.sha }}
3940

4041
- name: Compare gas reports

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,5 @@ jobs:
2323
env:
2424
POLYGON_RPC_URL: ${{ secrets.POLYGON_RPC_URL }}
2525
AVALANCHE_RPC_URL: ${{ secrets.AVALANCHE_RPC_URL }}
26+
ETHEREUM_RPC_URL: ${{ secrets.ETHEREUM_RPC_URL }}
2627
run: forge test -vvv

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ Some of the tests rely on an RPC connection for forking network state. Make sure
2020
```
2121
POLYGON_RPC_URL=[Your favourite Polygon RPC URL]
2222
AVALANCHE_RPC_URL=[Your favourite Avalanche RPC URL]
23+
ETHEREUM_RPC_URL=[Your favourite Ethereum RPC URL]
2324
```
2425

25-
The fork tests all use Polygon, except tests for claiming Aave rewards, which use Avalanche.
26+
The fork tests all use Polygon, except tests for claiming Aave rewards, which use Avalanche, and Merkl rewards, which use Ethereum.
2627

2728
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.
2829

foundry.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ runs = 256
88
gas_reports = ["ATokenVault"]
99
isolate = true
1010
ignored_warnings_from = ["lib/", "node_modules/"] # Ignore warnings from dependencies
11+
evm_version = 'cancun'
1112

1213
[fuzz]
1314
max_test_rejects = 65536

src/ATokenVault.sol

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {MetaTxHelpers} from "./libraries/MetaTxHelpers.sol";
2121
import "./libraries/Constants.sol";
2222
import {ATokenVaultStorage} from "./ATokenVaultStorage.sol";
2323

24+
import {console} from "forge-std/console.sol";
25+
2426
/**
2527
* @title ATokenVault
2628
* @author Aave Protocol
@@ -59,6 +61,7 @@ contract ATokenVault is ERC4626Upgradeable, OwnableUpgradeable, EIP712Upgradeabl
5961
UNDERLYING = IERC20Upgradeable(underlying);
6062

6163
address aTokenAddress = AAVE_POOL.getReserveData(address(underlying)).aTokenAddress;
64+
console.log("!!!!!!!!!!!!!!!!!!!!!! aTokenAddress:", aTokenAddress);
6265
require(aTokenAddress != address(0), "ASSET_NOT_SUPPORTED");
6366
ATOKEN = IAToken(aTokenAddress);
6467
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
// All Rights Reserved © AaveCo
3+
4+
pragma solidity ^0.8.10;
5+
6+
import {IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPoolAddressesProvider.sol";
7+
import {IERC20} from "@openzeppelin/interfaces/IERC20.sol";
8+
9+
import {ATokenVault} from "./ATokenVault.sol";
10+
import {IATokenVaultMerklRewardClaimer} from "./interfaces/IATokenVaultMerklRewardClaimer.sol";
11+
12+
interface IMerklDistributor {
13+
function claim(
14+
address[] calldata users,
15+
address[] calldata tokens,
16+
uint256[] calldata amounts,
17+
bytes32[][] calldata proofs
18+
) external;
19+
}
20+
21+
/**
22+
* @title ATokenVaultMerklRewardClaimer
23+
* @author Aave Protocol
24+
* @notice A contract that allows the owner to claim Merkl rewards for the ATokenVault
25+
*/
26+
contract ATokenVaultMerklRewardClaimer is ATokenVault, IATokenVaultMerklRewardClaimer {
27+
/**
28+
* @dev Constructor.
29+
* @param underlying The underlying ERC20 asset which can be supplied to Aave
30+
* @param referralCode The Aave referral code to use for deposits from this vault
31+
* @param poolAddressesProvider The address of the Aave v3 Pool Addresses Provider
32+
*/
33+
constructor(address underlying, uint16 referralCode, IPoolAddressesProvider poolAddressesProvider)
34+
ATokenVault(underlying, referralCode, poolAddressesProvider)
35+
{}
36+
37+
/// @inheritdoc IATokenVaultMerklRewardClaimer
38+
function getMerklDistributor() external view override returns (address) {
39+
return _s.merklDistributor;
40+
}
41+
42+
/// @inheritdoc IATokenVaultMerklRewardClaimer
43+
function setMerklDistributor(address merklDistributor) external override onlyOwner {
44+
require(merklDistributor != address(0), "ZERO_ADDRESS_NOT_VALID");
45+
address currentMerklDistributor = _s.merklDistributor;
46+
_s.merklDistributor = merklDistributor;
47+
emit MerklDistributorUpdated(currentMerklDistributor, merklDistributor);
48+
}
49+
50+
/// @inheritdoc IATokenVaultMerklRewardClaimer
51+
function claimMerklRewards(address[] calldata rewardTokens, uint256[] calldata amounts, bytes32[][] calldata proofs)
52+
public
53+
override
54+
onlyOwner
55+
{
56+
require(_s.merklDistributor != address(0), "MERKL_DISTRIBUTOR_NOT_SET");
57+
58+
address[] memory users = new address[](rewardTokens.length);
59+
for (uint256 i = 0; i < rewardTokens.length; i++) {
60+
// users represent depositors into Aave which is this contract
61+
users[i] = address(this);
62+
}
63+
64+
// The claim function does not return a list of tokens and amounts actually received.
65+
// It is possible for rewards to be in aTokens, the underlying asset or some other token.
66+
// If necessary the owner can use IATokenVault.emergencyRescue(...) to rescue the non-aToken rewards.
67+
IMerklDistributor(_s.merklDistributor).claim(users, rewardTokens, amounts, proofs);
68+
// Do not attempt to accrue yield as it can be delegated to subsequent calls to this contract.
69+
// We do not need to accrue before claiming because new shares are not granted anywhere (rewards are socialized across all current share holders).
70+
// We do not need to accrue after claiming because any subsequent call will trigger an accrual before state updates
71+
// and preview functions read the balance of aTokens on the vault at runtime.
72+
73+
emit MerklRewardsClaimed(rewardTokens, amounts);
74+
}
75+
}

src/ATokenVaultStorage.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ abstract contract ATokenVaultStorage {
2020
uint40 __deprecated_gap;
2121
// as a fraction of 1e18
2222
uint64 fee;
23+
// Merkl distributor contract address called to claim Merkl rewards
24+
address merklDistributor;
2325
// Reserved storage space to allow for layout changes in the future
24-
uint256[50] __gap;
26+
uint256[49] __gap;
2527
}
2628

2729
Storage internal _s;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
// All Rights Reserved © AaveCo
3+
4+
pragma solidity ^0.8.10;
5+
6+
/**
7+
* @title IATokenVaultMerklRewardClaimer
8+
* @author Aave Protocol
9+
*
10+
* @notice Defines the basic interface of the ATokenVaultMerklRewardClaimer
11+
*/
12+
interface IATokenVaultMerklRewardClaimer {
13+
/**
14+
* @dev Emitted when Merkl rewards are claimed by the vault contract
15+
* @dev The token addresses do not always match the actual tokens received by the vault contract after rewards are claimed
16+
* @dev The amounts do not always match the actual amounts received by the vault contract after rewards are claimed
17+
* @param tokens Addresses of the ERC-20 reward tokens claimed (the tokens passed as params to the Merkl distributor contract)
18+
* @param amounts Amounts of the reward tokens claimed for each token (the amounts passed as params to the Merkl distributor contract)
19+
*/
20+
event MerklRewardsClaimed(address[] tokens, uint256[] amounts);
21+
22+
/**
23+
* @dev Emitted when the Merkl distributor address is updated
24+
* @param oldMerklDistributor The old address of the Merkl distributor contract
25+
* @param newMerklDistributor The new address of the Merkl distributor contract
26+
*/
27+
event MerklDistributorUpdated(address indexed oldMerklDistributor, address indexed newMerklDistributor);
28+
29+
/**
30+
* @notice Getter for the contract address called to claim Merkl rewards
31+
* @return Address of the Merkl distributor contract
32+
*/
33+
function getMerklDistributor() external view returns (address);
34+
35+
/**
36+
* @notice Sets the Merkl distributor address for the vault uses to claim Merkl rewards.
37+
* @dev Only callable by the owner
38+
* @param merklDistributor Address of the new Merkl distributor contract
39+
*/
40+
function setMerklDistributor(address merklDistributor) external;
41+
42+
/**
43+
* @notice Claims Merkl rewards earned by deposits from this contract through the Merkl distributor contract
44+
* @dev Only callable by the owner
45+
* @dev Merkl distributor address must be set
46+
* @dev The IMerklDistributor.claim(...) function does not return a list of tokens and amounts the users actually receive
47+
* @dev The order of the tokens, amounts, and proofs must align with eachother
48+
* @param rewardTokens Addresses of the ERC-20 reward tokens to claim (the tokens passed as params to the Merkl distributor contract)
49+
* @param amounts Amounts of the reward tokens to claim for each token
50+
* @param proofs Merkl proof passed to the Merkl distributor contract
51+
*/
52+
function claimMerklRewards(address[] calldata rewardTokens, uint256[] calldata amounts, bytes32[][] calldata proofs)
53+
external;
54+
}

test/ATokenVaultBaseTest.t.sol

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPoolAddressesPro
1010
import {TransparentUpgradeableProxy} from "@openzeppelin/proxy/transparent/TransparentUpgradeableProxy.sol";
1111

1212
import {ATokenVault, MathUpgradeable} from "../src/ATokenVault.sol";
13+
import {ATokenVaultMerklRewardClaimer} from "../src/ATokenVaultMerklRewardClaimer.sol";
1314

1415
contract ATokenVaultBaseTest is Test {
1516
using SafeERC20Upgradeable for IERC20Upgradeable;
@@ -143,4 +144,31 @@ contract ATokenVaultBaseTest is Test {
143144

144145
vault = ATokenVault(address(proxy));
145146
}
147+
148+
function _deployATokenVaultMerklRewardClaimer(address underlying, address addressesProvider) internal {
149+
_deployATokenVaultMerklRewardClaimer(underlying, addressesProvider, 10e18);
150+
}
151+
152+
function _deployATokenVaultMerklRewardClaimer(address underlying, address addressesProvider, uint256 _initialLockDeposit) internal {
153+
initialLockDeposit = _initialLockDeposit;
154+
vault = new ATokenVaultMerklRewardClaimer(underlying, referralCode, IPoolAddressesProvider(addressesProvider));
155+
156+
bytes memory data = abi.encodeWithSelector(
157+
ATokenVault.initialize.selector,
158+
OWNER,
159+
fee,
160+
SHARE_NAME,
161+
SHARE_SYMBOL,
162+
_initialLockDeposit
163+
);
164+
165+
deal(underlying, address(this), _initialLockDeposit);
166+
address proxyAddr = computeCreateAddress(address(this), vm.getNonce(address(this)) + 1);
167+
168+
IERC20Upgradeable(underlying).safeApprove(address(proxyAddr), _initialLockDeposit);
169+
170+
TransparentUpgradeableProxy proxy = new TransparentUpgradeableProxy(address(vault), PROXY_ADMIN, data);
171+
172+
vault = ATokenVaultMerklRewardClaimer(address(proxy));
173+
}
146174
}

0 commit comments

Comments
 (0)