Skip to content
Open
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
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
80 changes: 80 additions & 0 deletions src/ATokenVaultMerklRewardClaimer.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 {IPoolAddressesProvider} from "@aave-v3-core/interfaces/IPoolAddressesProvider.sol";
import {IERC20Upgradeable} from "@openzeppelin-upgradeable/interfaces/IERC20Upgradeable.sol";
import {SafeERC20Upgradeable} from "@openzeppelin-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import {IMerklDistributor} from "./dependencies/merkl/DistributorInterface.sol";
import {ATokenVault} from "./ATokenVault.sol";
import {IATokenVaultMerklRewardClaimer} from "./interfaces/IATokenVaultMerklRewardClaimer.sol";

/**
* @title ATokenVaultMerklRewardClaimer
* @author Aave Protocol
* @notice ATokenVault, with Merkl reward claiming capability
*/
contract ATokenVaultMerklRewardClaimer is ATokenVault, IATokenVaultMerklRewardClaimer {
using SafeERC20Upgradeable for IERC20Upgradeable;

/**
* @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 claimMerklRewards(
address[] calldata tokens,
uint256[] calldata amounts,
bytes32[][] calldata proofs,
address[] calldata rewardTokensToForward,
address destination
)
public
override
onlyOwner
{
require(_s.merklDistributor != address(0), "MERKL_DISTRIBUTOR_NOT_SET");
require(tokens.length == amounts.length && tokens.length == proofs.length, "ARRAY_LENGTH_MISMATCH");

uint256[] memory currentBalancesOfRewardTokens = new uint256[](rewardTokensToForward.length);
for (uint256 i = 0; i < rewardTokensToForward.length; i++) {
currentBalancesOfRewardTokens[i] = IERC20Upgradeable(rewardTokensToForward[i]).balanceOf(address(this));
}

address[] memory users = new address[](tokens.length);
for (uint256 i = 0; i < tokens.length; i++) {
users[i] = address(this);
}
IMerklDistributor(_s.merklDistributor).claim(users, tokens, amounts, proofs);
emit MerklRewardsClaimed(_s.merklDistributor, tokens, amounts);

for (uint256 i = 0; i < rewardTokensToForward.length; i++) {
uint256 newBalance = IERC20Upgradeable(rewardTokensToForward[i]).balanceOf(address(this));
uint256 amountToForward = newBalance - currentBalancesOfRewardTokens[i];
if (amountToForward > 0) {
IERC20Upgradeable(rewardTokensToForward[i]).safeTransfer(destination, amountToForward);
emit MerklRewardsTokenForwarded(rewardTokensToForward[i], destination, amountToForward);
}
}
}

/// @inheritdoc IATokenVaultMerklRewardClaimer
/// @dev Allow setting address(0) to reset the Merkl distributor
function setMerklDistributor(address merklDistributor) external override onlyOwner {
address currentMerklDistributor = _s.merklDistributor;
_s.merklDistributor = merklDistributor;
emit MerklDistributorUpdated(currentMerklDistributor, merklDistributor);
}

/// @inheritdoc IATokenVaultMerklRewardClaimer
function getMerklDistributor() external view override returns (address) {
return _s.merklDistributor;
}
}
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 merklDistributor;
// Reserved storage space to allow for layout changes in the future
uint256[50] __gap;
uint256[49] __gap;
}

Storage internal _s;
Expand Down
12 changes: 12 additions & 0 deletions src/dependencies/merkl/DistributorInterface.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: BUSL-1.1
// Based on implementation from https://github.com/AngleProtocol/merkl-contracts/blob/b7bd0e65a3f366e4041bc83494cbd981f8852b16/contracts/Distributor.sol#L202
pragma solidity ^0.8.10;

interface IMerklDistributor {
function claim(
address[] calldata users,
address[] calldata tokens,
uint256[] calldata amounts,
bytes32[][] calldata proofs
) external;
}
69 changes: 69 additions & 0 deletions src/interfaces/IATokenVaultMerklRewardClaimer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 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 (the amounts may be the cumulative rewards earned by the user)
* @param distributor Address of the Merkl distributor contract
* @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 indexed distributor, address[] tokens, uint256[] amounts);

/**
* @dev Emitted when a reward token is forwarded to a destination
* @param token Address of the ERC-20 reward token
* @param destination Address of the destination receiving the reward token
* @param amount Amount of the reward token transferred to the destination
*/
event MerklRewardsTokenForwarded(address indexed token, address indexed destination, uint256 indexed amount);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do smt similar to RewardsClaimed, and emit the destination, tokens array and amounts array.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no strong preference, but I wanted to avoid emitting the event if the rewardTokensToForward array is empty. If you have a strong preference please lmk. Maybe we return early if rewardTokensToForward is empty then iterate through the tokens then emit an event at the event.

Imagine it would look like this:
event MerklRewardsTokenForwarded(address indexed destination, address[] tokens, uint256[] amounts);

We can also pass in an array of destinations to claimMerklRewards(...) if you think the flexibility would be valuable - then the single token/amount/destination event makes more sense.


/**
* @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 Claims Merkl protocol rewards accrued from vault deposits.
* @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 tokens 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
* @param rewardTokensToForward Addresses of the ERC-20 tokens received by the vault contract from reward claiming (to be set if rewards should be forwarded to the `destination`)
* @param destination Address to send the received tokens to
*/
function claimMerklRewards(
address[] calldata tokens,
uint256[] calldata amounts,
bytes32[][] calldata proofs,
address[] calldata rewardTokensToForward,
address destination
) external;

/**
* @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 Returns the address of the Merkl distributor contract.
* @return The address of the Merkl distributor contract
*/
function getMerklDistributor() external view returns (address);
}
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