Skip to content

Commit 1c758c8

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 1c758c8

File tree

9 files changed

+328
-2
lines changed

9 files changed

+328
-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
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
8+
import {ATokenVault} from "./ATokenVault.sol";
9+
import {IATokenVaultMerklRewardClaimer} from "./interfaces/IATokenVaultMerklRewardClaimer.sol";
10+
11+
interface IMerklDistributor {
12+
function claim(
13+
address[] calldata users,
14+
address[] calldata tokens,
15+
uint256[] calldata amounts,
16+
bytes32[][] calldata proofs
17+
) external;
18+
}
19+
20+
/**
21+
* @title ATokenVaultMerklRewardClaimer
22+
* @author Aave Protocol
23+
* @notice A contract that allows the owner to claim Merkl rewards for the ATokenVault
24+
*/
25+
contract ATokenVaultMerklRewardClaimer is ATokenVault, IATokenVaultMerklRewardClaimer {
26+
/**
27+
* @dev Constructor.
28+
* @param underlying The underlying ERC20 asset which can be supplied to Aave
29+
* @param referralCode The Aave referral code to use for deposits from this vault
30+
* @param poolAddressesProvider The address of the Aave v3 Pool Addresses Provider
31+
*/
32+
constructor(address underlying, uint16 referralCode, IPoolAddressesProvider poolAddressesProvider)
33+
ATokenVault(underlying, referralCode, poolAddressesProvider)
34+
{}
35+
36+
/// @inheritdoc IATokenVaultMerklRewardClaimer
37+
function getMerklDistributor() external view override returns (address) {
38+
return _s.merklDistributor;
39+
}
40+
41+
/// @inheritdoc IATokenVaultMerklRewardClaimer
42+
function setMerklDistributor(address merklDistributor) external override onlyOwner {
43+
require(merklDistributor != address(0), "ZERO_ADDRESS_NOT_VALID");
44+
address currentMerklDistributor = _s.merklDistributor;
45+
_s.merklDistributor = merklDistributor;
46+
emit MerklDistributorUpdated(currentMerklDistributor, merklDistributor);
47+
}
48+
49+
/// @inheritdoc IATokenVaultMerklRewardClaimer
50+
function claimMerklRewards(address[] calldata tokens, uint256[] calldata amounts, bytes32[][] calldata proofs)
51+
public
52+
override
53+
onlyOwner
54+
{
55+
require(_s.merklDistributor != address(0), "MERKL_DISTRIBUTOR_NOT_SET");
56+
57+
address[] memory users = new address[](tokens.length);
58+
for (uint256 i = 0; i < tokens.length; i++) {
59+
// users represent depositors into Aave which is this contract
60+
users[i] = address(this);
61+
}
62+
63+
IMerklDistributor(_s.merklDistributor).claim(users, tokens, amounts, proofs);
64+
65+
emit MerklRewardsClaimed(tokens, amounts);
66+
}
67+
}

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: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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 Rewards claimed are not re-deposited into the underlying Aave pool. The owner can use `IATokenVault.emergencyRescue(...)` to rescue token rewards
47+
* @dev The IMerklDistributor.claim(...) function does not return a list of tokens and amounts the users actually receive
48+
* @dev The order of the tokens, amounts, and proofs must align with eachother
49+
* @param tokens Addresses of the ERC-20 reward tokens to claim
50+
* @param amounts Amounts of the reward tokens to claim for each token
51+
* @param proofs Merkl proof passed to the Merkl distributor contract
52+
*/
53+
function claimMerklRewards(address[] calldata tokens, uint256[] calldata amounts, bytes32[][] calldata proofs)
54+
external;
55+
}

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
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.10;
4+
5+
import "forge-std/Test.sol";
6+
import {stdStorage, StdStorage} from "forge-std/Test.sol";
7+
8+
import {IAToken} from "@aave-v3-core/interfaces/IAToken.sol";
9+
import {IERC20} from "@openzeppelin/interfaces/IERC20.sol";
10+
11+
import {ATokenVaultBaseTest} from "./ATokenVaultBaseTest.t.sol";
12+
13+
import {IATokenVaultMerklRewardClaimer} from "../src/interfaces/IATokenVaultMerklRewardClaimer.sol";
14+
15+
/**
16+
* @title ATokenVaultMerklRewardClaimerTest
17+
* @notice Test suite for claiming Merkl rewards from the ATokenVault
18+
* @dev Forks Ethereum mainnet to etch the ATokenVault onto an address that has claimable rewards as of the forked block
19+
* @dev foundry.toml must use evm_version = 'cancun' to run this test
20+
*/
21+
contract ATokenVaultMerklRewardClaimerTest is ATokenVaultBaseTest {
22+
using stdStorage for StdStorage;
23+
uint256 ethereumFork;
24+
// The block before rewards claim in tx: https://etherscan.io/tx/0x42ef6b499d1b6e96a4250f2d5a005b60173386e2ac3a3e424aa407db3da802ea
25+
uint256 ETHEREUM_FORK_BLOCK = 23921479; // Dec 1st 2025
26+
27+
address constant MERKL_DISTRIBUTOR = 0x3Ef3D8bA38EBe18DB133cEc108f4D14CE00Dd9Ae;
28+
address constant ADDRESS_WITH_CLAIMABLE_REWARDS = 0x424629A0D581B6076322A952FB43b78624dB8A15;
29+
address constant ETHEREUM_POOL_ADDRESSES_PROVIDER = 0x2f39d218133AFaB8F2B819B1066c7E434Ad94E9e;
30+
address constant ETHEREUM_RLUSD = 0x8292Bb45bf1Ee4d140127049757C2E0fF06317eD;
31+
address constant A_HOR_RWA_RLUSD = 0xE3190143Eb552456F88464662f0c0C4aC67A77eB;
32+
address constant WRAPPED_A_HOR_RWA_RLUSD = 0x503D751B13a71D8e69Db021DF110bfa7aE1dA889;
33+
address constant A_ETH_RLUSD = 0xFa82580c16A31D0c1bC632A36F82e83EfEF3Eec0;
34+
address constant WRAPPED_A_ETH_RLUSD = 0x72eEED8043Dcce2Fe7CdAC950D928F80f472ab80;
35+
IAToken constant aHorRwaRLUSD = IAToken(A_HOR_RWA_RLUSD);
36+
37+
function setUp() public override {
38+
ethereumFork = vm.createFork(vm.envString("ETHEREUM_RPC_URL"));
39+
vm.selectFork(ethereumFork);
40+
vm.rollFork(ETHEREUM_FORK_BLOCK);
41+
42+
vaultAssetAddress = address(aHorRwaRLUSD);
43+
44+
// Sets the `vault`, but we will not use the vault deployment
45+
_deployATokenVaultMerklRewardClaimer(ETHEREUM_RLUSD, ETHEREUM_POOL_ADDRESSES_PROVIDER);
46+
}
47+
48+
/*//////////////////////////////////////////////////////////////
49+
ETHEREUM FORK TESTS
50+
//////////////////////////////////////////////////////////////*/
51+
52+
function testEthereumForkWorks() public {
53+
assertEq(vm.activeFork(), ethereumFork);
54+
}
55+
56+
function testEthereumForkAtExpectedBlock() public {
57+
assertEq(block.number, ETHEREUM_FORK_BLOCK);
58+
}
59+
60+
function testEthereumForkBalanceOfAddressWithClaimableRewards() public {
61+
assertEq(ADDRESS_WITH_CLAIMABLE_REWARDS.balance, 154288817306978598);
62+
}
63+
64+
/*//////////////////////////////////////////////////////////////
65+
MERKL REWARDS CLAIM TESTS
66+
//////////////////////////////////////////////////////////////*/
67+
68+
function testOwnerCanClaimMerklRewards() public {
69+
_setMerklDistributor();
70+
// Set the code for an address that has claimable rewards as of the fork block
71+
// We will use this in place of the vault deployment
72+
_etchVault(ADDRESS_WITH_CLAIMABLE_REWARDS);
73+
74+
uint256 beforeBalanceOfAHorRwaRLUSD = IERC20(A_HOR_RWA_RLUSD).balanceOf(ADDRESS_WITH_CLAIMABLE_REWARDS);
75+
uint256 beforeBalanceOfWrappedAHorRwaRLUSD = IERC20(WRAPPED_A_HOR_RWA_RLUSD).balanceOf(ADDRESS_WITH_CLAIMABLE_REWARDS);
76+
uint256 beforeBalanceOfAETHRLUSD = IERC20(A_ETH_RLUSD).balanceOf(ADDRESS_WITH_CLAIMABLE_REWARDS);
77+
uint256 beforeBalanceOfWrappedAETHRLUSD = IERC20(WRAPPED_A_ETH_RLUSD).balanceOf(ADDRESS_WITH_CLAIMABLE_REWARDS);
78+
79+
address[] memory tokens = new address[](2);
80+
tokens[0] = address(WRAPPED_A_HOR_RWA_RLUSD);
81+
tokens[1] = address(WRAPPED_A_ETH_RLUSD);
82+
83+
uint256[] memory amounts = new uint256[](2);
84+
amounts[0] = 108475300663546315531064;
85+
amounts[1] = 39262212365620131551622;
86+
87+
bytes32[][] memory proofs = new bytes32[][](2);
88+
proofs[0] = new bytes32[](18);
89+
proofs[1] = new bytes32[](18);
90+
proofs[0][0] = 0xcd60c655efb4b907fcd241863868d14b469487021d66d1edfdbda9472f9a64fa;
91+
proofs[0][1] = 0xb8fb6ed03a4fe66e40d0393936caaf89288f0f0d45a54b2d021e6436e08d5cab;
92+
proofs[0][2] = 0x51da9788480e72478a3983b1fcc6417d6e2fb23f17d82402980bf7a662951ebc;
93+
proofs[0][3] = 0xbd1e1f1f6b3bff2c96cdaac05eef648a12d7b26aff0cb6bf2e01df739326d221;
94+
proofs[0][4] = 0xc3666e909b4378eb3190a55407b3992791f45947ccfb78a450618fd0d2d186ef;
95+
proofs[0][5] = 0xeae511fe93c29449d7dc421a00989e9b90193b1ea95a6727edfd0ac23c85b1a8;
96+
proofs[0][6] = 0x7cb3e28ed93250acb734cf34cf07ae533800fc0161e936ee29c20f36c66e7d4a;
97+
proofs[0][7] = 0x467aa2d6889230db0b841374136b586ded4697016c9cb5b9259677357c9de435;
98+
proofs[0][8] = 0xb1b5b41688ba77f9426265f60c5f9a922062156ab08bdde674dceebacc1c92b3;
99+
proofs[0][9] = 0x51dda19784b96153bcda1086489d5d6c5f17566202c92a0d911cbd2aa7a3ff7a;
100+
proofs[0][10] = 0x4606c269974519cf0d4fe1907baab3ca27c5eef64560408e4137eaf62d273c08;
101+
proofs[0][11] = 0xcc7bdca69b12e1f75d7b26045ba5e0c108de8d85bcee444ac05a02b940a20ea7;
102+
proofs[0][12] = 0x67cfe6ef02168544a983543b06f1823d97e78d17e67bd6833f7fc6b8e3a2cc77;
103+
proofs[0][13] = 0x3f2a8d6cb9b7784fe51ac5719c41e1a973856a7f89f3847b2ec5e259b6977b90;
104+
proofs[0][14] = 0xed9131f643d7100fcdfcebbf4abb97e7bcf31ee08f7ab8fb3230231f6f3f7533;
105+
proofs[0][15] = 0x4dd1263f903416095b1556a5b9473082ee23a3eda267d6f98dbdcb200ba1d648;
106+
proofs[0][16] = 0x5df7d74a8a29e2552ddecdd29e57b8e1daedbc786f9f9ca6d308506f14b13995;
107+
proofs[0][17] = 0xa8568c4dc81b1ee1667791a9567914d57f698ab8f6444fd1bd9fa88b158598c6;
108+
109+
proofs[1][0] = 0x2553b0621d281b8ff48e95ae951cca33ea3468a8938b2e4df5432143652d4e4a;
110+
proofs[1][1] = 0xd98dc700cb20b65209167e4794e111a3e6f5b321e87addd9f31fad5b9227aa77;
111+
proofs[1][2] = 0xef27cdadbbce6f35a63b010602c810e1f610924d97671658f721c39b14314b23;
112+
proofs[1][3] = 0x6e0a90db4e79aaa85f7d02e5b51e731ae6d3c5a55e7f7823e69617dc658cf403;
113+
proofs[1][4] = 0xeaac81bbf63022e8203c818f098166e2a07dc5606ec3c78a8025910420c4bf5f;
114+
proofs[1][5] = 0x757d3fd4bfe411a574405bf91b1cbd2f11987fb4237b8d5fa64176cf0cb20fe3;
115+
proofs[1][6] = 0xbd699a6f29ee97f0234bf9322953a1d284e2c911a0b86aa256f5265fd03df512;
116+
proofs[1][7] = 0xe37c2c040c7977b9d4b05857925086e6ba7e7227b836f241c3f5dddd852852e6;
117+
proofs[1][8] = 0xd981d4e3f9077020c96ba37e849ec7d27de10a84269255a563f4f5387e15a42e;
118+
proofs[1][9] = 0x7755254f566658f3d7cae2cca32b7428ba4faa8471bbec83987cf0748b374f42;
119+
proofs[1][10] = 0x58cd9e5a37c2770d44fdc248cf3e2d7b335156a1613721c3ef390e03637748ac;
120+
proofs[1][11] = 0x53fea05c23a6e006a865c38e05f42ebb46f7ab9e09337e0ee2a064d4b8e2b90a;
121+
proofs[1][12] = 0x4d98b5449e9af673e13565b42fe5129c5d774be4a5d87d46ef7773913161679a;
122+
proofs[1][13] = 0x6b120817b20d5b394061ee70d0c5b222282054a4ecff068943c8f29455a5a0ae;
123+
proofs[1][14] = 0x880e3fa286a8e8f7cda32454aba6f4e03a5a4e6e12765c2ed81d02f1f387cd5d;
124+
proofs[1][15] = 0x9bfa48207586367dd5d549803d11cc165a50760ef6da82c2200289fae3cd9188;
125+
proofs[1][16] = 0xd973d9d68f5e750326e9811fccb748c601432c59b2d2ac08d334218027b0f754;
126+
proofs[1][17] = 0x6fd7ba9a8ab999e39fa33e96b722ab10d2e07d65b53319639a7145f9f0cbfece;
127+
128+
vm.prank(OWNER);
129+
IATokenVaultMerklRewardClaimer(ADDRESS_WITH_CLAIMABLE_REWARDS).claimMerklRewards(tokens, amounts, proofs);
130+
131+
uint256 afterBalanceOfAHorRwaRLUSD = IERC20(A_HOR_RWA_RLUSD).balanceOf(ADDRESS_WITH_CLAIMABLE_REWARDS);
132+
uint256 afterBalanceOfWrappedAHorRwaRLUSD = IERC20(WRAPPED_A_HOR_RWA_RLUSD).balanceOf(ADDRESS_WITH_CLAIMABLE_REWARDS);
133+
uint256 afterBalanceOfAETHRLUSD = IERC20(A_ETH_RLUSD).balanceOf(ADDRESS_WITH_CLAIMABLE_REWARDS);
134+
uint256 afterBalanceOfWrappedAETHRLUSD = IERC20(WRAPPED_A_ETH_RLUSD).balanceOf(ADDRESS_WITH_CLAIMABLE_REWARDS);
135+
136+
// Wrapped token balances should not change because the claimer receives the unwrapped tokens
137+
assertEq(afterBalanceOfWrappedAHorRwaRLUSD, beforeBalanceOfWrappedAHorRwaRLUSD);
138+
assertGt(afterBalanceOfAHorRwaRLUSD, beforeBalanceOfAHorRwaRLUSD);
139+
assertEq(afterBalanceOfWrappedAETHRLUSD, beforeBalanceOfWrappedAETHRLUSD);
140+
assertGt(afterBalanceOfAETHRLUSD, beforeBalanceOfAETHRLUSD);
141+
}
142+
143+
// TODO: add moar tests for the Merkl distributor setter and getter
144+
// TODO: add error path tests
145+
146+
function _setMerklDistributor() internal {
147+
vm.prank(OWNER);
148+
IATokenVaultMerklRewardClaimer(address(vault)).setMerklDistributor(MERKL_DISTRIBUTOR);
149+
}
150+
151+
function _etchVault(address target) internal {
152+
// 1. Fix the Proxy: Copy the implementation address from 'vault' to the etched address
153+
// EIP-1967 Implementation slot: bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
154+
bytes32 implSlot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
155+
bytes32 implementation = vm.load(address(vault), implSlot);
156+
157+
vm.etch(target, address(vault).code);
158+
vm.store(target, implSlot, implementation);
159+
160+
// 2. Fix State: Copy Owner and MerklDistributor
161+
// Based on forge inspect storage layout:
162+
// _owner is at slot 151
163+
uint256 ownerSlot = 151;
164+
vm.store(target, bytes32(ownerSlot), vm.load(address(vault), bytes32(ownerSlot)));
165+
// _s starts at slot 254
166+
// _s.merklDistributor is at slot 256 (254: balances/fees, 255: gap/fee, 256: merkl)
167+
uint256 merklSlot = 256;
168+
vm.store(target, bytes32(merklSlot), vm.load(address(vault), bytes32(merklSlot)));
169+
}
170+
}

0 commit comments

Comments
 (0)