-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
302 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,302 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.0; | ||
|
||
import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; | ||
import "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; | ||
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol"; | ||
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; | ||
import "forge-std/Test.sol"; | ||
|
||
import {NonShortCircuitEndpointV2Mock} from "test/mocks/NonShortCircuitEndpointV2Mock.sol"; | ||
|
||
import "../../src/core/Bootstrap.sol"; | ||
import "../../src/core/ExoCapsule.sol"; | ||
import "../../src/utils/BeaconProxyBytecode.sol"; | ||
import {BeaconChainProofs} from "src/libraries/BeaconChainProofs.sol"; | ||
import {Endian} from "src/libraries/Endian.sol"; | ||
import "test/mocks/ETHPOSDepositMock.sol"; | ||
|
||
import "../../src/core/ExoCapsule.sol"; | ||
import {ClientChainGateway} from "src/core/ClientChainGateway.sol"; | ||
import {Vault} from "src/core/Vault.sol"; | ||
import {CustomProxyAdmin} from "src/utils/CustomProxyAdmin.sol"; | ||
import {MyToken} from "test/foundry/unit/MyToken.sol"; | ||
|
||
contract BootstrapDepositNSTTest is Test { | ||
|
||
using Endian for bytes32; | ||
using stdStorage for StdStorage; | ||
|
||
Bootstrap bootstrap; | ||
Bootstrap bootstrapLogic; | ||
|
||
address deployer = address(0x1); | ||
address owner = address(0x2); | ||
address depositor = address(0x3); | ||
|
||
uint256 spawnTime; | ||
uint256 offsetDuration; | ||
uint16 exocoreChainId = 1; | ||
uint16 clientChainId = 2; | ||
address[] whitelistTokens; | ||
uint256[] tvlLimits; | ||
MyToken myToken; | ||
CustomProxyAdmin proxyAdmin; | ||
NonShortCircuitEndpointV2Mock clientChainLzEndpoint; | ||
|
||
EigenLayerBeaconOracle beaconOracle; | ||
IVault vaultImplementation; | ||
IExoCapsule capsuleImplementation; | ||
IBeacon vaultBeacon; | ||
IBeacon capsuleBeacon; | ||
BeaconProxyBytecode beaconProxyBytecode; | ||
ExoCapsule capsule; | ||
|
||
bytes32[] validatorContainer; | ||
BeaconChainProofs.ValidatorContainerProof validatorProof; | ||
bytes32 beaconBlockRoot; | ||
|
||
uint256 constant BEACON_CHAIN_GENESIS_TIME = 1_606_824_023; | ||
uint64 constant SLOTS_PER_EPOCH = 32; | ||
uint64 constant SECONDS_PER_SLOT = 12; | ||
uint64 constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; | ||
uint256 constant GWEI_TO_WEI = 1e9; | ||
address constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; | ||
IETHPOSDeposit constant ETH_POS = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); | ||
bytes constant BEACON_PROXY_BYTECODE = | ||
hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; | ||
|
||
event DepositResult(bool indexed success, address indexed token, address indexed depositor, uint256 amount); | ||
event CapsuleCreated(address owner, address capsule); | ||
event StakedWithCapsule(address staker, address capsule); | ||
|
||
function setUp() public { | ||
vm.startPrank(deployer); | ||
|
||
whitelistTokens.push(VIRTUAL_STAKED_ETH_ADDRESS); | ||
tvlLimits.push(0); | ||
|
||
// use arbitrary address as beaconOracle | ||
beaconOracle = EigenLayerBeaconOracle(address(0xa)); | ||
|
||
// deploy vault implementationcontract that has logics called by proxy | ||
vaultImplementation = new Vault(); | ||
capsuleImplementation = new ExoCapsule(); | ||
|
||
// deploy the vault beacon that store the implementation contract address | ||
vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); | ||
capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); | ||
|
||
// deploy BeaconProxyBytecode to store BeaconProxyBytecode | ||
beaconProxyBytecode = new BeaconProxyBytecode(); | ||
|
||
// then the ProxyAdmin | ||
proxyAdmin = new CustomProxyAdmin(); | ||
// then the logic | ||
clientChainLzEndpoint = new NonShortCircuitEndpointV2Mock(clientChainId, owner); | ||
// Create ImmutableConfig struct | ||
BootstrapStorage.ImmutableConfig memory config = BootstrapStorage.ImmutableConfig({ | ||
exocoreChainId: exocoreChainId, | ||
beaconOracleAddress: address(beaconOracle), | ||
vaultBeacon: address(vaultBeacon), | ||
exoCapsuleBeacon: address(capsuleBeacon), | ||
beaconProxyBytecode: address(beaconProxyBytecode) | ||
}); | ||
bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); | ||
|
||
ClientChainGateway clientGatewayLogic = | ||
new ClientChainGateway(address(clientChainLzEndpoint), config, address(0xb)); | ||
// we could also use encodeWithSelector and supply .initialize.selector instead. | ||
bytes memory initialization = abi.encodeCall(clientGatewayLogic.initialize, (payable(owner))); | ||
// then the params + proxy | ||
spawnTime = block.timestamp + 1 hours; | ||
offsetDuration = 30 minutes; | ||
bootstrap = Bootstrap( | ||
payable( | ||
address( | ||
new TransparentUpgradeableProxy( | ||
address(bootstrapLogic), | ||
address(proxyAdmin), | ||
abi.encodeCall( | ||
bootstrap.initialize, | ||
( | ||
owner, | ||
spawnTime, | ||
offsetDuration, | ||
whitelistTokens, | ||
tvlLimits, | ||
address(proxyAdmin), | ||
address(clientGatewayLogic), | ||
initialization | ||
) | ||
) | ||
) | ||
) | ||
) | ||
); | ||
// validate the initialization | ||
assertTrue(bootstrap.isWhitelistedToken(VIRTUAL_STAKED_ETH_ADDRESS)); | ||
assertTrue(bootstrap.getWhitelistedTokensCount() == 1); | ||
assertFalse(bootstrap.bootstrapped()); | ||
proxyAdmin.initialize(address(bootstrap)); | ||
|
||
vm.stopPrank(); | ||
|
||
// Load validator data from json file | ||
string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); | ||
|
||
// Parse validator container and proof data | ||
validatorContainer = stdJson.readBytes32Array(validatorInfo, ".ValidatorFields"); | ||
require(validatorContainer.length > 0, "validator container should not be empty"); | ||
|
||
validatorProof.stateRoot = stdJson.readBytes32(validatorInfo, ".beaconStateRoot"); | ||
require(validatorProof.stateRoot != bytes32(0), "state root should not be empty"); | ||
|
||
validatorProof.stateRootProof = | ||
stdJson.readBytes32Array(validatorInfo, ".StateRootAgainstLatestBlockHeaderProof"); | ||
require(validatorProof.stateRootProof.length == 3, "state root proof should have 3 nodes"); | ||
|
||
validatorProof.validatorContainerRootProof = | ||
stdJson.readBytes32Array(validatorInfo, ".WithdrawalCredentialProof"); | ||
require(validatorProof.validatorContainerRootProof.length == 46, "validator root proof should have 46 nodes"); | ||
|
||
validatorProof.validatorIndex = stdJson.readUint(validatorInfo, ".validatorIndex"); | ||
require(validatorProof.validatorIndex != 0, "validator root index should not be 0"); | ||
|
||
beaconBlockRoot = stdJson.readBytes32(validatorInfo, ".latestBlockHeaderRoot"); | ||
require(beaconBlockRoot != bytes32(0), "beacon block root should not be empty"); | ||
} | ||
|
||
function test_VerifyAndDepositNativeStake() public { | ||
vm.startPrank(depositor); | ||
|
||
// setup block environment like the current block timestamp | ||
_simulateBlockEnvironmentForNativeDeposit(); | ||
|
||
// 1. firstly depositor should stake to beacon chain by depositing 32 ETH to ETHPOS contract | ||
IExoCapsule expectedCapsule = IExoCapsule( | ||
Create2.computeAddress( | ||
bytes32(uint256(uint160(depositor))), | ||
keccak256(abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(capsuleBeacon), ""))), | ||
address(bootstrap) | ||
) | ||
); | ||
|
||
vm.expectEmit(true, true, true, true, address(bootstrap)); | ||
emit CapsuleCreated(depositor, address(expectedCapsule)); | ||
vm.expectEmit(address(bootstrap)); | ||
emit StakedWithCapsule(depositor, address(expectedCapsule)); | ||
|
||
vm.deal(depositor, 33 ether); // 32 ETH for deposit and 1 ETH for gas | ||
bootstrap.stake{value: 32 ether}(abi.encodePacked(_getPubkey(validatorContainer)), bytes(""), bytes32(0)); | ||
|
||
// do some hack to replace expectedCapsule address with capsule address loaded from proof file | ||
// because capsule address is expected to be compatible with validator container withdrawal credentails | ||
_attachCapsuleToWithdrawalCredentials(expectedCapsule, depositor); | ||
|
||
// Calculate expected deposit value | ||
uint256 expectedDepositValue = uint256(_getEffectiveBalance(validatorContainer)) * GWEI_TO_WEI; | ||
if (expectedDepositValue > 32 ether) { | ||
expectedDepositValue = 32 ether; | ||
} | ||
|
||
// Record initial states | ||
uint256 initialDeposit = bootstrap.totalDepositAmounts(depositor, VIRTUAL_STAKED_ETH_ADDRESS); | ||
uint256 initialWithdrawable = bootstrap.withdrawableAmounts(depositor, VIRTUAL_STAKED_ETH_ADDRESS); | ||
uint256 initialDepositsByToken = bootstrap.depositsByToken(VIRTUAL_STAKED_ETH_ADDRESS); | ||
|
||
// Verify and deposit with real validator data | ||
vm.expectEmit(true, true, true, true); | ||
emit DepositResult(true, VIRTUAL_STAKED_ETH_ADDRESS, depositor, expectedDepositValue); | ||
bootstrap.verifyAndDepositNativeStake(validatorContainer, validatorProof); | ||
|
||
// Verify state changes | ||
assertEq( | ||
bootstrap.totalDepositAmounts(depositor, VIRTUAL_STAKED_ETH_ADDRESS), | ||
initialDeposit + expectedDepositValue, | ||
"Total deposit amount should increase" | ||
); | ||
assertEq( | ||
bootstrap.withdrawableAmounts(depositor, VIRTUAL_STAKED_ETH_ADDRESS), | ||
initialWithdrawable + expectedDepositValue, | ||
"Withdrawable amount should increase" | ||
); | ||
assertEq( | ||
bootstrap.depositsByToken(VIRTUAL_STAKED_ETH_ADDRESS), | ||
initialDepositsByToken + expectedDepositValue, | ||
"Deposits by token should increase" | ||
); | ||
assertTrue(bootstrap.isDepositor(depositor), "Should be marked as depositor"); | ||
|
||
vm.stopPrank(); | ||
} | ||
|
||
function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { | ||
return address(bytes20(uint160(uint256(withdrawalCredentials)))); | ||
} | ||
|
||
function _getPubkey(bytes32[] storage vc) internal view returns (bytes32) { | ||
return vc[0]; | ||
} | ||
|
||
function _getWithdrawalCredentials(bytes32[] storage vc) internal view returns (bytes32) { | ||
return vc[1]; | ||
} | ||
|
||
function _getActivationEpoch(bytes32[] storage vc) internal view returns (uint64) { | ||
return vc[5].fromLittleEndianUint64(); | ||
} | ||
|
||
function _getExitEpoch(bytes32[] storage vc) internal view returns (uint64) { | ||
return vc[6].fromLittleEndianUint64(); | ||
} | ||
|
||
function _getEffectiveBalance(bytes32[] storage vc) internal view returns (uint64) { | ||
return vc[2].fromLittleEndianUint64(); | ||
} | ||
|
||
function _getWithdrawalAmount(bytes32[] storage wc) internal view returns (uint64) { | ||
return wc[3].fromLittleEndianUint64(); | ||
} | ||
|
||
function _simulateBlockEnvironmentForNativeDeposit() internal { | ||
/// we set the timestamp of proof to be exactly the timestamp that the validator container get activated on | ||
/// beacon chain | ||
uint256 activationTimestamp = | ||
BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; | ||
uint256 mockProofTimestamp = activationTimestamp; | ||
validatorProof.beaconBlockTimestamp = mockProofTimestamp; | ||
|
||
/// we set current block timestamp to be exactly one slot after the proof generation timestamp | ||
uint256 mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; | ||
vm.warp(mockCurrentBlockTimestamp); | ||
|
||
/// we mock the call beaconOracle.timestampToBlockRoot to return the expected block root in proof file | ||
vm.mockCall( | ||
address(beaconOracle), | ||
abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), | ||
abi.encode(beaconBlockRoot) | ||
); | ||
|
||
// mock the call ETH_POS.deposit to return true | ||
vm.mockCall(address(ETH_POS), abi.encodeWithSelector(ETH_POS.deposit.selector), abi.encode(true)); | ||
} | ||
|
||
function _attachCapsuleToWithdrawalCredentials(IExoCapsule createdCapsule, address depositor_) internal { | ||
address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); | ||
vm.etch(capsuleAddress, address(createdCapsule).code); | ||
capsule = ExoCapsule(payable(capsuleAddress)); | ||
stdstore.target(capsuleAddress).sig("_beacon()").checked_write(address(capsuleBeacon)); | ||
assertEq(stdstore.target(capsuleAddress).sig("_beacon()").read_address(), address(capsuleBeacon)); | ||
|
||
/// replace expectedCapsule with capsule | ||
bytes32 capsuleSlotInGateway = | ||
bytes32(stdstore.target(address(bootstrapLogic)).sig("ownerToCapsule(address)").with_key(depositor_).find()); | ||
vm.store(address(bootstrap), capsuleSlotInGateway, bytes32(uint256(uint160(address(capsule))))); | ||
assertEq(address(bootstrap.ownerToCapsule(depositor_)), address(capsule)); | ||
|
||
/// initialize replaced capsule | ||
capsule.initialize(address(bootstrap), payable(depositor_), address(beaconOracle)); | ||
} | ||
|
||
} |