From 9d710175aa6b0aa14172c45b02d54b1c2a364c2b Mon Sep 17 00:00:00 2001 From: adu Date: Tue, 20 Feb 2024 11:50:07 +0800 Subject: [PATCH 01/93] initialize ExocoreCapsule --- hardhat.config.js | 4 +- src/core/ExoCapsule.sol | 87 ++++++ src/interfaces/IETHPOSDeposit.sol | 41 +++ src/interfaces/IExoCapsule.sol | 32 ++ src/libraries/BeaconChainProofs.sol | 440 ++++++++++++++++++++++++++++ src/libraries/Endian.sol | 25 ++ src/libraries/Merkle.sol | 172 +++++++++++ src/storage/ExoCapsuleStorage.sol | 7 + 8 files changed, 806 insertions(+), 2 deletions(-) create mode 100644 src/core/ExoCapsule.sol create mode 100644 src/interfaces/IETHPOSDeposit.sol create mode 100644 src/interfaces/IExoCapsule.sol create mode 100644 src/libraries/BeaconChainProofs.sol create mode 100644 src/libraries/Endian.sol create mode 100644 src/libraries/Merkle.sol create mode 100644 src/storage/ExoCapsuleStorage.sol diff --git a/hardhat.config.js b/hardhat.config.js index 397e1205..dc5c602e 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -21,8 +21,8 @@ module.exports = { chainId: 9000, url: "http://23.162.56.84:8545", accounts: [ - process.env.EXOCORE_DEPLOYER_PRIVATE_KEY, - process.env.EXOCORE_VALIDATOR_SET_PRIVATE_KEY + // process.env.EXOCORE_DEPLOYER_PRIVATE_KEY, + // process.env.EXOCORE_VALIDATOR_SET_PRIVATE_KEY ] } } diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol new file mode 100644 index 00000000..d9c5077b --- /dev/null +++ b/src/core/ExoCapsule.sol @@ -0,0 +1,87 @@ +pragma solidity ^0.8.19; + +import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; +import {ExoCapsuleStorage} from "../storage/ExoCapsuleStorage.sol"; +import {BeaconChainProofs} from "../libraries/BeaconChainProofs.sol"; +import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; +import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; + +contract ExoCapsule is + Initializable, + OwnableUpgradeable, + PausableUpgradeable, + ExoCapsuleStorage, + IExoCapsule +{ + using BeaconChainProofs for bytes; + + IETHPOSDeposit public immutable ethPOS; + + constructor(IETHPOSDeposit _ethPOS) { + ethPOS = _ethPOS; + + _disableInitializers(); + } + + function initialize( + address payable _ExocoreValidatorSetAddress + ) + external + initializer + { + require(_ExocoreValidatorSetAddress != address(0), "invalid empty exocore validator set address"); + exocoreValidatorSetAddress = _ExocoreValidatorSetAddress; + + _transferOwnership(exocoreValidatorSetAddress); + __Pausable_init(); + } + + function pause() external { + require(msg.sender == exocoreValidatorSetAddress, "only Exocore validator set aggregated address could call this"); + _pause(); + } + + function unpause() external { + require(msg.sender == exocoreValidatorSetAddress, "only Exocore validator set aggregated address could call this"); + _unpause(); + } + + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable { + + } + + function deposit( + uint64 beaconBlockTimestamp, + bytes32 beaconStateRoot, + bytes[] calldata beaconStateRootProof, + bytes32[][] calldata validatorFields, + uint40[] calldata validatorProofIndices, + bytes[] calldata validatorFieldsProof + ) external { + + } + + function updateStakeBalance( + uint64 beaconBlockTimestamp, + bytes32 beaconStateRoot, + bytes[] calldata beaconStateRootProof, + bytes32[][] calldata validatorFields, + uint40[] calldata validatorProofIndices, + bytes[] calldata validatorFieldsProof + ) external { + + } + + function withdraw( + uint64 beaconBlockTimestamp, + bytes32 beaconStateRoot, + bytes[] calldata beaconStateRootProof, + bytes32[][] calldata withdrawalFields, + uint40[] calldata withdrawalProofIndices, + bytes[] calldata withdrawalFieldsProof + ) external { + + } +} diff --git a/src/interfaces/IETHPOSDeposit.sol b/src/interfaces/IETHPOSDeposit.sol new file mode 100644 index 00000000..5fc09a5c --- /dev/null +++ b/src/interfaces/IETHPOSDeposit.sol @@ -0,0 +1,41 @@ +// ┏━━━┓━┏┓━┏┓━━┏━━━┓━━┏━━━┓━━━━┏━━━┓━━━━━━━━━━━━━━━━━━━┏┓━━━━━┏━━━┓━━━━━━━━━┏┓━━━━━━━━━━━━━━┏┓━ +// ┃┏━━┛┏┛┗┓┃┃━━┃┏━┓┃━━┃┏━┓┃━━━━┗┓┏┓┃━━━━━━━━━━━━━━━━━━┏┛┗┓━━━━┃┏━┓┃━━━━━━━━┏┛┗┓━━━━━━━━━━━━┏┛┗┓ +// ┃┗━━┓┗┓┏┛┃┗━┓┗┛┏┛┃━━┃┃━┃┃━━━━━┃┃┃┃┏━━┓┏━━┓┏━━┓┏━━┓┏┓┗┓┏┛━━━━┃┃━┗┛┏━━┓┏━┓━┗┓┏┛┏━┓┏━━┓━┏━━┓┗┓┏┛ +// ┃┏━━┛━┃┃━┃┏┓┃┏━┛┏┛━━┃┃━┃┃━━━━━┃┃┃┃┃┏┓┃┃┏┓┃┃┏┓┃┃━━┫┣┫━┃┃━━━━━┃┃━┏┓┃┏┓┃┃┏┓┓━┃┃━┃┏┛┗━┓┃━┃┏━┛━┃┃━ +// ┃┗━━┓━┃┗┓┃┃┃┃┃┃┗━┓┏┓┃┗━┛┃━━━━┏┛┗┛┃┃┃━┫┃┗┛┃┃┗┛┃┣━━┃┃┃━┃┗┓━━━━┃┗━┛┃┃┗┛┃┃┃┃┃━┃┗┓┃┃━┃┗┛┗┓┃┗━┓━┃┗┓ +// ┗━━━┛━┗━┛┗┛┗┛┗━━━┛┗┛┗━━━┛━━━━┗━━━┛┗━━┛┃┏━┛┗━━┛┗━━┛┗┛━┗━┛━━━━┗━━━┛┗━━┛┗┛┗┛━┗━┛┗┛━┗━━━┛┗━━┛━┗━┛ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┃┃━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┗┛━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +// SPDX-License-Identifier: CC0-1.0 + +pragma solidity >=0.5.0; + +// This interface is designed to be compatible with the Vyper version. +/// @notice This is the Ethereum 2.0 deposit contract interface. +/// For more information see the Phase 0 specification under https://github.com/ethereum/eth2.0-specs +interface IETHPOSDeposit { + /// @notice A processed deposit event. + event DepositEvent(bytes pubkey, bytes withdrawal_credentials, bytes amount, bytes signature, bytes index); + + /// @notice Submit a Phase 0 DepositData object. + /// @param pubkey A BLS12-381 public key. + /// @param withdrawal_credentials Commitment to a public key for withdrawals. + /// @param signature A BLS12-381 signature. + /// @param deposit_data_root The SHA-256 hash of the SSZ-encoded DepositData object. + /// Used as a protection against malformed input. + function deposit( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 deposit_data_root + ) external payable; + + /// @notice Query the current deposit root hash. + /// @return The deposit root hash. + function get_deposit_root() external view returns (bytes32); + + /// @notice Query the current deposit count. + /// @return The deposit count encoded as a little endian 64-bit number. + function get_deposit_count() external view returns (bytes memory); +} diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol new file mode 100644 index 00000000..9f6449b7 --- /dev/null +++ b/src/interfaces/IExoCapsule.sol @@ -0,0 +1,32 @@ +pragma solidity ^0.8.19; + +interface IExoCapsule { + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; + + function deposit( + uint64 beaconBlockTimestamp, + bytes32 beaconStateRoot, + bytes[] calldata beaconStateRootProof, + bytes32[][] calldata validatorFields, + uint40[] calldata validatorProofIndices, + bytes[] calldata validatorFieldsProof + ) external; + + function updateStakeBalance( + uint64 beaconBlockTimestamp, + bytes32 beaconStateRoot, + bytes[] calldata beaconStateRootProof, + bytes32[][] calldata validatorFields, + uint40[] calldata validatorProofIndices, + bytes[] calldata validatorFieldsProof + ) external; + + function withdraw( + uint64 beaconBlockTimestamp, + bytes32 beaconStateRoot, + bytes[] calldata beaconStateRootProof, + bytes32[][] calldata withdrawalFields, + uint40[] calldata withdrawalProofIndices, + bytes[] calldata withdrawalFieldsProof + ) external; +} \ No newline at end of file diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol new file mode 100644 index 00000000..e488eedf --- /dev/null +++ b/src/libraries/BeaconChainProofs.sol @@ -0,0 +1,440 @@ +// SPDX-License-Identifier: BUSL-1.1 + +pragma solidity ^0.8.0; + +import "./Merkle.sol"; +import "../libraries/Endian.sol"; + +//Utility library for parsing and PHASE0 beacon chain block headers +//SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization +//BeaconBlockHeader Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader +//BeaconState Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate +library BeaconChainProofs { + // constants are the number of fields and the heights of the different merkle trees used in merkleizing beacon chain containers + uint256 internal constant NUM_BEACON_BLOCK_HEADER_FIELDS = 5; + uint256 internal constant BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT = 3; + + uint256 internal constant NUM_BEACON_BLOCK_BODY_FIELDS = 11; + uint256 internal constant BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT = 4; + + uint256 internal constant NUM_BEACON_STATE_FIELDS = 21; + uint256 internal constant BEACON_STATE_FIELD_TREE_HEIGHT = 5; + + uint256 internal constant NUM_ETH1_DATA_FIELDS = 3; + uint256 internal constant ETH1_DATA_FIELD_TREE_HEIGHT = 2; + + uint256 internal constant NUM_VALIDATOR_FIELDS = 8; + uint256 internal constant VALIDATOR_FIELD_TREE_HEIGHT = 3; + + uint256 internal constant NUM_EXECUTION_PAYLOAD_HEADER_FIELDS = 15; + uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT = 4; + + uint256 internal constant NUM_EXECUTION_PAYLOAD_FIELDS = 15; + uint256 internal constant EXECUTION_PAYLOAD_FIELD_TREE_HEIGHT = 4; + + // HISTORICAL_ROOTS_LIMIT = 2**24, so tree height is 24 + uint256 internal constant HISTORICAL_ROOTS_TREE_HEIGHT = 24; + + // HISTORICAL_BATCH is root of state_roots and block_root, so number of leaves = 2^1 + uint256 internal constant HISTORICAL_BATCH_TREE_HEIGHT = 1; + + // SLOTS_PER_HISTORICAL_ROOT = 2**13, so tree height is 13 + uint256 internal constant STATE_ROOTS_TREE_HEIGHT = 13; + uint256 internal constant BLOCK_ROOTS_TREE_HEIGHT = 13; + + //HISTORICAL_ROOTS_LIMIT = 2**24, so tree height is 24 + uint256 internal constant HISTORICAL_SUMMARIES_TREE_HEIGHT = 24; + + //Index of block_summary_root in historical_summary container + uint256 internal constant BLOCK_SUMMARY_ROOT_INDEX = 0; + + uint256 internal constant NUM_WITHDRAWAL_FIELDS = 4; + // tree height for hash tree of an individual withdrawal container + uint256 internal constant WITHDRAWAL_FIELD_TREE_HEIGHT = 2; + + uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; + + // MAX_WITHDRAWALS_PER_PAYLOAD = 2**4, making tree height = 4 + uint256 internal constant WITHDRAWALS_TREE_HEIGHT = 4; + + //in beacon block body https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconblockbody + uint256 internal constant EXECUTION_PAYLOAD_INDEX = 9; + + // in beacon block header https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader + uint256 internal constant SLOT_INDEX = 0; + uint256 internal constant PROPOSER_INDEX_INDEX = 1; + uint256 internal constant STATE_ROOT_INDEX = 3; + uint256 internal constant BODY_ROOT_INDEX = 4; + // in beacon state https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate + uint256 internal constant HISTORICAL_BATCH_STATE_ROOT_INDEX = 1; + uint256 internal constant BEACON_STATE_SLOT_INDEX = 2; + uint256 internal constant LATEST_BLOCK_HEADER_ROOT_INDEX = 4; + uint256 internal constant BLOCK_ROOTS_INDEX = 5; + uint256 internal constant STATE_ROOTS_INDEX = 6; + uint256 internal constant HISTORICAL_ROOTS_INDEX = 7; + uint256 internal constant ETH_1_ROOT_INDEX = 8; + uint256 internal constant VALIDATOR_TREE_ROOT_INDEX = 11; + uint256 internal constant EXECUTION_PAYLOAD_HEADER_INDEX = 24; + uint256 internal constant HISTORICAL_SUMMARIES_INDEX = 27; + + // in validator https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + uint256 internal constant VALIDATOR_PUBKEY_INDEX = 0; + uint256 internal constant VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX = 1; + uint256 internal constant VALIDATOR_BALANCE_INDEX = 2; + uint256 internal constant VALIDATOR_SLASHED_INDEX = 3; + uint256 internal constant VALIDATOR_WITHDRAWABLE_EPOCH_INDEX = 7; + + // in execution payload header + uint256 internal constant TIMESTAMP_INDEX = 9; + uint256 internal constant WITHDRAWALS_ROOT_INDEX = 14; + + //in execution payload + uint256 internal constant WITHDRAWALS_INDEX = 14; + + // in withdrawal + uint256 internal constant WITHDRAWAL_VALIDATOR_INDEX_INDEX = 1; + uint256 internal constant WITHDRAWAL_VALIDATOR_AMOUNT_INDEX = 3; + + //In historicalBatch + uint256 internal constant HISTORICALBATCH_STATEROOTS_INDEX = 1; + + //Misc Constants + + /// @notice The number of slots each epoch in the beacon chain + uint64 internal constant SLOTS_PER_EPOCH = 32; + + /// @notice The number of seconds in a slot in the beacon chain + uint64 internal constant SECONDS_PER_SLOT = 12; + + /// @notice Number of seconds per epoch: 384 == 32 slots/epoch * 12 seconds/slot + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + + bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; + + /// @notice This struct contains the merkle proofs and leaves needed to verify a partial/full withdrawal + struct WithdrawalProof { + bytes withdrawalProof; + bytes slotProof; + bytes executionPayloadProof; + bytes timestampProof; + bytes historicalSummaryBlockRootProof; + uint64 blockRootIndex; + uint64 historicalSummaryIndex; + uint64 withdrawalIndex; + bytes32 blockRoot; + bytes32 slotRoot; + bytes32 timestampRoot; + bytes32 executionPayloadRoot; + } + + /// @notice This struct contains the root and proof for verifying the state root against the oracle block root + struct StateRootProof { + bytes32 beaconStateRoot; + bytes proof; + } + + /** + * @notice This function verifies merkle proofs of the fields of a certain validator against a beacon chain state root + * @param validatorIndex the index of the proven validator + * @param beaconStateRoot is the beacon chain state root to be proven against. + * @param validatorFieldsProof is the data used in proving the validator's fields + * @param validatorFields the claimed fields of the validator + */ + function verifyValidatorFields( + bytes32 beaconStateRoot, + bytes32[] calldata validatorFields, + bytes calldata validatorFieldsProof, + uint40 validatorIndex + ) internal view { + require( + validatorFields.length == 2 ** VALIDATOR_FIELD_TREE_HEIGHT, + "BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length" + ); + + /** + * Note: the length of the validator merkle proof is BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1. + * There is an additional layer added by hashing the root with the length of the validator list + */ + require( + validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_FIELD_TREE_HEIGHT), + "BeaconChainProofs.verifyValidatorFields: Proof has incorrect length" + ); + uint256 index = (VALIDATOR_TREE_ROOT_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); + // merkleize the validatorFields to get the leaf to prove + bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields); + + // verify the proof of the validatorRoot against the beaconStateRoot + require( + Merkle.verifyInclusionSha256({ + proof: validatorFieldsProof, + root: beaconStateRoot, + leaf: validatorRoot, + index: index + }), + "BeaconChainProofs.verifyValidatorFields: Invalid merkle proof" + ); + } + + /** + * @notice This function verifies the latestBlockHeader against the state root. the latestBlockHeader is + * a tracked in the beacon state. + * @param beaconStateRoot is the beacon chain state root to be proven against. + * @param stateRootProof is the provided merkle proof + * @param latestBlockRoot is hashtree root of the latest block header in the beacon state + */ + function verifyStateRootAgainstLatestBlockRoot( + bytes32 latestBlockRoot, + bytes32 beaconStateRoot, + bytes calldata stateRootProof + ) internal view { + require( + stateRootProof.length == 32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT), + "BeaconChainProofs.verifyStateRootAgainstLatestBlockRoot: Proof has incorrect length" + ); + //Next we verify the slot against the blockRoot + require( + Merkle.verifyInclusionSha256({ + proof: stateRootProof, + root: latestBlockRoot, + leaf: beaconStateRoot, + index: STATE_ROOT_INDEX + }), + "BeaconChainProofs.verifyStateRootAgainstLatestBlockRoot: Invalid latest block header root merkle proof" + ); + } + + /** + * @notice This function verifies the slot and the withdrawal fields for a given withdrawal + * @param withdrawalProof is the provided set of merkle proofs + * @param withdrawalFields is the serialized withdrawal container to be proven + */ + function verifyWithdrawal( + bytes32 beaconStateRoot, + bytes32[] calldata withdrawalFields, + WithdrawalProof calldata withdrawalProof + ) internal view { + require( + withdrawalFields.length == 2 ** WITHDRAWAL_FIELD_TREE_HEIGHT, + "BeaconChainProofs.verifyWithdrawal: withdrawalFields has incorrect length" + ); + + require( + withdrawalProof.blockRootIndex < 2 ** BLOCK_ROOTS_TREE_HEIGHT, + "BeaconChainProofs.verifyWithdrawal: blockRootIndex is too large" + ); + require( + withdrawalProof.withdrawalIndex < 2 ** WITHDRAWALS_TREE_HEIGHT, + "BeaconChainProofs.verifyWithdrawal: withdrawalIndex is too large" + ); + + require( + withdrawalProof.historicalSummaryIndex < 2 ** HISTORICAL_SUMMARIES_TREE_HEIGHT, + "BeaconChainProofs.verifyWithdrawal: historicalSummaryIndex is too large" + ); + + require( + withdrawalProof.withdrawalProof.length == + 32 * (EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT + WITHDRAWALS_TREE_HEIGHT + 1), + "BeaconChainProofs.verifyWithdrawal: withdrawalProof has incorrect length" + ); + require( + withdrawalProof.executionPayloadProof.length == + 32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT), + "BeaconChainProofs.verifyWithdrawal: executionPayloadProof has incorrect length" + ); + require( + withdrawalProof.slotProof.length == 32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT), + "BeaconChainProofs.verifyWithdrawal: slotProof has incorrect length" + ); + require( + withdrawalProof.timestampProof.length == 32 * (EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT), + "BeaconChainProofs.verifyWithdrawal: timestampProof has incorrect length" + ); + + require( + withdrawalProof.historicalSummaryBlockRootProof.length == + 32 * + (BEACON_STATE_FIELD_TREE_HEIGHT + + (HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + + 1 + + (BLOCK_ROOTS_TREE_HEIGHT)), + "BeaconChainProofs.verifyWithdrawal: historicalSummaryBlockRootProof has incorrect length" + ); + /** + * Note: Here, the "1" in "1 + (BLOCK_ROOTS_TREE_HEIGHT)" signifies that extra step of choosing the "block_root_summary" within the individual + * "historical_summary". Everywhere else it signifies merkelize_with_mixin, where the length of an array is hashed with the root of the array, + * but not here. + */ + uint256 historicalBlockHeaderIndex = (HISTORICAL_SUMMARIES_INDEX << + ((HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + 1 + (BLOCK_ROOTS_TREE_HEIGHT))) | + (uint256(withdrawalProof.historicalSummaryIndex) << (1 + (BLOCK_ROOTS_TREE_HEIGHT))) | + (BLOCK_SUMMARY_ROOT_INDEX << (BLOCK_ROOTS_TREE_HEIGHT)) | + uint256(withdrawalProof.blockRootIndex); + + require( + Merkle.verifyInclusionSha256({ + proof: withdrawalProof.historicalSummaryBlockRootProof, + root: beaconStateRoot, + leaf: withdrawalProof.blockRoot, + index: historicalBlockHeaderIndex + }), + "BeaconChainProofs.verifyWithdrawal: Invalid historicalsummary merkle proof" + ); + + //Next we verify the slot against the blockRoot + require( + Merkle.verifyInclusionSha256({ + proof: withdrawalProof.slotProof, + root: withdrawalProof.blockRoot, + leaf: withdrawalProof.slotRoot, + index: SLOT_INDEX + }), + "BeaconChainProofs.verifyWithdrawal: Invalid slot merkle proof" + ); + + { + // Next we verify the executionPayloadRoot against the blockRoot + uint256 executionPayloadIndex = (BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT)) | + EXECUTION_PAYLOAD_INDEX; + require( + Merkle.verifyInclusionSha256({ + proof: withdrawalProof.executionPayloadProof, + root: withdrawalProof.blockRoot, + leaf: withdrawalProof.executionPayloadRoot, + index: executionPayloadIndex + }), + "BeaconChainProofs.verifyWithdrawal: Invalid executionPayload merkle proof" + ); + } + + // Next we verify the timestampRoot against the executionPayload root + require( + Merkle.verifyInclusionSha256({ + proof: withdrawalProof.timestampProof, + root: withdrawalProof.executionPayloadRoot, + leaf: withdrawalProof.timestampRoot, + index: TIMESTAMP_INDEX + }), + "BeaconChainProofs.verifyWithdrawal: Invalid blockNumber merkle proof" + ); + + { + /** + * Next we verify the withdrawal fields against the blockRoot: + * First we compute the withdrawal_index relative to the blockRoot by concatenating the indexes of all the + * intermediate root indexes from the bottom of the sub trees (the withdrawal container) to the top, the blockRoot. + * Then we calculate merkleize the withdrawalFields container to calculate the the withdrawalRoot. + * Finally we verify the withdrawalRoot against the executionPayloadRoot. + * + * + * Note: Merkleization of the withdrawals root tree uses MerkleizeWithMixin, i.e., the length of the array is hashed with the root of + * the array. Thus we shift the WITHDRAWALS_INDEX over by WITHDRAWALS_TREE_HEIGHT + 1 and not just WITHDRAWALS_TREE_HEIGHT. + */ + uint256 withdrawalIndex = (WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1)) | + uint256(withdrawalProof.withdrawalIndex); + bytes32 withdrawalRoot = Merkle.merkleizeSha256(withdrawalFields); + require( + Merkle.verifyInclusionSha256({ + proof: withdrawalProof.withdrawalProof, + root: withdrawalProof.executionPayloadRoot, + leaf: withdrawalRoot, + index: withdrawalIndex + }), + "BeaconChainProofs.verifyWithdrawal: Invalid withdrawal merkle proof" + ); + } + } + + /** + * @notice This function replicates the ssz hashing of a validator's pubkey, outlined below: + * hh := ssz.NewHasher() + * hh.PutBytes(validatorPubkey[:]) + * validatorPubkeyHash := hh.Hash() + * hh.Reset() + */ + function hashValidatorBLSPubkey(bytes memory validatorPubkey) internal pure returns (bytes32 pubkeyHash) { + require(validatorPubkey.length == 48, "Input should be 48 bytes in length"); + return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); + } + + /** + * @dev Retrieve the withdrawal timestamp + */ + function getWithdrawalTimestamp(WithdrawalProof memory withdrawalProof) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(withdrawalProof.timestampRoot); + } + + /** + * @dev Converts the withdrawal's slot to an epoch + */ + function getWithdrawalEpoch(WithdrawalProof memory withdrawalProof) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(withdrawalProof.slotRoot) / SLOTS_PER_EPOCH; + } + + /** + * Indices for validator fields (refer to consensus specs): + * 0: pubkey + * 1: withdrawal credentials + * 2: effective balance + * 3: slashed? + * 4: activation elligibility epoch + * 5: activation epoch + * 6: exit epoch + * 7: withdrawable epoch + */ + + /** + * @dev Retrieves a validator's pubkey hash + */ + function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return + validatorFields[VALIDATOR_PUBKEY_INDEX]; + } + + function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { + return + validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; + } + + /** + * @dev Retrieves a validator's effective balance (in gwei) + */ + function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); + } + + /** + * @dev Retrieves a validator's withdrawable epoch + */ + function getWithdrawableEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]); + } + + /** + * Indices for withdrawal fields (refer to consensus specs): + * 0: withdrawal index + * 1: validator index + * 2: execution address + * 3: withdrawal amount + */ + + /** + * @dev Retrieves a withdrawal's validator index + */ + function getValidatorIndex(bytes32[] memory withdrawalFields) internal pure returns (uint40) { + return + uint40(Endian.fromLittleEndianUint64(withdrawalFields[WITHDRAWAL_VALIDATOR_INDEX_INDEX])); + } + + /** + * @dev Retrieves a withdrawal's withdrawal amount (in gwei) + */ + function getWithdrawalAmountGwei(bytes32[] memory withdrawalFields) internal pure returns (uint64) { + return + Endian.fromLittleEndianUint64(withdrawalFields[WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); + } +} diff --git a/src/libraries/Endian.sol b/src/libraries/Endian.sol new file mode 100644 index 00000000..ac996ce3 --- /dev/null +++ b/src/libraries/Endian.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +library Endian { + /** + * @notice Converts a little endian-formatted uint64 to a big endian-formatted uint64 + * @param lenum little endian-formatted uint64 input, provided as 'bytes32' type + * @return n The big endian-formatted uint64 + * @dev Note that the input is formatted as a 'bytes32' type (i.e. 256 bits), but it is immediately truncated to a uint64 (i.e. 64 bits) + * through a right-shift/shr operation. + */ + function fromLittleEndianUint64(bytes32 lenum) internal pure returns (uint64 n) { + // the number needs to be stored in little-endian encoding (ie in bytes 0-8) + n = uint64(uint256(lenum >> 192)); + return + (n >> 56) | + ((0x00FF000000000000 & n) >> 40) | + ((0x0000FF0000000000 & n) >> 24) | + ((0x000000FF00000000 & n) >> 8) | + ((0x00000000FF000000 & n) << 8) | + ((0x0000000000FF0000 & n) << 24) | + ((0x000000000000FF00 & n) << 40) | + ((0x00000000000000FF & n) << 56); + } +} diff --git a/src/libraries/Merkle.sol b/src/libraries/Merkle.sol new file mode 100644 index 00000000..9da153a5 --- /dev/null +++ b/src/libraries/Merkle.sol @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: BUSL-1.1 +// Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) + +pragma solidity ^0.8.0; + +/** + * @dev These functions deal with verification of Merkle Tree proofs. + * + * The tree and the proofs can be generated using our + * https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. + * You will find a quickstart guide in the readme. + * + * WARNING: You should avoid using leaf values that are 64 bytes long prior to + * hashing, or use a hash function other than keccak256 for hashing leaves. + * This is because the concatenation of a sorted pair of internal nodes in + * the merkle tree could be reinterpreted as a leaf value. + * OpenZeppelin's JavaScript library generates merkle trees that are safe + * against this attack out of the box. + */ +library Merkle { + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function verifyInclusionKeccak( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal pure returns (bool) { + return processInclusionProofKeccak(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the keccak/sha3 hash function + */ + function processInclusionProofKeccak( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal pure returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofKeccak: proof length should be a non-zero multiple of 32" + ); + bytes32 computedHash = leaf; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, computedHash) + mstore(0x20, mload(add(proof, i))) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, computedHash) + computedHash := keccak256(0x00, 0x40) + index := div(index, 2) + } + } + } + return computedHash; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function verifyInclusionSha256( + bytes memory proof, + bytes32 root, + bytes32 leaf, + uint256 index + ) internal view returns (bool) { + return processInclusionProofSha256(proof, leaf, index) == root; + } + + /** + * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up + * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt + * hash matches the root of the tree. The tree is built assuming `leaf` is + * the 0 indexed `index`'th leaf from the bottom left of the tree. + * + * _Available since v4.4._ + * + * Note this is for a Merkle tree using the sha256 hash function + */ + function processInclusionProofSha256( + bytes memory proof, + bytes32 leaf, + uint256 index + ) internal view returns (bytes32) { + require( + proof.length != 0 && proof.length % 32 == 0, + "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" + ); + bytes32[1] memory computedHash = [leaf]; + for (uint256 i = 32; i <= proof.length; i += 32) { + if (index % 2 == 0) { + // if ith bit of index is 0, then computedHash is a left sibling + assembly { + mstore(0x00, mload(computedHash)) + mstore(0x20, mload(add(proof, i))) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { + revert(0, 0) + } + index := div(index, 2) + } + } else { + // if ith bit of index is 1, then computedHash is a right sibling + assembly { + mstore(0x00, mload(add(proof, i))) + mstore(0x20, mload(computedHash)) + if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { + revert(0, 0) + } + index := div(index, 2) + } + } + } + return computedHash[0]; + } + + /** + @notice this function returns the merkle root of a tree created from a set of leaves using sha256 as its hash function + @param leaves the leaves of the merkle tree + @return The computed Merkle root of the tree. + @dev A pre-condition to this function is that leaves.length is a power of two. If not, the function will merkleize the inputs incorrectly. + */ + function merkleizeSha256(bytes32[] memory leaves) internal pure returns (bytes32) { + //there are half as many nodes in the layer above the leaves + uint256 numNodesInLayer = leaves.length / 2; + //create a layer to store the internal nodes + bytes32[] memory layer = new bytes32[](numNodesInLayer); + //fill the layer with the pairwise hashes of the leaves + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + //while we haven't computed the root + while (numNodesInLayer != 0) { + //overwrite the first numNodesInLayer nodes in layer with the pairwise hashes of their children + for (uint256 i = 0; i < numNodesInLayer; i++) { + layer[i] = sha256(abi.encodePacked(layer[2 * i], layer[2 * i + 1])); + } + //the next layer above has half as many nodes + numNodesInLayer /= 2; + } + //the first node in the layer is the root + return layer[0]; + } +} diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol new file mode 100644 index 00000000..a06afe7f --- /dev/null +++ b/src/storage/ExoCapsuleStorage.sol @@ -0,0 +1,7 @@ +pragma solidity ^0.8.19; + +contract ExoCapsuleStorage { + address payable exocoreValidatorSetAddress; + + uint256[40] private __gap; +} \ No newline at end of file From e440d18a1a2e339cc5a7ec784c9f9c5fc3c178e8 Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 23 Feb 2024 00:18:43 +0800 Subject: [PATCH 02/93] add ValidatorContainer library --- src/core/ExoCapsule.sol | 27 ++++++++++----- src/interfaces/IExoCapsule.sol | 19 ++++++---- src/libraries/BeaconChainProofs.sol | 52 ++++++++++++++++++++++++++++ src/libraries/Merkle.sol | 6 ++-- src/libraries/ValidatorContainer.sol | 51 +++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 src/libraries/ValidatorContainer.sol diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index d9c5077b..cd51e538 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -7,6 +7,7 @@ import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Ini import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; +import {ValidatorContainer} from "../libraries/ValidatorContainer.sol", contract ExoCapsule is Initializable, @@ -19,6 +20,8 @@ contract ExoCapsule is IETHPOSDeposit public immutable ethPOS; + error InvalidValidatorContainer(); + constructor(IETHPOSDeposit _ethPOS) { ethPOS = _ethPOS; @@ -49,18 +52,16 @@ contract ExoCapsule is } function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable { - + require(msg.value == 32 ether, "stake value must be exactly 32 ether"); + ethPOS.deposit{value: 32 ether}(pubkey, _capsuleWithdrawalCredentials(), signature, depositDataRoot); + emit StakedWithThisCapsule(); } function deposit( - uint64 beaconBlockTimestamp, - bytes32 beaconStateRoot, - bytes[] calldata beaconStateRootProof, - bytes32[][] calldata validatorFields, - uint40[] calldata validatorProofIndices, - bytes[] calldata validatorFieldsProof + bytes32[] validatorContainer, + ValidatorContainerProof proof ) external { - + if (validatorContainer) } function updateStakeBalance( @@ -84,4 +85,14 @@ contract ExoCapsule is ) external { } + + function _capsuleWithdrawalCredentials() internal view returns (bytes memory) { + /** + * The withdrawal_credentials field must be such that: + * withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX + * withdrawal_credentials[1:12] == b'\x00' * 11 + * withdrawal_credentials[12:] == eth1_withdrawal_address + */ + return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); + } } diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 9f6449b7..4d69774b 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -1,15 +1,22 @@ pragma solidity ^0.8.19; interface IExoCapsule { + /// @notice This struct contains the infos needed for validator container validity verification + struct ValidatorContainerProof { + uint64 beaconBlockTimestamp; + bytes32 stateRoot; + bytes32[] stateRootProof; + bytes32[] validatorContainerRootProof; + uint256 validatorContainerRootIndex; + } + + event StakedWithThisCapsule(); + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; function deposit( - uint64 beaconBlockTimestamp, - bytes32 beaconStateRoot, - bytes[] calldata beaconStateRootProof, - bytes32[][] calldata validatorFields, - uint40[] calldata validatorProofIndices, - bytes[] calldata validatorFieldsProof + bytes32[] calldata validatorContainer, + ValidatorContainerProof calldata proof ) external; function updateStakeBalance( diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index e488eedf..16713ff9 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -133,6 +133,58 @@ library BeaconChainProofs { bytes proof; } + function verifyValidatorContainerRoot( + bytes32 validatorContainerRoot, + bytes32[] calldata validatorContainerRootProof, + uint256 validatorContainerRootIndex, + bytes32 beaconBlockRoot, + bytes32 stateRoot, + bytes32[] calldata stateRootProof + ) internal pure returns (bool valid) { + bool validStateRoot = verifyStateRoot(stateRoot, beaconBlockRoot, stateRootProof); + bool validVCRootAgainstStateRoot = verifyVCRootAgainstStateRoot(validatorContainerRoot, stateRoot, validatorContainerRootProof, validatorContainerRootIndex); + if (validStateRoot && validVCRootAgainstStateRoot) { + valid = true; + } + } + + function verifyStateRoot( + bytes32 stateRoot, + bytes32 beaconBlockRoot, + bytes32[] calldata stateRootProof + ) internal pure returns (bool) { + require( + stateRootProof.length == BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT, + "state root proof should have 3 nodes" + ); + + return Merkle.verifyInclusionSha256({ + proof: stateRootProof, + root: beaconBlockRoot, + leaf: stateRoot, + index: STATE_ROOT_INDEX + }); + } + + function verifyVCRootAgainstStateRoot( + bytes32 validatorContainerRoot, + bytes32 stateRoot, + bytes32[] calldata validatorContainerRootProof, + uint256 validatorContainerRootIndex + ) internal pure returns (bool) { + require( + validatorContainerRootProof.length == (VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_FIELD_TREE_HEIGHT, + "validator container root proof should have 46 nodes" + ); + + return Merkle.verifyInclusionSha256({ + proof: validatorContainerRootProof, + root: stateRoot, + leaf: validatorContainerRoot, + index: validatorContainerRootIndex + }); + } + /** * @notice This function verifies merkle proofs of the fields of a certain validator against a beacon chain state root * @param validatorIndex the index of the proven validator diff --git a/src/libraries/Merkle.sol b/src/libraries/Merkle.sol index 9da153a5..22b3829f 100644 --- a/src/libraries/Merkle.sol +++ b/src/libraries/Merkle.sol @@ -86,7 +86,7 @@ library Merkle { * Note this is for a Merkle tree using the sha256 hash function */ function verifyInclusionSha256( - bytes memory proof, + bytes32[] memory proof, bytes32 root, bytes32 leaf, uint256 index @@ -105,12 +105,12 @@ library Merkle { * Note this is for a Merkle tree using the sha256 hash function */ function processInclusionProofSha256( - bytes memory proof, + bytes32[] memory proof, bytes32 leaf, uint256 index ) internal view returns (bytes32) { require( - proof.length != 0 && proof.length % 32 == 0, + proof.length != 0, "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" ); bytes32[1] memory computedHash = [leaf]; diff --git a/src/libraries/ValidatorContainer.sol b/src/libraries/ValidatorContainer.sol new file mode 100644 index 00000000..0bb09772 --- /dev/null +++ b/src/libraries/ValidatorContainer.sol @@ -0,0 +1,51 @@ +pragma solidity ^0.8.19; + +library ValidatorContainer { + uint256 internal constant VALID_LENGTH = 8; + uint256 internal constant MERKLE_TREE_HEIGHT = 3; + + function verifyBasic(bytes32[] calldata validatorContainer) internal pure returns (bool) { + return validatorContainer.length == VALID_LENGTH; + } + + function getPubkey(bytes32[] calldata validatorContainer) internal pure returns (bytes32) { + return validatorContainer[0]; + } + + function getWithdrawalCredentials(bytes32[] calldata validatorContainer) internal pure returns (bytes32) { + return validatorContainer[1]; + } + + function getEffectionBalance(bytes32[] calldata validatorContainer) internal pure returns (uint64) { + return uint64(bytes8(validatorContainer[2])); + } + + function getSlashed(bytes32[] calldata validatorContainer) internal pure returns (bool) { + return uint8(bytes1(validatorContainer[3])) == 1; + } + + function getActivationEpoch(bytes32[] calldata validatorContainer) internal pure returns (uint64) { + return uint64(bytes8(validatorContainer[5])); + } + + function getExitEpoch(bytes32[] calldata validatorContainer) internal pure returns (uint64) { + return uint64(bytes8(validatorContainer[6])); + } + + function getWithdrawableEpoch(bytes32[] calldata validatorContainer) internal pure returns (uint64) { + return uint64(bytes8(validatorContainer[7])); + } + + function merklelize(bytes32[] calldata validatorContainer) internal pure returns (bytes32) { + bytes32[] memory leaves = validatorContainer; + for (uint i; i < MERKLE_TREE_HEIGHT; i++) { + bytes32[] memory roots = new bytes32[](leaves.length / 2); + for (uint j; j < leaves.length / 2; j++) { + roots[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + leaves = roots; + } + + return leaves[0]; + } +} \ No newline at end of file From d4b6d5b74d97c3e20e9894c4d3390ad2f7d03502 Mon Sep 17 00:00:00 2001 From: adu Date: Sat, 24 Feb 2024 17:37:31 +0800 Subject: [PATCH 03/93] implement capsule deposit --- src/core/ExoCapsule.sol | 53 ++++++++++++++++++++++++++++--- src/storage/ExoCapsuleStorage.sol | 19 +++++++++++ 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index cd51e538..5122f612 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -7,7 +7,7 @@ import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Ini import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; -import {ValidatorContainer} from "../libraries/ValidatorContainer.sol", +import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; contract ExoCapsule is Initializable, @@ -17,10 +17,13 @@ contract ExoCapsule is IExoCapsule { using BeaconChainProofs for bytes; + using ValidatorContainer for bytes32[]; IETHPOSDeposit public immutable ethPOS; - error InvalidValidatorContainer(); + error InvalidValidatorContainer(bytes32 pubkey); + error DoubleDepositedValidator(bytes32 pubkey); + error GetBeaconBlockRootFailure(uint64 timestamp); constructor(IETHPOSDeposit _ethPOS) { ethPOS = _ethPOS; @@ -59,9 +62,41 @@ contract ExoCapsule is function deposit( bytes32[] validatorContainer, - ValidatorContainerProof proof + ValidatorContainerProof calldata proof ) external { - if (validatorContainer) + bytes32 validatorPubkey = validatorContainer.getPubkey(); + bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); + ValidatorInfo storage validatorInfo = validatorStore[validatorPubkey]; + + if (!validatorContainer.verifyBasic()) { + revert InvalidValidatorContainer(validatorPubkey); + } + + if (withdrawalCredentials != _capsuleWithdrawalCredentials()) { + revert InvalidValidatorContainer(validatorPubkey); + } + + if (validatorInfo.status != VALIDATOR_STATUS.WITHDRAWN) { + revert DoubleDepositedValidator(validatorPubkey); + } + + bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); + bytes32 validatorContainerRoot = validatorContainer.merklelize(); + bool valid = validatorContainerRoot.verifyValidatorContainerRoot( + proof.validatorContainerRootProof, + proof.validatorContainerRootIndex, + beaconBlockRoot, + proof.stateRoot, + proof.stateRootProof + ); + if (!valid) { + revert InvalidValidatorContainer(validatorPubkey); + } + + validatorInfo.status = VALIDATOR_STATUS.ACTIVE; + validatorInfo.validatorIndex = proof.validatorContainerRootIndex; + validatorInfo.mostRecentBalanceUpdateTimestamp = proof.beaconBlockTimestamp; + validatorInfo.restakedBalanceGwei = validatorContainer.getEffectionBalance(); } function updateStakeBalance( @@ -95,4 +130,14 @@ contract ExoCapsule is */ return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); } + + function getBeaconBlockRoot(uint64 timestamp) public view returns (bytes32) { + (bool success, bytes memory rootBytes) = BEACON_ROOTS_ADDRESS.call{value: bytes32(bytes8(timestamp))} + if (!success) { + revert GetBeaconBlockRootFailure(timestamp); + } + + bytes32 beaconBlockRoot = abi.decode(rootBytes, (bytes32)); + return beaconBlockRoot; + } } diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index a06afe7f..78004c24 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -1,7 +1,26 @@ pragma solidity ^0.8.19; contract ExoCapsuleStorage { + enum VALIDATOR_STATUS { + INACTIVE, // doesnt exist + ACTIVE, // staked on ethpos and withdrawal credentials are pointed to the EigenPod + WITHDRAWN // withdrawn from the Beacon Chain + } + + struct ValidatorInfo { + // index of the validator in the beacon chain + uint64 validatorIndex; + // amount of beacon chain ETH restaked on EigenLayer in gwei + uint64 restakedBalanceGwei; + //timestamp of the validator's most recent balance update + uint64 mostRecentBalanceUpdateTimestamp; + // status of the validator + VALIDATOR_STATUS status; + } + + address public constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; address payable exocoreValidatorSetAddress; + mapping(bytes32 pubkey => ValidatorInfo info) validatorStore; uint256[40] private __gap; } \ No newline at end of file From b904a61882458dc0ced5734c4fbc9c4ba19c05ac Mon Sep 17 00:00:00 2001 From: adu Date: Wed, 20 Mar 2024 10:32:33 +0800 Subject: [PATCH 04/93] initiate updateStakingBalance --- src/core/ExoCapsule.sol | 83 ++++++++-- src/libraries/BeaconChainProofs.sol | 218 +-------------------------- src/libraries/ValidatorContainer.sol | 14 +- src/storage/ExoCapsuleStorage.sol | 8 +- 4 files changed, 89 insertions(+), 234 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 5122f612..3fde5501 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -24,6 +24,7 @@ contract ExoCapsule is error InvalidValidatorContainer(bytes32 pubkey); error DoubleDepositedValidator(bytes32 pubkey); error GetBeaconBlockRootFailure(uint64 timestamp); + error StaleValidatorContainer(bytes32 pubkey, uint64 timestamp); constructor(IETHPOSDeposit _ethPOS) { ethPOS = _ethPOS; @@ -61,23 +62,31 @@ contract ExoCapsule is } function deposit( - bytes32[] validatorContainer, + bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof ) external { bytes32 validatorPubkey = validatorContainer.getPubkey(); bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); - ValidatorInfo storage validatorInfo = validatorStore[validatorPubkey]; + ValidatorInfo storage validator = validatorStore[validatorPubkey]; + + if (validator.status != VALIDATOR_STATUS.UNREGISTERED) { + revert DoubleDepositedValidator(validatorPubkey); + } + + if (_isStaleProof(validator, proof.beaconBlockTimestamp)) { + revert StaleValidatorContainer(validatorPubkey, proof.beaconBlockTimestamp); + } if (!validatorContainer.verifyBasic()) { revert InvalidValidatorContainer(validatorPubkey); } - if (withdrawalCredentials != _capsuleWithdrawalCredentials()) { + if (!_isActivatedAtEpoch(validatorContainer, proof.beaconBlockTimestamp)) { revert InvalidValidatorContainer(validatorPubkey); } - if (validatorInfo.status != VALIDATOR_STATUS.WITHDRAWN) { - revert DoubleDepositedValidator(validatorPubkey); + if (withdrawalCredentials != _capsuleWithdrawalCredentials()) { + revert InvalidValidatorContainer(validatorPubkey); } bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); @@ -93,21 +102,35 @@ contract ExoCapsule is revert InvalidValidatorContainer(validatorPubkey); } - validatorInfo.status = VALIDATOR_STATUS.ACTIVE; - validatorInfo.validatorIndex = proof.validatorContainerRootIndex; - validatorInfo.mostRecentBalanceUpdateTimestamp = proof.beaconBlockTimestamp; - validatorInfo.restakedBalanceGwei = validatorContainer.getEffectionBalance(); + validator.status = VALIDATOR_STATUS.REGISTERED; + validator.validatorIndex = proof.validatorContainerRootIndex; + validator.mostRecentBalanceUpdateTimestamp = proof.beaconBlockTimestamp; + validator.restakedBalanceGwei = validatorContainer.getEffectiveBalance(); } function updateStakeBalance( - uint64 beaconBlockTimestamp, - bytes32 beaconStateRoot, - bytes[] calldata beaconStateRootProof, - bytes32[][] calldata validatorFields, - uint40[] calldata validatorProofIndices, - bytes[] calldata validatorFieldsProof + bytes32[] calldata validatorContainer, + ValidatorContainerProof calldata proof ) external { + bytes32 validatorPubkey = validatorContainer.getPubkey(); + bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); + ValidatorInfo storage validator = validatorStore[validatorPubkey]; + + if (validatorInfo.status != VALIDATOR_STATUS.REGISTERED) { + revert DoubleDepositedValidator(validatorPubkey); + } + + if (_isStaleProof(validator, proof.beaconBlockTimestamp)) { + revert StaleValidatorContainer(validatorPubkey, proof.beaconBlockTimestamp); + } + if (!validatorContainer.verifyBasic()) { + revert InvalidValidatorContainer(validatorPubkey); + } + + if (proof.beaconBlockTimestamp <= validatorInfo.mostRecentBalanceUpdateTimestamp) { + revert + } } function withdraw( @@ -140,4 +163,34 @@ contract ExoCapsule is bytes32 beaconBlockRoot = abi.decode(rootBytes, (bytes32)); return beaconBlockRoot; } + + function _isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint64 atTimestamp) internal view returns (bool) { + uint64 atEpoch = _timestampToEpoch(atTimestamp); + uint64 activationEpoch = validatorContainer.getActivationEpoch(); + uint64 exitEpoch = validatorContainer.getExitEpoch(); + + return (atEpoch >= activationEpoch && atEpoch < exitEpoch); + } + + function _isStaleProof(ValidatorInfo storage validator, uint64 proofTimestamp) internal view returns (bool) { + if (proofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS >= block.timestamp) { + if (proofTimestamp > validator.mostRecentBalanceUpdateTimestamp) { + return false; + } + } + } + + function _isMoreRecent(uint64 timestamp) internal view returns (bool) { + return timestamp > + } + + /** + * @dev Converts a timestamp to a beacon chain epoch by calculating the number of + * seconds since genesis, and dividing by seconds per epoch. + * reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md + */ + function _timestampToEpoch(uint64 timestamp) internal view returns (uint64) { + require(timestamp >= BEACON_CHAIN_GENESIS_TIME, "timestamp should be greater than beacon chain genesis timestamp"); + return (timestamp - BEACON_CHAIN_GENESIS_TIME) / BeaconChainProofs.SECONDS_PER_EPOCH; + } } diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 16713ff9..192c3545 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -140,7 +140,7 @@ library BeaconChainProofs { bytes32 beaconBlockRoot, bytes32 stateRoot, bytes32[] calldata stateRootProof - ) internal pure returns (bool valid) { + ) internal view returns (bool valid) { bool validStateRoot = verifyStateRoot(stateRoot, beaconBlockRoot, stateRootProof); bool validVCRootAgainstStateRoot = verifyVCRootAgainstStateRoot(validatorContainerRoot, stateRoot, validatorContainerRootProof, validatorContainerRootIndex); if (validStateRoot && validVCRootAgainstStateRoot) { @@ -152,7 +152,7 @@ library BeaconChainProofs { bytes32 stateRoot, bytes32 beaconBlockRoot, bytes32[] calldata stateRootProof - ) internal pure returns (bool) { + ) internal view returns (bool) { require( stateRootProof.length == BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT, "state root proof should have 3 nodes" @@ -171,7 +171,7 @@ library BeaconChainProofs { bytes32 stateRoot, bytes32[] calldata validatorContainerRootProof, uint256 validatorContainerRootIndex - ) internal pure returns (bool) { + ) internal view returns (bool) { require( validatorContainerRootProof.length == (VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_FIELD_TREE_HEIGHT, "validator container root proof should have 46 nodes" @@ -185,218 +185,6 @@ library BeaconChainProofs { }); } - /** - * @notice This function verifies merkle proofs of the fields of a certain validator against a beacon chain state root - * @param validatorIndex the index of the proven validator - * @param beaconStateRoot is the beacon chain state root to be proven against. - * @param validatorFieldsProof is the data used in proving the validator's fields - * @param validatorFields the claimed fields of the validator - */ - function verifyValidatorFields( - bytes32 beaconStateRoot, - bytes32[] calldata validatorFields, - bytes calldata validatorFieldsProof, - uint40 validatorIndex - ) internal view { - require( - validatorFields.length == 2 ** VALIDATOR_FIELD_TREE_HEIGHT, - "BeaconChainProofs.verifyValidatorFields: Validator fields has incorrect length" - ); - - /** - * Note: the length of the validator merkle proof is BeaconChainProofs.VALIDATOR_TREE_HEIGHT + 1. - * There is an additional layer added by hashing the root with the length of the validator list - */ - require( - validatorFieldsProof.length == 32 * ((VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_FIELD_TREE_HEIGHT), - "BeaconChainProofs.verifyValidatorFields: Proof has incorrect length" - ); - uint256 index = (VALIDATOR_TREE_ROOT_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); - // merkleize the validatorFields to get the leaf to prove - bytes32 validatorRoot = Merkle.merkleizeSha256(validatorFields); - - // verify the proof of the validatorRoot against the beaconStateRoot - require( - Merkle.verifyInclusionSha256({ - proof: validatorFieldsProof, - root: beaconStateRoot, - leaf: validatorRoot, - index: index - }), - "BeaconChainProofs.verifyValidatorFields: Invalid merkle proof" - ); - } - - /** - * @notice This function verifies the latestBlockHeader against the state root. the latestBlockHeader is - * a tracked in the beacon state. - * @param beaconStateRoot is the beacon chain state root to be proven against. - * @param stateRootProof is the provided merkle proof - * @param latestBlockRoot is hashtree root of the latest block header in the beacon state - */ - function verifyStateRootAgainstLatestBlockRoot( - bytes32 latestBlockRoot, - bytes32 beaconStateRoot, - bytes calldata stateRootProof - ) internal view { - require( - stateRootProof.length == 32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT), - "BeaconChainProofs.verifyStateRootAgainstLatestBlockRoot: Proof has incorrect length" - ); - //Next we verify the slot against the blockRoot - require( - Merkle.verifyInclusionSha256({ - proof: stateRootProof, - root: latestBlockRoot, - leaf: beaconStateRoot, - index: STATE_ROOT_INDEX - }), - "BeaconChainProofs.verifyStateRootAgainstLatestBlockRoot: Invalid latest block header root merkle proof" - ); - } - - /** - * @notice This function verifies the slot and the withdrawal fields for a given withdrawal - * @param withdrawalProof is the provided set of merkle proofs - * @param withdrawalFields is the serialized withdrawal container to be proven - */ - function verifyWithdrawal( - bytes32 beaconStateRoot, - bytes32[] calldata withdrawalFields, - WithdrawalProof calldata withdrawalProof - ) internal view { - require( - withdrawalFields.length == 2 ** WITHDRAWAL_FIELD_TREE_HEIGHT, - "BeaconChainProofs.verifyWithdrawal: withdrawalFields has incorrect length" - ); - - require( - withdrawalProof.blockRootIndex < 2 ** BLOCK_ROOTS_TREE_HEIGHT, - "BeaconChainProofs.verifyWithdrawal: blockRootIndex is too large" - ); - require( - withdrawalProof.withdrawalIndex < 2 ** WITHDRAWALS_TREE_HEIGHT, - "BeaconChainProofs.verifyWithdrawal: withdrawalIndex is too large" - ); - - require( - withdrawalProof.historicalSummaryIndex < 2 ** HISTORICAL_SUMMARIES_TREE_HEIGHT, - "BeaconChainProofs.verifyWithdrawal: historicalSummaryIndex is too large" - ); - - require( - withdrawalProof.withdrawalProof.length == - 32 * (EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT + WITHDRAWALS_TREE_HEIGHT + 1), - "BeaconChainProofs.verifyWithdrawal: withdrawalProof has incorrect length" - ); - require( - withdrawalProof.executionPayloadProof.length == - 32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT), - "BeaconChainProofs.verifyWithdrawal: executionPayloadProof has incorrect length" - ); - require( - withdrawalProof.slotProof.length == 32 * (BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT), - "BeaconChainProofs.verifyWithdrawal: slotProof has incorrect length" - ); - require( - withdrawalProof.timestampProof.length == 32 * (EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT), - "BeaconChainProofs.verifyWithdrawal: timestampProof has incorrect length" - ); - - require( - withdrawalProof.historicalSummaryBlockRootProof.length == - 32 * - (BEACON_STATE_FIELD_TREE_HEIGHT + - (HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + - 1 + - (BLOCK_ROOTS_TREE_HEIGHT)), - "BeaconChainProofs.verifyWithdrawal: historicalSummaryBlockRootProof has incorrect length" - ); - /** - * Note: Here, the "1" in "1 + (BLOCK_ROOTS_TREE_HEIGHT)" signifies that extra step of choosing the "block_root_summary" within the individual - * "historical_summary". Everywhere else it signifies merkelize_with_mixin, where the length of an array is hashed with the root of the array, - * but not here. - */ - uint256 historicalBlockHeaderIndex = (HISTORICAL_SUMMARIES_INDEX << - ((HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + 1 + (BLOCK_ROOTS_TREE_HEIGHT))) | - (uint256(withdrawalProof.historicalSummaryIndex) << (1 + (BLOCK_ROOTS_TREE_HEIGHT))) | - (BLOCK_SUMMARY_ROOT_INDEX << (BLOCK_ROOTS_TREE_HEIGHT)) | - uint256(withdrawalProof.blockRootIndex); - - require( - Merkle.verifyInclusionSha256({ - proof: withdrawalProof.historicalSummaryBlockRootProof, - root: beaconStateRoot, - leaf: withdrawalProof.blockRoot, - index: historicalBlockHeaderIndex - }), - "BeaconChainProofs.verifyWithdrawal: Invalid historicalsummary merkle proof" - ); - - //Next we verify the slot against the blockRoot - require( - Merkle.verifyInclusionSha256({ - proof: withdrawalProof.slotProof, - root: withdrawalProof.blockRoot, - leaf: withdrawalProof.slotRoot, - index: SLOT_INDEX - }), - "BeaconChainProofs.verifyWithdrawal: Invalid slot merkle proof" - ); - - { - // Next we verify the executionPayloadRoot against the blockRoot - uint256 executionPayloadIndex = (BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT)) | - EXECUTION_PAYLOAD_INDEX; - require( - Merkle.verifyInclusionSha256({ - proof: withdrawalProof.executionPayloadProof, - root: withdrawalProof.blockRoot, - leaf: withdrawalProof.executionPayloadRoot, - index: executionPayloadIndex - }), - "BeaconChainProofs.verifyWithdrawal: Invalid executionPayload merkle proof" - ); - } - - // Next we verify the timestampRoot against the executionPayload root - require( - Merkle.verifyInclusionSha256({ - proof: withdrawalProof.timestampProof, - root: withdrawalProof.executionPayloadRoot, - leaf: withdrawalProof.timestampRoot, - index: TIMESTAMP_INDEX - }), - "BeaconChainProofs.verifyWithdrawal: Invalid blockNumber merkle proof" - ); - - { - /** - * Next we verify the withdrawal fields against the blockRoot: - * First we compute the withdrawal_index relative to the blockRoot by concatenating the indexes of all the - * intermediate root indexes from the bottom of the sub trees (the withdrawal container) to the top, the blockRoot. - * Then we calculate merkleize the withdrawalFields container to calculate the the withdrawalRoot. - * Finally we verify the withdrawalRoot against the executionPayloadRoot. - * - * - * Note: Merkleization of the withdrawals root tree uses MerkleizeWithMixin, i.e., the length of the array is hashed with the root of - * the array. Thus we shift the WITHDRAWALS_INDEX over by WITHDRAWALS_TREE_HEIGHT + 1 and not just WITHDRAWALS_TREE_HEIGHT. - */ - uint256 withdrawalIndex = (WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1)) | - uint256(withdrawalProof.withdrawalIndex); - bytes32 withdrawalRoot = Merkle.merkleizeSha256(withdrawalFields); - require( - Merkle.verifyInclusionSha256({ - proof: withdrawalProof.withdrawalProof, - root: withdrawalProof.executionPayloadRoot, - leaf: withdrawalRoot, - index: withdrawalIndex - }), - "BeaconChainProofs.verifyWithdrawal: Invalid withdrawal merkle proof" - ); - } - } - /** * @notice This function replicates the ssz hashing of a validator's pubkey, outlined below: * hh := ssz.NewHasher() diff --git a/src/libraries/ValidatorContainer.sol b/src/libraries/ValidatorContainer.sol index 0bb09772..390668ee 100644 --- a/src/libraries/ValidatorContainer.sol +++ b/src/libraries/ValidatorContainer.sol @@ -1,5 +1,17 @@ pragma solidity ^0.8.19; +/** + * class Validator(Container): + pubkey: BLSPubkey + withdrawal_credentials: Bytes32 # Commitment to pubkey for withdrawals + effective_balance: Gwei # Balance at stake + slashed: boolean + # Status epochs + activation_eligibility_epoch: Epoch # When criteria for activation were met + activation_epoch: Epoch + exit_epoch: Epoch + withdrawable_epoch: Epoch # When validator can withdraw funds + */ library ValidatorContainer { uint256 internal constant VALID_LENGTH = 8; uint256 internal constant MERKLE_TREE_HEIGHT = 3; @@ -16,7 +28,7 @@ library ValidatorContainer { return validatorContainer[1]; } - function getEffectionBalance(bytes32[] calldata validatorContainer) internal pure returns (uint64) { + function getEffectiveBalance(bytes32[] calldata validatorContainer) internal pure returns (uint64) { return uint64(bytes8(validatorContainer[2])); } diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 78004c24..db13f924 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.19; contract ExoCapsuleStorage { enum VALIDATOR_STATUS { - INACTIVE, // doesnt exist - ACTIVE, // staked on ethpos and withdrawal credentials are pointed to the EigenPod - WITHDRAWN // withdrawn from the Beacon Chain + UNREGISTERED, // the validator has not been registered in this ExoCapsule + REGISTERED, // staked on ethpos and withdrawal credentials are pointed to the ExoCapsule + EXITED // withdrawn from the Beacon Chain } struct ValidatorInfo { @@ -19,6 +19,8 @@ contract ExoCapsuleStorage { } address public constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + uint64 public constant BEACON_CHAIN_GENESIS_TIME = 1606824023; + address payable exocoreValidatorSetAddress; mapping(bytes32 pubkey => ValidatorInfo info) validatorStore; From 12eb1832e4092f1697c7080a151740c02458d3cd Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 22 Mar 2024 21:23:07 +0800 Subject: [PATCH 05/93] add index=>pubkey mapping --- src/core/ExoCapsule.sol | 64 ++++++++++++++++++++++++++----- src/interfaces/IExoCapsule.sol | 16 +++++--- src/storage/ExoCapsuleStorage.sol | 5 ++- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 3fde5501..cdbcfd52 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -25,6 +25,8 @@ contract ExoCapsule is error DoubleDepositedValidator(bytes32 pubkey); error GetBeaconBlockRootFailure(uint64 timestamp); error StaleValidatorContainer(bytes32 pubkey, uint64 timestamp); + error UnregisteredOrWithdrawnValidatorContainer(bytes32 pubkey); + error FullyWithdrawnValidatorContainer(bytes32 pubkey); constructor(IETHPOSDeposit _ethPOS) { ethPOS = _ethPOS; @@ -67,7 +69,7 @@ contract ExoCapsule is ) external { bytes32 validatorPubkey = validatorContainer.getPubkey(); bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); - ValidatorInfo storage validator = validatorStore[validatorPubkey]; + Validator storage validator = _capsuleValidators[validatorPubkey]; if (validator.status != VALIDATOR_STATUS.UNREGISTERED) { revert DoubleDepositedValidator(validatorPubkey); @@ -106,6 +108,8 @@ contract ExoCapsule is validator.validatorIndex = proof.validatorContainerRootIndex; validator.mostRecentBalanceUpdateTimestamp = proof.beaconBlockTimestamp; validator.restakedBalanceGwei = validatorContainer.getEffectiveBalance(); + + _capsuleValidatorsByIndex[proof.ValidatorContainerRootIndex] = validatorPubkey; } function updateStakeBalance( @@ -114,10 +118,10 @@ contract ExoCapsule is ) external { bytes32 validatorPubkey = validatorContainer.getPubkey(); bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); - ValidatorInfo storage validator = validatorStore[validatorPubkey]; + Validator storage validator = _capsuleValidators[validatorPubkey]; - if (validatorInfo.status != VALIDATOR_STATUS.REGISTERED) { - revert DoubleDepositedValidator(validatorPubkey); + if (Validator.status != VALIDATOR_STATUS.REGISTERED) { + revert UnregisteredOrWithdrawnValidatorContainer(validatorPubkey); } if (_isStaleProof(validator, proof.beaconBlockTimestamp)) { @@ -128,9 +132,25 @@ contract ExoCapsule is revert InvalidValidatorContainer(validatorPubkey); } - if (proof.beaconBlockTimestamp <= validatorInfo.mostRecentBalanceUpdateTimestamp) { - revert + if (_hasFullyWithdrawn(validatorContainer)) { + revert FullyWithdrawnValidatorContainer(validatorPubkey); + } + + bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); + bytes32 validatorContainerRoot = validatorContainer.merklelize(); + bool valid = validatorContainerRoot.verifyValidatorContainerRoot( + proof.validatorContainerRootProof, + proof.validatorContainerRootIndex, + beaconBlockRoot, + proof.stateRoot, + proof.stateRootProof + ); + if (!valid) { + revert InvalidValidatorContainer(validatorPubkey); } + + validator.mostRecentBalanceUpdateTimestamp = proof.beaconBlockTimestamp; + validator.restakedBalanceGwei = validatorContainer.getEffectiveBalance(); } function withdraw( @@ -141,7 +161,25 @@ contract ExoCapsule is uint40[] calldata withdrawalProofIndices, bytes[] calldata withdrawalFieldsProof ) external { + bytes32 validatorPubkey = validatorContainer.getPubkey(); + bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); + Validator storage validator = _capsuleValidators[validatorPubkey]; + if (validator.status != VALIDATOR_STATUS.REGISTERED) { + revert UnregisteredOrWithdrawnValidatorContainer(validatorPubkey); + } + + if (_isStaleProof(validator, proof.beaconBlockTimestamp)) { + revert StaleValidatorContainer(validatorPubkey, proof.beaconBlockTimestamp); + } + + if (!validatorContainer.verifyBasic()) { + revert InvalidValidatorContainer(validatorPubkey); + } + + if (_hasFullyWithdrawn(validatorContainer)) { + revert FullyWithdrawnValidatorContainer(validatorPubkey); + } } function _capsuleWithdrawalCredentials() internal view returns (bytes memory) { @@ -172,16 +210,24 @@ contract ExoCapsule is return (atEpoch >= activationEpoch && atEpoch < exitEpoch); } - function _isStaleProof(ValidatorInfo storage validator, uint64 proofTimestamp) internal view returns (bool) { + function _isStaleProof(Validator storage validator, uint64 proofTimestamp) internal view returns (bool) { if (proofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS >= block.timestamp) { if (proofTimestamp > validator.mostRecentBalanceUpdateTimestamp) { return false; } } + + return true; } - function _isMoreRecent(uint64 timestamp) internal view returns (bool) { - return timestamp > + function _hasFullyWithdrawn(bytes32[] calldata validatorContainer) internal view returns (bool) { + if (validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(block.timestamp)) { + if (validatorContainer.getEffectiveBalance() == 0) { + return true; + } + } + + return false; } /** diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 4d69774b..f3e4f960 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -10,6 +10,14 @@ interface IExoCapsule { uint256 validatorContainerRootIndex; } + struct WithdrawalContainerProof { + uint64 beaconBlockTimestamp; + bytes32 stateRoot; + bytes32[] stateRootProof; + bytes32[] withdrawalContainerProof; + uint256 withdrawalContainerIndex; + } + event StakedWithThisCapsule(); function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; @@ -20,12 +28,8 @@ interface IExoCapsule { ) external; function updateStakeBalance( - uint64 beaconBlockTimestamp, - bytes32 beaconStateRoot, - bytes[] calldata beaconStateRootProof, - bytes32[][] calldata validatorFields, - uint40[] calldata validatorProofIndices, - bytes[] calldata validatorFieldsProof + bytes32[] calldata validatorContainer, + ValidatorContainerProof calldata proof ) external; function withdraw( diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index db13f924..06a9932b 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -7,7 +7,7 @@ contract ExoCapsuleStorage { EXITED // withdrawn from the Beacon Chain } - struct ValidatorInfo { + struct Validator { // index of the validator in the beacon chain uint64 validatorIndex; // amount of beacon chain ETH restaked on EigenLayer in gwei @@ -22,7 +22,8 @@ contract ExoCapsuleStorage { uint64 public constant BEACON_CHAIN_GENESIS_TIME = 1606824023; address payable exocoreValidatorSetAddress; - mapping(bytes32 pubkey => ValidatorInfo info) validatorStore; + mapping(bytes32 pubkey => Validator validator) _capsuleValidators; + mapping(uint64 index => bytes32 pubkey) _capsuleValidatorsByIndex; uint256[40] private __gap; } \ No newline at end of file From c73d017314eb06060d1e8308a0cbc392e4ea0a49 Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 12 Apr 2024 15:32:25 +0800 Subject: [PATCH 06/93] add withdrawal container and integrate exocapsule into clientchaingateway --- src/core/ExoCapsule.sol | 123 ++++++++++++------ src/interfaces/IExoCapsule.sol | 8 +- ...roller.sol => ILSTRestakingController.sol} | 2 +- src/interfaces/INativeRestakingController.sol | 64 +++++++++ src/libraries/BeaconChainProofs.sol | 10 +- src/libraries/WithdrawalContainer.sol | 46 +++++++ src/storage/ExoCapsuleStorage.sol | 6 + 7 files changed, 207 insertions(+), 52 deletions(-) rename src/interfaces/{IController.sol => ILSTRestakingController.sol} (99%) create mode 100644 src/interfaces/INativeRestakingController.sol create mode 100644 src/libraries/WithdrawalContainer.sol diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index cdbcfd52..a269594c 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -7,7 +7,9 @@ import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Ini import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; +import {IClientChainGateway} from "../interfaces/IClientChainGateway.sol"; import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; +import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; contract ExoCapsule is Initializable, @@ -18,6 +20,7 @@ contract ExoCapsule is { using BeaconChainProofs for bytes; using ValidatorContainer for bytes32[]; + using WithdrawalContainer for bytes32[]; IETHPOSDeposit public immutable ethPOS; @@ -27,9 +30,17 @@ contract ExoCapsule is error StaleValidatorContainer(bytes32 pubkey, uint64 timestamp); error UnregisteredOrWithdrawnValidatorContainer(bytes32 pubkey); error FullyWithdrawnValidatorContainer(bytes32 pubkey); + error UnmatchedValidatorAndWithdrawal(bytes32 pubkey); + error NotPartialWithdrawal(bytes32 pubkey); - constructor(IETHPOSDeposit _ethPOS) { - ethPOS = _ethPOS; + modifier onlyGateway() { + require(msg.sender == address(gateway), "only client chain gateway could call this function"); + _; + } + + constructor(address _ethPOS, address _gateway) { + ethPOS = IETHPOSDeposit(_ethPOS); + gateway = IClientChainGateway(_gateway); _disableInitializers(); } @@ -66,7 +77,7 @@ contract ExoCapsule is function deposit( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof - ) external { + ) external onlyGateway { bytes32 validatorPubkey = validatorContainer.getPubkey(); bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); Validator storage validator = _capsuleValidators[validatorPubkey]; @@ -91,18 +102,7 @@ contract ExoCapsule is revert InvalidValidatorContainer(validatorPubkey); } - bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); - bytes32 validatorContainerRoot = validatorContainer.merklelize(); - bool valid = validatorContainerRoot.verifyValidatorContainerRoot( - proof.validatorContainerRootProof, - proof.validatorContainerRootIndex, - beaconBlockRoot, - proof.stateRoot, - proof.stateRootProof - ); - if (!valid) { - revert InvalidValidatorContainer(validatorPubkey); - } + _verifyValidatorContainer(validatorContainer, proof); validator.status = VALIDATOR_STATUS.REGISTERED; validator.validatorIndex = proof.validatorContainerRootIndex; @@ -115,7 +115,7 @@ contract ExoCapsule is function updateStakeBalance( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof - ) external { + ) external onlyGateway { bytes32 validatorPubkey = validatorContainer.getPubkey(); bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); Validator storage validator = _capsuleValidators[validatorPubkey]; @@ -136,50 +136,70 @@ contract ExoCapsule is revert FullyWithdrawnValidatorContainer(validatorPubkey); } - bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); - bytes32 validatorContainerRoot = validatorContainer.merklelize(); - bool valid = validatorContainerRoot.verifyValidatorContainerRoot( - proof.validatorContainerRootProof, - proof.validatorContainerRootIndex, - beaconBlockRoot, - proof.stateRoot, - proof.stateRootProof - ); - if (!valid) { - revert InvalidValidatorContainer(validatorPubkey); - } + _verifyValidatorContainer(validatorContainer, proof); validator.mostRecentBalanceUpdateTimestamp = proof.beaconBlockTimestamp; validator.restakedBalanceGwei = validatorContainer.getEffectiveBalance(); } - function withdraw( - uint64 beaconBlockTimestamp, - bytes32 beaconStateRoot, - bytes[] calldata beaconStateRootProof, - bytes32[][] calldata withdrawalFields, - uint40[] calldata withdrawalProofIndices, - bytes[] calldata withdrawalFieldsProof - ) external { + function partiallyWithdraw( + bytes32[] calldata validatorContainer, + ValidatorContainerProof calldata validatorProof, + bytes32[] calldata withdrawalContainer, + WithdrawalContainerProof calldata withdrawalProof + ) external onlyGateway { bytes32 validatorPubkey = validatorContainer.getPubkey(); bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); + uint64 withdrawableEpoch = validatorContainer.getWithdrawableEpoch(); + Validator storage validator = _capsuleValidators[validatorPubkey]; + bool partialWithdrawal = _timestampToEpoch(validatorProof.beaconBlockTimestamp) < withdrawableEpoch; - if (validator.status != VALIDATOR_STATUS.REGISTERED) { - revert UnregisteredOrWithdrawnValidatorContainer(validatorPubkey); + if (!partialWithdrawal) { + revert NotPartialWithdrawal(validatorPubkey); } - if (_isStaleProof(validator, proof.beaconBlockTimestamp)) { - revert StaleValidatorContainer(validatorPubkey, proof.beaconBlockTimestamp); + if (validatorProof.beaconBlockTimestamp != withdrawalProof.beaconBlockTimestamp) { + revert UnmatchedValidatorAndWithdrawal(validatorPubkey); } if (!validatorContainer.verifyBasic()) { revert InvalidValidatorContainer(validatorPubkey); } - if (_hasFullyWithdrawn(validatorContainer)) { - revert FullyWithdrawnValidatorContainer(validatorPubkey); + _verifyValidatorContainer(validatorContainer, validatorProof); + _verifyWithdrawalContainer(withdrawalContainer, withdrawalProof); + } + + function fullyWithdraw( + bytes32[] calldata validatorContainer, + ValidatorContainerProof calldata validatorProof, + bytes32[] calldata withdrawalContainer, + WithdrawalContainerProof calldata withdrawalProof + ) external onlyGateway { + bytes32 validatorPubkey = validatorContainer.getPubkey(); + bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); + uint64 withdrawableEpoch = validatorContainer.getWithdrawableEpoch(); + + Validator storage validator = _capsuleValidators[validatorPubkey]; + bool fullyWithdrawal = _timestampToEpoch(validatorProof.beaconBlockTimestamp) > withdrawableEpoch; + + if (!fullyWithdrawal) { + revert NotPartialWithdrawal(validatorPubkey); + } + + if (validatorProof.beaconBlockTimestamp != withdrawalProof.beaconBlockTimestamp) { + revert UnmatchedValidatorAndWithdrawal(validatorPubkey); + } + + if (!validatorContainer.verifyBasic()) { + revert InvalidValidatorContainer(validatorPubkey); } + + _verifyValidatorContainer(validatorContainer, validatorProof); + _verifyWithdrawalContainer(withdrawalContainer, withdrawalProof); + + validator.status = VALIDATOR_STATUS.EXITED; } function _capsuleWithdrawalCredentials() internal view returns (bytes memory) { @@ -202,6 +222,25 @@ contract ExoCapsule is return beaconBlockRoot; } + function _verifyValidatorContainer(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) internal { + bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); + bytes32 validatorContainerRoot = validatorContainer.merklelize(); + bool valid = validatorContainerRoot.isValidValidatorContainerRoot( + proof.validatorContainerRootProof, + proof.validatorContainerRootIndex, + beaconBlockRoot, + proof.stateRoot, + proof.stateRootProof + ); + if (!valid) { + revert InvalidValidatorContainer(validatorPubkey); + } + } + + function _verifyWithdrawalContainer(bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata proof) internal { + + } + function _isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint64 atTimestamp) internal view returns (bool) { uint64 atEpoch = _timestampToEpoch(atTimestamp); uint64 activationEpoch = validatorContainer.getActivationEpoch(); diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index f3e4f960..d4e621b6 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -12,10 +12,10 @@ interface IExoCapsule { struct WithdrawalContainerProof { uint64 beaconBlockTimestamp; - bytes32 stateRoot; - bytes32[] stateRootProof; - bytes32[] withdrawalContainerProof; - uint256 withdrawalContainerIndex; + bytes32 executionPayloadRoot; + bytes32[] executionPayloadRootProof; + bytes32[] withdrawalContainerRootProof; + uint256 withdrawalContainerRootIndex; } event StakedWithThisCapsule(); diff --git a/src/interfaces/IController.sol b/src/interfaces/ILSTRestakingController.sol similarity index 99% rename from src/interfaces/IController.sol rename to src/interfaces/ILSTRestakingController.sol index 4f0af5e9..ea074b5e 100644 --- a/src/interfaces/IController.sol +++ b/src/interfaces/ILSTRestakingController.sol @@ -1,6 +1,6 @@ pragma solidity ^0.8.19; -interface IController { +interface ILSTRestakingController { // @notice this info is used to update specific user's owned tokens balance struct UserBalanceUpdateInfo { address user; diff --git a/src/interfaces/INativeRestakingController.sol b/src/interfaces/INativeRestakingController.sol new file mode 100644 index 00000000..b0971617 --- /dev/null +++ b/src/interfaces/INativeRestakingController.sol @@ -0,0 +1,64 @@ +pragma solidity ^0.8.19; + +import {IExoCapsule} from "./IExoCapsule.sol"; + +interface INativeRestakingController { + /// *** function signatures for staker operations *** + + /** + * @notice Ethereum native restaker should call this function to create owned ExoCapsule before staking to beacon chain. + */ + function createExoCapsule() external; + + /** + * @notice This is called to deposit ETH that is staked on Ethereum beacon chain to Exocore network to be restaked in future + * @dev Before deposit, staker should have created the ExoCapsule that it owns and point the validator's withdrawal crendentials + * to the ExoCapsule owned by staker. + */ + function depositAsBeaconValidator(bytes32[] validatorContainer, IExoCapsule.WithdrawalContainerProof proof) external; + + /** + * @notice After native restaker deposits and delegates on Exocore network, the restaker's principle balance could be influenced by + * rewards/penalties/slashing from both Ethereum beacon chain and Exocore chain, so principle balance update owing to beacon chain + * consensus should be accounted for by Exocore chain as well. + */ + function commitBeaconBalanceDelta(bytes32[] validatorContainer, IExoCapsule.WithdrawalContainerProof proof) external; + + /** + * @notice Client chain users call to withdraw principle from Exocore to client chain before they are granted to withdraw from the vault. + * @dev This function should ask Exocore validator set for withdrawal grant. If Exocore validator set responds + * with true or success, the corresponding assets should be unlocked to make them claimable by users themselves. Otherwise + * these assets should remain locked. + * @param token - The address of specific token that the user wants to withdraw from Exocore. + * @param principleAmount - principle means the assets user deposits into Exocore for delegating and staking. + * we suppose that After deposit, its amount could only remain unchanged or decrease owing to slashing, which means that direct + * transfer of principle is not possible. + */ + function withdrawPrincipleFromExocore(address token, uint256 principleAmount) external payable; + + function withdrawRewardFromExocore(address token, uint256 rewardAmount) external payable; + + /** + * @notice Client chain users call to claim their unlocked assets from the vault. + * @dev This function assumes that the claimable assets should have been unlocked before calling this. + * @dev This function does not ask for grant from Exocore validator set. + * @param token - The address of specific token that the user wants to claim from the vault. + * @param amount - The amount of @param token that the user wants to claim from the vault. + * @param recipient - The destination address that the assets would be transfered to. + */ + function claim(address token, uint256 amount, address recipient) external; + + /// *** function signatures for commands of Exocore validator set forwarded by Gateway *** + + /** + * @notice This should only be called by Exocore validator set through Gateway to update user's involved + * lastly updated token balance. + * @dev Only Exocore validato set could indirectly call this function through Gateway contract. + * @dev This function could be called in two scenaries: + * 1) Exocore validator set periodically calls this to update user principle and reward balance. + * 2) Exocore validator set sends reponse for the request of withdrawPrincipleFromExocore and unlock part of + * the vault assets and update user's withdrawable balance correspondingly. + * @param info - The info needed for updating users balance. + */ + function updateUsersBalances(UserBalanceUpdateInfo[] calldata info) external; +} diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 192c3545..d73233cc 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -133,7 +133,7 @@ library BeaconChainProofs { bytes proof; } - function verifyValidatorContainerRoot( + function isValidValidatorContainerRoot( bytes32 validatorContainerRoot, bytes32[] calldata validatorContainerRootProof, uint256 validatorContainerRootIndex, @@ -141,14 +141,14 @@ library BeaconChainProofs { bytes32 stateRoot, bytes32[] calldata stateRootProof ) internal view returns (bool valid) { - bool validStateRoot = verifyStateRoot(stateRoot, beaconBlockRoot, stateRootProof); - bool validVCRootAgainstStateRoot = verifyVCRootAgainstStateRoot(validatorContainerRoot, stateRoot, validatorContainerRootProof, validatorContainerRootIndex); + bool validStateRoot = isValidStateRoot(stateRoot, beaconBlockRoot, stateRootProof); + bool validVCRootAgainstStateRoot = isValidVCRootAgainstStateRoot(validatorContainerRoot, stateRoot, validatorContainerRootProof, validatorContainerRootIndex); if (validStateRoot && validVCRootAgainstStateRoot) { valid = true; } } - function verifyStateRoot( + function isValidStateRoot( bytes32 stateRoot, bytes32 beaconBlockRoot, bytes32[] calldata stateRootProof @@ -166,7 +166,7 @@ library BeaconChainProofs { }); } - function verifyVCRootAgainstStateRoot( + function isValidVCRootAgainstStateRoot( bytes32 validatorContainerRoot, bytes32 stateRoot, bytes32[] calldata validatorContainerRootProof, diff --git a/src/libraries/WithdrawalContainer.sol b/src/libraries/WithdrawalContainer.sol new file mode 100644 index 00000000..ea8b67c3 --- /dev/null +++ b/src/libraries/WithdrawalContainer.sol @@ -0,0 +1,46 @@ +pragma solidity ^0.8.19; + +/** + * class Withdrawal(Container): + index: WithdrawalIndex + validator_index: ValidatorIndex + address: ExecutionAddress + amount: Gwei + */ +library WithdrawalContainer { + uint256 internal constant VALID_LENGTH = 4; + uint256 internal constant MERKLE_TREE_HEIGHT = 2; + + function verifyBasic(bytes32[] calldata withdrawalContainer) internal pure returns (bool) { + return withdrawalContainer.length == VALID_LENGTH; + } + + function getWithdrawalIndex(bytes32[] calldata withdrawalContainer) internal pure returns (uint64) { + return uint64(bytes8(withdrawalContainer[0])); + } + + function getValidatorIndex(bytes32[] calldata withdrawalContainer) internal pure returns (uint64) { + return uint64(bytes8(withdrawalContainer[1])); + } + + function getExecutionAddress(bytes32[] calldata withdrawalContainer) internal pure returns (address) { + return address(bytes20(withdrawalContainer[2])); + } + + function getAmount(bytes32[] calldata withdrawalContainer) internal pure returns (uint64) { + return uint64(bytes8(withdrawalContainer[3])); + } + + function merklelize(bytes32[] calldata withdrawalContainer) internal pure returns (bytes32) { + bytes32[] memory leaves = withdrawalContainer; + for (uint i; i < MERKLE_TREE_HEIGHT; i++) { + bytes32[] memory roots = new bytes32[](leaves.length / 2); + for (uint j; j < leaves.length / 2; j++) { + roots[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + } + leaves = roots; + } + + return leaves[0]; + } +} \ No newline at end of file diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 06a9932b..0df24261 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -1,5 +1,8 @@ pragma solidity ^0.8.19; +import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; +import {IClientChainGateway} from "../interfaces/IClientChainGateway.sol"; + contract ExoCapsuleStorage { enum VALIDATOR_STATUS { UNREGISTERED, // the validator has not been registered in this ExoCapsule @@ -22,6 +25,9 @@ contract ExoCapsuleStorage { uint64 public constant BEACON_CHAIN_GENESIS_TIME = 1606824023; address payable exocoreValidatorSetAddress; + IETHPOSDeposit public ethPOS; + IClientChainGateway public gateway; + mapping(bytes32 pubkey => Validator validator) _capsuleValidators; mapping(uint64 index => bytes32 pubkey) _capsuleValidatorsByIndex; From e11feb4a7f8c596ee2d43b1e22165bca067ab9d3 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 15 Apr 2024 16:11:57 +0800 Subject: [PATCH 07/93] add isValidWithdrawalContainerRoot implementation to verify withdrawal merkle proof --- src/core/ExoCapsule.sol | 16 +++++++- src/libraries/BeaconChainProofs.sol | 63 +++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index a269594c..398a2ea1 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -25,6 +25,7 @@ contract ExoCapsule is IETHPOSDeposit public immutable ethPOS; error InvalidValidatorContainer(bytes32 pubkey); + error InvalidWithdrawalContainer(uint64 validatorIndex); error DoubleDepositedValidator(bytes32 pubkey); error GetBeaconBlockRootFailure(uint64 timestamp); error StaleValidatorContainer(bytes32 pubkey, uint64 timestamp); @@ -233,12 +234,23 @@ contract ExoCapsule is proof.stateRootProof ); if (!valid) { - revert InvalidValidatorContainer(validatorPubkey); + revert InvalidValidatorContainer(validatorContainer.getPubkey()); } } function _verifyWithdrawalContainer(bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata proof) internal { - + bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); + bytes32 withdrawalContainerRoot = withdrawalContainer.merklelize(); + bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot( + proof.withdrawalContainerRootProof, + proof.withdrawalContainerRootIndex, + beaconBlockRoot, + proof.executionPayloadRoot, + proof.executionPayloadRootProof + ); + if (!valid) { + revert InvalidWithdrawalContainer(withdrawalContainer.getValidatorIndex()); + } } function _isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint64 atTimestamp) internal view returns (bool) { diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index d73233cc..3894f956 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -185,6 +185,69 @@ library BeaconChainProofs { }); } + function isValidWithdrawalContainerRoot( + bytes32 withdrawalContainerRoot, + bytes32[] calldata withdrawalContainerRootProof, + uint256 withdrawalContainerRootIndex, + bytes32 beaconBlockRoot, + bytes32 executionPayloadRoot, + bytes32[] calldata executionPayloadRootProof + ) internal view returns (bool valid) { + bool validExecutionPayloadRoot = isValidExecutionPayloadRoot(executionPayloadRoot, beaconBlockRoot, executionPayloadRootProof); + bool validWCRootAgainstExecutionPayloadRoot = isValidWCRootAgainstExecutionPayloadRoot( + withdrawalContainerRoot, + executionPayloadRoot, + withdrawalContainerRootProof, + withdrawalContainerRootIndex + ); + if (validExecutionPayloadRoot && validWCRootAgainstExecutionPayloadRoot) { + valid = true; + } + } + + function isValidExecutionPayloadRoot( + bytes32 executionPayloadRoot, + bytes32 beaconBlockRoot, + bytes32[] calldata executionPayloadRootProof + ) internal view returns (bool) { + require( + executionPayloadRootProof.length == BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT, + "state root proof should have 3 nodes" + ); + + uint256 executionPayloadIndex = (BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT)) | + EXECUTION_PAYLOAD_INDEX; + + return Merkle.verifyInclusionSha256({ + proof: executionPayloadRootProof, + root: beaconBlockRoot, + leaf: executionPayloadRoot, + index: executionPayloadIndex + }); + } + + function isValidWCRootAgainstExecutionPayloadRoot( + bytes32 withdrawalContainerRoot, + bytes32 executionPayloadRoot, + bytes32[] calldata withdrawalContainerRootProof, + uint256 withdrawalContainerRootIndex + ) internal view returns (bool) { + require( + withdrawalContainerRootProof.length == (VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_FIELD_TREE_HEIGHT, + "validator container root proof should have 46 nodes" + ); + + uint256 withdrawalIndex = (WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1)) | + uint256(withdrawalContainerRootIndex); + + return Merkle.verifyInclusionSha256({ + proof: withdrawalContainerRootProof, + root: executionPayloadRoot, + leaf: withdrawalContainerRoot, + index: withdrawalIndex + }); + } + /** * @notice This function replicates the ssz hashing of a validator's pubkey, outlined below: * hh := ssz.NewHasher() From 0d1fa3dc1bd29c8ebb93111b0a8c40134d8685d5 Mon Sep 17 00:00:00 2001 From: adu Date: Tue, 16 Apr 2024 20:28:37 +0800 Subject: [PATCH 08/93] add NativeRestakingController contract --- lib/eigenlayer-beacon-oracle | 1 + src/core/ClientChainGateway.sol | 11 +- src/core/ClientChainLzReceiver.sol | 170 ++++++++++++++++++ src/core/ExoCapsule.sol | 30 ---- ...troller.sol => LSTRestakingController.sol} | 5 +- src/core/NativeRestakingController.sol | 18 ++ src/interfaces/IExoCapsule.sol | 18 +- src/interfaces/INativeRestakingController.sol | 77 ++++---- 8 files changed, 245 insertions(+), 85 deletions(-) create mode 160000 lib/eigenlayer-beacon-oracle create mode 100644 src/core/ClientChainLzReceiver.sol rename src/core/{Controller.sol => LSTRestakingController.sol} (97%) create mode 100644 src/core/NativeRestakingController.sol diff --git a/lib/eigenlayer-beacon-oracle b/lib/eigenlayer-beacon-oracle new file mode 160000 index 00000000..a4aba332 --- /dev/null +++ b/lib/eigenlayer-beacon-oracle @@ -0,0 +1 @@ +Subproject commit a4aba33207b07bc55f1a10338507fba97f93d41f diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index d6ae4e49..17e10106 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -2,7 +2,6 @@ pragma solidity ^0.8.19; import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; -import {IController} from "../interfaces/IController.sol"; import {IVault} from "../interfaces/IVault.sol"; import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -13,8 +12,9 @@ import {OAppSenderUpgradeable, MessagingFee} from "../lzApp/OAppSenderUpgradeabl import {OAppReceiverUpgradeable} from "../lzApp/OAppReceiverUpgradeable.sol"; import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; -import {Controller} from "./Controller.sol"; -import {ClientGatewayLzReceiver} from "./ClientGatewayLzReceiver.sol"; +import {LSTRestakingController} from "./LSTRestakingController.sol"; +import {NativeRestakingController} from "./NativeRestakingController.sol"; +import {ClientChainLzReceiver} from "./ClientChainLzReceiver.sol"; import {TSSReceiver} from "./TSSReceiver.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; import {IClientChainGateway} from "../interfaces/IClientChainGateway.sol"; @@ -25,8 +25,9 @@ contract ClientChainGateway is PausableUpgradeable, OwnableUpgradeable, IClientChainGateway, - Controller, - ClientGatewayLzReceiver, + LSTRestakingController, + NativeRestakingController, + ClientChainLzReceiver, TSSReceiver { using SafeERC20 for IERC20; diff --git a/src/core/ClientChainLzReceiver.sol b/src/core/ClientChainLzReceiver.sol new file mode 100644 index 00000000..a2061161 --- /dev/null +++ b/src/core/ClientChainLzReceiver.sol @@ -0,0 +1,170 @@ +pragma solidity ^0.8.19; + +import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; +import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; +import {IVault} from "../interfaces/IVault.sol"; +import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import {OAppReceiverUpgradeable, Origin} from "../lzApp/OAppReceiverUpgradeable.sol"; +import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; +import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; +import {ILayerZeroReceiver} from "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroReceiver.sol"; + +abstract contract ClientChainLzReceiver is PausableUpgradeable, OAppReceiverUpgradeable, ClientChainGatewayStorage { + using SafeERC20 for IERC20; + + modifier onlyCalledFromThis() { + require(msg.sender == address(this), "ClientChainLzReceiver: could only be called from this contract itself with low level call"); + _; + } + + function _lzReceive(Origin calldata _origin, bytes calldata payload) internal virtual override { + if (_origin.srcEid != exocoreChainId) { + revert UnexpectedSourceChain(_origin.srcEid); + } + + _consumeInboundNonce(_origin.srcEid, _origin.sender, _origin.nonce); + + Action act = Action(uint8(payload[0])); + if (act == Action.RESPOND) { + uint64 requestId = uint64(bytes8(payload[1:9])); + + Action requestAct = registeredRequestActions[requestId]; + bytes4 hookSelector = registeredResponseHooks[requestAct]; + if (hookSelector == bytes4(0)) { + revert UnsupportedResponse(act); + } + + bytes memory requestPayload = registeredRequests[requestId]; + if (requestPayload.length == 0) { + revert UnexpectedResponse(requestId); + } + + (bool success, bytes memory reason) = + address(this).call(abi.encodePacked(hookSelector, abi.encode(requestPayload, payload[9:]))); + if (!success) { + revert RequestOrResponseExecuteFailed(act, _origin.nonce, reason); + } + + delete registeredRequests[requestId]; + } else { + bytes4 selector_ = whiteListFunctionSelectors[act]; + if (selector_ == bytes4(0)) { + revert UnsupportedRequest(act); + } + + (bool success, bytes memory reason) = + address(this).call(abi.encodePacked(selector_, abi.encode(payload[1:]))); + if (!success) { + revert RequestOrResponseExecuteFailed(act, _origin.nonce, reason); + } + } + } + + function nextNonce(uint32 srcEid, bytes32 sender) + public + view + virtual + override(OAppReceiverUpgradeable) + returns (uint64) + { + return inboundNonce[srcEid][sender] + 1; + } + + function _consumeInboundNonce(uint32 srcEid, bytes32 sender, uint64 nonce) internal { + inboundNonce[srcEid][sender] += 1; + if (nonce != inboundNonce[srcEid][sender]) { + revert UnexpectedInboundNonce(inboundNonce[srcEid][sender], nonce); + } + } + + function afterReceiveDepositResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, address depositor, uint256 amount) = abi.decode(requestPayload, (address, address, uint256)); + + bool success = (uint8(bytes1(responsePayload[0])) == 1); + uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:])); + if (success) { + IVault vault = tokenVaults[token]; + if (address(vault) == address(0)) { + revert VaultNotExist(); + } + + vault.updatePrincipleBalance(depositor, lastlyUpdatedPrincipleBalance); + } + + emit DepositResult(success, token, depositor, amount); + } + + function afterReceiveWithdrawPrincipleResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, address withdrawer, uint256 unlockPrincipleAmount) = + abi.decode(requestPayload, (address, address, uint256)); + + bool success = (uint8(bytes1(responsePayload[0])) == 1); + uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:33])); + if (success) { + IVault vault = tokenVaults[token]; + if (address(vault) == address(0)) { + revert VaultNotExist(); + } + + vault.updatePrincipleBalance(withdrawer, lastlyUpdatedPrincipleBalance); + vault.updateWithdrawableBalance(withdrawer, unlockPrincipleAmount, 0); + } + + emit WithdrawPrincipleResult(success, token, withdrawer, unlockPrincipleAmount); + } + + function afterReceiveWithdrawRewardResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, address withdrawer, uint256 unlockRewardAmount) = + abi.decode(requestPayload, (address, address, uint256)); + + bool success = (uint8(bytes1(responsePayload[0])) == 1); + uint256 lastlyUpdatedRewardBalance = uint256(bytes32(responsePayload[1:33])); + if (success) { + IVault vault = tokenVaults[token]; + if (address(vault) == address(0)) { + revert VaultNotExist(); + } + + vault.updateRewardBalance(withdrawer, lastlyUpdatedRewardBalance); + vault.updateWithdrawableBalance(withdrawer, 0, unlockRewardAmount); + } + + emit WithdrawRewardResult(success, token, withdrawer, unlockRewardAmount); + } + + function afterReceiveDelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, string memory operator, address delegator, uint256 amount) = + abi.decode(requestPayload, (address, string, address, uint256)); + + bool success = (uint8(bytes1(responsePayload[0])) == 1); + + emit DelegateResult(success, delegator, operator, token, amount); + } + + function afterReceiveUndelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, string memory operator, address undelegator, uint256 amount) = + abi.decode(requestPayload, (address, string, address, uint256)); + + bool success = (uint8(bytes1(responsePayload[0])) == 1); + + emit UndelegateResult(success, undelegator, operator, token, amount); + } +} diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 398a2ea1..9b2d9820 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -113,36 +113,6 @@ contract ExoCapsule is _capsuleValidatorsByIndex[proof.ValidatorContainerRootIndex] = validatorPubkey; } - function updateStakeBalance( - bytes32[] calldata validatorContainer, - ValidatorContainerProof calldata proof - ) external onlyGateway { - bytes32 validatorPubkey = validatorContainer.getPubkey(); - bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); - Validator storage validator = _capsuleValidators[validatorPubkey]; - - if (Validator.status != VALIDATOR_STATUS.REGISTERED) { - revert UnregisteredOrWithdrawnValidatorContainer(validatorPubkey); - } - - if (_isStaleProof(validator, proof.beaconBlockTimestamp)) { - revert StaleValidatorContainer(validatorPubkey, proof.beaconBlockTimestamp); - } - - if (!validatorContainer.verifyBasic()) { - revert InvalidValidatorContainer(validatorPubkey); - } - - if (_hasFullyWithdrawn(validatorContainer)) { - revert FullyWithdrawnValidatorContainer(validatorPubkey); - } - - _verifyValidatorContainer(validatorContainer, proof); - - validator.mostRecentBalanceUpdateTimestamp = proof.beaconBlockTimestamp; - validator.restakedBalanceGwei = validatorContainer.getEffectiveBalance(); - } - function partiallyWithdraw( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, diff --git a/src/core/Controller.sol b/src/core/LSTRestakingController.sol similarity index 97% rename from src/core/Controller.sol rename to src/core/LSTRestakingController.sol index a794f1cd..904d7d62 100644 --- a/src/core/Controller.sol +++ b/src/core/LSTRestakingController.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; -import {IController} from "../interfaces/IController.sol"; +import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; import {IVault} from "../interfaces/IVault.sol"; import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; @@ -13,8 +13,7 @@ import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA. import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; -abstract contract Controller is -PausableUpgradeable, OAppSenderUpgradeable, ClientChainGatewayStorage, IController { +abstract contract LSTRestakingController is PausableUpgradeable, OAppSenderUpgradeable, ClientChainGatewayStorage, ILSTRestakingController { using SafeERC20 for IERC20; using OptionsBuilder for bytes; diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol new file mode 100644 index 00000000..3809c248 --- /dev/null +++ b/src/core/NativeRestakingController.sol @@ -0,0 +1,18 @@ +pragma solidity ^0.8.19; + +import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; +import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; +import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; +import {IVault} from "../interfaces/IVault.sol"; +import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import {OAppSenderUpgradeable, MessagingFee, MessagingReceipt} from "../lzApp/OAppSenderUpgradeable.sol"; +import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; +import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; +import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; + +abstract contract NativeRestakingController is PausableUpgradeable, OAppSenderUpgradeable, ClientChainGatewayStorage, INativeRestakingController { + +} \ No newline at end of file diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index d4e621b6..d149d83b 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -27,17 +27,17 @@ interface IExoCapsule { ValidatorContainerProof calldata proof ) external; - function updateStakeBalance( + function partiallyWithdraw( bytes32[] calldata validatorContainer, - ValidatorContainerProof calldata proof + ValidatorContainerProof calldata validatorProof, + bytes32[] calldata withdrawalContainer, + WithdrawalContainerProof calldata withdrawalProof ) external; - function withdraw( - uint64 beaconBlockTimestamp, - bytes32 beaconStateRoot, - bytes[] calldata beaconStateRootProof, - bytes32[][] calldata withdrawalFields, - uint40[] calldata withdrawalProofIndices, - bytes[] calldata withdrawalFieldsProof + function fullyWithdraw( + bytes32[] calldata validatorContainer, + ValidatorContainerProof calldata validatorProof, + bytes32[] calldata withdrawalContainer, + WithdrawalContainerProof calldata withdrawalProof ) external; } \ No newline at end of file diff --git a/src/interfaces/INativeRestakingController.sol b/src/interfaces/INativeRestakingController.sol index b0971617..d80ef9be 100644 --- a/src/interfaces/INativeRestakingController.sol +++ b/src/interfaces/INativeRestakingController.sol @@ -13,52 +13,53 @@ interface INativeRestakingController { /** * @notice This is called to deposit ETH that is staked on Ethereum beacon chain to Exocore network to be restaked in future * @dev Before deposit, staker should have created the ExoCapsule that it owns and point the validator's withdrawal crendentials - * to the ExoCapsule owned by staker. + * to the ExoCapsule owned by staker. The effective balance of `validatorContainer` would be credited as deposited value by Exocore network. + * @ param */ - function depositAsBeaconValidator(bytes32[] validatorContainer, IExoCapsule.WithdrawalContainerProof proof) external; + function depositBeaconChainValidator(bytes32[] calldata validatorContainer, IExoCapsule.WithdrawalContainerProof calldata proof) external; /** - * @notice After native restaker deposits and delegates on Exocore network, the restaker's principle balance could be influenced by - * rewards/penalties/slashing from both Ethereum beacon chain and Exocore chain, so principle balance update owing to beacon chain - * consensus should be accounted for by Exocore chain as well. + * @notice When a beacon chain partial withdrawal to an ExoCapsule contract happens(the withdrawal time is less than validator's withdrawable_epoch), + * this function could be called with `validatorContainer`, `withdrawalContainer` and corresponding proofs to prove this partial withdrawal + * from beacon chain is done and unlock withdrawn ETH to be claimable for ExoCapsule owner. + * @param validatorContainer is the data structure included in `BeaconState` of `BeaconBlock` that contains beacon chain validator information, + * refer to: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * @param validatorProof is the merkle proof needed for verifying that `validatorContainer` is included in some beacon block root. + * @param withdrawalContainer is the data structure included in `ExecutionPayload` of `BeaconBlockBody` that contains + * withdrawals from beacon chain to execution layer(partial/full), refer to: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#withdrawal + * @param withdrawalProof is the merkle proof needed for verifying that `withdrawalContainer` is included in some beacon block root. */ - function commitBeaconBalanceDelta(bytes32[] validatorContainer, IExoCapsule.WithdrawalContainerProof proof) external; + function processBeaconChainPartialWithdrawal( + bytes32[] calldata validatorContainer, + IExoCapsule.ValidatorContainerProof calldata validatorProof, + bytes32[] calldata withdrawalContainer, + IExoCapsule.WithdrawalContainerProof calldata withdrawalProof + ) external; /** - * @notice Client chain users call to withdraw principle from Exocore to client chain before they are granted to withdraw from the vault. - * @dev This function should ask Exocore validator set for withdrawal grant. If Exocore validator set responds - * with true or success, the corresponding assets should be unlocked to make them claimable by users themselves. Otherwise - * these assets should remain locked. - * @param token - The address of specific token that the user wants to withdraw from Exocore. - * @param principleAmount - principle means the assets user deposits into Exocore for delegating and staking. - * we suppose that After deposit, its amount could only remain unchanged or decrease owing to slashing, which means that direct - * transfer of principle is not possible. + * @notice When a beacon chain full withdrawal to this capsule contract happens(the withdrawal time is euqal to or greater than + * validator's withdrawable_epoch), this function could be called with `validatorContainer`, `withdrawalContainer` and corresponding + * proofs to prove this full withdrawal from beacon chain is done, send withdrawal request to Exocore network to be processed. + * After Exocore network finishs dealing with withdrawal request and sending back the response, ExoCapsule would unlock corresponding ETH + * in response to be cliamable for ExoCapsule owner. + * @param validatorContainer is the data structure included in `BeaconState` of `BeaconBlock` that contains beacon chain validator information, + * refer to: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator + * @param validatorProof is the merkle proof needed for verifying that `validatorContainer` is included in some beacon block root. + * @param withdrawalContainer is the data structure included in `ExecutionPayload` of `BeaconBlockBody` that contains + * withdrawals from beacon chain to execution layer(partial/full), refer to: + * https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#withdrawal + * @param withdrawalProof is the merkle proof needed for verifying that `withdrawalContainer` is included in some beacon block root. */ - function withdrawPrincipleFromExocore(address token, uint256 principleAmount) external payable; - - function withdrawRewardFromExocore(address token, uint256 rewardAmount) external payable; - - /** - * @notice Client chain users call to claim their unlocked assets from the vault. - * @dev This function assumes that the claimable assets should have been unlocked before calling this. - * @dev This function does not ask for grant from Exocore validator set. - * @param token - The address of specific token that the user wants to claim from the vault. - * @param amount - The amount of @param token that the user wants to claim from the vault. - * @param recipient - The destination address that the assets would be transfered to. - */ - function claim(address token, uint256 amount, address recipient) external; - - /// *** function signatures for commands of Exocore validator set forwarded by Gateway *** + function processBeaconChainFullWithdrawal( + bytes32[] calldata validatorContainer, + IExoCapsule.ValidatorContainerProof calldata validatorProof, + bytes32[] calldata withdrawalContainer, + IExoCapsule.WithdrawalContainerProof calldata withdrawalProof + ) external; /** - * @notice This should only be called by Exocore validator set through Gateway to update user's involved - * lastly updated token balance. - * @dev Only Exocore validato set could indirectly call this function through Gateway contract. - * @dev This function could be called in two scenaries: - * 1) Exocore validator set periodically calls this to update user principle and reward balance. - * 2) Exocore validator set sends reponse for the request of withdrawPrincipleFromExocore and unlock part of - * the vault assets and update user's withdrawable balance correspondingly. - * @param info - The info needed for updating users balance. + * @notice Owner of ExoCapsule call this function to claim unlocked ETH from owned ExoCapsule contract. */ - function updateUsersBalances(UserBalanceUpdateInfo[] calldata info) external; + function claim(uint256 amount, address recipient) external; } From 2e11c2ba58dfd5d0051cf8fb000c53f8fd6d0a1b Mon Sep 17 00:00:00 2001 From: adu Date: Wed, 17 Apr 2024 16:38:56 +0800 Subject: [PATCH 09/93] add createExoCapsule function --- src/core/NativeRestakingController.sol | 12 +++++++++++- src/storage/ClientChainGatewayStorage.sol | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 3809c248..b9fe498e 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -4,6 +4,8 @@ import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.so import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; import {IVault} from "../interfaces/IVault.sol"; +import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; +import {ExoCapsule} from "./ExoCapsule.sol"; import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; @@ -14,5 +16,13 @@ import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/Pau import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; abstract contract NativeRestakingController is PausableUpgradeable, OAppSenderUpgradeable, ClientChainGatewayStorage, INativeRestakingController { - + function createExoCapsule() external { + require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); + + IExoCapsule capsule = new ExoCapsule(ETH_STAKING_DEPOSIT_CONTRACT_ADDRESS, address(this)); + capsule.initialize(exocoreValidatorSetAddress); + ownerToCapsule[msg.sender] = capsule; + + emit CapsuleCreated(msg.sender, address(capsule)); + } } \ No newline at end of file diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 9fda51cc..e789e34c 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -1,6 +1,9 @@ pragma solidity ^0.8.19; import {BootstrapStorage} from "./BootstrapStorage.sol"; +import {IVault} from "../interfaces/IVault.sol"; +import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; +import {GatewayStorage} from "./GatewayStorage.sol"; contract ClientChainGatewayStorage is BootstrapStorage { mapping(uint64 => bytes) public registeredRequests; @@ -12,8 +15,28 @@ contract ClientChainGatewayStorage is BootstrapStorage { uint128 constant DESTINATION_GAS_LIMIT = 500000; uint128 constant DESTINATION_MSG_VALUE = 0; + // native restaking state variables + mapping(address => IExoCapsule) public ownerToCapsule; + address constant ETH_STAKING_DEPOSIT_CONTRACT_ADDRESS = 0x00000000219ab540356cBB839Cbe05303d7705Fa; + + event WhitelistTokenAdded(address _token); + event WhitelistTokenRemoved(address _token); + event VaultAdded(address _vault); + event MessageProcessed(uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload); + event MessageFailed(uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload, bytes _reason); + event MessageSent(Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); + event DepositResult(bool indexed success, address indexed token, address indexed depositor, uint256 amount); + event WithdrawPrincipleResult( + bool indexed success, address indexed token, address indexed withdrawer, uint256 amount + ); event WithdrawRewardResult(bool indexed success, address indexed token, address indexed withdrawer, uint256 amount); + // native restaking events + event CapsuleCreated(address owner, address capsule); + + error UnauthorizedSigner(); + error UnauthorizedToken(); + error UnsupportedRequest(Action act); error UnsupportedResponse(Action act); error UnexpectedResponse(uint64 nonce); From 5ff831c2a1e918fea612ab65d685b2f757db39c0 Mon Sep 17 00:00:00 2001 From: adu Date: Wed, 17 Apr 2024 20:23:18 +0800 Subject: [PATCH 10/93] add depositBeaconChainValidator implementation --- src/core/ExoCapsule.sol | 16 +++------ src/core/LSTRestakingController.sol | 2 +- src/core/NativeRestakingController.sol | 40 ++++++++++++++++++++++- src/core/TSSReceiver.sol | 4 +++ src/core/Vault.sol | 4 +-- src/storage/ClientChainGatewayStorage.sol | 6 ++++ src/storage/ExoCapsuleStorage.sol | 4 +-- src/storage/VaultStorage.sol | 4 +-- 8 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 9b2d9820..09885885 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -4,7 +4,6 @@ import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; import {ExoCapsuleStorage} from "../storage/ExoCapsuleStorage.sol"; import {BeaconChainProofs} from "../libraries/BeaconChainProofs.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; import {IClientChainGateway} from "../interfaces/IClientChainGateway.sol"; @@ -13,7 +12,6 @@ import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; contract ExoCapsule is Initializable, - OwnableUpgradeable, PausableUpgradeable, ExoCapsuleStorage, IExoCapsule @@ -46,16 +44,10 @@ contract ExoCapsule is _disableInitializers(); } - function initialize( - address payable _ExocoreValidatorSetAddress - ) - external - initializer - { - require(_ExocoreValidatorSetAddress != address(0), "invalid empty exocore validator set address"); - exocoreValidatorSetAddress = _ExocoreValidatorSetAddress; + function initialize(address _capsuleOwner) external initializer { + require(_capsuleOwner != address(0), "invalid empty exocore validator set address"); + capsuleOwner = _capsuleOwner; - _transferOwnership(exocoreValidatorSetAddress); __Pausable_init(); } @@ -170,7 +162,7 @@ contract ExoCapsule is _verifyValidatorContainer(validatorContainer, validatorProof); _verifyWithdrawalContainer(withdrawalContainer, withdrawalProof); - validator.status = VALIDATOR_STATUS.EXITED; + validator.status = VALIDATOR_STATUS.WITHDRAWN; } function _capsuleWithdrawalCredentials() internal view returns (bytes memory) { diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index 904d7d62..97ced02e 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -150,7 +150,7 @@ abstract contract LSTRestakingController is PausableUpgradeable, OAppSenderUpgra _sendInterchainMsg(Action.REQUEST_UNDELEGATE_FROM, actionArgs); } - function _sendInterchainMsg(Action act, bytes memory actionArgs) internal { + function _sendMsgToExocore(Action act, bytes memory actionArgs) internal { outboundNonce++; bytes memory payload = abi.encodePacked(act, actionArgs); bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption( diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index b9fe498e..da2c51cb 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -16,13 +16,51 @@ import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/Pau import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; abstract contract NativeRestakingController is PausableUpgradeable, OAppSenderUpgradeable, ClientChainGatewayStorage, INativeRestakingController { + using ValidatorContainer for bytes32[]; + using WithdrawalContainer for bytes32[]; + function createExoCapsule() external { require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); IExoCapsule capsule = new ExoCapsule(ETH_STAKING_DEPOSIT_CONTRACT_ADDRESS, address(this)); - capsule.initialize(exocoreValidatorSetAddress); + capsule.initialize(msg.sender); ownerToCapsule[msg.sender] = capsule; + isExoCapsule[capsule] = true; emit CapsuleCreated(msg.sender, address(capsule)); } + + function depositBeaconChainValidator(bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata proof) external { + IExoCapsule capsule = ownerToCapsule[msg.sender]; + if (address(capsule) == address(0)) { + revert CapsuleNotExistForOwner(msg.sender); + } + + capsule.deposit(validatorContainer, proof); + + uint256 depositValue = uint256(validatorContainer.getEffectiveBalance()) * GWEI_TO_WEI; + registeredRequests[outboundNonce + 1] = abi.encode(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, depositValue); + registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DEPOSIT; + + bytes memory actionArgs = abi.encodePacked( + bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), + bytes32(bytes20(msg.sender)), + depositValue + ); + + _sendMsgToExocore(Action.REQUEST_DEPOSIT, actionArgs); + } + + function _sendMsgToExocore(Action act, bytes memory actionArgs) internal { + outboundNonce++; + bytes memory payload = abi.encodePacked(act, actionArgs); + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption( + DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE + ).addExecutorOrderedExecutionOption(); + MessagingFee memory fee = _quote(exocoreChainId, payload, options, false); + + MessagingReceipt memory receipt = + _lzSend(exocoreChainId, payload, options, MessagingFee(fee.nativeFee, 0), exocoreValidatorSetAddress, false); + emit MessageSent(act, receipt.guid, receipt.nonce, receipt.fee.nativeFee); + } } \ No newline at end of file diff --git a/src/core/TSSReceiver.sol b/src/core/TSSReceiver.sol index de872374..3d405680 100644 --- a/src/core/TSSReceiver.sol +++ b/src/core/TSSReceiver.sol @@ -2,6 +2,10 @@ pragma solidity ^0.8.19; import {BootstrapStorage} from "../storage/BootstrapStorage.sol"; import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; +import {IVault} from "../interfaces/IVault.sol"; +import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; diff --git a/src/core/Vault.sol b/src/core/Vault.sol index e0405889..318e0d44 100644 --- a/src/core/Vault.sol +++ b/src/core/Vault.sol @@ -5,7 +5,7 @@ import {IVault} from "../interfaces/IVault.sol"; import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import {IController} from "../interfaces/IController.sol"; +import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; contract Vault is Initializable, VaultStorage, IVault { using SafeERC20 for IERC20; @@ -29,7 +29,7 @@ contract Vault is Initializable, VaultStorage, IVault { function initialize(address _underlyingToken, address _gateway) external initializer { underlyingToken = IERC20(_underlyingToken); - gateway = IController(_gateway); + gateway = ILSTRestakingController(_gateway); } function withdraw(address withdrawer, address recipient, uint256 amount) external onlyGateway { diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index e789e34c..325d34fb 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -17,7 +17,10 @@ contract ClientChainGatewayStorage is BootstrapStorage { // native restaking state variables mapping(address => IExoCapsule) public ownerToCapsule; + mapping(IExoCapsule => bool) public isExoCapsule; address constant ETH_STAKING_DEPOSIT_CONTRACT_ADDRESS = 0x00000000219ab540356cBB839Cbe05303d7705Fa; + address constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + uint256 constant GWEI_TO_WEI = 1e9; event WhitelistTokenAdded(address _token); event WhitelistTokenRemoved(address _token); @@ -40,5 +43,8 @@ contract ClientChainGatewayStorage is BootstrapStorage { error UnsupportedResponse(Action act); error UnexpectedResponse(uint64 nonce); + // native restaking errors + error CapsuleNotExistForOwner(address owner); + uint256[40] private __gap; } diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 0df24261..b3c9fc79 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -7,7 +7,7 @@ contract ExoCapsuleStorage { enum VALIDATOR_STATUS { UNREGISTERED, // the validator has not been registered in this ExoCapsule REGISTERED, // staked on ethpos and withdrawal credentials are pointed to the ExoCapsule - EXITED // withdrawn from the Beacon Chain + WITHDRAWN // withdrawn from the Beacon Chain } struct Validator { @@ -24,7 +24,7 @@ contract ExoCapsuleStorage { address public constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; uint64 public constant BEACON_CHAIN_GENESIS_TIME = 1606824023; - address payable exocoreValidatorSetAddress; + address public capsuelOwner; IETHPOSDeposit public ethPOS; IClientChainGateway public gateway; diff --git a/src/storage/VaultStorage.sol b/src/storage/VaultStorage.sol index c148e1ee..038dcb1a 100644 --- a/src/storage/VaultStorage.sol +++ b/src/storage/VaultStorage.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.19; import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {IController} from "../interfaces/IController.sol"; +import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; contract VaultStorage { IERC20 public underlyingToken; @@ -12,7 +12,7 @@ contract VaultStorage { mapping(address => uint256) public totalDepositedPrincipleAmount; mapping(address => uint256) public totalUnlockPrincipleAmount; - IController public gateway; + ILSTRestakingController public gateway; event PrincipleBalanceUpdated(address, uint256); event RewardBalanceUpdated(address, uint256); From 92f7dd63b096591ba38f355cc8bb756f2fa708ab Mon Sep 17 00:00:00 2001 From: adu Date: Thu, 18 Apr 2024 10:53:04 +0800 Subject: [PATCH 11/93] abstract CommonRestakingController --- src/core/CommonRestakingController.sol | 96 +++++++++++++++++++++++ src/core/ExoCapsule.sol | 10 ++- src/core/LSTRestakingController.sol | 67 +++------------- src/core/NativeRestakingController.sol | 24 +++--- src/interfaces/IExoCapsule.sol | 8 +- src/storage/ClientChainGatewayStorage.sol | 7 ++ 6 files changed, 135 insertions(+), 77 deletions(-) create mode 100644 src/core/CommonRestakingController.sol diff --git a/src/core/CommonRestakingController.sol b/src/core/CommonRestakingController.sol new file mode 100644 index 00000000..bb58420f --- /dev/null +++ b/src/core/CommonRestakingController.sol @@ -0,0 +1,96 @@ +pragma solidity ^0.8.19; + +import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; +import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; +import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; +import {IVault} from "../interfaces/IVault.sol"; +import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import {OAppSenderUpgradeable, MessagingFee, MessagingReceipt} from "../lzApp/OAppSenderUpgradeable.sol"; +import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; +import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; +import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; +import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; + +abstract contract LSTRestakingController is PausableUpgradeable, OAppSenderUpgradeable, ClientChainGatewayStorage { + using SafeERC20 for IERC20; + using OptionsBuilder for bytes; + + receive() external payable {} + + function claim(address token, uint256 amount, address recipient) external whenNotPaused { + require(whitelistTokens[token], "Controller: token is not whitelisted"); + require(amount > 0, "Controller: amount should be greater than zero"); + + if (token == VIRTUAL_STAKED_ETH_ADDRESS) { + IExoCapsule capsule = ownerToCapsule[msg.sender]; + if (address(capsule) == address(0)) { + revert CapsuleNotExistForOwner(msg.sender); + } + + capsule.withdraw(amount, recipient); + + emit ClaimSucceeded(token, recipient, amount); + } else { + IVault vault = tokenVaults[token]; + if (address(vault) == address(0)) { + revert VaultNotExist(); + } + + vault.withdraw(msg.sender, recipient, amount); + + emit ClaimSucceeded(token, recipient, amount); + } + } + + function delegateTo(string calldata operator, address token, uint256 amount) external payable whenNotPaused { + require(whitelistTokens[token], "Controller: token is not whitelisted"); + require(amount > 0, "Controller: amount should be greater than zero"); + require(bytes(operator).length == 44, "Controller: invalid bech32 address"); + + IVault vault = tokenVaults[token]; + if (address(vault) == address(0)) { + revert VaultNotExist(); + } + + registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); + registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DELEGATE_TO; + + bytes memory actionArgs = + abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), bytes(operator), amount); + _sendMsgToExocore(Action.REQUEST_DELEGATE_TO, actionArgs); + } + + function undelegateFrom(string calldata operator, address token, uint256 amount) external payable whenNotPaused { + require(whitelistTokens[token], "Controller: token is not whitelisted"); + require(amount > 0, "Controller: amount should be greater than zero"); + require(bytes(operator).length == 42, "Controller: invalid bech32 address"); + + IVault vault = tokenVaults[token]; + if (address(vault) == address(0)) { + revert VaultNotExist(); + } + + registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); + registeredRequestActions[outboundNonce + 1] = Action.REQUEST_UNDELEGATE_FROM; + + bytes memory actionArgs = + abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), bytes(operator), amount); + _sendMsgToExocore(Action.REQUEST_UNDELEGATE_FROM, actionArgs); + } + + function _sendMsgToExocore(Action act, bytes memory actionArgs) internal { + outboundNonce++; + bytes memory payload = abi.encodePacked(act, actionArgs); + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption( + DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE + ).addExecutorOrderedExecutionOption(); + MessagingFee memory fee = _quote(exocoreChainId, payload, options, false); + + MessagingReceipt memory receipt = + _lzSend(exocoreChainId, payload, options, MessagingFee(fee.nativeFee, 0), exocoreValidatorSetAddress, false); + emit MessageSent(act, receipt.guid, receipt.nonce, receipt.fee.nativeFee); + } +} diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 09885885..42e4172c 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -67,7 +67,7 @@ contract ExoCapsule is emit StakedWithThisCapsule(); } - function deposit( + function verifyDepositProof( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof ) external onlyGateway { @@ -105,7 +105,7 @@ contract ExoCapsule is _capsuleValidatorsByIndex[proof.ValidatorContainerRootIndex] = validatorPubkey; } - function partiallyWithdraw( + function verifyPartialWithdrawalProof( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, @@ -134,7 +134,7 @@ contract ExoCapsule is _verifyWithdrawalContainer(withdrawalContainer, withdrawalProof); } - function fullyWithdraw( + function verifyFullWithdrawalProof( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, @@ -165,6 +165,10 @@ contract ExoCapsule is validator.status = VALIDATOR_STATUS.WITHDRAWN; } + function withdraw(uint256 amount, address recipient) external { + + } + function _capsuleWithdrawalCredentials() internal view returns (bytes memory) { /** * The withdrawal_credentials field must be such that: diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index 97ced02e..a55216b5 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -12,13 +12,17 @@ import {OAppSenderUpgradeable, MessagingFee, MessagingReceipt} from "../lzApp/OA import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; - -abstract contract LSTRestakingController is PausableUpgradeable, OAppSenderUpgradeable, ClientChainGatewayStorage, ILSTRestakingController { +import {CommonRestakingController} from "./CommonRestakingController.sol"; + +abstract contract LSTRestakingController is + PausableUpgradeable, + OAppSenderUpgradeable, + ILSTRestakingController, + CommonRestakingController +{ using SafeERC20 for IERC20; using OptionsBuilder for bytes; - receive() external payable {} - function deposit(address token, uint256 amount) external payable whenNotPaused { require(whitelistTokens[token], "Controller: token is not whitelisted"); require(amount > 0, "Controller: amount should be greater than zero"); @@ -32,7 +36,7 @@ abstract contract LSTRestakingController is PausableUpgradeable, OAppSenderUpgra registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DEPOSIT; bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), amount); - _sendInterchainMsg(Action.REQUEST_DEPOSIT, actionArgs); + _sendMsgToExocore(Action.REQUEST_DEPOSIT, actionArgs); } function withdrawPrincipleFromExocore(address token, uint256 principleAmount) external payable whenNotPaused { @@ -49,7 +53,7 @@ abstract contract LSTRestakingController is PausableUpgradeable, OAppSenderUpgra bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), principleAmount); - _sendInterchainMsg(Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, actionArgs); + _sendMsgToExocore(Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, actionArgs); } function withdrawRewardFromExocore(address token, uint256 rewardAmount) external payable whenNotPaused { @@ -65,7 +69,7 @@ abstract contract LSTRestakingController is PausableUpgradeable, OAppSenderUpgra registeredRequestActions[outboundNonce + 1] = Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE; bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), rewardAmount); - _sendInterchainMsg(Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, actionArgs); + _sendMsgToExocore(Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, actionArgs); } function claim(address token, uint256 amount, address recipient) external whenNotPaused { @@ -113,53 +117,4 @@ abstract contract LSTRestakingController is PausableUpgradeable, OAppSenderUpgra } } } - - function delegateTo(string calldata operator, address token, uint256 amount) external payable whenNotPaused { - require(whitelistTokens[token], "Controller: token is not whitelisted"); - require(amount > 0, "Controller: amount should be greater than zero"); - require(bytes(operator).length == 44, "Controller: invalid bech32 address"); - - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - - registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); - registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DELEGATE_TO; - - bytes memory actionArgs = - abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), bytes(operator), amount); - _sendInterchainMsg(Action.REQUEST_DELEGATE_TO, actionArgs); - } - - function undelegateFrom(string calldata operator, address token, uint256 amount) external payable whenNotPaused { - require(whitelistTokens[token], "Controller: token is not whitelisted"); - require(amount > 0, "Controller: amount should be greater than zero"); - require(bytes(operator).length == 44, "Controller: invalid bech32 address"); - - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - - registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); - registeredRequestActions[outboundNonce + 1] = Action.REQUEST_UNDELEGATE_FROM; - - bytes memory actionArgs = - abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), bytes(operator), amount); - _sendInterchainMsg(Action.REQUEST_UNDELEGATE_FROM, actionArgs); - } - - function _sendMsgToExocore(Action act, bytes memory actionArgs) internal { - outboundNonce++; - bytes memory payload = abi.encodePacked(act, actionArgs); - bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption( - DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE - ).addExecutorOrderedExecutionOption(); - MessagingFee memory fee = _quote(exocoreChainId, payload, options, false); - - MessagingReceipt memory receipt = - _lzSend(exocoreChainId, payload, options, MessagingFee(fee.nativeFee, 0), exocoreValidatorSetAddress, false); - emit MessageSent(act, receipt.guid, receipt.nonce, receipt.fee.nativeFee); - } } diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index da2c51cb..9bba5d81 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -14,8 +14,15 @@ import {OAppSenderUpgradeable, MessagingFee, MessagingReceipt} from "../lzApp/OA import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; +import {CommonRestakingController} from "./CommonRestakingController.sol"; -abstract contract NativeRestakingController is PausableUpgradeable, OAppSenderUpgradeable, ClientChainGatewayStorage, INativeRestakingController { + +abstract contract NativeRestakingController is + PausableUpgradeable, + OAppSenderUpgradeable, + INativeRestakingController, + CommonRestakingController +{ using ValidatorContainer for bytes32[]; using WithdrawalContainer for bytes32[]; @@ -36,7 +43,7 @@ abstract contract NativeRestakingController is PausableUpgradeable, OAppSenderUp revert CapsuleNotExistForOwner(msg.sender); } - capsule.deposit(validatorContainer, proof); + capsule.verifyPartialWithdrawalProof(validatorContainer, proof); uint256 depositValue = uint256(validatorContainer.getEffectiveBalance()) * GWEI_TO_WEI; registeredRequests[outboundNonce + 1] = abi.encode(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, depositValue); @@ -50,17 +57,4 @@ abstract contract NativeRestakingController is PausableUpgradeable, OAppSenderUp _sendMsgToExocore(Action.REQUEST_DEPOSIT, actionArgs); } - - function _sendMsgToExocore(Action act, bytes memory actionArgs) internal { - outboundNonce++; - bytes memory payload = abi.encodePacked(act, actionArgs); - bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption( - DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE - ).addExecutorOrderedExecutionOption(); - MessagingFee memory fee = _quote(exocoreChainId, payload, options, false); - - MessagingReceipt memory receipt = - _lzSend(exocoreChainId, payload, options, MessagingFee(fee.nativeFee, 0), exocoreValidatorSetAddress, false); - emit MessageSent(act, receipt.guid, receipt.nonce, receipt.fee.nativeFee); - } } \ No newline at end of file diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index d149d83b..0c7417e0 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -22,22 +22,24 @@ interface IExoCapsule { function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; - function deposit( + function verifyDepositProof( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof ) external; - function partiallyWithdraw( + function verifyPartialWithdrawalProof( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof ) external; - function fullyWithdraw( + function verifyFullWithdrawalProof( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof ) external; + + function withdraw(uint256 amount, address recipient) external; } \ No newline at end of file diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 325d34fb..85b75ce1 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -33,6 +33,13 @@ contract ClientChainGatewayStorage is BootstrapStorage { bool indexed success, address indexed token, address indexed withdrawer, uint256 amount ); event WithdrawRewardResult(bool indexed success, address indexed token, address indexed withdrawer, uint256 amount); + event DelegateResult( + bool indexed success, address indexed delegator, string delegatee, address token, uint256 amount + ); + event UndelegateResult( + bool indexed success, address indexed undelegator, string indexed undelegatee, address token, uint256 amount + ); + event ClaimSucceeded(address token, address recipient, uint256 amount); // native restaking events event CapsuleCreated(address owner, address capsule); From c201ee13b20dd085f2e7249522846dad11ddd9a6 Mon Sep 17 00:00:00 2001 From: adu Date: Thu, 18 Apr 2024 11:26:58 +0800 Subject: [PATCH 12/93] add whenNotPaused modifier to functions --- src/core/ClientChainLzReceiver.sol | 2 +- src/core/NativeRestakingController.sol | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/ClientChainLzReceiver.sol b/src/core/ClientChainLzReceiver.sol index a2061161..cbfcab5d 100644 --- a/src/core/ClientChainLzReceiver.sol +++ b/src/core/ClientChainLzReceiver.sol @@ -20,7 +20,7 @@ abstract contract ClientChainLzReceiver is PausableUpgradeable, OAppReceiverUpgr _; } - function _lzReceive(Origin calldata _origin, bytes calldata payload) internal virtual override { + function _lzReceive(Origin calldata _origin, bytes calldata payload) internal virtual override whenNotPaused { if (_origin.srcEid != exocoreChainId) { revert UnexpectedSourceChain(_origin.srcEid); } diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 9bba5d81..632d134c 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -26,7 +26,7 @@ abstract contract NativeRestakingController is using ValidatorContainer for bytes32[]; using WithdrawalContainer for bytes32[]; - function createExoCapsule() external { + function createExoCapsule() external whenNotPaused { require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); IExoCapsule capsule = new ExoCapsule(ETH_STAKING_DEPOSIT_CONTRACT_ADDRESS, address(this)); @@ -37,7 +37,10 @@ abstract contract NativeRestakingController is emit CapsuleCreated(msg.sender, address(capsule)); } - function depositBeaconChainValidator(bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata proof) external { + function depositBeaconChainValidator( + bytes32[] calldata validatorContainer, + IExoCapsule.ValidatorContainerProof calldata proof + ) external whenNotPaused { IExoCapsule capsule = ownerToCapsule[msg.sender]; if (address(capsule) == address(0)) { revert CapsuleNotExistForOwner(msg.sender); From c6b241b478eb5ab5daee3ceaa5cdfd2ed09d4b91 Mon Sep 17 00:00:00 2001 From: adu Date: Thu, 18 Apr 2024 11:31:08 +0800 Subject: [PATCH 13/93] rename CommonRestakingController as BaseRestakingController --- ...onRestakingController.sol => BaseRestakingController.sol} | 2 +- src/core/LSTRestakingController.sol | 4 ++-- src/core/NativeRestakingController.sol | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) rename src/core/{CommonRestakingController.sol => BaseRestakingController.sol} (97%) diff --git a/src/core/CommonRestakingController.sol b/src/core/BaseRestakingController.sol similarity index 97% rename from src/core/CommonRestakingController.sol rename to src/core/BaseRestakingController.sol index bb58420f..da16f4cf 100644 --- a/src/core/CommonRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -14,7 +14,7 @@ import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/Pau import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; -abstract contract LSTRestakingController is PausableUpgradeable, OAppSenderUpgradeable, ClientChainGatewayStorage { +abstract contract BaseRestakingController is PausableUpgradeable, OAppSenderUpgradeable, ClientChainGatewayStorage { using SafeERC20 for IERC20; using OptionsBuilder for bytes; diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index a55216b5..84085ec0 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -12,13 +12,13 @@ import {OAppSenderUpgradeable, MessagingFee, MessagingReceipt} from "../lzApp/OA import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; -import {CommonRestakingController} from "./CommonRestakingController.sol"; +import {BaseRestakingController} from "./BaseRestakingController.sol"; abstract contract LSTRestakingController is PausableUpgradeable, OAppSenderUpgradeable, ILSTRestakingController, - CommonRestakingController + BaseRestakingController { using SafeERC20 for IERC20; using OptionsBuilder for bytes; diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 632d134c..b53ee115 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -14,14 +14,13 @@ import {OAppSenderUpgradeable, MessagingFee, MessagingReceipt} from "../lzApp/OA import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; -import {CommonRestakingController} from "./CommonRestakingController.sol"; - +import {BaseRestakingController} from "./BaseRestakingController.sol"; abstract contract NativeRestakingController is PausableUpgradeable, OAppSenderUpgradeable, INativeRestakingController, - CommonRestakingController + BaseRestakingController { using ValidatorContainer for bytes32[]; using WithdrawalContainer for bytes32[]; From 973a8e85b1efc3cde3233cbb8dba31039c182b4c Mon Sep 17 00:00:00 2001 From: adu Date: Thu, 18 Apr 2024 16:03:11 +0800 Subject: [PATCH 14/93] remove unused imports and fix errors --- src/core/BaseRestakingController.sol | 22 ++--- src/core/ClientChainGateway.sol | 21 ++--- src/core/ClientChainLzReceiver.sol | 10 +- src/core/ExoCapsule.sol | 54 ++++------- src/core/ExocoreGateway.sol | 6 +- src/core/LSTRestakingController.sol | 27 +----- src/core/NativeRestakingController.sol | 38 +++++--- src/core/TSSReceiver.sol | 8 +- src/core/Vault.sol | 3 +- src/interfaces/IBaseRestakingController.sol | 26 ++++++ src/interfaces/IClientChainGateway.sol | 9 +- src/interfaces/ILSTRestakingController.sol | 25 +---- src/interfaces/INativeRestakingController.sol | 10 +- src/interfaces/IVault.sol | 2 - src/libraries/BeaconChainProofs.sol | 93 ------------------- src/libraries/ValidatorContainer.sol | 4 +- src/libraries/WithdrawalContainer.sol | 4 +- src/storage/ExoCapsuleStorage.sol | 11 ++- test/foundry/ClientChainGateway.t.sol | 5 +- test/foundry/Delegation.t.sol | 1 - test/foundry/DepositWithdrawPrinciple.t.sol | 13 +-- test/foundry/WithdrawReward.t.sol | 1 - 22 files changed, 128 insertions(+), 265 deletions(-) create mode 100644 src/interfaces/IBaseRestakingController.sol diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index da16f4cf..6dde44eb 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -1,21 +1,21 @@ pragma solidity ^0.8.19; import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; -import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; -import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; -import {IVault} from "../interfaces/IVault.sol"; -import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {OAppSenderUpgradeable, MessagingFee, MessagingReceipt} from "../lzApp/OAppSenderUpgradeable.sol"; -import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; +import {IVault} from "../interfaces/IVault.sol"; +import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; +import {IBaseRestakingController} from "../interfaces/IBaseRestakingController.sol"; + import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; -import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; -abstract contract BaseRestakingController is PausableUpgradeable, OAppSenderUpgradeable, ClientChainGatewayStorage { - using SafeERC20 for IERC20; + +abstract contract BaseRestakingController is + PausableUpgradeable, + OAppSenderUpgradeable, + IBaseRestakingController, + ClientChainGatewayStorage +{ using OptionsBuilder for bytes; receive() external payable {} diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index 17e10106..5b0705c1 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -1,23 +1,20 @@ pragma solidity ^0.8.19; -import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; -import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; import {IVault} from "../interfaces/IVault.sol"; -import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import {IOAppCore} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppCore.sol"; import {OAppCoreUpgradeable} from "../lzApp/OAppCoreUpgradeable.sol"; import {OAppSenderUpgradeable, MessagingFee} from "../lzApp/OAppSenderUpgradeable.sol"; import {OAppReceiverUpgradeable} from "../lzApp/OAppReceiverUpgradeable.sol"; -import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {LSTRestakingController} from "./LSTRestakingController.sol"; import {NativeRestakingController} from "./NativeRestakingController.sol"; import {ClientChainLzReceiver} from "./ClientChainLzReceiver.sol"; +import {IClientChainGateway} from "../interfaces/IClientChainGateway.sol"; import {TSSReceiver} from "./TSSReceiver.sol"; + +import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; +import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; -import {IClientChainGateway} from "../interfaces/IClientChainGateway.sol"; import {IOAppCore} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppCore.sol"; contract ClientChainGateway is @@ -27,11 +24,9 @@ contract ClientChainGateway is IClientChainGateway, LSTRestakingController, NativeRestakingController, - ClientChainLzReceiver, - TSSReceiver + TSSReceiver, + ClientChainLzReceiver { - using SafeERC20 for IERC20; - using ECDSA for bytes32; using OptionsBuilder for bytes; constructor(address _endpoint) OAppCoreUpgradeable(_endpoint) { diff --git a/src/core/ClientChainLzReceiver.sol b/src/core/ClientChainLzReceiver.sol index cbfcab5d..b663b623 100644 --- a/src/core/ClientChainLzReceiver.sol +++ b/src/core/ClientChainLzReceiver.sol @@ -1,20 +1,12 @@ pragma solidity ^0.8.19; import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; -import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; import {IVault} from "../interfaces/IVault.sol"; -import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {OAppReceiverUpgradeable, Origin} from "../lzApp/OAppReceiverUpgradeable.sol"; -import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; + import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; -import {ILayerZeroReceiver} from "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroReceiver.sol"; abstract contract ClientChainLzReceiver is PausableUpgradeable, OAppReceiverUpgradeable, ClientChainGatewayStorage { - using SafeERC20 for IERC20; - modifier onlyCalledFromThis() { require(msg.sender == address(this), "ClientChainLzReceiver: could only be called from this contract itself with low level call"); _; diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 42e4172c..a5558c0c 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -3,25 +3,22 @@ pragma solidity ^0.8.19; import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; import {ExoCapsuleStorage} from "../storage/ExoCapsuleStorage.sol"; import {BeaconChainProofs} from "../libraries/BeaconChainProofs.sol"; -import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/security/PausableUpgradeable.sol"; import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; -import {IClientChainGateway} from "../interfaces/IClientChainGateway.sol"; +import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; +import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; + contract ExoCapsule is Initializable, - PausableUpgradeable, ExoCapsuleStorage, IExoCapsule { - using BeaconChainProofs for bytes; + using BeaconChainProofs for bytes32; using ValidatorContainer for bytes32[]; using WithdrawalContainer for bytes32[]; - IETHPOSDeposit public immutable ethPOS; - error InvalidValidatorContainer(bytes32 pubkey); error InvalidWithdrawalContainer(uint64 validatorIndex); error DoubleDepositedValidator(bytes32 pubkey); @@ -39,7 +36,7 @@ contract ExoCapsule is constructor(address _ethPOS, address _gateway) { ethPOS = IETHPOSDeposit(_ethPOS); - gateway = IClientChainGateway(_gateway); + gateway = INativeRestakingController(_gateway); _disableInitializers(); } @@ -47,21 +44,9 @@ contract ExoCapsule is function initialize(address _capsuleOwner) external initializer { require(_capsuleOwner != address(0), "invalid empty exocore validator set address"); capsuleOwner = _capsuleOwner; - - __Pausable_init(); - } - - function pause() external { - require(msg.sender == exocoreValidatorSetAddress, "only Exocore validator set aggregated address could call this"); - _pause(); - } - - function unpause() external { - require(msg.sender == exocoreValidatorSetAddress, "only Exocore validator set aggregated address could call this"); - _unpause(); } - function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable { + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable onlyGateway { require(msg.value == 32 ether, "stake value must be exactly 32 ether"); ethPOS.deposit{value: 32 ether}(pubkey, _capsuleWithdrawalCredentials(), signature, depositDataRoot); emit StakedWithThisCapsule(); @@ -83,7 +68,7 @@ contract ExoCapsule is revert StaleValidatorContainer(validatorPubkey, proof.beaconBlockTimestamp); } - if (!validatorContainer.verifyBasic()) { + if (!validatorContainer.verifyValidatorContainerBasic()) { revert InvalidValidatorContainer(validatorPubkey); } @@ -91,7 +76,7 @@ contract ExoCapsule is revert InvalidValidatorContainer(validatorPubkey); } - if (withdrawalCredentials != _capsuleWithdrawalCredentials()) { + if (withdrawalCredentials != bytes32(_capsuleWithdrawalCredentials())) { revert InvalidValidatorContainer(validatorPubkey); } @@ -102,7 +87,7 @@ contract ExoCapsule is validator.mostRecentBalanceUpdateTimestamp = proof.beaconBlockTimestamp; validator.restakedBalanceGwei = validatorContainer.getEffectiveBalance(); - _capsuleValidatorsByIndex[proof.ValidatorContainerRootIndex] = validatorPubkey; + _capsuleValidatorsByIndex[proof.validatorContainerRootIndex] = validatorPubkey; } function verifyPartialWithdrawalProof( @@ -112,10 +97,8 @@ contract ExoCapsule is WithdrawalContainerProof calldata withdrawalProof ) external onlyGateway { bytes32 validatorPubkey = validatorContainer.getPubkey(); - bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); uint64 withdrawableEpoch = validatorContainer.getWithdrawableEpoch(); - Validator storage validator = _capsuleValidators[validatorPubkey]; bool partialWithdrawal = _timestampToEpoch(validatorProof.beaconBlockTimestamp) < withdrawableEpoch; if (!partialWithdrawal) { @@ -126,7 +109,7 @@ contract ExoCapsule is revert UnmatchedValidatorAndWithdrawal(validatorPubkey); } - if (!validatorContainer.verifyBasic()) { + if (!validatorContainer.verifyValidatorContainerBasic()) { revert InvalidValidatorContainer(validatorPubkey); } @@ -141,7 +124,6 @@ contract ExoCapsule is WithdrawalContainerProof calldata withdrawalProof ) external onlyGateway { bytes32 validatorPubkey = validatorContainer.getPubkey(); - bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); uint64 withdrawableEpoch = validatorContainer.getWithdrawableEpoch(); Validator storage validator = _capsuleValidators[validatorPubkey]; @@ -155,7 +137,7 @@ contract ExoCapsule is revert UnmatchedValidatorAndWithdrawal(validatorPubkey); } - if (!validatorContainer.verifyBasic()) { + if (!validatorContainer.verifyValidatorContainerBasic()) { revert InvalidValidatorContainer(validatorPubkey); } @@ -165,7 +147,7 @@ contract ExoCapsule is validator.status = VALIDATOR_STATUS.WITHDRAWN; } - function withdraw(uint256 amount, address recipient) external { + function withdraw(uint256 amount, address recipient) external onlyGateway { } @@ -179,8 +161,8 @@ contract ExoCapsule is return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); } - function getBeaconBlockRoot(uint64 timestamp) public view returns (bytes32) { - (bool success, bytes memory rootBytes) = BEACON_ROOTS_ADDRESS.call{value: bytes32(bytes8(timestamp))} + function getBeaconBlockRoot(uint64 timestamp) public returns (bytes32) { + (bool success, bytes memory rootBytes) = BEACON_ROOTS_ADDRESS.call(abi.encodePacked(timestamp)); if (!success) { revert GetBeaconBlockRootFailure(timestamp); } @@ -191,7 +173,7 @@ contract ExoCapsule is function _verifyValidatorContainer(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) internal { bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); - bytes32 validatorContainerRoot = validatorContainer.merklelize(); + bytes32 validatorContainerRoot = validatorContainer.merklelizeValidatorContainer(); bool valid = validatorContainerRoot.isValidValidatorContainerRoot( proof.validatorContainerRootProof, proof.validatorContainerRootIndex, @@ -206,7 +188,7 @@ contract ExoCapsule is function _verifyWithdrawalContainer(bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata proof) internal { bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); - bytes32 withdrawalContainerRoot = withdrawalContainer.merklelize(); + bytes32 withdrawalContainerRoot = withdrawalContainer.merklelizeWithdrawalContainer(); bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot( proof.withdrawalContainerRootProof, proof.withdrawalContainerRootIndex, @@ -238,7 +220,7 @@ contract ExoCapsule is } function _hasFullyWithdrawn(bytes32[] calldata validatorContainer) internal view returns (bool) { - if (validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(block.timestamp)) { + if (validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(uint64(block.timestamp))) { if (validatorContainer.getEffectiveBalance() == 0) { return true; } @@ -252,7 +234,7 @@ contract ExoCapsule is * seconds since genesis, and dividing by seconds per epoch. * reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md */ - function _timestampToEpoch(uint64 timestamp) internal view returns (uint64) { + function _timestampToEpoch(uint64 timestamp) internal pure returns (uint64) { require(timestamp >= BEACON_CHAIN_GENESIS_TIME, "timestamp should be greater than beacon chain genesis timestamp"); return (timestamp - BEACON_CHAIN_GENESIS_TIME) / BeaconChainProofs.SECONDS_PER_EPOCH; } diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index 6638901f..d1bf9bc1 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -1,8 +1,7 @@ pragma solidity ^0.8.19; import {ExocoreGatewayStorage} from "../storage/ExocoreGatewayStorage.sol"; -import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {IExocoreGateway} from "../interfaces/IExocoreGateway.sol"; import { OAppReceiverUpgradeable, OAppUpgradeable, @@ -10,10 +9,11 @@ import { MessagingFee, MessagingReceipt } from "../lzApp/OAppUpgradeable.sol"; + +import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; -import {IExocoreGateway} from "../interfaces/IExocoreGateway.sol"; import {ILayerZeroReceiver} from "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroReceiver.sol"; import {IClientChains} from "../interfaces/precompiles/IClientChains.sol"; diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index 84085ec0..2d020e24 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -1,28 +1,17 @@ pragma solidity ^0.8.19; import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; -import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; import {IVault} from "../interfaces/IVault.sol"; -import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; -import {OAppSenderUpgradeable, MessagingFee, MessagingReceipt} from "../lzApp/OAppSenderUpgradeable.sol"; -import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; -import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; import {BaseRestakingController} from "./BaseRestakingController.sol"; +import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; + abstract contract LSTRestakingController is PausableUpgradeable, - OAppSenderUpgradeable, ILSTRestakingController, BaseRestakingController { - using SafeERC20 for IERC20; - using OptionsBuilder for bytes; - function deposit(address token, uint256 amount) external payable whenNotPaused { require(whitelistTokens[token], "Controller: token is not whitelisted"); require(amount > 0, "Controller: amount should be greater than zero"); @@ -72,18 +61,6 @@ abstract contract LSTRestakingController is _sendMsgToExocore(Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, actionArgs); } - function claim(address token, uint256 amount, address recipient) external whenNotPaused { - require(whitelistTokens[token], "Controller: token is not whitelisted"); - require(amount > 0, "Controller: amount should be greater than zero"); - - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - - vault.withdraw(msg.sender, recipient, amount); - } - function updateUsersBalances(UserBalanceUpdateInfo[] calldata info) public whenNotPaused { require(msg.sender == address(this), "Controller: caller must be client chain gateway itself"); for (uint256 i = 0; i < info.length; i++) { diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index b53ee115..e34c3293 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -1,34 +1,24 @@ pragma solidity ^0.8.19; -import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; -import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; -import {IVault} from "../interfaces/IVault.sol"; import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; import {ExoCapsule} from "./ExoCapsule.sol"; -import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; -import {OAppSenderUpgradeable, MessagingFee, MessagingReceipt} from "../lzApp/OAppSenderUpgradeable.sol"; -import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; -import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; import {BaseRestakingController} from "./BaseRestakingController.sol"; +import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; + +import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; abstract contract NativeRestakingController is PausableUpgradeable, - OAppSenderUpgradeable, INativeRestakingController, BaseRestakingController { using ValidatorContainer for bytes32[]; - using WithdrawalContainer for bytes32[]; function createExoCapsule() external whenNotPaused { require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); - IExoCapsule capsule = new ExoCapsule(ETH_STAKING_DEPOSIT_CONTRACT_ADDRESS, address(this)); + ExoCapsule capsule = new ExoCapsule(ETH_STAKING_DEPOSIT_CONTRACT_ADDRESS, address(this)); capsule.initialize(msg.sender); ownerToCapsule[msg.sender] = capsule; isExoCapsule[capsule] = true; @@ -45,7 +35,7 @@ abstract contract NativeRestakingController is revert CapsuleNotExistForOwner(msg.sender); } - capsule.verifyPartialWithdrawalProof(validatorContainer, proof); + capsule.verifyDepositProof(validatorContainer, proof); uint256 depositValue = uint256(validatorContainer.getEffectiveBalance()) * GWEI_TO_WEI; registeredRequests[outboundNonce + 1] = abi.encode(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, depositValue); @@ -59,4 +49,22 @@ abstract contract NativeRestakingController is _sendMsgToExocore(Action.REQUEST_DEPOSIT, actionArgs); } + + function processBeaconChainPartialWithdrawal( + bytes32[] calldata validatorContainer, + IExoCapsule.ValidatorContainerProof calldata validatorProof, + bytes32[] calldata withdrawalContainer, + IExoCapsule.WithdrawalContainerProof calldata withdrawalProof + ) external whenNotPaused { + + } + + function processBeaconChainFullWithdrawal( + bytes32[] calldata validatorContainer, + IExoCapsule.ValidatorContainerProof calldata validatorProof, + bytes32[] calldata withdrawalContainer, + IExoCapsule.WithdrawalContainerProof calldata withdrawalProof + ) external whenNotPaused { + + } } \ No newline at end of file diff --git a/src/core/TSSReceiver.sol b/src/core/TSSReceiver.sol index 3d405680..4bf67eae 100644 --- a/src/core/TSSReceiver.sol +++ b/src/core/TSSReceiver.sol @@ -2,15 +2,11 @@ pragma solidity ^0.8.19; import {BootstrapStorage} from "../storage/BootstrapStorage.sol"; import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; -import {IVault} from "../interfaces/IVault.sol"; -import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; -import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; -import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; + import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; -abstract contract TSSReceiver is PausableUpgradeable, BootstrapStorage, ITSSReceiver { +abstract contract TSSReceiver is PausableUpgradeable, ClientChainGatewayStorage, ITSSReceiver { using ECDSA for bytes32; function receiveInterchainMsg(InterchainMsg calldata _msg, bytes calldata signature) external whenNotPaused { diff --git a/src/core/Vault.sol b/src/core/Vault.sol index 318e0d44..ab62c8fd 100644 --- a/src/core/Vault.sol +++ b/src/core/Vault.sol @@ -2,10 +2,11 @@ pragma solidity ^0.8.19; import {VaultStorage} from "../storage/VaultStorage.sol"; import {IVault} from "../interfaces/IVault.sol"; +import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; + import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {SafeERC20} from "@openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; contract Vault is Initializable, VaultStorage, IVault { using SafeERC20 for IERC20; diff --git a/src/interfaces/IBaseRestakingController.sol b/src/interfaces/IBaseRestakingController.sol new file mode 100644 index 00000000..14d79b02 --- /dev/null +++ b/src/interfaces/IBaseRestakingController.sol @@ -0,0 +1,26 @@ +pragma solidity ^0.8.19; + +interface IBaseRestakingController { + /// *** function signatures for staker operations *** + + /** + * @notice Client chain users call to delegate deposited token to specific node operator. + * @dev This assumes that the delegated assets should have already been deposited to Exocore system. + * @param operator - The address of a registered node operator that the user wants to delegate to. + * @param token - The address of specific token that the user wants to delegate to. + * @param amount - The amount of @param token that the user wants to delegate to node operator. + */ + function delegateTo(string calldata operator, address token, uint256 amount) external payable; + + function undelegateFrom(string calldata, address token, uint256 amount) external payable; + + /** + * @notice Client chain users call to claim their unlocked assets from the vault. + * @dev This function assumes that the claimable assets should have been unlocked before calling this. + * @dev This function does not ask for grant from Exocore validator set. + * @param token - The address of specific token that the user wants to claim from the vault. + * @param amount - The amount of @param token that the user wants to claim from the vault. + * @param recipient - The destination address that the assets would be transfered to. + */ + function claim(address token, uint256 amount, address recipient) external; +} diff --git a/src/interfaces/IClientChainGateway.sol b/src/interfaces/IClientChainGateway.sol index b08a41d2..73abab7c 100644 --- a/src/interfaces/IClientChainGateway.sol +++ b/src/interfaces/IClientChainGateway.sol @@ -2,10 +2,13 @@ pragma solidity ^0.8.19; import {IOAppReceiver} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppReceiver.sol"; import {IOAppCore} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppCore.sol"; -import {IController} from "./IController.sol"; -import {ITokenWhitelister} from "./ITokenWhitelister.sol"; +import {ILSTRestakingController} from "./ILSTRestakingController.sol"; +import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; import {ITSSReceiver} from "./ITSSReceiver.sol"; -interface IClientChainGateway is IOAppReceiver, IOAppCore, IController, ITokenWhitelister, ITSSReceiver { +interface IClientChainGateway is IOAppReceiver, IOAppCore, ILSTRestakingController, INativeRestakingController, ITSSReceiver { + function addWhitelistToken(address _token) external; + function removeWhitelistToken(address _token) external; + function addTokenVaults(address[] calldata vaults) external; function quote(bytes memory _message) external view returns (uint256 nativeFee); } diff --git a/src/interfaces/ILSTRestakingController.sol b/src/interfaces/ILSTRestakingController.sol index ea074b5e..042f01c9 100644 --- a/src/interfaces/ILSTRestakingController.sol +++ b/src/interfaces/ILSTRestakingController.sol @@ -1,6 +1,8 @@ pragma solidity ^0.8.19; -interface ILSTRestakingController { +import {IBaseRestakingController} from "./IBaseRestakingController.sol"; + +interface ILSTRestakingController is IBaseRestakingController { // @notice this info is used to update specific user's owned tokens balance struct UserBalanceUpdateInfo { address user; @@ -29,17 +31,6 @@ interface ILSTRestakingController { */ function deposit(address token, uint256 amount) external payable; - /** - * @notice Client chain users call to delegate deposited token to specific node operator. - * @dev This assumes that the delegated assets should have already been deposited to Exocore system. - * @param operator - The address of a registered node operator that the user wants to delegate to. - * @param token - The address of specific token that the user wants to delegate to. - * @param amount - The amount of @param token that the user wants to delegate to node operator. - */ - function delegateTo(string calldata operator, address token, uint256 amount) external payable; - - function undelegateFrom(string calldata, address token, uint256 amount) external payable; - /** * @notice Client chain users call to withdraw principle from Exocore to client chain before they are granted to withdraw from the vault. * @dev This function should ask Exocore validator set for withdrawal grant. If Exocore validator set responds @@ -54,16 +45,6 @@ interface ILSTRestakingController { function withdrawRewardFromExocore(address token, uint256 rewardAmount) external payable; - /** - * @notice Client chain users call to claim their unlocked assets from the vault. - * @dev This function assumes that the claimable assets should have been unlocked before calling this. - * @dev This function does not ask for grant from Exocore validator set. - * @param token - The address of specific token that the user wants to claim from the vault. - * @param amount - The amount of @param token that the user wants to claim from the vault. - * @param recipient - The destination address that the assets would be transfered to. - */ - function claim(address token, uint256 amount, address recipient) external; - /// *** function signatures for commands of Exocore validator set forwarded by Gateway *** /** diff --git a/src/interfaces/INativeRestakingController.sol b/src/interfaces/INativeRestakingController.sol index d80ef9be..420809e5 100644 --- a/src/interfaces/INativeRestakingController.sol +++ b/src/interfaces/INativeRestakingController.sol @@ -1,8 +1,9 @@ pragma solidity ^0.8.19; import {IExoCapsule} from "./IExoCapsule.sol"; +import {IBaseRestakingController} from "./IBaseRestakingController.sol"; -interface INativeRestakingController { +interface INativeRestakingController is IBaseRestakingController { /// *** function signatures for staker operations *** /** @@ -16,7 +17,7 @@ interface INativeRestakingController { * to the ExoCapsule owned by staker. The effective balance of `validatorContainer` would be credited as deposited value by Exocore network. * @ param */ - function depositBeaconChainValidator(bytes32[] calldata validatorContainer, IExoCapsule.WithdrawalContainerProof calldata proof) external; + function depositBeaconChainValidator(bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata proof) external; /** * @notice When a beacon chain partial withdrawal to an ExoCapsule contract happens(the withdrawal time is less than validator's withdrawable_epoch), @@ -57,9 +58,4 @@ interface INativeRestakingController { bytes32[] calldata withdrawalContainer, IExoCapsule.WithdrawalContainerProof calldata withdrawalProof ) external; - - /** - * @notice Owner of ExoCapsule call this function to claim unlocked ETH from owned ExoCapsule contract. - */ - function claim(uint256 amount, address recipient) external; } diff --git a/src/interfaces/IVault.sol b/src/interfaces/IVault.sol index 088914f1..4f2a9f59 100644 --- a/src/interfaces/IVault.sol +++ b/src/interfaces/IVault.sol @@ -1,7 +1,5 @@ pragma solidity ^0.8.19; -import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; - interface IVault { function withdraw(address withdrawer, address recipient, uint256 amount) external; diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 3894f956..3410d802 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -247,97 +247,4 @@ library BeaconChainProofs { index: withdrawalIndex }); } - - /** - * @notice This function replicates the ssz hashing of a validator's pubkey, outlined below: - * hh := ssz.NewHasher() - * hh.PutBytes(validatorPubkey[:]) - * validatorPubkeyHash := hh.Hash() - * hh.Reset() - */ - function hashValidatorBLSPubkey(bytes memory validatorPubkey) internal pure returns (bytes32 pubkeyHash) { - require(validatorPubkey.length == 48, "Input should be 48 bytes in length"); - return sha256(abi.encodePacked(validatorPubkey, bytes16(0))); - } - - /** - * @dev Retrieve the withdrawal timestamp - */ - function getWithdrawalTimestamp(WithdrawalProof memory withdrawalProof) internal pure returns (uint64) { - return - Endian.fromLittleEndianUint64(withdrawalProof.timestampRoot); - } - - /** - * @dev Converts the withdrawal's slot to an epoch - */ - function getWithdrawalEpoch(WithdrawalProof memory withdrawalProof) internal pure returns (uint64) { - return - Endian.fromLittleEndianUint64(withdrawalProof.slotRoot) / SLOTS_PER_EPOCH; - } - - /** - * Indices for validator fields (refer to consensus specs): - * 0: pubkey - * 1: withdrawal credentials - * 2: effective balance - * 3: slashed? - * 4: activation elligibility epoch - * 5: activation epoch - * 6: exit epoch - * 7: withdrawable epoch - */ - - /** - * @dev Retrieves a validator's pubkey hash - */ - function getPubkeyHash(bytes32[] memory validatorFields) internal pure returns (bytes32) { - return - validatorFields[VALIDATOR_PUBKEY_INDEX]; - } - - function getWithdrawalCredentials(bytes32[] memory validatorFields) internal pure returns (bytes32) { - return - validatorFields[VALIDATOR_WITHDRAWAL_CREDENTIALS_INDEX]; - } - - /** - * @dev Retrieves a validator's effective balance (in gwei) - */ - function getEffectiveBalanceGwei(bytes32[] memory validatorFields) internal pure returns (uint64) { - return - Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_BALANCE_INDEX]); - } - - /** - * @dev Retrieves a validator's withdrawable epoch - */ - function getWithdrawableEpoch(bytes32[] memory validatorFields) internal pure returns (uint64) { - return - Endian.fromLittleEndianUint64(validatorFields[VALIDATOR_WITHDRAWABLE_EPOCH_INDEX]); - } - - /** - * Indices for withdrawal fields (refer to consensus specs): - * 0: withdrawal index - * 1: validator index - * 2: execution address - * 3: withdrawal amount - */ - - /** - * @dev Retrieves a withdrawal's validator index - */ - function getValidatorIndex(bytes32[] memory withdrawalFields) internal pure returns (uint40) { - return - uint40(Endian.fromLittleEndianUint64(withdrawalFields[WITHDRAWAL_VALIDATOR_INDEX_INDEX])); - } - - /** - * @dev Retrieves a withdrawal's withdrawal amount (in gwei) - */ - function getWithdrawalAmountGwei(bytes32[] memory withdrawalFields) internal pure returns (uint64) { - return - Endian.fromLittleEndianUint64(withdrawalFields[WITHDRAWAL_VALIDATOR_AMOUNT_INDEX]); - } } diff --git a/src/libraries/ValidatorContainer.sol b/src/libraries/ValidatorContainer.sol index 390668ee..458f9f06 100644 --- a/src/libraries/ValidatorContainer.sol +++ b/src/libraries/ValidatorContainer.sol @@ -16,7 +16,7 @@ library ValidatorContainer { uint256 internal constant VALID_LENGTH = 8; uint256 internal constant MERKLE_TREE_HEIGHT = 3; - function verifyBasic(bytes32[] calldata validatorContainer) internal pure returns (bool) { + function verifyValidatorContainerBasic(bytes32[] calldata validatorContainer) internal pure returns (bool) { return validatorContainer.length == VALID_LENGTH; } @@ -48,7 +48,7 @@ library ValidatorContainer { return uint64(bytes8(validatorContainer[7])); } - function merklelize(bytes32[] calldata validatorContainer) internal pure returns (bytes32) { + function merklelizeValidatorContainer(bytes32[] calldata validatorContainer) internal pure returns (bytes32) { bytes32[] memory leaves = validatorContainer; for (uint i; i < MERKLE_TREE_HEIGHT; i++) { bytes32[] memory roots = new bytes32[](leaves.length / 2); diff --git a/src/libraries/WithdrawalContainer.sol b/src/libraries/WithdrawalContainer.sol index ea8b67c3..55744679 100644 --- a/src/libraries/WithdrawalContainer.sol +++ b/src/libraries/WithdrawalContainer.sol @@ -11,7 +11,7 @@ library WithdrawalContainer { uint256 internal constant VALID_LENGTH = 4; uint256 internal constant MERKLE_TREE_HEIGHT = 2; - function verifyBasic(bytes32[] calldata withdrawalContainer) internal pure returns (bool) { + function verifyWithdrawalContainerBasic(bytes32[] calldata withdrawalContainer) internal pure returns (bool) { return withdrawalContainer.length == VALID_LENGTH; } @@ -31,7 +31,7 @@ library WithdrawalContainer { return uint64(bytes8(withdrawalContainer[3])); } - function merklelize(bytes32[] calldata withdrawalContainer) internal pure returns (bytes32) { + function merklelizeWithdrawalContainer(bytes32[] calldata withdrawalContainer) internal pure returns (bytes32) { bytes32[] memory leaves = withdrawalContainer; for (uint i; i < MERKLE_TREE_HEIGHT; i++) { bytes32[] memory roots = new bytes32[](leaves.length / 2); diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index b3c9fc79..663253cd 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.19; import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; -import {IClientChainGateway} from "../interfaces/IClientChainGateway.sol"; +import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; contract ExoCapsuleStorage { enum VALIDATOR_STATUS { @@ -12,7 +12,7 @@ contract ExoCapsuleStorage { struct Validator { // index of the validator in the beacon chain - uint64 validatorIndex; + uint256 validatorIndex; // amount of beacon chain ETH restaked on EigenLayer in gwei uint64 restakedBalanceGwei; //timestamp of the validator's most recent balance update @@ -23,13 +23,14 @@ contract ExoCapsuleStorage { address public constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; uint64 public constant BEACON_CHAIN_GENESIS_TIME = 1606824023; + uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; - address public capsuelOwner; + address public capsuleOwner; IETHPOSDeposit public ethPOS; - IClientChainGateway public gateway; + INativeRestakingController public gateway; mapping(bytes32 pubkey => Validator validator) _capsuleValidators; - mapping(uint64 index => bytes32 pubkey) _capsuleValidatorsByIndex; + mapping(uint256 index => bytes32 pubkey) _capsuleValidatorsByIndex; uint256[40] private __gap; } \ No newline at end of file diff --git a/test/foundry/ClientChainGateway.t.sol b/test/foundry/ClientChainGateway.t.sol index f7ce51b7..29a2ffed 100644 --- a/test/foundry/ClientChainGateway.t.sol +++ b/test/foundry/ClientChainGateway.t.sol @@ -3,12 +3,13 @@ pragma solidity ^0.8.19; import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import "forge-std/console.sol"; +import "forge-std/Test.sol"; + import "../../src/core/ClientChainGateway.sol"; import {Vault} from "../../src/core/Vault.sol"; import "../../src/core/ExocoreGateway.sol"; import {EndpointV2Mock} from "../mocks/EndpointV2Mock.sol"; -import "forge-std/console.sol"; -import "forge-std/Test.sol"; import "../../src/interfaces/precompiles/IDelegation.sol"; import "../../src/interfaces/precompiles/IDeposit.sol"; import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; diff --git a/test/foundry/Delegation.t.sol b/test/foundry/Delegation.t.sol index 74034713..8d418edd 100644 --- a/test/foundry/Delegation.t.sol +++ b/test/foundry/Delegation.t.sol @@ -4,7 +4,6 @@ import "./ExocoreDeployer.t.sol"; import "forge-std/Test.sol"; import "../../src/core/ExocoreGateway.sol"; import "../../src/storage/GatewayStorage.sol"; -import "../../src/interfaces/IController.sol"; import "../../src/interfaces/precompiles/IDelegation.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index 5118f397..fc0a0792 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -4,8 +4,9 @@ import "./ExocoreDeployer.t.sol"; import "forge-std/Test.sol"; import "../../src/core/ExocoreGateway.sol"; import "../../src/storage/GatewayStorage.sol"; -import "../../src/interfaces/IController.sol"; import "../../src/interfaces/ITSSReceiver.sol"; +import {ILSTRestakingController} from "../../src/interfaces/ILSTRestakingController.sol"; + import "forge-std/console.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; @@ -287,17 +288,17 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { ) internal { vm.chainId(clientChainId); vm.startPrank(relayer.addr); - IController.TokenBalanceUpdateInfo[] memory tokenBalances = new IController.TokenBalanceUpdateInfo[](1); - tokenBalances[0] = IController.TokenBalanceUpdateInfo({ + ILSTRestakingController.TokenBalanceUpdateInfo[] memory tokenBalances = new ILSTRestakingController.TokenBalanceUpdateInfo[](1); + tokenBalances[0] = ILSTRestakingController.TokenBalanceUpdateInfo({ token: address(restakeToken), lastlyUpdatedPrincipleBalance: depositAmount - withdrawAmount, lastlyUpdatedRewardBalance: 0, unlockPrincipleAmount: withdrawAmount, unlockRewardAmount: 0 }); - IController.UserBalanceUpdateInfo[] memory userBalances = new IController.UserBalanceUpdateInfo[](1); + ILSTRestakingController.UserBalanceUpdateInfo[] memory userBalances = new ILSTRestakingController.UserBalanceUpdateInfo[](1); userBalances[0] = - IController.UserBalanceUpdateInfo({user: depositor.addr, updatedAt: 1, tokenBalances: tokenBalances}); + ILSTRestakingController.UserBalanceUpdateInfo({user: depositor.addr, updatedAt: 1, tokenBalances: tokenBalances}); (ITSSReceiver.InterchainMsg memory _msg, bytes memory signature) = prepareEVSMsgAndSignature(userBalances); vm.expectEmit(false, false, false, true, address(clientGateway)); @@ -310,7 +311,7 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { assertEq(vault.totalUnlockPrincipleAmount(depositor.addr), withdrawAmount); } - function prepareEVSMsgAndSignature(IController.UserBalanceUpdateInfo[] memory userBalances) + function prepareEVSMsgAndSignature(ILSTRestakingController.UserBalanceUpdateInfo[] memory userBalances) internal view returns (ITSSReceiver.InterchainMsg memory _msg, bytes memory signature) diff --git a/test/foundry/WithdrawReward.t.sol b/test/foundry/WithdrawReward.t.sol index fc17ec7a..b9c5a2d4 100644 --- a/test/foundry/WithdrawReward.t.sol +++ b/test/foundry/WithdrawReward.t.sol @@ -4,7 +4,6 @@ import "./ExocoreDeployer.t.sol"; import "forge-std/Test.sol"; import "../../src/core/ExocoreGateway.sol"; import "../../src/storage/GatewayStorage.sol"; -import "../../src/interfaces/IController.sol"; import "../../src/interfaces/ITSSReceiver.sol"; import "forge-std/console.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; From a5e8b3f927c1b9cc5c31ee57573ebc4f59a09659 Mon Sep 17 00:00:00 2001 From: adu Date: Thu, 18 Apr 2024 16:32:59 +0800 Subject: [PATCH 15/93] fix operator address length --- src/core/BaseRestakingController.sol | 2 +- src/core/ExocoreGateway.sol | 28 +++++++++++++++++++++++---- src/storage/ExocoreGatewayStorage.sol | 9 +++++++++ test/foundry/Delegation.t.sol | 2 +- test/mocks/DelegationMock.sol | 4 ++-- 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index 6dde44eb..c6ab389c 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -48,7 +48,7 @@ abstract contract BaseRestakingController is function delegateTo(string calldata operator, address token, uint256 amount) external payable whenNotPaused { require(whitelistTokens[token], "Controller: token is not whitelisted"); require(amount > 0, "Controller: amount should be greater than zero"); - require(bytes(operator).length == 44, "Controller: invalid bech32 address"); + require(bytes(operator).length == 42, "Controller: invalid bech32 address"); IVault vault = tokenVaults[token]; if (address(vault) == address(0)) { diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index d1bf9bc1..4cdcb100 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -105,6 +105,10 @@ contract ExocoreGateway is } function requestDeposit(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) public onlyCalledFromThis { + if (payload.length != DEPOSIT_REQUEST_LENGTH) { + revert InvalidRequestLength(Action.REQUEST_DEPOSIT, DEPOSIT_REQUEST_LENGTH, payload.length); + } + bytes calldata token = payload[:32]; bytes calldata depositor = payload[32:64]; uint256 amount = uint256(bytes32(payload[64:96])); @@ -132,6 +136,10 @@ contract ExocoreGateway is public onlyCalledFromThis { + if (payload.length != WITHDRAW_PRINCIPLE_REQUEST_LENGTH) { + revert InvalidRequestLength(Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, WITHDRAW_PRINCIPLE_REQUEST_LENGTH, payload.length); + } + bytes calldata token = payload[:32]; bytes calldata withdrawer = payload[32:64]; uint256 amount = uint256(bytes32(payload[64:96])); @@ -159,6 +167,10 @@ contract ExocoreGateway is public onlyCalledFromThis { + if (payload.length != CLAIM_REWARD_REQUEST_LENGTH) { + revert InvalidRequestLength(Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, CLAIM_REWARD_REQUEST_LENGTH, payload.length); + } + bytes calldata token = payload[:32]; bytes calldata withdrawer = payload[32:64]; uint256 amount = uint256(bytes32(payload[64:96])); @@ -181,10 +193,14 @@ contract ExocoreGateway is } function requestDelegateTo(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) public onlyCalledFromThis { + if (payload.length != DELEGATE_REQUEST_LENGTH) { + revert InvalidRequestLength(Action.REQUEST_DELEGATE_TO, DELEGATE_REQUEST_LENGTH, payload.length); + } + bytes calldata token = payload[:32]; bytes calldata delegator = payload[32:64]; - bytes calldata operator = payload[64:108]; - uint256 amount = uint256(bytes32(payload[108:140])); + bytes calldata operator = payload[64:106]; + uint256 amount = uint256(bytes32(payload[106:138])); (bool success,) = DELEGATION_PRECOMPILE_ADDRESS.call( abi.encodeWithSelector( @@ -204,10 +220,14 @@ contract ExocoreGateway is public onlyCalledFromThis { + if (payload.length != UNDELEGATE_REQUEST_LENGTH) { + revert InvalidRequestLength(Action.REQUEST_UNDELEGATE_FROM, UNDELEGATE_REQUEST_LENGTH, payload.length); + } + bytes memory token = payload[1:32]; bytes memory delegator = payload[32:64]; - bytes memory operator = payload[64:108]; - uint256 amount = uint256(bytes32(payload[108:140])); + bytes memory operator = payload[64:106]; + uint256 amount = uint256(bytes32(payload[106:138])); (bool success,) = DELEGATION_PRECOMPILE_ADDRESS.call( abi.encodeWithSelector( diff --git a/src/storage/ExocoreGatewayStorage.sol b/src/storage/ExocoreGatewayStorage.sol index 35d78d34..1be25a64 100644 --- a/src/storage/ExocoreGatewayStorage.sol +++ b/src/storage/ExocoreGatewayStorage.sol @@ -18,6 +18,12 @@ contract ExocoreGatewayStorage is GatewayStorage { bytes4(keccak256("withdrawPrinciple(uint32,bytes,bytes,uint256)")); bytes4 constant CLAIM_REWARD_FUNCTION_SELECTOR = bytes4(keccak256("claimReward(uint32,bytes,bytes,uint256)")); + uint256 constant DEPOSIT_REQUEST_LENGTH = 96; + uint256 constant DELEGATE_REQUEST_LENGTH = 138; + uint256 constant UNDELEGATE_REQUEST_LENGTH = 138; + uint256 constant WITHDRAW_PRINCIPLE_REQUEST_LENGTH = 96; + uint256 constant CLAIM_REWARD_REQUEST_LENGTH = 96; + uint128 constant DESTINATION_GAS_LIMIT = 500000; uint128 constant DESTINATION_MSG_VALUE = 0; @@ -26,6 +32,9 @@ contract ExocoreGatewayStorage is GatewayStorage { error RequestExecuteFailed(Action act, uint64 nonce, bytes reason); error PrecompileCallFailed(bytes4 selector_, bytes reason); + error UnexpectedInboundNonce(uint64 expectedNonce, uint64 actualNonce); + error UnexpectedSourceChain(uint32 unexpectedSrcEndpointId); + error InvalidRequestLength(Action act, uint256 expectedLength, uint256 actualLength); uint256[40] private __gap; } diff --git a/test/foundry/Delegation.t.sol b/test/foundry/Delegation.t.sol index 8d418edd..38753eac 100644 --- a/test/foundry/Delegation.t.sol +++ b/test/foundry/Delegation.t.sol @@ -40,7 +40,7 @@ contract DelegateTest is ExocoreDeployer { function test_Delegation() public { Player memory delegator = players[0]; - string memory operatorAddress = "evmos1v4s6vtjpmxwu9rlhqms5urzrc3tc2ae2gnuqhc"; + string memory operatorAddress = "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"; deal(delegator.addr, 1e22); deal(address(clientGateway), 1e22); diff --git a/test/mocks/DelegationMock.sol b/test/mocks/DelegationMock.sol index 53a510d0..74756355 100644 --- a/test/mocks/DelegationMock.sol +++ b/test/mocks/DelegationMock.sol @@ -32,7 +32,7 @@ contract DelegationMock is IDelegation { ) external returns (bool success) { require(assetsAddress.length == 32, "invalid asset address"); require(stakerAddress.length == 32, "invalid staker address"); - require(operatorAddr.length == 44, "invalid operator address"); + require(operatorAddr.length == 42, "invalid operator address"); delegateTo[stakerAddress][operatorAddr][clientChainLzId][assetsAddress] += opAmount; emit DelegateRequestProcessed( clientChainLzId, lzNonce, assetsAddress, stakerAddress, string(operatorAddr), opAmount @@ -49,7 +49,7 @@ contract DelegationMock is IDelegation { ) external returns (bool success) { require(assetsAddress.length == 32, "invalid asset address"); require(stakerAddress.length == 32, "invalid staker address"); - require(operatorAddr.length == 44, "invalid operator address"); + require(operatorAddr.length == 42, "invalid operator address"); require(opAmount <= delegateTo[stakerAddress][operatorAddr][clientChainLzId][assetsAddress], "amount overflow"); delegateTo[stakerAddress][operatorAddr][clientChainLzId][assetsAddress] -= opAmount; emit UndelegateRequestProcessed( From 44f3e63578e49dd013be7b8a8a44cd00ae82abd8 Mon Sep 17 00:00:00 2001 From: bwhour Date: Thu, 18 Apr 2024 19:04:49 +0800 Subject: [PATCH 16/93] optimize some code with DRY princple --- src/core/BaseRestakingController.sol | 83 +++++++++++++++++----------- src/core/ExoCapsule.sol | 6 +- src/core/LSTRestakingController.sol | 41 ++++---------- 3 files changed, 64 insertions(+), 66 deletions(-) diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index c6ab389c..524d9998 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -10,20 +10,49 @@ import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/Pau import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; -abstract contract BaseRestakingController is - PausableUpgradeable, - OAppSenderUpgradeable, - IBaseRestakingController, - ClientChainGatewayStorage +abstract contract BaseRestakingController is + PausableUpgradeable, + OAppSenderUpgradeable, + IBaseRestakingController, + ClientChainGatewayStorage { using OptionsBuilder for bytes; receive() external payable {} - function claim(address token, uint256 amount, address recipient) external whenNotPaused { + modifier isTokenWhitelisted(address token) { require(whitelistTokens[token], "Controller: token is not whitelisted"); + _; + } + + modifier isValidAmount(uint256 amount) { require(amount > 0, "Controller: amount should be greater than zero"); + _; + } + + modifier vaultExists(address token) { + require(address(tokenVaults[token]) != address(0), "Controller: no vault added for this token"); + _; + } + + modifier isValidBech32Address(string memory operator) { + require(bytes(operator).length == 44, "Controller: invalid bech32 address"); + _; + } + + function _getVault(address token) internal view returns (IVault) { + IVault vault = tokenVaults[token]; + if (address(vault) == address(0)) { + revert VaultNotExist(); + } + return vault; + } + function claim(address token, uint256 amount, address recipient) + external + isTokenWhitelisted(token) + isValidAmount(amount) + whenNotPaused { if (token == VIRTUAL_STAKED_ETH_ADDRESS) { IExoCapsule capsule = ownerToCapsule[msg.sender]; if (address(capsule) == address(0)) { @@ -34,27 +63,20 @@ abstract contract BaseRestakingController is emit ClaimSucceeded(token, recipient, amount); } else { - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - + IVault vault = _getVault(token); vault.withdraw(msg.sender, recipient, amount); - + emit ClaimSucceeded(token, recipient, amount); } } - function delegateTo(string calldata operator, address token, uint256 amount) external payable whenNotPaused { - require(whitelistTokens[token], "Controller: token is not whitelisted"); - require(amount > 0, "Controller: amount should be greater than zero"); - require(bytes(operator).length == 42, "Controller: invalid bech32 address"); - - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - + function delegateTo(string calldata operator, address token, uint256 amount) + external payable + isTokenWhitelisted(token) + isValidAmount(amount) + isValidBech32Address(operator) + whenNotPaused { + _getVault(token); registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DELEGATE_TO; @@ -63,16 +85,13 @@ abstract contract BaseRestakingController is _sendMsgToExocore(Action.REQUEST_DELEGATE_TO, actionArgs); } - function undelegateFrom(string calldata operator, address token, uint256 amount) external payable whenNotPaused { - require(whitelistTokens[token], "Controller: token is not whitelisted"); - require(amount > 0, "Controller: amount should be greater than zero"); - require(bytes(operator).length == 42, "Controller: invalid bech32 address"); - - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - + function undelegateFrom(string calldata operator, address token, uint256 amount) + external payable + isTokenWhitelisted(token) + isValidAmount(amount) + isValidBech32Address(operator) + whenNotPaused { + _getVault(token); registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); registeredRequestActions[outboundNonce + 1] = Action.REQUEST_UNDELEGATE_FROM; diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index a5558c0c..950757a3 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -10,7 +10,7 @@ import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -contract ExoCapsule is +contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule @@ -201,11 +201,11 @@ contract ExoCapsule is } } - function _isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint64 atTimestamp) internal view returns (bool) { + function _isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint64 atTimestamp) internal pure returns (bool) { uint64 atEpoch = _timestampToEpoch(atTimestamp); uint64 activationEpoch = validatorContainer.getActivationEpoch(); uint64 exitEpoch = validatorContainer.getExitEpoch(); - + return (atEpoch >= activationEpoch && atEpoch < exitEpoch); } diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index 2d020e24..2520a78d 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -7,20 +7,13 @@ import {BaseRestakingController} from "./BaseRestakingController.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; -abstract contract LSTRestakingController is - PausableUpgradeable, - ILSTRestakingController, - BaseRestakingController -{ - function deposit(address token, uint256 amount) external payable whenNotPaused { - require(whitelistTokens[token], "Controller: token is not whitelisted"); - require(amount > 0, "Controller: amount should be greater than zero"); - - IVault vault = tokenVaults[token]; - require(address(vault) != address(0), "Controller: no vault added for this token"); - +abstract contract LSTRestakingController is + PausableUpgradeable, + ILSTRestakingController, + BaseRestakingController { + function deposit(address token, uint256 amount) external payable isTokenWhitelisted(token) isValidAmount(amount) whenNotPaused { + IVault vault = _getVault(token); vault.deposit(msg.sender, amount); - registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, amount); registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DEPOSIT; @@ -28,15 +21,8 @@ abstract contract LSTRestakingController is _sendMsgToExocore(Action.REQUEST_DEPOSIT, actionArgs); } - function withdrawPrincipleFromExocore(address token, uint256 principleAmount) external payable whenNotPaused { - require(whitelistTokens[token], "Controller: token is not whitelisted"); - require(principleAmount > 0, "Controller: amount should be greater than zero"); - - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - + function withdrawPrincipleFromExocore(address token, uint256 principleAmount) external payable isTokenWhitelisted(token) isValidAmount(principleAmount) whenNotPaused { + _getVault(token); registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, principleAmount); registeredRequestActions[outboundNonce + 1] = Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE; @@ -45,15 +31,8 @@ abstract contract LSTRestakingController is _sendMsgToExocore(Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, actionArgs); } - function withdrawRewardFromExocore(address token, uint256 rewardAmount) external payable whenNotPaused { - require(whitelistTokens[token], "Controller: token is not whitelisted"); - require(rewardAmount > 0, "Controller: amount should be greater than zero"); - - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - + function withdrawRewardFromExocore(address token, uint256 rewardAmount) external payable isTokenWhitelisted(token) isValidAmount(rewardAmount) whenNotPaused { + _getVault(token); registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, rewardAmount); registeredRequestActions[outboundNonce + 1] = Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE; From b18324e9100a025bd76c596e36ddf8bfd35c7164 Mon Sep 17 00:00:00 2001 From: bwhour Date: Thu, 18 Apr 2024 21:06:29 +0800 Subject: [PATCH 17/93] update log and reuse some code --- src/core/BaseRestakingController.sol | 8 ++++---- src/core/LSTRestakingController.sol | 8 ++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index 524d9998..354b1c31 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -21,22 +21,22 @@ abstract contract BaseRestakingController is receive() external payable {} modifier isTokenWhitelisted(address token) { - require(whitelistTokens[token], "Controller: token is not whitelisted"); + require(whitelistTokens[token], "BaseRestakingController: token is not whitelisted"); _; } modifier isValidAmount(uint256 amount) { - require(amount > 0, "Controller: amount should be greater than zero"); + require(amount > 0, "BaseRestakingController: amount should be greater than zero"); _; } modifier vaultExists(address token) { - require(address(tokenVaults[token]) != address(0), "Controller: no vault added for this token"); + require(address(tokenVaults[token]) != address(0), "BaseRestakingController: no vault added for this token"); _; } modifier isValidBech32Address(string memory operator) { - require(bytes(operator).length == 44, "Controller: invalid bech32 address"); + require(bytes(operator).length == 44, "BaseRestakingController: invalid bech32 address"); _; } diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index 2520a78d..01042daf 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -41,17 +41,13 @@ abstract contract LSTRestakingController is } function updateUsersBalances(UserBalanceUpdateInfo[] calldata info) public whenNotPaused { - require(msg.sender == address(this), "Controller: caller must be client chain gateway itself"); + require(msg.sender == address(this), "LSTRestakingController: caller must be client chain gateway itself"); for (uint256 i = 0; i < info.length; i++) { UserBalanceUpdateInfo memory userBalanceUpdate = info[i]; for (uint256 j = 0; j < userBalanceUpdate.tokenBalances.length; j++) { TokenBalanceUpdateInfo memory tokenBalanceUpdate = userBalanceUpdate.tokenBalances[j]; require(whitelistTokens[tokenBalanceUpdate.token], "Controller: token is not whitelisted"); - - IVault vault = tokenVaults[tokenBalanceUpdate.token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } + IVault vault = _getVault(tokenVaults[tokenBalanceUpdate.token]); if (tokenBalanceUpdate.lastlyUpdatedPrincipleBalance > 0) { vault.updatePrincipleBalance( From 68b75057c3a32a0d7ca9b8ea8b444e707139be8e Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 19 Apr 2024 09:56:47 +0800 Subject: [PATCH 18/93] fix test --- src/core/BaseRestakingController.sol | 2 +- src/core/LSTRestakingController.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index 354b1c31..f38582e4 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -36,7 +36,7 @@ abstract contract BaseRestakingController is } modifier isValidBech32Address(string memory operator) { - require(bytes(operator).length == 44, "BaseRestakingController: invalid bech32 address"); + require(bytes(operator).length == 42, "BaseRestakingController: invalid bech32 address"); _; } diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index 01042daf..79221ea5 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -47,7 +47,7 @@ abstract contract LSTRestakingController is for (uint256 j = 0; j < userBalanceUpdate.tokenBalances.length; j++) { TokenBalanceUpdateInfo memory tokenBalanceUpdate = userBalanceUpdate.tokenBalances[j]; require(whitelistTokens[tokenBalanceUpdate.token], "Controller: token is not whitelisted"); - IVault vault = _getVault(tokenVaults[tokenBalanceUpdate.token]); + IVault vault = _getVault(tokenBalanceUpdate.token); if (tokenBalanceUpdate.lastlyUpdatedPrincipleBalance > 0) { vault.updatePrincipleBalance( From bbb45270e199b55da7409bdf7f77a4afad699351 Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 19 Apr 2024 16:11:04 +0800 Subject: [PATCH 19/93] move stake interface to NativeRestakingController --- src/core/BaseRestakingController.sol | 2 +- src/core/ExoCapsule.sol | 17 ++++---------- src/core/NativeRestakingController.sol | 23 +++++++++++++++---- src/interfaces/IExoCapsule.sol | 6 ++--- src/interfaces/INativeRestakingController.sol | 15 ++++++++++-- src/storage/ClientChainGatewayStorage.sol | 7 +++--- src/storage/ExoCapsuleStorage.sol | 1 - 7 files changed, 43 insertions(+), 28 deletions(-) diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index f38582e4..c175d118 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -56,7 +56,7 @@ abstract contract BaseRestakingController is if (token == VIRTUAL_STAKED_ETH_ADDRESS) { IExoCapsule capsule = ownerToCapsule[msg.sender]; if (address(capsule) == address(0)) { - revert CapsuleNotExistForOwner(msg.sender); + revert CapsuleNotExist(); } capsule.withdraw(amount, recipient); diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 950757a3..2818b88e 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -30,28 +30,21 @@ contract ExoCapsule is error NotPartialWithdrawal(bytes32 pubkey); modifier onlyGateway() { - require(msg.sender == address(gateway), "only client chain gateway could call this function"); + require(msg.sender == address(gateway), "ExoCapsule: only client chain gateway could call this function"); _; } - constructor(address _ethPOS, address _gateway) { - ethPOS = IETHPOSDeposit(_ethPOS); + constructor(address _gateway) { gateway = INativeRestakingController(_gateway); _disableInitializers(); } function initialize(address _capsuleOwner) external initializer { - require(_capsuleOwner != address(0), "invalid empty exocore validator set address"); + require(_capsuleOwner != address(0), "ExoCapsule: capsule owner address can not be empty"); capsuleOwner = _capsuleOwner; } - function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable onlyGateway { - require(msg.value == 32 ether, "stake value must be exactly 32 ether"); - ethPOS.deposit{value: 32 ether}(pubkey, _capsuleWithdrawalCredentials(), signature, depositDataRoot); - emit StakedWithThisCapsule(); - } - function verifyDepositProof( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof @@ -76,7 +69,7 @@ contract ExoCapsule is revert InvalidValidatorContainer(validatorPubkey); } - if (withdrawalCredentials != bytes32(_capsuleWithdrawalCredentials())) { + if (withdrawalCredentials != bytes32(capsuleWithdrawalCredentials())) { revert InvalidValidatorContainer(validatorPubkey); } @@ -151,7 +144,7 @@ contract ExoCapsule is } - function _capsuleWithdrawalCredentials() internal view returns (bytes memory) { + function capsuleWithdrawalCredentials() public view returns (bytes memory) { /** * The withdrawal_credentials field must be such that: * withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index e34c3293..149635ef 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -15,15 +15,28 @@ abstract contract NativeRestakingController is { using ValidatorContainer for bytes32[]; - function createExoCapsule() external whenNotPaused { - require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable whenNotPaused { + require(msg.value == 32 ether, "NativeRestakingController: stake value must be exactly 32 ether"); + + IExoCapsule capsule = ownerToCapsule[msg.sender]; + if (address(capsule) == address(0)) { + capsule = IExoCapsule(createExoCapsule()); + } - ExoCapsule capsule = new ExoCapsule(ETH_STAKING_DEPOSIT_CONTRACT_ADDRESS, address(this)); + ETH_POS.deposit{value: 32 ether}(pubkey, capsule.capsuleWithdrawalCredentials(), signature, depositDataRoot); + emit StakedWithCapsule(msg.sender, address(capsule)); + } + + function createExoCapsule() public whenNotPaused returns (address) { + require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); + + ExoCapsule capsule = new ExoCapsule(address(this)); capsule.initialize(msg.sender); ownerToCapsule[msg.sender] = capsule; - isExoCapsule[capsule] = true; emit CapsuleCreated(msg.sender, address(capsule)); + + return address(capsule); } function depositBeaconChainValidator( @@ -32,7 +45,7 @@ abstract contract NativeRestakingController is ) external whenNotPaused { IExoCapsule capsule = ownerToCapsule[msg.sender]; if (address(capsule) == address(0)) { - revert CapsuleNotExistForOwner(msg.sender); + revert CapsuleNotExist(); } capsule.verifyDepositProof(validatorContainer, proof); diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 0c7417e0..b9309878 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -18,10 +18,6 @@ interface IExoCapsule { uint256 withdrawalContainerRootIndex; } - event StakedWithThisCapsule(); - - function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; - function verifyDepositProof( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof @@ -42,4 +38,6 @@ interface IExoCapsule { ) external; function withdraw(uint256 amount, address recipient) external; + + function capsuleWithdrawalCredentials() external view returns (bytes memory); } \ No newline at end of file diff --git a/src/interfaces/INativeRestakingController.sol b/src/interfaces/INativeRestakingController.sol index 420809e5..ea385ef3 100644 --- a/src/interfaces/INativeRestakingController.sol +++ b/src/interfaces/INativeRestakingController.sol @@ -7,9 +7,20 @@ interface INativeRestakingController is IBaseRestakingController { /// *** function signatures for staker operations *** /** - * @notice Ethereum native restaker should call this function to create owned ExoCapsule before staking to beacon chain. + * @notice Stakers call this function to deposit to beacon chain validator, and point withdrawal_credentials of + * beacon chain validator to staker's ExoCapsule contract address. An ExoCapsule contract owned by staker would + * be created if it does not exist. + * @param pubkey the BLS pubkey of beacon chain validator + * @param signature the BLS signature + * @param depositDataRoot The SHA-256 hash of the SSZ-encoded DepositData object. + * Used as a protection against malformed input. */ - function createExoCapsule() external; + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable; + + /** + * @notice Ethereum native restaker could call this function to create owned ExoCapsule before staking to beacon chain. + */ + function createExoCapsule() external returns (address capsule); /** * @notice This is called to deposit ETH that is staked on Ethereum beacon chain to Exocore network to be restaked in future diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 85b75ce1..32e52fd1 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.19; import {BootstrapStorage} from "./BootstrapStorage.sol"; import {IVault} from "../interfaces/IVault.sol"; import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; +import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; import {GatewayStorage} from "./GatewayStorage.sol"; contract ClientChainGatewayStorage is BootstrapStorage { @@ -17,8 +18,7 @@ contract ClientChainGatewayStorage is BootstrapStorage { // native restaking state variables mapping(address => IExoCapsule) public ownerToCapsule; - mapping(IExoCapsule => bool) public isExoCapsule; - address constant ETH_STAKING_DEPOSIT_CONTRACT_ADDRESS = 0x00000000219ab540356cBB839Cbe05303d7705Fa; + IETHPOSDeposit constant ETH_POS = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); address constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint256 constant GWEI_TO_WEI = 1e9; @@ -43,6 +43,7 @@ contract ClientChainGatewayStorage is BootstrapStorage { // native restaking events event CapsuleCreated(address owner, address capsule); + event StakedWithCapsule(address staker, address capsule); error UnauthorizedSigner(); error UnauthorizedToken(); @@ -51,7 +52,7 @@ contract ClientChainGatewayStorage is BootstrapStorage { error UnexpectedResponse(uint64 nonce); // native restaking errors - error CapsuleNotExistForOwner(address owner); + error CapsuleNotExist(); uint256[40] private __gap; } diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 663253cd..620e37e6 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -26,7 +26,6 @@ contract ExoCapsuleStorage { uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; address public capsuleOwner; - IETHPOSDeposit public ethPOS; INativeRestakingController public gateway; mapping(bytes32 pubkey => Validator validator) _capsuleValidators; From e2d1b34b6d089c3696ba8114bcc7214c7556912f Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 19 Apr 2024 19:04:44 +0800 Subject: [PATCH 20/93] update principleBalance upon receiving deposit response --- src/core/BaseRestakingController.sol | 8 ------ src/core/ClientChainLzReceiver.sol | 25 +++++++++-------- src/core/ExoCapsule.sol | 33 ++++++++++++++++++++--- src/core/NativeRestakingController.sol | 4 +-- src/core/Vault.sol | 2 ++ src/interfaces/IExoCapsule.sol | 4 +++ src/storage/ClientChainGatewayStorage.sol | 19 +++++++++++++ src/storage/ExoCapsuleStorage.sol | 3 +++ src/storage/VaultStorage.sol | 1 + 9 files changed, 72 insertions(+), 27 deletions(-) diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index c175d118..77c06d3b 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -40,14 +40,6 @@ abstract contract BaseRestakingController is _; } - function _getVault(address token) internal view returns (IVault) { - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - return vault; - } - function claim(address token, uint256 amount, address recipient) external isTokenWhitelisted(token) diff --git a/src/core/ClientChainLzReceiver.sol b/src/core/ClientChainLzReceiver.sol index b663b623..cbdfc88d 100644 --- a/src/core/ClientChainLzReceiver.sol +++ b/src/core/ClientChainLzReceiver.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.19; import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; import {IVault} from "../interfaces/IVault.sol"; +import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; import {OAppReceiverUpgradeable, Origin} from "../lzApp/OAppReceiverUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; @@ -80,12 +81,16 @@ abstract contract ClientChainLzReceiver is PausableUpgradeable, OAppReceiverUpgr bool success = (uint8(bytes1(responsePayload[0])) == 1); uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:])); - if (success) { - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } + if (!success) { + revert DepositShouldNotFailOnExocore(token, depositor); + } + + if (token == VIRTUAL_STAKED_ETH_ADDRESS) { + IExoCapsule capsule = _getCapsule(depositor); + capsule.updatePrincipleBalance(lastlyUpdatedPrincipleBalance); + } else { + IVault vault = _getVault(token); vault.updatePrincipleBalance(depositor, lastlyUpdatedPrincipleBalance); } @@ -102,10 +107,7 @@ abstract contract ClientChainLzReceiver is PausableUpgradeable, OAppReceiverUpgr bool success = (uint8(bytes1(responsePayload[0])) == 1); uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:33])); if (success) { - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } + IVault vault = _getVault(token); vault.updatePrincipleBalance(withdrawer, lastlyUpdatedPrincipleBalance); vault.updateWithdrawableBalance(withdrawer, unlockPrincipleAmount, 0); @@ -124,10 +126,7 @@ abstract contract ClientChainLzReceiver is PausableUpgradeable, OAppReceiverUpgr bool success = (uint8(bytes1(responsePayload[0])) == 1); uint256 lastlyUpdatedRewardBalance = uint256(bytes32(responsePayload[1:33])); if (success) { - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } + IVault vault = _getVault(token); vault.updateRewardBalance(withdrawer, lastlyUpdatedRewardBalance); vault.updateWithdrawableBalance(withdrawer, 0, unlockRewardAmount); diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 2818b88e..2e844cf3 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -19,6 +19,10 @@ contract ExoCapsule is using ValidatorContainer for bytes32[]; using WithdrawalContainer for bytes32[]; + event PrincipleBalanceUpdated(address, uint256); + event WithdrawableBalanceUpdated(address, uint256); + event WithdrawalSuccess(address, address, uint256); + error InvalidValidatorContainer(bytes32 pubkey); error InvalidWithdrawalContainer(uint64 validatorIndex); error DoubleDepositedValidator(bytes32 pubkey); @@ -34,14 +38,15 @@ contract ExoCapsule is _; } - constructor(address _gateway) { - gateway = INativeRestakingController(_gateway); - + constructor() { _disableInitializers(); } - function initialize(address _capsuleOwner) external initializer { + function initialize(address _gateway, address _capsuleOwner) external initializer { require(_capsuleOwner != address(0), "ExoCapsule: capsule owner address can not be empty"); + require(_gateway != address(0), "ExoCapsule: gateway address can not be empty"); + + gateway = INativeRestakingController(_gateway); capsuleOwner = _capsuleOwner; } @@ -141,7 +146,27 @@ contract ExoCapsule is } function withdraw(uint256 amount, address recipient) external onlyGateway { + require( + amount <= withdrawableBalance, + "ExoCapsule: withdrawal amount is larger than staker's withdrawable balance" + ); + + withdrawableBalance -= amount; + recipient.call{value: amount}(""); + + emit WithdrawalSuccess(capsuleOwner, recipient, amount); + } + + function updatePrincipleBalance(uint256 lastlyUpdatedPrincipleBalance) external onlyGateway { + principleBalance = lastlyUpdatedPrincipleBalance; + + emit PrincipleBalanceUpdated(capsuleOwner, lastlyUpdatedPrincipleBalance); + } + + function updateWithdrawableBalance(uint256 unlockPrincipleAmount) external onlyGateway { + withdrawableBalance += unlockPrincipleAmount; + emit WithdrawableBalanceUpdated(capsuleOwner, unlockPrincipleAmount); } function capsuleWithdrawalCredentials() public view returns (bytes memory) { diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 149635ef..c95ce4c2 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -30,8 +30,8 @@ abstract contract NativeRestakingController is function createExoCapsule() public whenNotPaused returns (address) { require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); - ExoCapsule capsule = new ExoCapsule(address(this)); - capsule.initialize(msg.sender); + ExoCapsule capsule = new ExoCapsule(); + capsule.initialize(address(this), msg.sender); ownerToCapsule[msg.sender] = capsule; emit CapsuleCreated(msg.sender, address(capsule)); diff --git a/src/core/Vault.sol b/src/core/Vault.sol index ab62c8fd..970aec6b 100644 --- a/src/core/Vault.sol +++ b/src/core/Vault.sol @@ -41,6 +41,8 @@ contract Vault is Initializable, VaultStorage, IVault { withdrawableBalances[withdrawer] -= amount; underlyingToken.safeTransfer(recipient, amount); + + emit WithdrawalSuccess(withdrawer, recipient, amount); } function deposit(address depositor, uint256 amount) external payable onlyGateway { diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index b9309878..b5b90cc2 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -39,5 +39,9 @@ interface IExoCapsule { function withdraw(uint256 amount, address recipient) external; + function updatePrincipleBalance(uint256 lastlyUpdatedPrincipleBalance) external; + + function updateWithdrawableBalance(uint256 unlockPrincipleAmount) external; + function capsuleWithdrawalCredentials() external view returns (bytes memory); } \ No newline at end of file diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 32e52fd1..3550ac01 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -50,9 +50,28 @@ contract ClientChainGatewayStorage is BootstrapStorage { error UnsupportedRequest(Action act); error UnsupportedResponse(Action act); error UnexpectedResponse(uint64 nonce); + error UnexpectedInboundNonce(uint64 expectedNonce, uint64 actualNonce); + error UnexpectedSourceChain(uint32 unexpectedSrcEndpointId); + error DepositShouldNotFailOnExocore(address token, address depositor); // native restaking errors error CapsuleNotExist(); uint256[40] private __gap; + + function _getVault(address token) internal view returns (IVault) { + IVault vault = tokenVaults[token]; + if (address(vault) == address(0)) { + revert VaultNotExist(); + } + return vault; + } + + function _getCapsule(address owner) internal view returns (IExoCapsule) { + IExoCapsule capsule = ownerToCapsule[owner]; + if (address(capsule) == address(0)) { + revert CapsuleNotExist(); + } + return capsule; + } } diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 620e37e6..21dface8 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -28,6 +28,9 @@ contract ExoCapsuleStorage { address public capsuleOwner; INativeRestakingController public gateway; + uint256 public principleBalance; + uint256 public withdrawableBalance; + mapping(bytes32 pubkey => Validator validator) _capsuleValidators; mapping(uint256 index => bytes32 pubkey) _capsuleValidatorsByIndex; diff --git a/src/storage/VaultStorage.sol b/src/storage/VaultStorage.sol index 038dcb1a..37ae4d18 100644 --- a/src/storage/VaultStorage.sol +++ b/src/storage/VaultStorage.sol @@ -17,4 +17,5 @@ contract VaultStorage { event PrincipleBalanceUpdated(address, uint256); event RewardBalanceUpdated(address, uint256); event WithdrawableBalanceUpdated(address, uint256, uint256); + event WithdrawalSuccess(address, address, uint256); } From dd99dd7ce38288b3ec05f34a64a23bd776d74825 Mon Sep 17 00:00:00 2001 From: adu Date: Sun, 21 Apr 2024 21:09:37 +0800 Subject: [PATCH 21/93] draft plantuml diagram --- docs/native_deposit_workflow.wsd | 137 +++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 docs/native_deposit_workflow.wsd diff --git a/docs/native_deposit_workflow.wsd b/docs/native_deposit_workflow.wsd new file mode 100644 index 00000000..7cfa31d5 --- /dev/null +++ b/docs/native_deposit_workflow.wsd @@ -0,0 +1,137 @@ +### all functions + +@startuml +actor User +participant NativeStakingController +participant ETHPOS +participant ClientChainLzReceiver +participant ExoCapsule +participant ClientChainL0Endpoint +participant ExocoreL0Endpoint +participant ExocoreGateway +participant DepositPrecompile +participant DepositNativeModule + +User -> NativeStakingController: 1.1:stake(pubkey, signature, depositDataRoot) +activate NativeStakingController +NativeStakingController -> ETHPOS: 1.2:deposit() +activate ETHPOS +ETHPOS -> NativeStakingController: 1.3:DepositSuccess +deactivate ETHPOS +deactivate NativeStakingController + +User -> NativeStakingController: 2.1:depositBeaconChainValidator(validatorContainer, proof) +activate NativeStakingController +NativeStakingController -> ExoCapsule: 2.2:verifyDepositProof(validatorContainer, proof) +activate ExoCapsule +ExoCapsule -> NativeStakingController: 2.3:return (isValidValidatorContainer) +deactivate ExoCapsule +NativeStakingController -> ClientChainL0Endpoint: 2.4:send(request) +activate ClientChainL0Endpoint +ClientChainL0Endpoint -> NativeStakingController: 2.5:emit (requestSent) +deactivate ClientChainL0Endpoint +deactivate NativeStakingController +ClientChainL0Endpoint -> ExocoreL0Endpoint: 3.1:lzReceive(request) +activate ExocoreL0Endpoint +ExocoreL0Endpoint -> ExocoreGateway: 3.2:lzReceive(request) +activate ExocoreGateway +ExocoreGateway -> ExocoreGateway: 3.3requestDepositTo(payload) +ExocoreGateway -> DepositPrecompile: 3.4:depositTo(payload) +activate DepositPrecompile +DepositPrecompile -> DepositNativeModule: 3.5:depositTo(payload) +activate DepositNativeModule +DepositNativeModule -> DepositPrecompile: 3.6:return (result, balance) +deactivate DepositNativeModule +DepositPrecompile -> ExocoreGateway: 3.7:return (result, balance) +deactivate DepositPrecompile +ExocoreGateway -> ExocoreL0Endpoint: 3.8:send(response) +ExocoreL0Endpoint -> ExocoreGateway: 3.9:emit (requestSent) +deactivate ExocoreGateway +deactivate ExocoreL0Endpoint +ExocoreL0Endpoint -> ClientChainL0Endpoint: 4.1:lzReceive(response) +activate ClientChainL0Endpoint +ClientChainL0Endpoint -> ClientChainLzReceiver: 4.2:lzReceive(response) +activate ClientChainLzReceiver +ClientChainLzReceiver -> ClientChainLzReceiver: 4.3:afterReceiveDepositResponse(payload) +ClientChainLzReceiver -> ExoCapsule: 4.4:updatePrincipleBalance(lastlyUpdatedPrincipleBalance) +activate ExoCapsule +ExoCapsule -> ClientChainLzReceiver: 4.5:emit (DepositResult) +deactivate ExoCapsule +ClientChainLzReceiver -> ClientChainL0Endpoint: finish lzReceive +deactivate ClientChainLzReceiver +deactivate ClientChainL0Endpoint + +@enduml + + +@startuml +title NativeRestakingController: stake() function + +start +if (msg.value != 32 ether) then (no) + :Revert; + stop +endif + +:Get the capsule associated with the message sender; +if (capsule == address(0)) then (yes) + :Create a new ExoCapsule; + :Initialize the capsule with the contract address and message sender; + :Assign the new capsule to ownerToCapsule[msg.sender]; + :Emit CapsuleCreated event; +endif + +:Deposit 32 ETH to the ETH_POS contract; +:Emit StakedWithCapsule event; + +stop + +@enduml + +@startuml +title NativeRestakingController: depositBeaconChainValidator() function + +start + +:Check if the caller is the gateway; +if (msg.sender != gateway) then (no) + :Revert; + stop +endif + +:Get the validator pubkey and withdrawal credentials from the validatorContainer; +:Get the Validator struct for the pubkey from _capsuleValidators mapping; + +if (validator.status != UNREGISTERED) then (yes) + :Revert; + stop +endif + +if (_isStaleProof(validator, proof.beaconBlockTimestamp)) then (yes) + :Revert; + stop +endif + +if (!validatorContainer.verifyValidatorContainerBasic()) then (no) + :Revert; + stop +endif + +if (!_isActivatedAtEpoch(validatorContainer, proof.beaconBlockTimestamp)) then (no) + :Revert ; + stop +endif + +if (withdrawalCredentials != capsuleWithdrawalCredentials()) then (yes) + :Revert; + stop +endif + +:Verify the validator container using _verifyValidatorContainer(); + +:Update the validator struct with the new status, index, and balance; +:Store the validator pubkey in _capsuleValidatorsByIndex mapping; + +stop + +@enduml \ No newline at end of file From 9d87ca4b45798a18e2fe234d7f23fcf3baffdfde Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 22 Apr 2024 09:30:41 +0800 Subject: [PATCH 22/93] fix depositBeaconChainValidator diagram --- docs/native_deposit_workflow.wsd | 106 ++++++++++++++----------------- 1 file changed, 48 insertions(+), 58 deletions(-) diff --git a/docs/native_deposit_workflow.wsd b/docs/native_deposit_workflow.wsd index 7cfa31d5..41c3b13e 100644 --- a/docs/native_deposit_workflow.wsd +++ b/docs/native_deposit_workflow.wsd @@ -65,72 +65,62 @@ deactivate ClientChainL0Endpoint @startuml -title NativeRestakingController: stake() function +title NativeRestakingController: depositBeaconChainValidator() function start -if (msg.value != 32 ether) then (no) - :Revert; - stop -endif :Get the capsule associated with the message sender; if (capsule == address(0)) then (yes) - :Create a new ExoCapsule; - :Initialize the capsule with the contract address and message sender; - :Assign the new capsule to ownerToCapsule[msg.sender]; - :Emit CapsuleCreated event; -endif - -:Deposit 32 ETH to the ETH_POS contract; -:Emit StakedWithCapsule event; - -stop - -@enduml - -@startuml -title NativeRestakingController: depositBeaconChainValidator() function - -start - -:Check if the caller is the gateway; -if (msg.sender != gateway) then (no) - :Revert; - stop -endif - -:Get the validator pubkey and withdrawal credentials from the validatorContainer; -:Get the Validator struct for the pubkey from _capsuleValidators mapping; - -if (validator.status != UNREGISTERED) then (yes) - :Revert; - stop -endif - -if (_isStaleProof(validator, proof.beaconBlockTimestamp)) then (yes) - :Revert; - stop -endif - -if (!validatorContainer.verifyValidatorContainerBasic()) then (no) - :Revert; - stop -endif - -if (!_isActivatedAtEpoch(validatorContainer, proof.beaconBlockTimestamp)) then (no) - :Revert ; + :Revert with CapsuleNotExist error; stop endif -if (withdrawalCredentials != capsuleWithdrawalCredentials()) then (yes) - :Revert; - stop -endif - -:Verify the validator container using _verifyValidatorContainer(); - -:Update the validator struct with the new status, index, and balance; -:Store the validator pubkey in _capsuleValidatorsByIndex mapping; +:Call capsule.verifyDepositProof(validatorContainer, proof); +fork + :Check if caller is gateway; + if (msg.sender != gateway) then (no) + :Revert with "ExoCapsule: only client chain gateway could call this function"; + stop + endif + + :Get validator pubkey and withdrawal credentials from validatorContainer; + :Get Validator struct for pubkey from _capsuleValidators; + + if (validator.status != UNREGISTERED) then (yes) + :Revert with DoubleDepositedValidator error; + stop + endif + + if (_isStaleProof(validator, proof.beaconBlockTimestamp)) then (yes) + :Revert with StaleValidatorContainer error; + stop + endif + + if (!validatorContainer.verifyValidatorContainerBasic()) then (no) + :Revert with InvalidValidatorContainer error; + stop + endif + + if (!_isActivatedAtEpoch(validatorContainer, proof.beaconBlockTimestamp)) then (no) + :Revert with InvalidValidatorContainer error; + stop + endif + + if (withdrawalCredentials != capsuleWithdrawalCredentials()) then (yes) + :Revert with InvalidValidatorContainer error; + stop + endif + + :Verify validator container using _verifyValidatorContainer(); + :Update Validator struct with new status, index, and balance; + :Store validator pubkey in _capsuleValidatorsByIndex; +fork again + +:Calculate the depositValue using validatorContainer.getEffectiveBalance(); +:Store the request details in registeredRequests and registeredRequestActions; + +:Encode the request action arguments; +:Send the request action to ExoCore using _sendMsgToExocore(); stop From aeb5e69fd933098c58b8e96b5997042e6328b999 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 22 Apr 2024 19:50:46 +0800 Subject: [PATCH 23/93] fix container merklizatiion function --- src/libraries/ValidatorContainer.sol | 2 +- src/libraries/WithdrawalContainer.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/ValidatorContainer.sol b/src/libraries/ValidatorContainer.sol index 458f9f06..0abc28cc 100644 --- a/src/libraries/ValidatorContainer.sol +++ b/src/libraries/ValidatorContainer.sol @@ -53,7 +53,7 @@ library ValidatorContainer { for (uint i; i < MERKLE_TREE_HEIGHT; i++) { bytes32[] memory roots = new bytes32[](leaves.length / 2); for (uint j; j < leaves.length / 2; j++) { - roots[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + roots[j] = sha256(abi.encodePacked(leaves[2 * j], leaves[2 * j + 1])); } leaves = roots; } diff --git a/src/libraries/WithdrawalContainer.sol b/src/libraries/WithdrawalContainer.sol index 55744679..558ccef3 100644 --- a/src/libraries/WithdrawalContainer.sol +++ b/src/libraries/WithdrawalContainer.sol @@ -36,7 +36,7 @@ library WithdrawalContainer { for (uint i; i < MERKLE_TREE_HEIGHT; i++) { bytes32[] memory roots = new bytes32[](leaves.length / 2); for (uint j; j < leaves.length / 2; j++) { - roots[i] = sha256(abi.encodePacked(leaves[2 * i], leaves[2 * i + 1])); + roots[j] = sha256(abi.encodePacked(leaves[2 * j], leaves[2 * j + 1])); } leaves = roots; } From 8fc724f7531606a6d7a19210c0a3e8e96e5e017b Mon Sep 17 00:00:00 2001 From: adu Date: Tue, 23 Apr 2024 10:03:23 +0800 Subject: [PATCH 24/93] forge install: eigenlayer-beacon-oracle --- .gitmodules | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitmodules b/.gitmodules index 44123eba..b6a0feb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [submodule "lib/solidity-bytes-utils"] path = lib/solidity-bytes-utils url = https://github.com/GNSPS/solidity-bytes-utils +[submodule "lib/eigenlayer-beacon-oracle"] + path = lib/eigenlayer-beacon-oracle + url = https://github.com/succinctlabs/eigenlayer-beacon-oracle From c6e459cae011bef4f2c9a0b4ecfab958ef9db71c Mon Sep 17 00:00:00 2001 From: adu Date: Tue, 23 Apr 2024 16:54:25 +0800 Subject: [PATCH 25/93] integrate EigenLayerBeaconOracle into ExoCapsule --- remappings.txt | 1 + script/1_Prerequisities.s.sol | 24 ++++++++++++++ script/2_DeployBoth.s.sol | 22 ++++++++----- script/BaseScript.sol | 2 ++ src/core/ClientChainGateway.sol | 6 +++- src/core/ExoCapsule.sol | 38 +++++++++++++---------- src/core/NativeRestakingController.sol | 2 +- src/interfaces/IExoCapsule.sol | 4 +-- src/storage/ClientChainGatewayStorage.sol | 3 +- src/storage/ExoCapsuleStorage.sol | 5 ++- test/foundry/ClientChainGateway.t.sol | 35 +++++++++++++++++++-- test/foundry/ExocoreDeployer.t.sol | 28 +++++++++++++++-- 12 files changed, 135 insertions(+), 35 deletions(-) diff --git a/remappings.txt b/remappings.txt index 4234b241..6c83a02f 100644 --- a/remappings.txt +++ b/remappings.txt @@ -8,4 +8,5 @@ forge-std/=lib/forge-std/src/ @layerzerolabs/lz-evm-oapp-v2=lib/LayerZero-v2/oapp/ @layerzerolabs/lz-evm-oapp-v2=lib/LayerZero-v2/oapp/ @layerzerolabs/lz-evm-messagelib-v2=lib/LayerZero-v2/messagelib/ +@beacon-oracle=lib/eigenlayer-beacon-oracle/ solidity-bytes-utils/=lib/solidity-bytes-utils/ \ No newline at end of file diff --git a/script/1_Prerequisities.s.sol b/script/1_Prerequisities.s.sol index f0a75962..717a941f 100644 --- a/script/1_Prerequisities.s.sol +++ b/script/1_Prerequisities.s.sol @@ -7,6 +7,7 @@ import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import "test/mocks/ClaimRewardMock.sol"; import "test/mocks/DelegationMock.sol"; import "test/mocks/DepositWithdrawMock.sol"; +import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; import "./BaseScript.sol"; contract PrerequisitiesScript is BaseScript { @@ -55,10 +56,14 @@ contract PrerequisitiesScript is BaseScript { // use deployed ERC20 token as restake token restakeToken = ERC20PresetFixedSupply(erc20TokenAddress); + // deploy beacon chain oracle + beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); + string memory deployedContracts = "deployedContracts"; string memory clientChainContracts = "clientChainContracts"; string memory exocoreContracts = "exocoreContracts"; vm.serializeAddress(clientChainContracts, "lzEndpoint", address(clientChainLzEndpoint)); + vm.serializeAddress(clientChainContracts, "beaconOracle", address(beaconOracle)); string memory clientChainContractsOutput = vm.serializeAddress(clientChainContracts, "erc20Token", address(restakeToken)); @@ -77,4 +82,23 @@ contract PrerequisitiesScript is BaseScript { vm.writeJson(finalJson, "script/prerequisitContracts.json"); } + + function _deployBeaconOracle() internal returns (address) { + uint256 GENESIS_BLOCK_TIMESTAMP; + + if (block.chainid == 1) { + GENESIS_BLOCK_TIMESTAMP = 1606824023; + } else if (block.chainid == 5) { + GENESIS_BLOCK_TIMESTAMP = 1616508000; + } else if (block.chainid == 11155111) { + GENESIS_BLOCK_TIMESTAMP = 1655733600; + } else if (block.chainid == 17000) { + GENESIS_BLOCK_TIMESTAMP = 1695902400; + } else { + revert("Unsupported chainId."); + } + + EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); + return address(oracle); + } } diff --git a/script/2_DeployBoth.s.sol b/script/2_DeployBoth.s.sol index dc83ece8..27716653 100644 --- a/script/2_DeployBoth.s.sol +++ b/script/2_DeployBoth.s.sol @@ -9,34 +9,38 @@ import {Vault} from "../src/core/Vault.sol"; import "../src/core/ExocoreGateway.sol"; import "../test/mocks/ExocoreGatewayMock.sol"; import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; import {BaseScript} from "./BaseScript.sol"; contract DeployScript is BaseScript { function setUp() public virtual override { super.setUp(); - string memory deployedContracts = vm.readFile("script/prerequisitContracts.json"); + string memory prerequisities = vm.readFile("script/prerequisitContracts.json"); - clientChainLzEndpoint = ILayerZeroEndpointV2(stdJson.readAddress(deployedContracts, ".clientChain.lzEndpoint")); + clientChainLzEndpoint = ILayerZeroEndpointV2(stdJson.readAddress(prerequisities, ".clientChain.lzEndpoint")); require(address(clientChainLzEndpoint) != address(0), "client chain l0 endpoint should not be empty"); - restakeToken = ERC20PresetFixedSupply(stdJson.readAddress(deployedContracts, ".clientChain.erc20Token")); + beaconOracle = IBeaconChainOracle(stdJson.readAddress(prerequisities, ".clientChain.beaconOracle")); + require(address(beaconOracle) != address(0), "client chain beacon oracle should not be empty"); + + restakeToken = ERC20PresetFixedSupply(stdJson.readAddress(prerequisities, ".clientChain.erc20Token")); require(address(restakeToken) != address(0), "restake token address should not be empty"); - exocoreLzEndpoint = ILayerZeroEndpointV2(stdJson.readAddress(deployedContracts, ".exocore.lzEndpoint")); + exocoreLzEndpoint = ILayerZeroEndpointV2(stdJson.readAddress(prerequisities, ".exocore.lzEndpoint")); require(address(exocoreLzEndpoint) != address(0), "exocore l0 endpoint should not be empty"); if (useExocorePrecompileMock) { - depositMock = stdJson.readAddress(deployedContracts, ".exocore.depositPrecompileMock"); + depositMock = stdJson.readAddress(prerequisities, ".exocore.depositPrecompileMock"); require(depositMock != address(0), "depositMock should not be empty"); - withdrawMock = stdJson.readAddress(deployedContracts, ".exocore.withdrawPrecompileMock"); + withdrawMock = stdJson.readAddress(prerequisities, ".exocore.withdrawPrecompileMock"); require(withdrawMock != address(0), "withdrawMock should not be empty"); - delegationMock = stdJson.readAddress(deployedContracts, ".exocore.delegationPrecompileMock"); + delegationMock = stdJson.readAddress(prerequisities, ".exocore.delegationPrecompileMock"); require(delegationMock != address(0), "delegationMock should not be empty"); - claimRewardMock = stdJson.readAddress(deployedContracts, ".exocore.claimRewardPrecompileMock"); + claimRewardMock = stdJson.readAddress(prerequisities, ".exocore.claimRewardPrecompileMock"); require(claimRewardMock != address(0), "claimRewardMock should not be empty"); } @@ -68,6 +72,7 @@ contract DeployScript is BaseScript { clientGatewayLogic.initialize.selector, exocoreChainId, payable(exocoreValidatorSet.addr), + address(beaconOracle), whitelistTokens ) ) @@ -134,6 +139,7 @@ contract DeployScript is BaseScript { string memory clientChainContracts = "clientChainContracts"; string memory exocoreContracts = "exocoreContracts"; vm.serializeAddress(clientChainContracts, "lzEndpoint", address(clientChainLzEndpoint)); + vm.serializeAddress(clientChainContracts, "beaconOracle", address(beaconOracle)); vm.serializeAddress(clientChainContracts, "clientChainGateway", address(clientGateway)); vm.serializeAddress(clientChainContracts, "resVault", address(vault)); vm.serializeAddress(clientChainContracts, "erc20Token", address(restakeToken)); diff --git a/script/BaseScript.sol b/script/BaseScript.sol index 3f7a2a64..b8dfdb7a 100644 --- a/script/BaseScript.sol +++ b/script/BaseScript.sol @@ -5,6 +5,7 @@ import "../src/interfaces/IVault.sol"; import "../src/interfaces/IExocoreGateway.sol"; import {ERC20PresetFixedSupply} from "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; import "forge-std/Script.sol"; contract BaseScript is Script { @@ -30,6 +31,7 @@ contract BaseScript is Script { IExocoreGateway exocoreGateway; ILayerZeroEndpointV2 clientChainLzEndpoint; ILayerZeroEndpointV2 exocoreLzEndpoint; + IBeaconChainOracle beaconOracle; ERC20PresetFixedSupply restakeToken; address delegationMock; diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index 5b0705c1..648a6613 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -38,13 +38,17 @@ contract ClientChainGateway is function initialize( uint32 _exocoreChainId, address payable _exocoreValidatorSetAddress, + address _beaconOracleAddress, address[] calldata _whitelistTokens ) external reinitializer(2) { clearBootstrapData(); - require(_exocoreValidatorSetAddress != address(0), "ClientChainGateway: exocore validator set address should not be empty"); + require(_exocoreChainId != 0, "ClientChainGateway: exocore chain id should not be empty"); + require(_exocoreValidatorSetAddress != address(0), "ClientChainGateway: exocore validator set address should not be empty"); + require(_beaconOracleAddress != address(0), "ClientChainGateway: beacon chain oracle address should not be empty"); exocoreValidatorSetAddress = _exocoreValidatorSetAddress; + beaconOracleAddress = _beaconOracleAddress; exocoreChainId = _exocoreChainId; for (uint256 i = 0; i < _whitelistTokens.length; i++) { diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 2e844cf3..4ef18493 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -9,6 +9,7 @@ import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; contract ExoCapsule is Initializable, @@ -26,12 +27,13 @@ contract ExoCapsule is error InvalidValidatorContainer(bytes32 pubkey); error InvalidWithdrawalContainer(uint64 validatorIndex); error DoubleDepositedValidator(bytes32 pubkey); - error GetBeaconBlockRootFailure(uint64 timestamp); - error StaleValidatorContainer(bytes32 pubkey, uint64 timestamp); + error StaleValidatorContainer(bytes32 pubkey, uint256 timestamp); error UnregisteredOrWithdrawnValidatorContainer(bytes32 pubkey); error FullyWithdrawnValidatorContainer(bytes32 pubkey); error UnmatchedValidatorAndWithdrawal(bytes32 pubkey); error NotPartialWithdrawal(bytes32 pubkey); + error BeaconChainOracleNotUpdatedAtTime(address oracle, uint256 timestamp); + error WithdrawalFailure(address withdrawer, address recipient, uint256 amount); modifier onlyGateway() { require(msg.sender == address(gateway), "ExoCapsule: only client chain gateway could call this function"); @@ -42,12 +44,14 @@ contract ExoCapsule is _disableInitializers(); } - function initialize(address _gateway, address _capsuleOwner) external initializer { + function initialize(address _gateway, address _capsuleOwner, address _beaconOracle) external initializer { require(_capsuleOwner != address(0), "ExoCapsule: capsule owner address can not be empty"); require(_gateway != address(0), "ExoCapsule: gateway address can not be empty"); + require(_beaconOracle != address(0), "ExoCapsule: beacon chain oracle address should not be empty"); - gateway = INativeRestakingController(_gateway); capsuleOwner = _capsuleOwner; + gateway = INativeRestakingController(_gateway); + beaconOracle = IBeaconChainOracle(_beaconOracle); } function verifyDepositProof( @@ -152,7 +156,10 @@ contract ExoCapsule is ); withdrawableBalance -= amount; - recipient.call{value: amount}(""); + (bool sent, ) = recipient.call{value: amount}(""); + if (!sent) { + revert WithdrawalFailure(capsuleOwner, recipient, amount); + } emit WithdrawalSuccess(capsuleOwner, recipient, amount); } @@ -179,14 +186,13 @@ contract ExoCapsule is return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); } - function getBeaconBlockRoot(uint64 timestamp) public returns (bytes32) { - (bool success, bytes memory rootBytes) = BEACON_ROOTS_ADDRESS.call(abi.encodePacked(timestamp)); - if (!success) { - revert GetBeaconBlockRootFailure(timestamp); + function getBeaconBlockRoot(uint256 timestamp) public returns (bytes32) { + bytes32 root = beaconOracle.timestampToBlockRoot(timestamp); + if (root == bytes32(0)) { + revert BeaconChainOracleNotUpdatedAtTime(address(beaconOracle), timestamp); } - bytes32 beaconBlockRoot = abi.decode(rootBytes, (bytes32)); - return beaconBlockRoot; + return root; } function _verifyValidatorContainer(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) internal { @@ -219,7 +225,7 @@ contract ExoCapsule is } } - function _isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint64 atTimestamp) internal pure returns (bool) { + function _isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint256 atTimestamp) internal pure returns (bool) { uint64 atEpoch = _timestampToEpoch(atTimestamp); uint64 activationEpoch = validatorContainer.getActivationEpoch(); uint64 exitEpoch = validatorContainer.getExitEpoch(); @@ -227,7 +233,7 @@ contract ExoCapsule is return (atEpoch >= activationEpoch && atEpoch < exitEpoch); } - function _isStaleProof(Validator storage validator, uint64 proofTimestamp) internal view returns (bool) { + function _isStaleProof(Validator storage validator, uint256 proofTimestamp) internal view returns (bool) { if (proofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS >= block.timestamp) { if (proofTimestamp > validator.mostRecentBalanceUpdateTimestamp) { return false; @@ -238,7 +244,7 @@ contract ExoCapsule is } function _hasFullyWithdrawn(bytes32[] calldata validatorContainer) internal view returns (bool) { - if (validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(uint64(block.timestamp))) { + if (validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(block.timestamp)) { if (validatorContainer.getEffectiveBalance() == 0) { return true; } @@ -252,8 +258,8 @@ contract ExoCapsule is * seconds since genesis, and dividing by seconds per epoch. * reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md */ - function _timestampToEpoch(uint64 timestamp) internal pure returns (uint64) { + function _timestampToEpoch(uint256 timestamp) internal pure returns (uint64) { require(timestamp >= BEACON_CHAIN_GENESIS_TIME, "timestamp should be greater than beacon chain genesis timestamp"); - return (timestamp - BEACON_CHAIN_GENESIS_TIME) / BeaconChainProofs.SECONDS_PER_EPOCH; + return uint64((timestamp - BEACON_CHAIN_GENESIS_TIME) / BeaconChainProofs.SECONDS_PER_EPOCH); } } diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index c95ce4c2..192bfc3b 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -31,7 +31,7 @@ abstract contract NativeRestakingController is require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); ExoCapsule capsule = new ExoCapsule(); - capsule.initialize(address(this), msg.sender); + capsule.initialize(address(this), msg.sender, beaconOracleAddress); ownerToCapsule[msg.sender] = capsule; emit CapsuleCreated(msg.sender, address(capsule)); diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index b5b90cc2..7c3d568c 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; interface IExoCapsule { /// @notice This struct contains the infos needed for validator container validity verification struct ValidatorContainerProof { - uint64 beaconBlockTimestamp; + uint256 beaconBlockTimestamp; bytes32 stateRoot; bytes32[] stateRootProof; bytes32[] validatorContainerRootProof; @@ -11,7 +11,7 @@ interface IExoCapsule { } struct WithdrawalContainerProof { - uint64 beaconBlockTimestamp; + uint256 beaconBlockTimestamp; bytes32 executionPayloadRoot; bytes32[] executionPayloadRootProof; bytes32[] withdrawalContainerRootProof; diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 3550ac01..6ebad096 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -17,10 +17,11 @@ contract ClientChainGatewayStorage is BootstrapStorage { uint128 constant DESTINATION_MSG_VALUE = 0; // native restaking state variables - mapping(address => IExoCapsule) public ownerToCapsule; IETHPOSDeposit constant ETH_POS = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); address constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint256 constant GWEI_TO_WEI = 1e9; + address beaconOracleAddress; + mapping(address => IExoCapsule) public ownerToCapsule; event WhitelistTokenAdded(address _token); event WhitelistTokenRemoved(address _token); diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 21dface8..8bf90066 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.19; import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; +import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; + contract ExoCapsuleStorage { enum VALIDATOR_STATUS { UNREGISTERED, // the validator has not been registered in this ExoCapsule @@ -16,7 +18,7 @@ contract ExoCapsuleStorage { // amount of beacon chain ETH restaked on EigenLayer in gwei uint64 restakedBalanceGwei; //timestamp of the validator's most recent balance update - uint64 mostRecentBalanceUpdateTimestamp; + uint256 mostRecentBalanceUpdateTimestamp; // status of the validator VALIDATOR_STATUS status; } @@ -27,6 +29,7 @@ contract ExoCapsuleStorage { address public capsuleOwner; INativeRestakingController public gateway; + IBeaconChainOracle public beaconOracle; uint256 public principleBalance; uint256 public withdrawableBalance; diff --git a/test/foundry/ClientChainGateway.t.sol b/test/foundry/ClientChainGateway.t.sol index 29a2ffed..e5d5edb9 100644 --- a/test/foundry/ClientChainGateway.t.sol +++ b/test/foundry/ClientChainGateway.t.sol @@ -14,6 +14,7 @@ import "../../src/interfaces/precompiles/IDelegation.sol"; import "../../src/interfaces/precompiles/IDeposit.sol"; import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import "../../src/interfaces/ITSSReceiver.sol"; +import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; contract ClientChainGatewayTest is Test { Player[] players; @@ -28,10 +29,11 @@ contract ClientChainGatewayTest is Test { ExocoreGateway exocoreGateway; EndpointV2Mock clientChainLzEndpoint; EndpointV2Mock exocoreLzEndpoint; + IBeaconChainOracle beaconOracle; string operatorAddress = "exo1v4s6vtjpmxwu9rlhqms5urzrc3tc2ae2gnuqhc"; - uint16 exocoreChainId = 1; - uint16 clientChainId = 2; + uint16 exocoreChainId = 2; + uint16 clientChainId = 1; struct Player { uint256 privateKey; @@ -51,6 +53,7 @@ contract ClientChainGatewayTest is Test { exocoreValidatorSet = Player({privateKey: uint256(0xa), addr: vm.addr(uint256(0xa))}); deployer = Player({privateKey: uint256(0xb), addr: vm.addr(uint256(0xb))}); + vm.chainId(clientChainId); _deploy(); vm.prank(exocoreValidatorSet.addr); @@ -60,6 +63,8 @@ contract ClientChainGatewayTest is Test { function _deploy() internal { vm.startPrank(deployer.addr); + beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); + restakeToken = new ERC20PresetFixedSupply("rest", "rest", 1e16, exocoreValidatorSet.addr); whitelistTokens.push(address(restakeToken)); @@ -73,12 +78,36 @@ contract ClientChainGatewayTest is Test { Vault vaultLogic = new Vault(); vault = Vault(address(new TransparentUpgradeableProxy(address(vaultLogic), address(proxyAdmin), ""))); - clientGateway.initialize(exocoreChainId, payable(exocoreValidatorSet.addr), whitelistTokens); + clientGateway.initialize( + exocoreChainId, + payable(exocoreValidatorSet.addr), + address(beaconOracle), + whitelistTokens + ); vault.initialize(address(restakeToken), address(clientGateway)); vaults.push(address(vault)); vm.stopPrank(); } + function _deployBeaconOracle() internal returns (address) { + uint256 GENESIS_BLOCK_TIMESTAMP; + + if (block.chainid == 1) { + GENESIS_BLOCK_TIMESTAMP = 1606824023; + } else if (block.chainid == 5) { + GENESIS_BLOCK_TIMESTAMP = 1616508000; + } else if (block.chainid == 11155111) { + GENESIS_BLOCK_TIMESTAMP = 1655733600; + } else if (block.chainid == 17000) { + GENESIS_BLOCK_TIMESTAMP = 1695902400; + } else { + revert("Unsupported chainId."); + } + + EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); + return address(oracle); + } + function test_PauseClientChainGateway() public { vm.expectEmit(true, true, true, true, address(clientGateway)); emit Paused(exocoreValidatorSet.addr); diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index fb0e6d55..7e088405 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -16,6 +16,7 @@ import "../../src/interfaces/precompiles/IClaimReward.sol"; import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; +import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; contract ExocoreDeployer is Test { using AddressCast for address; @@ -31,9 +32,10 @@ contract ExocoreDeployer is Test { ExocoreGateway exocoreGateway; ILayerZeroEndpointV2 clientChainLzEndpoint; ILayerZeroEndpointV2 exocoreLzEndpoint; + IBeaconChainOracle beaconOracle; - uint32 exocoreChainId = 1; - uint32 clientChainId = 2; + uint32 exocoreChainId = 2; + uint32 clientChainId = 1; struct Player { uint256 privateKey; @@ -46,6 +48,7 @@ contract ExocoreDeployer is Test { players.push(Player({privateKey: uint256(0x3), addr: vm.addr(uint256(0x3))})); exocoreValidatorSet = Player({privateKey: uint256(0xa), addr: vm.addr(uint256(0xa))}); + vm.chainId(clientChainId); _deploy(); } @@ -54,6 +57,7 @@ contract ExocoreDeployer is Test { restakeToken = new ERC20PresetFixedSupply("rest", "rest", 1e16, exocoreValidatorSet.addr); clientChainLzEndpoint = new NonShortCircuitEndpointV2Mock(clientChainId, exocoreValidatorSet.addr); exocoreLzEndpoint = new NonShortCircuitEndpointV2Mock(exocoreChainId, exocoreValidatorSet.addr); + beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); // deploy and initialize client chain contracts ProxyAdmin proxyAdmin = new ProxyAdmin(); @@ -70,6 +74,7 @@ contract ExocoreDeployer is Test { clientGatewayLogic.initialize.selector, exocoreChainId, payable(exocoreValidatorSet.addr), + address(beaconOracle), whitelistTokens ) ) @@ -137,4 +142,23 @@ contract ExocoreDeployer is Test { bytes memory WithdrawRewardMockCode = vm.getDeployedCode("ClaimRewardMock.sol"); vm.etch(CLAIM_REWARD_PRECOMPILE_ADDRESS, WithdrawRewardMockCode); } + + function _deployBeaconOracle() internal returns (address) { + uint256 GENESIS_BLOCK_TIMESTAMP; + + if (block.chainid == 1) { + GENESIS_BLOCK_TIMESTAMP = 1606824023; + } else if (block.chainid == 5) { + GENESIS_BLOCK_TIMESTAMP = 1616508000; + } else if (block.chainid == 11155111) { + GENESIS_BLOCK_TIMESTAMP = 1655733600; + } else if (block.chainid == 17000) { + GENESIS_BLOCK_TIMESTAMP = 1695902400; + } else { + revert("Unsupported chainId."); + } + + EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); + return address(oracle); + } } From 8404a4211eaf6f73c7410b57cba83c4dcb30c8fd Mon Sep 17 00:00:00 2001 From: adu Date: Thu, 25 Apr 2024 15:23:29 +0800 Subject: [PATCH 26/93] add uint test for ExoCapsule.verifyDepositProof --- src/core/ExoCapsule.sol | 37 +++--- src/core/NativeRestakingController.sol | 3 +- src/interfaces/IExoCapsule.sol | 4 +- src/libraries/BeaconChainProofs.sol | 26 ++-- src/libraries/Merkle.sol | 7 +- src/storage/ExoCapsuleStorage.sol | 2 +- test/foundry/ExoCapsule.t.sol | 100 +++++++++++++++ .../validator_container_proof_302913.json | 115 ++++++++++++++++++ 8 files changed, 255 insertions(+), 39 deletions(-) create mode 100644 test/foundry/ExoCapsule.t.sol create mode 100644 test/foundry/test-data/validator_container_proof_302913.json diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 4ef18493..756b9f8f 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -8,11 +8,9 @@ import {INativeRestakingController} from "../interfaces/INativeRestakingControll import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; -import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; contract ExoCapsule is - Initializable, ExoCapsuleStorage, IExoCapsule { @@ -34,17 +32,18 @@ contract ExoCapsule is error NotPartialWithdrawal(bytes32 pubkey); error BeaconChainOracleNotUpdatedAtTime(address oracle, uint256 timestamp); error WithdrawalFailure(address withdrawer, address recipient, uint256 amount); + error WithdrawalCredentialsNotMatch(); + error InactiveValidatorContainer(bytes32 pubkey); + error InvalidGateway(address, address); modifier onlyGateway() { - require(msg.sender == address(gateway), "ExoCapsule: only client chain gateway could call this function"); + if (msg.sender != address(gateway)) { + revert InvalidGateway(address(gateway), msg.sender); + } _; } - constructor() { - _disableInitializers(); - } - - function initialize(address _gateway, address _capsuleOwner, address _beaconOracle) external initializer { + constructor(address _gateway, address _capsuleOwner, address _beaconOracle) { require(_capsuleOwner != address(0), "ExoCapsule: capsule owner address can not be empty"); require(_gateway != address(0), "ExoCapsule: gateway address can not be empty"); require(_beaconOracle != address(0), "ExoCapsule: beacon chain oracle address should not be empty"); @@ -74,22 +73,22 @@ contract ExoCapsule is revert InvalidValidatorContainer(validatorPubkey); } - if (!_isActivatedAtEpoch(validatorContainer, proof.beaconBlockTimestamp)) { - revert InvalidValidatorContainer(validatorPubkey); - } + // if (!_isActivatedAtEpoch(validatorContainer, proof.beaconBlockTimestamp)) { + // revert InactiveValidatorContainer(validatorPubkey); + // } if (withdrawalCredentials != bytes32(capsuleWithdrawalCredentials())) { - revert InvalidValidatorContainer(validatorPubkey); + revert WithdrawalCredentialsNotMatch(); } _verifyValidatorContainer(validatorContainer, proof); validator.status = VALIDATOR_STATUS.REGISTERED; - validator.validatorIndex = proof.validatorContainerRootIndex; + validator.validatorIndex = proof.validatorIndex; validator.mostRecentBalanceUpdateTimestamp = proof.beaconBlockTimestamp; validator.restakedBalanceGwei = validatorContainer.getEffectiveBalance(); - _capsuleValidatorsByIndex[proof.validatorContainerRootIndex] = validatorPubkey; + _capsuleValidatorsByIndex[proof.validatorIndex] = validatorPubkey; } function verifyPartialWithdrawalProof( @@ -186,7 +185,7 @@ contract ExoCapsule is return abi.encodePacked(bytes1(uint8(1)), bytes11(0), address(this)); } - function getBeaconBlockRoot(uint256 timestamp) public returns (bytes32) { + function getBeaconBlockRoot(uint256 timestamp) public view returns (bytes32) { bytes32 root = beaconOracle.timestampToBlockRoot(timestamp); if (root == bytes32(0)) { revert BeaconChainOracleNotUpdatedAtTime(address(beaconOracle), timestamp); @@ -195,12 +194,12 @@ contract ExoCapsule is return root; } - function _verifyValidatorContainer(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) internal { + function _verifyValidatorContainer(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) internal view { bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); bytes32 validatorContainerRoot = validatorContainer.merklelizeValidatorContainer(); bool valid = validatorContainerRoot.isValidValidatorContainerRoot( proof.validatorContainerRootProof, - proof.validatorContainerRootIndex, + proof.validatorIndex, beaconBlockRoot, proof.stateRoot, proof.stateRootProof @@ -210,12 +209,12 @@ contract ExoCapsule is } } - function _verifyWithdrawalContainer(bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata proof) internal { + function _verifyWithdrawalContainer(bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata proof) internal view { bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); bytes32 withdrawalContainerRoot = withdrawalContainer.merklelizeWithdrawalContainer(); bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot( proof.withdrawalContainerRootProof, - proof.withdrawalContainerRootIndex, + proof.withdrawalIndex, beaconBlockRoot, proof.executionPayloadRoot, proof.executionPayloadRootProof diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 192bfc3b..e9578cfb 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -30,8 +30,7 @@ abstract contract NativeRestakingController is function createExoCapsule() public whenNotPaused returns (address) { require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); - ExoCapsule capsule = new ExoCapsule(); - capsule.initialize(address(this), msg.sender, beaconOracleAddress); + ExoCapsule capsule = new ExoCapsule(address(this), msg.sender, beaconOracleAddress); ownerToCapsule[msg.sender] = capsule; emit CapsuleCreated(msg.sender, address(capsule)); diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 7c3d568c..608b08b1 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -7,7 +7,7 @@ interface IExoCapsule { bytes32 stateRoot; bytes32[] stateRootProof; bytes32[] validatorContainerRootProof; - uint256 validatorContainerRootIndex; + uint256 validatorIndex; } struct WithdrawalContainerProof { @@ -15,7 +15,7 @@ interface IExoCapsule { bytes32 executionPayloadRoot; bytes32[] executionPayloadRootProof; bytes32[] withdrawalContainerRootProof; - uint256 withdrawalContainerRootIndex; + uint256 withdrawalIndex; } function verifyDepositProof( diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 3410d802..184cbd24 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -136,13 +136,13 @@ library BeaconChainProofs { function isValidValidatorContainerRoot( bytes32 validatorContainerRoot, bytes32[] calldata validatorContainerRootProof, - uint256 validatorContainerRootIndex, + uint256 validatorIndex, bytes32 beaconBlockRoot, bytes32 stateRoot, bytes32[] calldata stateRootProof ) internal view returns (bool valid) { bool validStateRoot = isValidStateRoot(stateRoot, beaconBlockRoot, stateRootProof); - bool validVCRootAgainstStateRoot = isValidVCRootAgainstStateRoot(validatorContainerRoot, stateRoot, validatorContainerRootProof, validatorContainerRootIndex); + bool validVCRootAgainstStateRoot = isValidVCRootAgainstStateRoot(validatorContainerRoot, stateRoot, validatorContainerRootProof, validatorIndex); if (validStateRoot && validVCRootAgainstStateRoot) { valid = true; } @@ -170,25 +170,27 @@ library BeaconChainProofs { bytes32 validatorContainerRoot, bytes32 stateRoot, bytes32[] calldata validatorContainerRootProof, - uint256 validatorContainerRootIndex + uint256 validatorIndex ) internal view returns (bool) { require( validatorContainerRootProof.length == (VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_FIELD_TREE_HEIGHT, "validator container root proof should have 46 nodes" ); + uint256 leafIndex = (VALIDATOR_TREE_ROOT_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); + return Merkle.verifyInclusionSha256({ proof: validatorContainerRootProof, root: stateRoot, leaf: validatorContainerRoot, - index: validatorContainerRootIndex + index: leafIndex }); } function isValidWithdrawalContainerRoot( bytes32 withdrawalContainerRoot, bytes32[] calldata withdrawalContainerRootProof, - uint256 withdrawalContainerRootIndex, + uint256 withdrawalIndex, bytes32 beaconBlockRoot, bytes32 executionPayloadRoot, bytes32[] calldata executionPayloadRootProof @@ -198,7 +200,7 @@ library BeaconChainProofs { withdrawalContainerRoot, executionPayloadRoot, withdrawalContainerRootProof, - withdrawalContainerRootIndex + withdrawalIndex ); if (validExecutionPayloadRoot && validWCRootAgainstExecutionPayloadRoot) { valid = true; @@ -215,14 +217,14 @@ library BeaconChainProofs { "state root proof should have 3 nodes" ); - uint256 executionPayloadIndex = (BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT)) | + uint256 leafIndex = (BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT)) | EXECUTION_PAYLOAD_INDEX; return Merkle.verifyInclusionSha256({ proof: executionPayloadRootProof, root: beaconBlockRoot, leaf: executionPayloadRoot, - index: executionPayloadIndex + index: leafIndex }); } @@ -230,21 +232,21 @@ library BeaconChainProofs { bytes32 withdrawalContainerRoot, bytes32 executionPayloadRoot, bytes32[] calldata withdrawalContainerRootProof, - uint256 withdrawalContainerRootIndex + uint256 withdrawalIndex ) internal view returns (bool) { require( withdrawalContainerRootProof.length == (VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_FIELD_TREE_HEIGHT, "validator container root proof should have 46 nodes" ); - uint256 withdrawalIndex = (WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1)) | - uint256(withdrawalContainerRootIndex); + uint256 leafIndex = (WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1)) | + uint256(withdrawalIndex); return Merkle.verifyInclusionSha256({ proof: withdrawalContainerRootProof, root: executionPayloadRoot, leaf: withdrawalContainerRoot, - index: withdrawalIndex + index: leafIndex }); } } diff --git a/src/libraries/Merkle.sol b/src/libraries/Merkle.sol index 22b3829f..52630da4 100644 --- a/src/libraries/Merkle.sol +++ b/src/libraries/Merkle.sol @@ -114,12 +114,13 @@ library Merkle { "Merkle.processInclusionProofSha256: proof length should be a non-zero multiple of 32" ); bytes32[1] memory computedHash = [leaf]; - for (uint256 i = 32; i <= proof.length; i += 32) { + for (uint256 i = 0; i < proof.length; i++) { + bytes32[1] memory node = [proof[i]]; if (index % 2 == 0) { // if ith bit of index is 0, then computedHash is a left sibling assembly { mstore(0x00, mload(computedHash)) - mstore(0x20, mload(add(proof, i))) + mstore(0x20, mload(node)) if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) } @@ -128,7 +129,7 @@ library Merkle { } else { // if ith bit of index is 1, then computedHash is a right sibling assembly { - mstore(0x00, mload(add(proof, i))) + mstore(0x00, mload(node)) mstore(0x20, mload(computedHash)) if iszero(staticcall(sub(gas(), 2000), 2, 0x00, 0x40, computedHash, 0x20)) { revert(0, 0) diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 8bf90066..b2a97667 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -24,7 +24,7 @@ contract ExoCapsuleStorage { } address public constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; - uint64 public constant BEACON_CHAIN_GENESIS_TIME = 1606824023; + uint256 public constant BEACON_CHAIN_GENESIS_TIME = 1606824023; uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; address public capsuleOwner; diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol new file mode 100644 index 00000000..9721fcc8 --- /dev/null +++ b/test/foundry/ExoCapsule.t.sol @@ -0,0 +1,100 @@ +pragma solidity ^0.8.19; + +import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import "forge-std/console.sol"; +import "forge-std/Test.sol"; +import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; + +import "src/interfaces/IExoCapsule.sol"; +import "src/core/ExoCapsule.sol"; +import "src/libraries/BeaconChainProofs.sol"; + +contract SetUp is Test { + using stdStorage for StdStorage; + + bytes32[] validatorContainer; + /** + struct ValidatorContainerProof { + uint256 beaconBlockTimestamp; + bytes32 stateRoot; + bytes32[] stateRootProof; + bytes32[] validatorContainerRootProof; + uint256 validatorContainerRootIndex; + } + */ + IExoCapsule.ValidatorContainerProof validatorProof; + bytes32 beaconBlockRoot; + + ExoCapsule capsule; + IBeaconChainOracle beaconOracle; + address capsuleOwner; + + uint256 constant BEACON_CHAIN_GENESIS_TIME = 1606824023; + uint256 constant mockProofSlotsAfterGenesis = 8000; + uint256 constant mockCurrentBlockSlotsAfterGenesis = 7500; + + function setUp() public { + string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); + + validatorContainer = stdJson.readBytes32Array(validatorInfo, ".ValidatorFields"); + require(validatorContainer.length > 0, "validator container should not be empty"); + + vm.warp(BEACON_CHAIN_GENESIS_TIME + mockCurrentBlockSlotsAfterGenesis * 12); + validatorProof.beaconBlockTimestamp = BEACON_CHAIN_GENESIS_TIME + mockProofSlotsAfterGenesis * 12; + + 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"); + + beaconOracle = IBeaconChainOracle(address(0x123)); + vm.etch(address(beaconOracle), bytes("aabb")); + + capsuleOwner = address(0x125); + + ExoCapsule phantomCapsule = new ExoCapsule(address(this), capsuleOwner, address(beaconOracle)); + + address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); + vm.etch(capsuleAddress, address(phantomCapsule).code); + capsule = ExoCapsule(capsuleAddress); + + bytes32 gatewaySlot = bytes32(stdstore.target(capsuleAddress).sig("gateway()").find()); + vm.store(capsuleAddress, gatewaySlot, bytes32(uint256(uint160(address(this))))); + + bytes32 ownerSlot = bytes32(stdstore.target(capsuleAddress).sig("capsuleOwner()").find()); + vm.store(capsuleAddress, ownerSlot, bytes32(uint256(uint160(capsuleOwner)))); + + bytes32 beaconOraclerSlot = bytes32(stdstore.target(capsuleAddress).sig("beaconOracle()").find()); + vm.store(capsuleAddress, beaconOraclerSlot, bytes32(uint256(uint160(address(beaconOracle))))); + } + + function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { + return address(bytes20(uint160(uint256(withdrawalCredentials)))); + } + + function _getWithdrawalCredentials(bytes32[] storage vc) internal view returns (bytes32) { + return vc[1]; + } +} + +contract VerifyDepositProof is SetUp { + using BeaconChainProofs for bytes32; + function test_verifyDepositProof() public { + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(beaconBlockRoot) + ); + + capsule.verifyDepositProof(validatorContainer, validatorProof); + } +} \ No newline at end of file diff --git a/test/foundry/test-data/validator_container_proof_302913.json b/test/foundry/test-data/validator_container_proof_302913.json new file mode 100644 index 00000000..b35ae433 --- /dev/null +++ b/test/foundry/test-data/validator_container_proof_302913.json @@ -0,0 +1,115 @@ +{ + "validatorIndex": 302913, + "beaconStateRoot": "0x4678e21381463879490925e8911492b890c0ce7ba347cce57fd8d4e0bb27f04a", + "balanceRoot": "0x6cba5d7307000000e5015b730700000008bd5d7307000000239e5d7307000000", + "latestBlockHeaderRoot": "0x859816e76b0944c3110a31f26acc301718122e1acfe8fb161df9c0e684515593", + "ValidatorBalanceProof": [ + "0x26805d73070000002aa55d7307000000218e5d730700000056d35d7307000000", + "0x24846a54724508d8394a9e30f819bc19fe6727745be4f19e1e3924b985b4b8e9", + "0x48fb4849c31d88b413f650c17bd55bb11fe38bcef5d234dba8532737ae78f776", + "0x0d7d38f90a04ef49d15ca10af0e818e227134e4b46c7b22dd10e149f2a2bc612", + "0xdd345235f28f92719c39c9a5042b91f814d535ae5df574c0f6cd38f77791fc2b", + "0x0855928643f4210d9d8d0986847674139be32ebf3db984b6d81f3ed136475b6d", + "0x28460d51c1ce75299f8c0dff8bca9f498fef0edc57c343bbd76592b3d6b984a3", + "0x068a141709053afb48698290176068f448867cba9c6ff10f986b1d02691f6e70", + "0x9f0958275fe6ea80b1825bcd11907d1076600719ce574ae2a609e51a975dde54", + "0xd43fbb18b30449f5c81148848fd6579b4f6cebfe66187bd1c36a293b22085fc2", + "0x7bf0aac502053da7dfd5356960db63ab48d3cc6ee5f36f41639eadacd34ac272", + "0x866e3b213d02f1cb86149c2472789ab9d7fb3fb52371a893dc8b6f8936ebd0c1", + "0xcec7cc4b82fb8dd889257128af3e4433700fcc0fb0505b431aa5f592111e785b", + "0xdd08667067baa43e2b41b793464a591a29643b5582c8fc9c2c045f984c5848d7", + "0x842ec80062a4f2f7e9d80ab408374ae42817acf5036aff40b3ade345ab196090", + "0xfe1ae22b8ba21fce84b25f5da3e37a91db49158b7025d6371d5a54c03adba125", + "0x8c039a81d68952077db2cc11713ae6f5a9ecc5ebe859c0612ff7215c0ccd7aba", + "0x5415d3199f1da31287d646d03c2691728396d87f7ef52c142596301e27a343ca", + "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", + "0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff", + "0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5", + "0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d", + "0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c", + "0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327", + "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", + "0x846b080000000000000000000000000000000000000000000000000000000000", + "0x7d46d32db9a97974916a2114aeffc11fc031a954b2110742267054a438055c1a", + "0x81e1234eb27473f180f890c98019ecb7655a3e58105c7182d8daf726d75aaf8d", + "0xa158c9a519bf3c8d2296ebb6a2df54283c7014e7b84524da0574c95fe600a1fd", + "0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b", + "0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d" + ], + "WithdrawalCredentialProof": [ + "0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e", + "0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee", + "0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1", + "0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1", + "0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac", + "0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537", + "0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38", + "0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748", + "0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a", + "0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c", + "0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b", + "0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e", + "0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee", + "0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9", + "0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74", + "0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3", + "0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f", + "0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6", + "0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528", + "0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3", + "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", + "0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff", + "0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5", + "0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d", + "0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c", + "0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327", + "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", + "0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76", + "0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f", + "0x846b080000000000000000000000000000000000000000000000000000000000", + "0x5a6c050000000000000000000000000000000000000000000000000000000000", + "0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e", + "0x3f918c09ea95d520538a81f767de0f1be3f30fc9d599805c68048ef5c23a85e2", + "0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b", + "0x5ba049ff558dd0ff1eadf2cef346aac41b7059433f21925b345f1024af18057d" + ], + "ValidatorFields": [ + "0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d", + "0x01000000000000000000000049c486e3f4303bc11c02f952fe5b08d0ab22d443", + "0xe5015b7307000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xea65010000000000000000000000000000000000000000000000000000000000", + "0xf265010000000000000000000000000000000000000000000000000000000000", + "0xffffffffffffffff000000000000000000000000000000000000000000000000", + "0xffffffffffffffff000000000000000000000000000000000000000000000000" + ], + "StateRootAgainstLatestBlockHeaderProof": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71" + ] +} \ No newline at end of file From 30103b83c127f039d4ab911fbbf09bf90f12e829 Mon Sep 17 00:00:00 2001 From: bwhour Date: Fri, 26 Apr 2024 10:52:19 +0800 Subject: [PATCH 27/93] Optimize reuse code --- src/core/BaseRestakingController.sol | 6 +----- src/core/NativeRestakingController.sol | 21 ++++++++------------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index 77c06d3b..10a3a89f 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -46,11 +46,7 @@ abstract contract BaseRestakingController is isValidAmount(amount) whenNotPaused { if (token == VIRTUAL_STAKED_ETH_ADDRESS) { - IExoCapsule capsule = ownerToCapsule[msg.sender]; - if (address(capsule) == address(0)) { - revert CapsuleNotExist(); - } - + IExoCapsule capsule = _getCapsule(msg.sender); capsule.withdraw(amount, recipient); emit ClaimSucceeded(token, recipient, amount); diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index e9578cfb..e0220b0c 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -8,8 +8,8 @@ import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; -abstract contract NativeRestakingController is - PausableUpgradeable, +abstract contract NativeRestakingController is + PausableUpgradeable, INativeRestakingController, BaseRestakingController { @@ -29,7 +29,6 @@ abstract contract NativeRestakingController is function createExoCapsule() public whenNotPaused returns (address) { require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); - ExoCapsule capsule = new ExoCapsule(address(this), msg.sender, beaconOracleAddress); ownerToCapsule[msg.sender] = capsule; @@ -39,14 +38,10 @@ abstract contract NativeRestakingController is } function depositBeaconChainValidator( - bytes32[] calldata validatorContainer, + bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata proof ) external whenNotPaused { - IExoCapsule capsule = ownerToCapsule[msg.sender]; - if (address(capsule) == address(0)) { - revert CapsuleNotExist(); - } - + IExoCapsule capsule = _getCapsule(msg.sender); capsule.verifyDepositProof(validatorContainer, proof); uint256 depositValue = uint256(validatorContainer.getEffectiveBalance()) * GWEI_TO_WEI; @@ -54,11 +49,11 @@ abstract contract NativeRestakingController is registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DEPOSIT; bytes memory actionArgs = abi.encodePacked( - bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), - bytes32(bytes20(msg.sender)), + bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), + bytes32(bytes20(msg.sender)), depositValue ); - + _sendMsgToExocore(Action.REQUEST_DEPOSIT, actionArgs); } @@ -79,4 +74,4 @@ abstract contract NativeRestakingController is ) external whenNotPaused { } -} \ No newline at end of file +} From db30157dd0c79409a3f2767d890a8c053fb141d9 Mon Sep 17 00:00:00 2001 From: adu Date: Sun, 28 Apr 2024 15:28:31 +0800 Subject: [PATCH 28/93] add other uint tests for ExoCapsule.verifyDepositProof --- src/core/ExoCapsule.sol | 6 +- src/libraries/ValidatorContainer.sol | 12 +- src/libraries/WithdrawalContainer.sol | 10 +- test/foundry/ExoCapsule.t.sol | 156 +++++++++++++++++++++++++- 4 files changed, 169 insertions(+), 15 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 756b9f8f..bb57ac9c 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -73,9 +73,9 @@ contract ExoCapsule is revert InvalidValidatorContainer(validatorPubkey); } - // if (!_isActivatedAtEpoch(validatorContainer, proof.beaconBlockTimestamp)) { - // revert InactiveValidatorContainer(validatorPubkey); - // } + if (!_isActivatedAtEpoch(validatorContainer, proof.beaconBlockTimestamp)) { + revert InactiveValidatorContainer(validatorPubkey); + } if (withdrawalCredentials != bytes32(capsuleWithdrawalCredentials())) { revert WithdrawalCredentialsNotMatch(); diff --git a/src/libraries/ValidatorContainer.sol b/src/libraries/ValidatorContainer.sol index 0abc28cc..7cf0750d 100644 --- a/src/libraries/ValidatorContainer.sol +++ b/src/libraries/ValidatorContainer.sol @@ -1,5 +1,7 @@ pragma solidity ^0.8.19; +import "../libraries/Endian.sol"; + /** * class Validator(Container): pubkey: BLSPubkey @@ -13,6 +15,8 @@ pragma solidity ^0.8.19; withdrawable_epoch: Epoch # When validator can withdraw funds */ library ValidatorContainer { + using Endian for bytes32; + uint256 internal constant VALID_LENGTH = 8; uint256 internal constant MERKLE_TREE_HEIGHT = 3; @@ -29,7 +33,7 @@ library ValidatorContainer { } function getEffectiveBalance(bytes32[] calldata validatorContainer) internal pure returns (uint64) { - return uint64(bytes8(validatorContainer[2])); + return validatorContainer[2].fromLittleEndianUint64(); } function getSlashed(bytes32[] calldata validatorContainer) internal pure returns (bool) { @@ -37,15 +41,15 @@ library ValidatorContainer { } function getActivationEpoch(bytes32[] calldata validatorContainer) internal pure returns (uint64) { - return uint64(bytes8(validatorContainer[5])); + return validatorContainer[5].fromLittleEndianUint64(); } function getExitEpoch(bytes32[] calldata validatorContainer) internal pure returns (uint64) { - return uint64(bytes8(validatorContainer[6])); + return validatorContainer[6].fromLittleEndianUint64(); } function getWithdrawableEpoch(bytes32[] calldata validatorContainer) internal pure returns (uint64) { - return uint64(bytes8(validatorContainer[7])); + return validatorContainer[7].fromLittleEndianUint64(); } function merklelizeValidatorContainer(bytes32[] calldata validatorContainer) internal pure returns (bytes32) { diff --git a/src/libraries/WithdrawalContainer.sol b/src/libraries/WithdrawalContainer.sol index 558ccef3..a5254671 100644 --- a/src/libraries/WithdrawalContainer.sol +++ b/src/libraries/WithdrawalContainer.sol @@ -1,5 +1,7 @@ pragma solidity ^0.8.19; +import "../libraries/Endian.sol"; + /** * class Withdrawal(Container): index: WithdrawalIndex @@ -8,6 +10,8 @@ pragma solidity ^0.8.19; amount: Gwei */ library WithdrawalContainer { + using Endian for bytes32; + uint256 internal constant VALID_LENGTH = 4; uint256 internal constant MERKLE_TREE_HEIGHT = 2; @@ -16,11 +20,11 @@ library WithdrawalContainer { } function getWithdrawalIndex(bytes32[] calldata withdrawalContainer) internal pure returns (uint64) { - return uint64(bytes8(withdrawalContainer[0])); + return withdrawalContainer[0].fromLittleEndianUint64(); } function getValidatorIndex(bytes32[] calldata withdrawalContainer) internal pure returns (uint64) { - return uint64(bytes8(withdrawalContainer[1])); + return withdrawalContainer[1].fromLittleEndianUint64(); } function getExecutionAddress(bytes32[] calldata withdrawalContainer) internal pure returns (address) { @@ -28,7 +32,7 @@ library WithdrawalContainer { } function getAmount(bytes32[] calldata withdrawalContainer) internal pure returns (uint64) { - return uint64(bytes8(withdrawalContainer[3])); + return withdrawalContainer[3].fromLittleEndianUint64(); } function merklelizeWithdrawalContainer(bytes32[] calldata withdrawalContainer) internal pure returns (bytes32) { diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 9721fcc8..5332849e 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -10,9 +10,11 @@ import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; import "src/interfaces/IExoCapsule.sol"; import "src/core/ExoCapsule.sol"; import "src/libraries/BeaconChainProofs.sol"; +import "src/libraries/Endian.sol"; contract SetUp is Test { using stdStorage for StdStorage; + using Endian for bytes32; bytes32[] validatorContainer; /** @@ -32,17 +34,22 @@ contract SetUp is Test { address capsuleOwner; uint256 constant BEACON_CHAIN_GENESIS_TIME = 1606824023; - uint256 constant mockProofSlotsAfterGenesis = 8000; - uint256 constant mockCurrentBlockSlotsAfterGenesis = 7500; + /// @notice The number of slots each epoch in the beacon chain + uint64 internal constant SLOTS_PER_EPOCH = 32; + /// @notice The number of seconds in a slot in the beacon chain + uint64 internal constant SECONDS_PER_SLOT = 12; + /// @notice Number of seconds per epoch: 384 == 32 slots/epoch * 12 seconds/slot + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; + + uint256 mockProofTimestamp; + uint256 mockCurrentBlockTimestamp; function setUp() public { string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); validatorContainer = stdJson.readBytes32Array(validatorInfo, ".ValidatorFields"); require(validatorContainer.length > 0, "validator container should not be empty"); - - vm.warp(BEACON_CHAIN_GENESIS_TIME + mockCurrentBlockSlotsAfterGenesis * 12); - validatorProof.beaconBlockTimestamp = BEACON_CHAIN_GENESIS_TIME + mockProofSlotsAfterGenesis * 12; validatorProof.stateRoot = stdJson.readBytes32(validatorInfo, ".beaconStateRoot"); require(validatorProof.stateRoot != bytes32(0), "state root should not be empty"); @@ -81,14 +88,48 @@ contract SetUp is Test { 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(); + } } contract VerifyDepositProof is SetUp { using BeaconChainProofs for bytes32; function test_verifyDepositProof() public { + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + mockProofTimestamp = activationTimestamp; + mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(beaconBlockRoot) + ); + + capsule.verifyDepositProof(validatorContainer, validatorProof); + } + + function test_verifyDepositProof_revert_validatorAlreadyDeposited() public { + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + mockProofTimestamp = activationTimestamp; + mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + vm.mockCall( address(beaconOracle), abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), @@ -96,5 +137,110 @@ contract VerifyDepositProof is SetUp { ); capsule.verifyDepositProof(validatorContainer, validatorProof); + + // deposit again should revert + vm.expectRevert(abi.encodeWithSelector(ExoCapsule.DoubleDepositedValidator.selector, _getPubkey(validatorContainer))); + capsule.verifyDepositProof(validatorContainer, validatorProof); + } + + function test_verifyDepositProof_revert_staleProof() public { + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + mockProofTimestamp = activationTimestamp + 1 hours; + mockCurrentBlockTimestamp = mockProofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS + 1 seconds; + vm.warp(mockCurrentBlockTimestamp); + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(beaconBlockRoot) + ); + + // deposit should revert because of proof is stale + vm.expectRevert(abi.encodeWithSelector(ExoCapsule.StaleValidatorContainer.selector, _getPubkey(validatorContainer), mockProofTimestamp)); + capsule.verifyDepositProof(validatorContainer, validatorProof); + } + + function test_verifyDepositProof_revert_malformedValidatorContainer() public { + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + mockProofTimestamp = activationTimestamp; + mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(beaconBlockRoot) + ); + + uint256 snapshot = vm.snapshot(); + + // construct malformed validator container that has extra fields + validatorContainer.push(bytes32(uint256(123))); + vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer))); + capsule.verifyDepositProof(validatorContainer, validatorProof); + + vm.revertTo(snapshot); + // construct malformed validator container that misses fields + validatorContainer.pop(); + vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer))); + capsule.verifyDepositProof(validatorContainer, validatorProof); + } + + function test_verifyDepositProof_revert_inactiveValidatorContainer() public { + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(beaconBlockRoot) + ); + + // set proof timestamp before activation epoch + mockProofTimestamp = activationTimestamp - 1 seconds; + mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InactiveValidatorContainer.selector, _getPubkey(validatorContainer))); + capsule.verifyDepositProof(validatorContainer, validatorProof); + } + + function test_verifyDepositProof_revert_mismatchWithdrawalCredentials() public { + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + mockProofTimestamp = activationTimestamp; + mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(beaconBlockRoot) + ); + + // validator container withdrawal credentials are pointed to another capsule + ExoCapsule anotherCapsule = new ExoCapsule(address(this), capsuleOwner, address(beaconOracle)); + vm.expectRevert(abi.encodeWithSelector(ExoCapsule.WithdrawalCredentialsNotMatch.selector)); + anotherCapsule.verifyDepositProof(validatorContainer, validatorProof); + } + + function test_verifyDepositProof_revert_proofNotMatchWithBeaconRoot() public { + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + mockProofTimestamp = activationTimestamp; + mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + bytes32 mismatchBeaconBlockRoot = bytes32(uint256(123)); + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(mismatchBeaconBlockRoot) + ); + + // verify proof against mismatch beacon block root + vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer))); + capsule.verifyDepositProof(validatorContainer, validatorProof); } } \ No newline at end of file From e1ca5bdb34ef542778cd4d83687e3bc99b55faed Mon Sep 17 00:00:00 2001 From: adu Date: Tue, 30 Apr 2024 21:00:06 +0800 Subject: [PATCH 29/93] adapt to use beacon proxies and create2 for vaults and capsules creation --- error.txt | 1 + script/2_DeployBoth.s.sol | 52 ++++++---- script/3_Setup.s.sol | 4 +- script/BaseScript.sol | 8 ++ src/core/BaseRestakingController.sol | 11 +- src/core/ClientChainGateway.sol | 87 ++++++++++++---- src/core/ClientChainLzReceiver.sol | 30 +++++- src/core/ExoCapsule.sol | 20 ++-- src/core/ExocoreGateway.sol | 20 ++-- src/core/LSTRestakingController.sol | 12 +-- src/core/NativeRestakingController.sol | 18 +++- src/core/TSSReceiver.sol | 5 + src/core/Vault.sol | 13 ++- src/interfaces/IClientChainGateway.sol | 1 - src/storage/ClientChainGatewayStorage.sol | 89 ++++++++-------- src/storage/ExoCapsuleStorage.sol | 6 +- src/storage/GatewayStorage.sol | 2 +- src/storage/VaultStorage.sol | 2 +- test/foundry/ClientChainGateway.t.sol | 41 +++++--- test/foundry/DepositWithdrawPrinciple.t.sol | 25 ++++- test/foundry/ExoCapsule.t.sol | 16 ++- test/foundry/ExocoreDeployer.t.sol | 107 +++++++++++++++----- test/mocks/ETHPOSDepositMock.sol | 27 +++++ 23 files changed, 414 insertions(+), 183 deletions(-) create mode 100644 error.txt create mode 100644 test/mocks/ETHPOSDepositMock.sol diff --git a/error.txt b/error.txt new file mode 100644 index 00000000..65140093 --- /dev/null +++ b/error.txt @@ -0,0 +1 @@ +2024-04-30T12:14:44.163331Z ERROR foundry_compilers::resolver: failed to resolve versions diff --git a/script/2_DeployBoth.s.sol b/script/2_DeployBoth.s.sol index 27716653..c6fa7f99 100644 --- a/script/2_DeployBoth.s.sol +++ b/script/2_DeployBoth.s.sol @@ -1,13 +1,16 @@ pragma solidity ^0.8.19; -import "forge-std/Script.sol"; -import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; -import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {ERC20PresetFixedSupply} from "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "../src/core/ClientChainGateway.sol"; import {Vault} from "../src/core/Vault.sol"; import "../src/core/ExocoreGateway.sol"; import "../test/mocks/ExocoreGatewayMock.sol"; +import "../src/core/ExoCapsule.sol"; + +import "forge-std/Script.sol"; +import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; +import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {UpgradeableBeacon} from "@openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import {ERC20PresetFixedSupply} from "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; import {BaseScript} from "./BaseScript.sol"; @@ -56,12 +59,30 @@ contract DeployScript is BaseScript { } function run() public { - // deploy gateway and vault on client chain via rpc + // deploy clientchaingateway on client chain via rpc vm.selectFork(clientChain); vm.startBroadcast(deployer.privateKey); - ProxyAdmin clientChainProxyAdmin = new ProxyAdmin(); + + /// deploy vault implementation contract and capsule implementation contract + /// that has logics called by proxy + vaultImplementation = new Vault(); + capsuleImplementation = new ExoCapsule(); + + /// deploy the vault beacon and capsule beacon that store the implementation contract address + vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); + capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); + + /// deploy client chain gateway whitelistTokens.push(address(restakeToken)); - ClientChainGateway clientGatewayLogic = new ClientChainGateway(address(clientChainLzEndpoint)); + + ProxyAdmin clientChainProxyAdmin = new ProxyAdmin(); + ClientChainGateway clientGatewayLogic = new ClientChainGateway( + address(clientChainLzEndpoint), + exocoreChainId, + address(beaconOracle), + address(vaultBeacon), + address(capsuleBeacon) + ); clientGateway = ClientChainGateway( payable( address( @@ -70,27 +91,14 @@ contract DeployScript is BaseScript { address(clientChainProxyAdmin), abi.encodeWithSelector( clientGatewayLogic.initialize.selector, - exocoreChainId, payable(exocoreValidatorSet.addr), - address(beaconOracle), whitelistTokens ) ) ) ) ); - Vault vaultLogic = new Vault(); - vault = Vault( - address( - new TransparentUpgradeableProxy( - address(vaultLogic), - address(clientChainProxyAdmin), - abi.encodeWithSelector( - vaultLogic.initialize.selector, address(restakeToken), address(clientGateway) - ) - ) - ) - ); + vm.stopBroadcast(); // deploy on Exocore via rpc @@ -143,6 +151,8 @@ contract DeployScript is BaseScript { vm.serializeAddress(clientChainContracts, "clientChainGateway", address(clientGateway)); vm.serializeAddress(clientChainContracts, "resVault", address(vault)); vm.serializeAddress(clientChainContracts, "erc20Token", address(restakeToken)); + vm.serializeAddress(clientChainContracts, "vaultBeacon", address(vaultBeacon)); + vm.serializeAddress(clientChainContracts, "capsuleBeacon", address(capsuleBeacon)); string memory clientChainContractsOutput = vm.serializeAddress(clientChainContracts, "proxyAdmin", address(clientChainProxyAdmin)); diff --git a/script/3_Setup.s.sol b/script/3_Setup.s.sol index 4a08154b..08637535 100644 --- a/script/3_Setup.s.sol +++ b/script/3_Setup.s.sol @@ -66,9 +66,7 @@ contract SetupScript is BaseScript { address(exocoreGateway), address(exocoreLzEndpoint) ); } - // add token vaults to gateway - vaults.push(address(vault)); - clientGateway.addTokenVaults(vaults); + // as LzReceivers, gateway should set bytes(sourceChainGatewayAddress+thisAddress) as trusted remote to receive messages clientGateway.setPeer(exocoreChainId, address(exocoreGateway).toBytes32()); vm.stopBroadcast(); diff --git a/script/BaseScript.sol b/script/BaseScript.sol index b8dfdb7a..51d3fcb3 100644 --- a/script/BaseScript.sol +++ b/script/BaseScript.sol @@ -3,9 +3,13 @@ pragma solidity ^0.8.19; import "../src/interfaces/IClientChainGateway.sol"; import "../src/interfaces/IVault.sol"; import "../src/interfaces/IExocoreGateway.sol"; +import "../src/interfaces/IVault.sol"; +import "../src/interfaces/IExoCapsule.sol"; + import {ERC20PresetFixedSupply} from "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; +import {IBeacon} from "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; import "forge-std/Script.sol"; contract BaseScript is Script { @@ -33,6 +37,10 @@ contract BaseScript is Script { ILayerZeroEndpointV2 exocoreLzEndpoint; IBeaconChainOracle beaconOracle; ERC20PresetFixedSupply restakeToken; + IVault vaultImplementation; + IExoCapsule capsuleImplementation; + IBeacon vaultBeacon; + IBeacon capsuleBeacon; address delegationMock; address depositMock; diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index 10a3a89f..3b0b0213 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -18,6 +18,9 @@ abstract contract BaseRestakingController is { using OptionsBuilder for bytes; + event ClaimSucceeded(address token, address recipient, uint256 amount); + event MessageSent(Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); + receive() external payable {} modifier isTokenWhitelisted(address token) { @@ -65,8 +68,8 @@ abstract contract BaseRestakingController is isValidBech32Address(operator) whenNotPaused { _getVault(token); - registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); - registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DELEGATE_TO; + _registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); + _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DELEGATE_TO; bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), bytes(operator), amount); @@ -80,8 +83,8 @@ abstract contract BaseRestakingController is isValidBech32Address(operator) whenNotPaused { _getVault(token); - registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); - registeredRequestActions[outboundNonce + 1] = Action.REQUEST_UNDELEGATE_FROM; + _registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); + _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_UNDELEGATE_FROM; bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), bytes(operator), amount); diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index 648a6613..5cbd94a0 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -10,12 +10,15 @@ import {NativeRestakingController} from "./NativeRestakingController.sol"; import {ClientChainLzReceiver} from "./ClientChainLzReceiver.sol"; import {IClientChainGateway} from "../interfaces/IClientChainGateway.sol"; import {TSSReceiver} from "./TSSReceiver.sol"; +import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; +import {Vault} from "./Vault.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; import {IOAppCore} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppCore.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; contract ClientChainGateway is Initializable, @@ -29,40 +32,59 @@ contract ClientChainGateway is { using OptionsBuilder for bytes; - constructor(address _endpoint) OAppCoreUpgradeable(_endpoint) { + event WhitelistTokenAdded(address _token); + event WhitelistTokenRemoved(address _token); + event VaultCreated(address _underlyingToken, address _vault); + + /** + * @notice This constructor initializes only immutable state variables + * @param endpoint_ is the layerzero endpoint address deployed on this chain + * @param exocoreChainId_ is the id of layerzero endpoint on Exocore chain + * @param beaconOracleAddress_ is the Ethereum beacon chain oracle that is used for fetching beacon block root + * @param exoCapsuleBeacon_ is the UpgradeableBeacon contract address for ExoCapsule beacon proxy + * @param vaultBeacon_ is the UpgradeableBeacon contract address for Vault beacon proxy + */ + constructor( + address endpoint_, + uint32 exocoreChainId_, + address beaconOracleAddress_, + address vaultBeacon_, + address exoCapsuleBeacon_ + ) + OAppCoreUpgradeable(endpoint_) + ClientChainGatewayStorage(exocoreChainId_, beaconOracleAddress_, vaultBeacon_, exoCapsuleBeacon_) + { _disableInitializers(); } // initialization happens from another contract so it must be external. // reinitializer(2) is used so that the ownable and oappcore functions can be called again. function initialize( - uint32 _exocoreChainId, - address payable _exocoreValidatorSetAddress, - address _beaconOracleAddress, - address[] calldata _whitelistTokens + address payable exocoreValidatorSetAddress_, + address[] calldata whitelistTokens_ ) external reinitializer(2) { clearBootstrapData(); - require(_exocoreChainId != 0, "ClientChainGateway: exocore chain id should not be empty"); - require(_exocoreValidatorSetAddress != address(0), "ClientChainGateway: exocore validator set address should not be empty"); - require(_beaconOracleAddress != address(0), "ClientChainGateway: beacon chain oracle address should not be empty"); + require(exocoreValidatorSetAddress_ != address(0), "ClientChainGateway: exocore validator set address should not be empty"); + + exocoreValidatorSetAddress = exocoreValidatorSetAddress_; - exocoreValidatorSetAddress = _exocoreValidatorSetAddress; - beaconOracleAddress = _beaconOracleAddress; - exocoreChainId = _exocoreChainId; + for (uint256 i = 0; i < whitelistTokens_.length; i++) { + address underlyingToken = whitelistTokens_[i]; + whitelistTokens[underlyingToken] = true; + emit WhitelistTokenAdded(underlyingToken); - for (uint256 i = 0; i < _whitelistTokens.length; i++) { - whitelistTokens[_whitelistTokens[i]] = true; + _deployVault(underlyingToken); } - whiteListFunctionSelectors[Action.UPDATE_USERS_BALANCES] = this.updateUsersBalances.selector; + _whiteListFunctionSelectors[Action.UPDATE_USERS_BALANCES] = this.updateUsersBalances.selector; - registeredResponseHooks[Action.REQUEST_DEPOSIT] = this.afterReceiveDepositResponse.selector; - registeredResponseHooks[Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE] = + _registeredResponseHooks[Action.REQUEST_DEPOSIT] = this.afterReceiveDepositResponse.selector; + _registeredResponseHooks[Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE] = this.afterReceiveWithdrawPrincipleResponse.selector; - registeredResponseHooks[Action.REQUEST_DELEGATE_TO] = this.afterReceiveDelegateResponse.selector; - registeredResponseHooks[Action.REQUEST_UNDELEGATE_FROM] = this.afterReceiveUndelegateResponse.selector; - registeredResponseHooks[Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE] = + _registeredResponseHooks[Action.REQUEST_DELEGATE_TO] = this.afterReceiveDelegateResponse.selector; + _registeredResponseHooks[Action.REQUEST_UNDELEGATE_FROM] = this.afterReceiveUndelegateResponse.selector; + _registeredResponseHooks[Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE] = this.afterReceiveWithdrawRewardResponse.selector; bootstrapped = true; @@ -138,10 +160,14 @@ contract ClientChainGateway is } function addWhitelistToken(address _token) external onlyOwner whenNotPaused { - require(!whitelistTokens[_token], "ClientChainGateway: token should be not whitelisted before"); + require(!whitelistTokens[_token], "ClientChainGateway: token should not be whitelisted before"); whitelistTokens[_token] = true; - emit WhitelistTokenAdded(_token); + + // deploy the corresponding vault if not deployed before + if (address(tokenVaults[_token]) == address(0)) { + _deployVault(_token); + } } function removeWhitelistToken(address _token) external onlyOwner whenNotPaused { @@ -151,6 +177,7 @@ contract ClientChainGateway is emit WhitelistTokenRemoved(_token); } +<<<<<<< HEAD function addTokenVaults(address[] calldata vaults) external onlyOwner whenNotPaused { for (uint256 i = 0; i < vaults.length; i++) { address underlyingToken = IVault(vaults[i]).getUnderlyingToken(); @@ -166,6 +193,8 @@ contract ClientChainGateway is } } +======= +>>>>>>> ee404c5 (adapt to use beacon proxies and create2 for vaults and capsules creation) function quote(bytes memory _message) public view returns (uint256 nativeFee) { bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption( DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE @@ -189,6 +218,7 @@ contract ClientChainGateway is return (SENDER_VERSION, RECEIVER_VERSION); } +<<<<<<< HEAD function afterReceiveDepositResponse(bytes memory requestPayload, bytes calldata responsePayload) public onlyCalledFromThis @@ -275,5 +305,20 @@ contract ClientChainGateway is bool success = (uint8(bytes1(responsePayload[0])) == 1); emit UndelegateResult(success, undelegator, operator, token, amount); +======= + function _deployVault(address underlyingToken) internal returns (IVault) { + Vault vault = Vault( + Create2.deploy( + 0, + bytes32(uint256(uint160(underlyingToken))), + // set the beacon address for beacon proxy + abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(vaultBeacon), "")) + ) + ); + vault.initialize(underlyingToken, address(this)); + emit VaultCreated(underlyingToken, address(vault)); + + tokenVaults[underlyingToken] = vault; +>>>>>>> ee404c5 (adapt to use beacon proxies and create2 for vaults and capsules creation) } } diff --git a/src/core/ClientChainLzReceiver.sol b/src/core/ClientChainLzReceiver.sol index cbdfc88d..82225e61 100644 --- a/src/core/ClientChainLzReceiver.sol +++ b/src/core/ClientChainLzReceiver.sol @@ -8,6 +8,26 @@ import {OAppReceiverUpgradeable, Origin} from "../lzApp/OAppReceiverUpgradeable. import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; abstract contract ClientChainLzReceiver is PausableUpgradeable, OAppReceiverUpgradeable, ClientChainGatewayStorage { + event DepositResult(bool indexed success, address indexed token, address indexed depositor, uint256 amount); + event WithdrawPrincipleResult( + bool indexed success, address indexed token, address indexed withdrawer, uint256 amount + ); + event WithdrawRewardResult(bool indexed success, address indexed token, address indexed withdrawer, uint256 amount); + event DelegateResult( + bool indexed success, address indexed delegator, string delegatee, address token, uint256 amount + ); + event UndelegateResult( + bool indexed success, address indexed undelegator, string indexed undelegatee, address token, uint256 amount + ); + + error UnsupportedRequest(Action act); + error UnsupportedResponse(Action act); + error RequestOrResponseExecuteFailed(Action act, uint64 nonce, bytes reason); + error UnexpectedResponse(uint64 nonce); + error UnexpectedInboundNonce(uint64 expectedNonce, uint64 actualNonce); + error UnexpectedSourceChain(uint32 unexpectedSrcEndpointId); + error DepositShouldNotFailOnExocore(address token, address depositor); + modifier onlyCalledFromThis() { require(msg.sender == address(this), "ClientChainLzReceiver: could only be called from this contract itself with low level call"); _; @@ -24,13 +44,13 @@ abstract contract ClientChainLzReceiver is PausableUpgradeable, OAppReceiverUpgr if (act == Action.RESPOND) { uint64 requestId = uint64(bytes8(payload[1:9])); - Action requestAct = registeredRequestActions[requestId]; - bytes4 hookSelector = registeredResponseHooks[requestAct]; + Action requestAct = _registeredRequestActions[requestId]; + bytes4 hookSelector = _registeredResponseHooks[requestAct]; if (hookSelector == bytes4(0)) { revert UnsupportedResponse(act); } - bytes memory requestPayload = registeredRequests[requestId]; + bytes memory requestPayload = _registeredRequests[requestId]; if (requestPayload.length == 0) { revert UnexpectedResponse(requestId); } @@ -41,9 +61,9 @@ abstract contract ClientChainLzReceiver is PausableUpgradeable, OAppReceiverUpgr revert RequestOrResponseExecuteFailed(act, _origin.nonce, reason); } - delete registeredRequests[requestId]; + delete _registeredRequests[requestId]; } else { - bytes4 selector_ = whiteListFunctionSelectors[act]; + bytes4 selector_ = _whiteListFunctionSelectors[act]; if (selector_ == bytes4(0)) { revert UnsupportedRequest(act); } diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index bb57ac9c..8faae6a8 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -9,8 +9,10 @@ import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; +import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; contract ExoCapsule is + Initializable, ExoCapsuleStorage, IExoCapsule { @@ -43,14 +45,18 @@ contract ExoCapsule is _; } - constructor(address _gateway, address _capsuleOwner, address _beaconOracle) { - require(_capsuleOwner != address(0), "ExoCapsule: capsule owner address can not be empty"); - require(_gateway != address(0), "ExoCapsule: gateway address can not be empty"); - require(_beaconOracle != address(0), "ExoCapsule: beacon chain oracle address should not be empty"); + constructor() { + _disableInitializers(); + } + + function initialize(address gateway_, address capsuleOwner_, address beaconOracle_) external initializer { + require(gateway_ != address(0), "ExoCapsuleStorage: gateway address can not be empty"); + require(capsuleOwner_ != address(0), "ExoCapsule: capsule owner address can not be empty"); + require(beaconOracle_ != address(0), "ExoCapsuleStorage: beacon chain oracle address should not be empty"); - capsuleOwner = _capsuleOwner; - gateway = INativeRestakingController(_gateway); - beaconOracle = IBeaconChainOracle(_beaconOracle); + gateway = INativeRestakingController(gateway_); + beaconOracle = IBeaconChainOracle(beaconOracle_); + capsuleOwner = capsuleOwner_; } function verifyDepositProof( diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index 4cdcb100..3d9f39e6 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -32,23 +32,23 @@ contract ExocoreGateway is _; } - constructor(address _endpoint) OAppUpgradeable(_endpoint) { + constructor(address endpoint_) OAppUpgradeable(endpoint_) { _disableInitializers(); } receive() external payable {} - function initialize(address payable _exocoreValidatorSetAddress) external initializer { - require(_exocoreValidatorSetAddress != address(0), "ExocoreGateway: invalid empty exocore validator set address"); + function initialize(address payable exocoreValidatorSetAddress_) external initializer { + require(exocoreValidatorSetAddress_ != address(0), "ExocoreGateway: invalid empty exocore validator set address"); - exocoreValidatorSetAddress = _exocoreValidatorSetAddress; + exocoreValidatorSetAddress = exocoreValidatorSetAddress_; - whiteListFunctionSelectors[Action.REQUEST_DEPOSIT] = this.requestDeposit.selector; - whiteListFunctionSelectors[Action.REQUEST_DELEGATE_TO] = this.requestDelegateTo.selector; - whiteListFunctionSelectors[Action.REQUEST_UNDELEGATE_FROM] = this.requestUndelegateFrom.selector; - whiteListFunctionSelectors[Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE] = + _whiteListFunctionSelectors[Action.REQUEST_DEPOSIT] = this.requestDeposit.selector; + _whiteListFunctionSelectors[Action.REQUEST_DELEGATE_TO] = this.requestDelegateTo.selector; + _whiteListFunctionSelectors[Action.REQUEST_UNDELEGATE_FROM] = this.requestUndelegateFrom.selector; + _whiteListFunctionSelectors[Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE] = this.requestWithdrawPrinciple.selector; - whiteListFunctionSelectors[Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE] = this.requestWithdrawReward.selector; + _whiteListFunctionSelectors[Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE] = this.requestWithdrawReward.selector; __Ownable_init_unchained(exocoreValidatorSetAddress); __OAppCore_init_unchained(exocoreValidatorSetAddress); @@ -92,7 +92,7 @@ contract ExocoreGateway is _consumeInboundNonce(_origin.srcEid, _origin.sender, _origin.nonce); Action act = Action(uint8(payload[0])); - bytes4 selector_ = whiteListFunctionSelectors[act]; + bytes4 selector_ = _whiteListFunctionSelectors[act]; if (selector_ == bytes4(0)) { revert UnsupportedRequest(act); } diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index 79221ea5..ebee292e 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -14,8 +14,8 @@ abstract contract LSTRestakingController is function deposit(address token, uint256 amount) external payable isTokenWhitelisted(token) isValidAmount(amount) whenNotPaused { IVault vault = _getVault(token); vault.deposit(msg.sender, amount); - registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, amount); - registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DEPOSIT; + _registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, amount); + _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DEPOSIT; bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), amount); _sendMsgToExocore(Action.REQUEST_DEPOSIT, actionArgs); @@ -23,8 +23,8 @@ abstract contract LSTRestakingController is function withdrawPrincipleFromExocore(address token, uint256 principleAmount) external payable isTokenWhitelisted(token) isValidAmount(principleAmount) whenNotPaused { _getVault(token); - registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, principleAmount); - registeredRequestActions[outboundNonce + 1] = Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE; + _registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, principleAmount); + _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE; bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), principleAmount); @@ -33,8 +33,8 @@ abstract contract LSTRestakingController is function withdrawRewardFromExocore(address token, uint256 rewardAmount) external payable isTokenWhitelisted(token) isValidAmount(rewardAmount) whenNotPaused { _getVault(token); - registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, rewardAmount); - registeredRequestActions[outboundNonce + 1] = Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE; + _registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, rewardAmount); + _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE; bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), rewardAmount); _sendMsgToExocore(Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, actionArgs); diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index e0220b0c..6e3716c5 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -7,6 +7,7 @@ import {BaseRestakingController} from "./BaseRestakingController.sol"; import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; abstract contract NativeRestakingController is PausableUpgradeable, @@ -15,6 +16,9 @@ abstract contract NativeRestakingController is { using ValidatorContainer for bytes32[]; + event CapsuleCreated(address owner, address capsule); + event StakedWithCapsule(address staker, address capsule); + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable whenNotPaused { require(msg.value == 32 ether, "NativeRestakingController: stake value must be exactly 32 ether"); @@ -29,7 +33,15 @@ abstract contract NativeRestakingController is function createExoCapsule() public whenNotPaused returns (address) { require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); - ExoCapsule capsule = new ExoCapsule(address(this), msg.sender, beaconOracleAddress); + ExoCapsule capsule = ExoCapsule( + Create2.deploy( + 0, + bytes32(uint256(uint160(msg.sender))), + // set the beacon address for beacon proxy + abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(exoCapsuleBeacon), "")) + ) + ); + capsule.initialize(address(this), msg.sender, beaconOracleAddress); ownerToCapsule[msg.sender] = capsule; emit CapsuleCreated(msg.sender, address(capsule)); @@ -45,8 +57,8 @@ abstract contract NativeRestakingController is capsule.verifyDepositProof(validatorContainer, proof); uint256 depositValue = uint256(validatorContainer.getEffectiveBalance()) * GWEI_TO_WEI; - registeredRequests[outboundNonce + 1] = abi.encode(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, depositValue); - registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DEPOSIT; + _registeredRequests[outboundNonce + 1] = abi.encode(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, depositValue); + _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DEPOSIT; bytes memory actionArgs = abi.encodePacked( bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), diff --git a/src/core/TSSReceiver.sol b/src/core/TSSReceiver.sol index 4bf67eae..92db75e3 100644 --- a/src/core/TSSReceiver.sol +++ b/src/core/TSSReceiver.sol @@ -9,6 +9,11 @@ import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/Pau abstract contract TSSReceiver is PausableUpgradeable, ClientChainGatewayStorage, ITSSReceiver { using ECDSA for bytes32; + event MessageProcessed(uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload); + event MessageFailed(uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload, bytes _reason); + + error UnauthorizedSigner(); + function receiveInterchainMsg(InterchainMsg calldata _msg, bytes calldata signature) external whenNotPaused { require(_msg.nonce == ++lastMessageNonce, "TSSReceiver: message nonce is not expected"); require(_msg.srcChainID == exocoreChainId, "TSSReceiver: source chain id is incorrect"); diff --git a/src/core/Vault.sol b/src/core/Vault.sol index 970aec6b..067ede9f 100644 --- a/src/core/Vault.sol +++ b/src/core/Vault.sol @@ -20,6 +20,14 @@ contract Vault is Initializable, VaultStorage, IVault { _disableInitializers(); } + function initialize(address underlyingToken_, address gateway_) external initializer { + require(underlyingToken_ != address(0), "Vault: underlying token can not be empty"); + require(gateway_!= address(0), "VaultStorage: the gateway address should not be empty"); + + underlyingToken = IERC20(underlyingToken_); + gateway = ILSTRestakingController(gateway_); + } + function getUnderlyingToken() public view returns (address) { return address(underlyingToken); } @@ -28,11 +36,6 @@ contract Vault is Initializable, VaultStorage, IVault { return withdrawableBalances[withdrawer]; } - function initialize(address _underlyingToken, address _gateway) external initializer { - underlyingToken = IERC20(_underlyingToken); - gateway = ILSTRestakingController(_gateway); - } - function withdraw(address withdrawer, address recipient, uint256 amount) external onlyGateway { require( amount <= withdrawableBalances[withdrawer], diff --git a/src/interfaces/IClientChainGateway.sol b/src/interfaces/IClientChainGateway.sol index 73abab7c..d49caa1d 100644 --- a/src/interfaces/IClientChainGateway.sol +++ b/src/interfaces/IClientChainGateway.sol @@ -9,6 +9,5 @@ import {ITSSReceiver} from "./ITSSReceiver.sol"; interface IClientChainGateway is IOAppReceiver, IOAppCore, ILSTRestakingController, INativeRestakingController, ITSSReceiver { function addWhitelistToken(address _token) external; function removeWhitelistToken(address _token) external; - function addTokenVaults(address[] calldata vaults) external; function quote(bytes memory _message) external view returns (uint256 nativeFee); } diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 6ebad096..9f0a3592 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -6,59 +6,60 @@ import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; import {GatewayStorage} from "./GatewayStorage.sol"; -contract ClientChainGatewayStorage is BootstrapStorage { - mapping(uint64 => bytes) public registeredRequests; - mapping(uint64 => Action) public registeredRequestActions; - mapping(Action => bytes4) public registeredResponseHooks; +import {IBeacon} from "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; - uint64 outboundNonce; +contract ClientChainGatewayStorage is GatewayStorage { + uint256 public lastMessageNonce; + uint64 public outboundNonce; + mapping(uint32 eid => mapping(bytes32 sender => uint64 nonce)) public inboundNonce; + mapping(address => bool) public whitelistTokens; + mapping(address => IVault) public tokenVaults; + mapping(address => IExoCapsule) public ownerToCapsule; + mapping(uint64 => bytes) _registeredRequests; + mapping(uint64 => Action) _registeredRequestActions; + mapping(Action => bytes4) _registeredResponseHooks; + + // immutable state variables + uint32 public immutable exocoreChainId; + address public immutable beaconOracleAddress; + IBeacon public immutable exoCapsuleBeacon; + IBeacon public immutable vaultBeacon; + // constant state variables uint128 constant DESTINATION_GAS_LIMIT = 500000; uint128 constant DESTINATION_MSG_VALUE = 0; - - // native restaking state variables - IETHPOSDeposit constant ETH_POS = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); - address constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; uint256 constant GWEI_TO_WEI = 1e9; - address beaconOracleAddress; - mapping(address => IExoCapsule) public ownerToCapsule; - - event WhitelistTokenAdded(address _token); - event WhitelistTokenRemoved(address _token); - event VaultAdded(address _vault); - event MessageProcessed(uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload); - event MessageFailed(uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload, bytes _reason); - event MessageSent(Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); - event DepositResult(bool indexed success, address indexed token, address indexed depositor, uint256 amount); - event WithdrawPrincipleResult( - bool indexed success, address indexed token, address indexed withdrawer, uint256 amount - ); - event WithdrawRewardResult(bool indexed success, address indexed token, address indexed withdrawer, uint256 amount); - event DelegateResult( - bool indexed success, address indexed delegator, string delegatee, address token, uint256 amount - ); - event UndelegateResult( - bool indexed success, address indexed undelegator, string indexed undelegatee, address token, uint256 amount - ); - event ClaimSucceeded(address token, address recipient, uint256 amount); - - // native restaking events - event CapsuleCreated(address owner, address capsule); - event StakedWithCapsule(address staker, address capsule); + address constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + IETHPOSDeposit constant ETH_POS = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); + /** + * @notice Stored code of type(BeaconProxy).creationCode + * @dev Maintained as a constant to solve an edge case - changes to OpenZeppelin's BeaconProxy code should not cause + * addresses of EigenPods that are pre-computed with Create2 to change, even upon upgrading this contract, changing compiler version, etc. + */ + bytes constant BEACON_PROXY_BYTECODE = + hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; - error UnauthorizedSigner(); - error UnauthorizedToken(); - error UnsupportedRequest(Action act); - error UnsupportedResponse(Action act); - error UnexpectedResponse(uint64 nonce); - error UnexpectedInboundNonce(uint64 expectedNonce, uint64 actualNonce); - error UnexpectedSourceChain(uint32 unexpectedSrcEndpointId); - error DepositShouldNotFailOnExocore(address token, address depositor); + uint256[40] private __gap; - // native restaking errors error CapsuleNotExist(); + error VaultNotExist(); - uint256[40] private __gap; + constructor( + uint32 exocoreChainId_, + address beaconOracleAddress_, + address vaultBeacon_, + address exoCapsuleBeacon_ + ) { + require(exocoreChainId_ != 0, "ClientChainGatewayStorage: exocore chain id should not be empty"); + require(beaconOracleAddress_ != address(0), "ClientChainGatewayStorage: beacon chain oracle address should not be empty"); + require(vaultBeacon_ != address(0), "ClientChainGatewayStorage: the vaultBeacon address for beacon proxy should not be empty"); + require(exoCapsuleBeacon_ != address(0), "ClientChainGatewayStorage: the exoCapsuleBeacon address for beacon proxy should not be empty"); + + exocoreChainId = exocoreChainId_; + beaconOracleAddress = beaconOracleAddress_; + exoCapsuleBeacon = IBeacon(exoCapsuleBeacon_); + vaultBeacon = IBeacon(vaultBeacon_); + } function _getVault(address token) internal view returns (IVault) { IVault vault = tokenVaults[token]; diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index b2a97667..39334eb4 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -23,17 +23,17 @@ contract ExoCapsuleStorage { VALIDATOR_STATUS status; } + // constant state variables address public constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; uint256 public constant BEACON_CHAIN_GENESIS_TIME = 1606824023; uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; address public capsuleOwner; + uint256 public principleBalance; + uint256 public withdrawableBalance; INativeRestakingController public gateway; IBeaconChainOracle public beaconOracle; - uint256 public principleBalance; - uint256 public withdrawableBalance; - mapping(bytes32 pubkey => Validator validator) _capsuleValidators; mapping(uint256 index => bytes32 pubkey) _capsuleValidatorsByIndex; diff --git a/src/storage/GatewayStorage.sol b/src/storage/GatewayStorage.sol index 000700d5..cf1ade49 100644 --- a/src/storage/GatewayStorage.sol +++ b/src/storage/GatewayStorage.sol @@ -12,7 +12,7 @@ contract GatewayStorage { MARK_BOOTSTRAP } - mapping(Action => bytes4) public whiteListFunctionSelectors; + mapping(Action => bytes4) _whiteListFunctionSelectors; address payable public exocoreValidatorSetAddress; event MessageSent(Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); diff --git a/src/storage/VaultStorage.sol b/src/storage/VaultStorage.sol index 37ae4d18..3a90ffca 100644 --- a/src/storage/VaultStorage.sol +++ b/src/storage/VaultStorage.sol @@ -4,7 +4,6 @@ import {IERC20} from "@openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; contract VaultStorage { - IERC20 public underlyingToken; mapping(address => uint256) public principleBalances; mapping(address => uint256) public rewardBalances; mapping(address => uint256) public withdrawableBalances; @@ -12,6 +11,7 @@ contract VaultStorage { mapping(address => uint256) public totalDepositedPrincipleAmount; mapping(address => uint256) public totalUnlockPrincipleAmount; + IERC20 public underlyingToken; ILSTRestakingController public gateway; event PrincipleBalanceUpdated(address, uint256); diff --git a/test/foundry/ClientChainGateway.t.sol b/test/foundry/ClientChainGateway.t.sol index e5d5edb9..45c58be5 100644 --- a/test/foundry/ClientChainGateway.t.sol +++ b/test/foundry/ClientChainGateway.t.sol @@ -3,18 +3,24 @@ pragma solidity ^0.8.19; import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; +import "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; +import "@openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol"; import "forge-std/console.sol"; import "forge-std/Test.sol"; import "../../src/core/ClientChainGateway.sol"; import {Vault} from "../../src/core/Vault.sol"; +import "../../src/core/ExoCapsule.sol"; import "../../src/core/ExocoreGateway.sol"; import {EndpointV2Mock} from "../mocks/EndpointV2Mock.sol"; import "../../src/interfaces/precompiles/IDelegation.sol"; import "../../src/interfaces/precompiles/IDeposit.sol"; import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import "../../src/interfaces/ITSSReceiver.sol"; -import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; +import "../../src/interfaces/IVault.sol"; +import "../../src/interfaces/IExoCapsule.sol"; + contract ClientChainGatewayTest is Test { Player[] players; @@ -25,11 +31,14 @@ contract ClientChainGatewayTest is Test { ERC20PresetFixedSupply restakeToken; ClientChainGateway clientGateway; - Vault vault; ExocoreGateway exocoreGateway; EndpointV2Mock clientChainLzEndpoint; EndpointV2Mock exocoreLzEndpoint; IBeaconChainOracle beaconOracle; + IVault vaultImplementation; + IExoCapsule capsuleImplementation; + IBeacon vaultBeacon; + IBeacon capsuleBeacon; string operatorAddress = "exo1v4s6vtjpmxwu9rlhqms5urzrc3tc2ae2gnuqhc"; uint16 exocoreChainId = 2; @@ -55,37 +64,40 @@ contract ClientChainGatewayTest is Test { vm.chainId(clientChainId); _deploy(); - - vm.prank(exocoreValidatorSet.addr); - clientGateway.addTokenVaults(vaults); } function _deploy() internal { vm.startPrank(deployer.addr); beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); + + vaultImplementation = new Vault(); + capsuleImplementation = new ExoCapsule(); + + vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); + capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); restakeToken = new ERC20PresetFixedSupply("rest", "rest", 1e16, exocoreValidatorSet.addr); whitelistTokens.push(address(restakeToken)); clientChainLzEndpoint = new EndpointV2Mock(clientChainId); ProxyAdmin proxyAdmin = new ProxyAdmin(); - ClientChainGateway clientGatewayLogic = new ClientChainGateway(address(clientChainLzEndpoint)); + ClientChainGateway clientGatewayLogic = new ClientChainGateway( + address(clientChainLzEndpoint), + exocoreChainId, + address(beaconOracle), + address(vaultBeacon), + address(capsuleBeacon) + ); clientGateway = ClientChainGateway( payable(address(new TransparentUpgradeableProxy(address(clientGatewayLogic), address(proxyAdmin), ""))) ); - Vault vaultLogic = new Vault(); - vault = Vault(address(new TransparentUpgradeableProxy(address(vaultLogic), address(proxyAdmin), ""))); - clientGateway.initialize( - exocoreChainId, payable(exocoreValidatorSet.addr), - address(beaconOracle), whitelistTokens ); - vault.initialize(address(restakeToken), address(clientGateway)); - vaults.push(address(vault)); + vm.stopPrank(); } @@ -140,9 +152,6 @@ contract ClientChainGatewayTest is Test { vm.startPrank(exocoreValidatorSet.addr); clientGateway.pause(); - vm.expectRevert(EnforcedPause.selector); - clientGateway.addTokenVaults(vaults); - vm.expectRevert(EnforcedPause.selector); clientGateway.claim(address(restakeToken), uint256(1), deployer.addr); diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index fc0a0792..7deb3c10 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -22,18 +22,20 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { event MessageSent(GatewayStorage.Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); event MessageProcessed(uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload); event NewPacket(uint32, address, bytes32, uint64, bytes); + event CapsuleCreated(address owner, address capsule); + event StakedWithCapsule(address staker, address capsule); uint256 constant DEFAULT_ENDPOINT_CALL_GAS_LIMIT = 200000; - function test_DepositWithdrawByLayerZero() public { + function test_LSTDepositWithdrawByLayerZero() public { Player memory depositor = players[0]; vm.startPrank(exocoreValidatorSet.addr); restakeToken.transfer(depositor.addr, 1000000); vm.stopPrank(); - // Commented for testing 0 relay fee + // transfer some gas fee to depositor deal(depositor.addr, 1e22); - deal(address(clientGateway), 1e22); + // transfer some gas fee to exocore gateway as it has to pay for the relay fee to layerzero endpoint when sending back response deal(address(exocoreGateway), 1e22); uint256 depositAmount = 10000; @@ -189,6 +191,23 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { ); } + // function test_NativeDepositWithdraw() public { + // Player memory depositor = players[0]; + + // // transfer some gas fee to depositor + // deal(depositor.addr, 1e22); + // // transfer some gas fee to exocore gateway as it has to pay for the relay fee to layerzero endpoint when sending back response + // deal(address(exocoreGateway), 1e22); + + // // firstly depositor should stake to beacon chain by depositing 32 ETH to ETHPOS contract + // vm.expectEmit(true, true, true, true, address(clientGateway)); + // emit CapsuleCreated(depositor.addr, address(0x1)); + // emit StakedWithCapsule(depositor.addr, address(0x1)); + + // vm.startPrank(depositor.addr); + // clientGateway.stake() + // } + function test_TSSReceiver() public { Player memory depositor = players[0]; Player memory relayer = players[1]; diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 5332849e..e50808e5 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -68,7 +68,7 @@ contract SetUp is Test { capsuleOwner = address(0x125); - ExoCapsule phantomCapsule = new ExoCapsule(address(this), capsuleOwner, address(beaconOracle)); + ExoCapsule phantomCapsule = new ExoCapsule(); address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); vm.etch(capsuleAddress, address(phantomCapsule).code); @@ -107,6 +107,8 @@ contract SetUp is Test { contract VerifyDepositProof is SetUp { using BeaconChainProofs for bytes32; + using stdStorage for StdStorage; + function test_verifyDepositProof() public { uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; @@ -220,7 +222,17 @@ contract VerifyDepositProof is SetUp { ); // validator container withdrawal credentials are pointed to another capsule - ExoCapsule anotherCapsule = new ExoCapsule(address(this), capsuleOwner, address(beaconOracle)); + ExoCapsule anotherCapsule = new ExoCapsule(); + + bytes32 gatewaySlot = bytes32(stdstore.target(address(anotherCapsule)).sig("gateway()").find()); + vm.store(address(anotherCapsule), gatewaySlot, bytes32(uint256(uint160(address(this))))); + + bytes32 ownerSlot = bytes32(stdstore.target(address(anotherCapsule)).sig("capsuleOwner()").find()); + vm.store(address(anotherCapsule), ownerSlot, bytes32(uint256(uint160(capsuleOwner)))); + + bytes32 beaconOraclerSlot = bytes32(stdstore.target(address(anotherCapsule)).sig("beaconOracle()").find()); + vm.store(address(anotherCapsule), beaconOraclerSlot, bytes32(uint256(uint160(address(beaconOracle))))); + vm.expectRevert(abi.encodeWithSelector(ExoCapsule.WithdrawalCredentialsNotMatch.selector)); anotherCapsule.verifyDepositProof(validatorContainer, validatorProof); } diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index 7e088405..b911b89f 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -3,20 +3,26 @@ pragma solidity ^0.8.19; import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; +import "@openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; +import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; +import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; +import "forge-std/console.sol"; +import "forge-std/Test.sol"; + import "../../src/core/ClientChainGateway.sol"; import {Vault} from "../../src/core/Vault.sol"; +import "../../src/core/ExoCapsule.sol"; import "../../src/core/ExocoreGateway.sol"; import {NonShortCircuitEndpointV2Mock} from "../mocks/NonShortCircuitEndpointV2Mock.sol"; -import "forge-std/console.sol"; -import "forge-std/Test.sol"; import "../../src/interfaces/precompiles/IDelegation.sol"; import "../../src/interfaces/precompiles/IDeposit.sol"; import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import "../../src/interfaces/precompiles/IClaimReward.sol"; -import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; -import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; -import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; -import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; +import "test/mocks/ETHPOSDepositMock.sol"; +import "src/core/ExoCapsule.sol"; contract ExocoreDeployer is Test { using AddressCast for address; @@ -33,6 +39,16 @@ contract ExocoreDeployer is Test { ILayerZeroEndpointV2 clientChainLzEndpoint; ILayerZeroEndpointV2 exocoreLzEndpoint; IBeaconChainOracle beaconOracle; + IVault vaultImplementation; + IExoCapsule capsuleImplementation; + IBeacon vaultBeacon; + IBeacon capsuleBeacon; + + IETHPOSDeposit constant ETH_POS = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); + + bytes32[] validatorContainer; + bytes32 beaconBlockRoot; + IExoCapsule.ValidatorContainerProof validatorProof; uint32 exocoreChainId = 2; uint32 clientChainId = 1; @@ -48,22 +64,73 @@ contract ExocoreDeployer is Test { players.push(Player({privateKey: uint256(0x3), addr: vm.addr(uint256(0x3))})); exocoreValidatorSet = Player({privateKey: uint256(0xa), addr: vm.addr(uint256(0xa))}); + // load beacon chain validator container and proof from json file + _loadValidatorContainer(); + _loadValidatorProof(); + _loadBeaconBlockRoot; + vm.chainId(clientChainId); _deploy(); } + function _loadValidatorContainer() internal { + string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); + + validatorContainer = stdJson.readBytes32Array(validatorInfo, ".ValidatorFields"); + require(validatorContainer.length > 0, "validator container should not be empty"); + } + + function _loadValidatorProof() internal { + string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); + + 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"); + } + + function _loadBeaconBlockRoot() internal { + string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); + + beaconBlockRoot = stdJson.readBytes32(validatorInfo, ".latestBlockHeaderRoot"); + require(beaconBlockRoot != bytes32(0), "beacon block root should not be empty"); + } + function _deploy() internal { // prepare outside contracts like ERC20 token contract and layerzero endpoint contract - restakeToken = new ERC20PresetFixedSupply("rest", "rest", 1e16, exocoreValidatorSet.addr); + restakeToken = new ERC20PresetFixedSupply("rest", "rest", 1e34, exocoreValidatorSet.addr); clientChainLzEndpoint = new NonShortCircuitEndpointV2Mock(clientChainId, exocoreValidatorSet.addr); exocoreLzEndpoint = new NonShortCircuitEndpointV2Mock(exocoreChainId, exocoreValidatorSet.addr); beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); - // deploy and initialize client chain contracts - ProxyAdmin proxyAdmin = new ProxyAdmin(); + // deploy vault implementation contract and capsule implementation contract + // that has logics called by proxy + vaultImplementation = new Vault(); + capsuleImplementation = new ExoCapsule(); + + // deploy the vault beacon and capsule beacon that store the implementation contract address + vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); + capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); + // attach ETHPOSDepositMock contract code to constant address + ETHPOSDepositMock ethPOSDepositMock = new ETHPOSDepositMock(); + vm.etch(address(ETH_POS), address(ethPOSDepositMock).code); + + // deploy and initialize client chain contracts whitelistTokens.push(address(restakeToken)); - ClientChainGateway clientGatewayLogic = new ClientChainGateway(address(clientChainLzEndpoint)); + + ProxyAdmin proxyAdmin = new ProxyAdmin(); + ClientChainGateway clientGatewayLogic = new ClientChainGateway( + address(clientChainLzEndpoint), + exocoreChainId, + address(beaconOracle), + address(vaultBeacon), + address(capsuleBeacon) + ); clientGateway = ClientChainGateway( payable( address( @@ -72,9 +139,7 @@ contract ExocoreDeployer is Test { address(proxyAdmin), abi.encodeWithSelector( clientGatewayLogic.initialize.selector, - exocoreChainId, payable(exocoreValidatorSet.addr), - address(beaconOracle), whitelistTokens ) ) @@ -82,18 +147,8 @@ contract ExocoreDeployer is Test { ) ); - Vault vaultLogic = new Vault(); - vault = Vault( - address( - new TransparentUpgradeableProxy( - address(vaultLogic), - address(proxyAdmin), - abi.encodeWithSelector( - vaultLogic.initialize.selector, address(restakeToken), address(clientGateway) - ) - ) - ) - ); + // find vault according to uderlying token address + vault = Vault(address(clientGateway.tokenVaults(address(restakeToken)))); // deploy Exocore network contracts ExocoreGateway exocoreGatewayLogic = new ExocoreGateway(address(exocoreLzEndpoint)); @@ -121,9 +176,7 @@ contract ExocoreDeployer is Test { // Exocore validator set should be the owner of gateway contracts and only owner could call these functions. vm.startPrank(exocoreValidatorSet.addr); - // add token vaults to gateway - vaults.push(address(vault)); - clientGateway.addTokenVaults(vaults); + // as LzReceivers, gateway should set bytes(sourceChainGatewayAddress+thisAddress) as trusted remote to receive messages clientGateway.setPeer(exocoreChainId, address(exocoreGateway).toBytes32()); exocoreGateway.setPeer(clientChainId, address(clientGateway).toBytes32()); diff --git a/test/mocks/ETHPOSDepositMock.sol b/test/mocks/ETHPOSDepositMock.sol new file mode 100644 index 00000000..8dee88c1 --- /dev/null +++ b/test/mocks/ETHPOSDepositMock.sol @@ -0,0 +1,27 @@ +pragma solidity ^0.8.19; + +import "src/interfaces/IETHPOSDeposit.sol"; + +contract ETHPOSDepositMock is IETHPOSDeposit { + + function deposit( + bytes calldata pubkey, + bytes calldata withdrawal_credentials, + bytes calldata signature, + bytes32 deposit_data_root + ) external payable {} + + + function get_deposit_root() external pure returns (bytes32) { + bytes32 root; + return root; + } + + /// @notice Query the current deposit count. + /// @return The deposit count encoded as a little endian 64-bit number. + function get_deposit_count() external pure returns (bytes memory) { + bytes memory root; + return root; + } +} + From 52b894d78ed883a6c71197be790633279dc93641 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 6 May 2024 15:08:47 +0800 Subject: [PATCH 30/93] add nativedepositwithdraw integration test --- src/core/NativeRestakingController.sol | 6 +- src/interfaces/INativeRestakingController.sol | 6 +- src/storage/ExoCapsuleStorage.sol | 2 +- test/foundry/DepositWithdrawPrinciple.t.sol | 159 ++++++++++++++++-- test/foundry/ExoCapsule.t.sol | 11 +- test/foundry/ExocoreDeployer.t.sol | 49 +++++- 6 files changed, 203 insertions(+), 30 deletions(-) diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 6e3716c5..4c64e69f 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -52,7 +52,7 @@ abstract contract NativeRestakingController is function depositBeaconChainValidator( bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata proof - ) external whenNotPaused { + ) external payable whenNotPaused { IExoCapsule capsule = _getCapsule(msg.sender); capsule.verifyDepositProof(validatorContainer, proof); @@ -74,7 +74,7 @@ abstract contract NativeRestakingController is IExoCapsule.ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, IExoCapsule.WithdrawalContainerProof calldata withdrawalProof - ) external whenNotPaused { + ) external payable whenNotPaused { } @@ -83,7 +83,7 @@ abstract contract NativeRestakingController is IExoCapsule.ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, IExoCapsule.WithdrawalContainerProof calldata withdrawalProof - ) external whenNotPaused { + ) external payable whenNotPaused { } } diff --git a/src/interfaces/INativeRestakingController.sol b/src/interfaces/INativeRestakingController.sol index ea385ef3..709d585a 100644 --- a/src/interfaces/INativeRestakingController.sol +++ b/src/interfaces/INativeRestakingController.sol @@ -28,7 +28,7 @@ interface INativeRestakingController is IBaseRestakingController { * to the ExoCapsule owned by staker. The effective balance of `validatorContainer` would be credited as deposited value by Exocore network. * @ param */ - function depositBeaconChainValidator(bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata proof) external; + function depositBeaconChainValidator(bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata proof) payable external; /** * @notice When a beacon chain partial withdrawal to an ExoCapsule contract happens(the withdrawal time is less than validator's withdrawable_epoch), @@ -47,7 +47,7 @@ interface INativeRestakingController is IBaseRestakingController { IExoCapsule.ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, IExoCapsule.WithdrawalContainerProof calldata withdrawalProof - ) external; + ) payable external; /** * @notice When a beacon chain full withdrawal to this capsule contract happens(the withdrawal time is euqal to or greater than @@ -68,5 +68,5 @@ interface INativeRestakingController is IBaseRestakingController { IExoCapsule.ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, IExoCapsule.WithdrawalContainerProof calldata withdrawalProof - ) external; + ) payable external; } diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 39334eb4..1dc7a243 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -28,9 +28,9 @@ contract ExoCapsuleStorage { uint256 public constant BEACON_CHAIN_GENESIS_TIME = 1606824023; uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; - address public capsuleOwner; uint256 public principleBalance; uint256 public withdrawableBalance; + address public capsuleOwner; INativeRestakingController public gateway; IBeaconChainOracle public beaconOracle; diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index 7deb3c10..5aabc255 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -5,14 +5,17 @@ import "forge-std/Test.sol"; import "../../src/core/ExocoreGateway.sol"; import "../../src/storage/GatewayStorage.sol"; import "../../src/interfaces/ITSSReceiver.sol"; +import "../../src/core/ExoCapsule.sol"; import {ILSTRestakingController} from "../../src/interfaces/ILSTRestakingController.sol"; import "forge-std/console.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; +import "@openzeppelin/contracts/utils/Create2.sol"; contract DepositWithdrawPrincipleTest is ExocoreDeployer { using AddressCast for address; + using stdStorage for StdStorage; event DepositResult(bool indexed success, address indexed token, address indexed depositor, uint256 amount); event WithdrawPrincipleResult( @@ -191,22 +194,152 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { ); } - // function test_NativeDepositWithdraw() public { - // Player memory depositor = players[0]; + function test_NativeDepositWithdraw() public { + Player memory depositor = players[0]; + Player memory relayer = players[1]; + + // transfer some ETH to depositor for staking and paying for gas fee + deal(depositor.addr, 1e22); + // transfer some gas fee to relayer for paying for onboarding cross-chain message packet + deal(relayer.addr, 1e22); + // transfer some gas fee to exocore gateway as it has to pay for the relay fee to layerzero endpoint when sending back response + deal(address(exocoreGateway), 1e22); + + // before native stake and deposit, we simulate proper block environment states to make proof valid + + /// 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; + mockProofTimestamp = activationTimestamp; + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + /// we set current block timestamp to be exactly one slot after the proof generation timestamp + 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) + ); + + // 1. firstly depositor should stake to beacon chain by depositing 32 ETH to ETHPOS contract + ExoCapsule expectedCapsule = ExoCapsule(Create2.computeAddress( + bytes32(uint256(uint160(depositor.addr))), + keccak256(abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(capsuleBeacon), ""))), + address(clientGateway) + )); + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit CapsuleCreated(depositor.addr, address(expectedCapsule)); + emit StakedWithCapsule(depositor.addr, address(expectedCapsule)); + + vm.startPrank(depositor.addr); + clientGateway.stake{value: 32 ether}(abi.encodePacked(_getPubkey(validatorContainer)), bytes(""), bytes32(0)); + vm.stopPrank(); + + // 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 + address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); + vm.etch(capsuleAddress, address(expectedCapsule).code); + capsule = ExoCapsule(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(clientGatewayLogic)) + .sig("ownerToCapsule(address)") + .with_key(depositor.addr) + .find() + ); + vm.store(address(clientGateway), capsuleSlotInGateway, bytes32(uint256(uint160(address(capsule))))); + assertEq(address(clientGateway.ownerToCapsule(depositor.addr)), address(capsule)); + + /// initialize replaced capsule + capsule.initialize(address(clientGateway), depositor.addr, address(beaconOracle)); + + // 2. next depositor call clientGateway.depositBeaconChainValidator to deposit into Exocore from client chain through layerzero - // // transfer some gas fee to depositor - // deal(depositor.addr, 1e22); - // // transfer some gas fee to exocore gateway as it has to pay for the relay fee to layerzero endpoint when sending back response - // deal(address(exocoreGateway), 1e22); + /// client chain layerzero endpoint should emit the message packet including deposit payload. + uint256 depositAmount = uint256(_getEffectiveBalance(validatorContainer)) * GWEI_TO_WEI; + bytes memory depositRequestPayload = abi.encodePacked( + GatewayStorage.Action.REQUEST_DEPOSIT, + bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), + bytes32(bytes20(depositor.addr)), + depositAmount + ); + uint256 depositRequestNativeFee = clientGateway.quote(depositRequestPayload); + bytes32 depositRequestId = generateUID(1, true); - // // firstly depositor should stake to beacon chain by depositing 32 ETH to ETHPOS contract - // vm.expectEmit(true, true, true, true, address(clientGateway)); - // emit CapsuleCreated(depositor.addr, address(0x1)); - // emit StakedWithCapsule(depositor.addr, address(0x1)); + vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); + emit NewPacket( + exocoreChainId, + address(clientGateway), + address(exocoreGateway).toBytes32(), + uint64(1), + depositRequestPayload + ); + + /// client chain gateway should emit MessageSent event + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit MessageSent(GatewayStorage.Action.REQUEST_DEPOSIT, depositRequestId, uint64(1), depositRequestNativeFee); + + /// call depositBeaconChainValidator to see if these events are emitted as expected + vm.startPrank(depositor.addr); + clientGateway.depositBeaconChainValidator{value: depositRequestNativeFee}(validatorContainer, validatorProof); + vm.stopPrank(); - // vm.startPrank(depositor.addr); - // clientGateway.stake() - // } + // 3. thirdly layerzero relayers should watch the request message packet and relay the message to destination endpoint + + /// exocore gateway should return response message to exocore network layerzero endpoint + uint256 lastlyUpdatedPrincipleBalance = depositAmount; + bytes memory depositResponsePayload = + abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true, lastlyUpdatedPrincipleBalance); + uint256 depositResponseNativeFee = exocoreGateway.quote(clientChainId, depositResponsePayload); + bytes32 depositResponseId = generateUID(1, false); + + vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); + emit NewPacket( + clientChainId, + address(exocoreGateway), + address(clientGateway).toBytes32(), + uint64(1), + depositResponsePayload + ); + + /// exocore gateway should emit MessageSent event + vm.expectEmit(true, true, true, true, address(exocoreGateway)); + emit MessageSent(GatewayStorage.Action.RESPOND, depositResponseId, uint64(1), depositResponseNativeFee); + + /// relayer catches the request message packet by listening to client chain event and feed it to Exocore network + vm.startPrank(relayer.addr); + exocoreLzEndpoint.lzReceive( + Origin(clientChainId, address(clientGateway).toBytes32(), uint64(1)), + address(exocoreGateway), + depositRequestId, + depositRequestPayload, + bytes("") + ); + vm.stopPrank(); + + // At last layerzero relayers should watch the response message packet and relay the message back to source chain endpoint + + /// client chain gateway should execute the response hook and emit depositResult event + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit DepositResult(true, VIRTUAL_STAKED_ETH_ADDRESS, depositor.addr, depositAmount); + + /// relayer catches the response message packet by listening to Exocore event and feed it to client chain + vm.startPrank(relayer.addr); + clientChainLzEndpoint.lzReceive( + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(1)), + address(clientGateway), + depositResponseId, + depositResponsePayload, + bytes("") + ); + vm.stopPrank(); + } function test_TSSReceiver() public { Player memory depositor = players[0]; diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index e50808e5..73455a6d 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -74,14 +74,11 @@ contract SetUp is Test { vm.etch(capsuleAddress, address(phantomCapsule).code); capsule = ExoCapsule(capsuleAddress); - bytes32 gatewaySlot = bytes32(stdstore.target(capsuleAddress).sig("gateway()").find()); - vm.store(capsuleAddress, gatewaySlot, bytes32(uint256(uint160(address(this))))); + stdstore.target(capsuleAddress).sig("gateway()").checked_write(bytes32(uint256(uint160(address(this))))); - bytes32 ownerSlot = bytes32(stdstore.target(capsuleAddress).sig("capsuleOwner()").find()); - vm.store(capsuleAddress, ownerSlot, bytes32(uint256(uint160(capsuleOwner)))); + stdstore.target(capsuleAddress).sig("capsuleOwner()").checked_write(bytes32(uint256(uint160(capsuleOwner)))); - bytes32 beaconOraclerSlot = bytes32(stdstore.target(capsuleAddress).sig("beaconOracle()").find()); - vm.store(capsuleAddress, beaconOraclerSlot, bytes32(uint256(uint160(address(beaconOracle))))); + stdstore.target(capsuleAddress).sig("beaconOracle()").checked_write(bytes32(uint256(uint160(address(beaconOracle))))); } function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { @@ -108,7 +105,7 @@ contract SetUp is Test { contract VerifyDepositProof is SetUp { using BeaconChainProofs for bytes32; using stdStorage for StdStorage; - + function test_verifyDepositProof() public { uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index b911b89f..51a683ca 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -22,10 +22,12 @@ import "../../src/interfaces/precompiles/IDeposit.sol"; import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import "../../src/interfaces/precompiles/IClaimReward.sol"; import "test/mocks/ETHPOSDepositMock.sol"; +import "src/libraries/Endian.sol"; import "src/core/ExoCapsule.sol"; contract ExocoreDeployer is Test { using AddressCast for address; + using Endian for bytes32; Player[] players; address[] whitelistTokens; @@ -34,8 +36,11 @@ contract ExocoreDeployer is Test { ERC20PresetFixedSupply restakeToken; ClientChainGateway clientGateway; + ClientChainGateway clientGatewayLogic; Vault vault; + ExoCapsule capsule; ExocoreGateway exocoreGateway; + ExocoreGateway exocoreGatewayLogic; ILayerZeroEndpointV2 clientChainLzEndpoint; ILayerZeroEndpointV2 exocoreLzEndpoint; IBeaconChainOracle beaconOracle; @@ -44,11 +49,25 @@ contract ExocoreDeployer is Test { IBeacon vaultBeacon; IBeacon capsuleBeacon; + uint256 constant BEACON_CHAIN_GENESIS_TIME = 1606824023; + /// @notice The number of slots each epoch in the beacon chain + uint64 internal constant SLOTS_PER_EPOCH = 32; + /// @notice The number of seconds in a slot in the beacon chain + uint64 internal constant SECONDS_PER_SLOT = 12; + /// @notice Number of seconds per epoch: 384 == 32 slots/epoch * 12 seconds/slot + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; + 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"; bytes32[] validatorContainer; bytes32 beaconBlockRoot; IExoCapsule.ValidatorContainerProof validatorProof; + uint256 mockProofTimestamp; + uint256 mockCurrentBlockTimestamp; uint32 exocoreChainId = 2; uint32 clientChainId = 1; @@ -67,7 +86,7 @@ contract ExocoreDeployer is Test { // load beacon chain validator container and proof from json file _loadValidatorContainer(); _loadValidatorProof(); - _loadBeaconBlockRoot; + _loadBeaconBlockRoot(); vm.chainId(clientChainId); _deploy(); @@ -124,7 +143,7 @@ contract ExocoreDeployer is Test { whitelistTokens.push(address(restakeToken)); ProxyAdmin proxyAdmin = new ProxyAdmin(); - ClientChainGateway clientGatewayLogic = new ClientChainGateway( + clientGatewayLogic = new ClientChainGateway( address(clientChainLzEndpoint), exocoreChainId, address(beaconOracle), @@ -151,7 +170,7 @@ contract ExocoreDeployer is Test { vault = Vault(address(clientGateway.tokenVaults(address(restakeToken)))); // deploy Exocore network contracts - ExocoreGateway exocoreGatewayLogic = new ExocoreGateway(address(exocoreLzEndpoint)); + exocoreGatewayLogic = new ExocoreGateway(address(exocoreLzEndpoint)); exocoreGateway = ExocoreGateway( payable( address( @@ -214,4 +233,28 @@ contract ExocoreDeployer is Test { EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); return address(oracle); } + + 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(); + } } From 4c8329cbf80e6a1095d58e50c5f8e044f6c48682 Mon Sep 17 00:00:00 2001 From: adu Date: Thu, 9 May 2024 17:25:59 +0800 Subject: [PATCH 31/93] fix #30(remove TSSReceiver), deploy vaults with beacon proxy, fix incompatibilities --- script/BaseScript.sol | 1 - script/integration/1_DeployBootstrap.s.sol | 41 ++-- src/core/BaseRestakingController.sol | 27 ++- src/core/Bootstrap.sol | 132 ++++++------ src/core/BootstrapLzReceiver.sol | 3 +- src/core/ClientChainGateway.sol | 172 ++++----------- src/core/ClientChainLzReceiver.sol | 181 ---------------- src/core/ClientGatewayLzReceiver.sol | 137 ++++++++++-- src/core/LSTRestakingController.sol | 33 +-- src/core/NativeRestakingController.sol | 3 - src/core/TSSReceiver.sol | 80 ------- src/interfaces/IClientChainGateway.sol | 3 +- src/interfaces/ILSTRestakingController.sol | 14 -- src/interfaces/ITSSReceiver.sol | 31 --- src/interfaces/ITokenWhitelister.sol | 13 +- src/libraries/BeaconChainProofs.sol | 1 - src/storage/BootstrapStorage.sol | 104 +++++---- src/storage/ClientChainGatewayStorage.sol | 56 ++--- src/storage/ExocoreGatewayStorage.sol | 2 - test/foundry/Bootstrap.t.sol | 220 ++++++++++---------- test/foundry/ClientChainGateway.t.sol | 5 - test/foundry/DepositWithdrawPrinciple.t.sol | 147 ------------- test/foundry/ExocoreDeployer.t.sol | 2 +- test/foundry/ExocoreGateway.t.sol | 1 - test/foundry/WithdrawReward.t.sol | 1 - 25 files changed, 478 insertions(+), 932 deletions(-) delete mode 100644 src/core/ClientChainLzReceiver.sol delete mode 100644 src/core/TSSReceiver.sol delete mode 100644 src/interfaces/ITSSReceiver.sol diff --git a/script/BaseScript.sol b/script/BaseScript.sol index 51d3fcb3..37d471e7 100644 --- a/script/BaseScript.sol +++ b/script/BaseScript.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.19; import "../src/interfaces/IClientChainGateway.sol"; import "../src/interfaces/IVault.sol"; import "../src/interfaces/IExocoreGateway.sol"; -import "../src/interfaces/IVault.sol"; import "../src/interfaces/IExoCapsule.sol"; import {ERC20PresetFixedSupply} from "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index 063ad4a5..5a26b63e 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -5,6 +5,8 @@ import "forge-std/console.sol"; import "forge-std/Script.sol"; import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IBeacon} from "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; +import {UpgradeableBeacon} from "@openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol"; import {EndpointV2Mock} from "../../test/mocks/EndpointV2Mock.sol"; @@ -13,6 +15,8 @@ import {Bootstrap} from "../../src/core/Bootstrap.sol"; import {CustomProxyAdmin} from "../../src/core/CustomProxyAdmin.sol"; import {MyToken} from "../../test/foundry/MyToken.sol"; import {Vault} from "../../src/core/Vault.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; + // Technically this is used for testing but it is marked as a script // because it is a script that is used to deploy the contracts on Anvil @@ -44,6 +48,9 @@ contract DeployContracts is Script { Vault[] vaults; CustomProxyAdmin proxyAdmin; + IVault vaultImplementation; + IBeacon vaultBeacon; + function setUp() private { // these are default values for Anvil's usual mnemonic. uint256[] memory ANVIL_OPERATORS = new uint256[](3); @@ -95,29 +102,22 @@ contract DeployContracts is Script { } } - function deployVaults() private { - vm.startBroadcast(contractDeployer); - Vault vaultLogic = new Vault(); - for(uint256 i = 0; i < whitelistTokens.length; i++) { - Vault vault = Vault(address(new TransparentUpgradeableProxy( - address(vaultLogic), address(proxyAdmin), "" - ))); - vault.initialize(whitelistTokens[i], address(bootstrap)); - vaults.push(vault); - } - address[] memory vaultAddresses = new address[](vaults.length); - for(uint256 i = 0; i < whitelistTokens.length; i++) { - vaultAddresses[i] = address(vaults[i]); - } - bootstrap.addTokenVaults(vaultAddresses); - vm.stopBroadcast(); - } - function deployContract() private { vm.startBroadcast(contractDeployer); + + /// deploy vault implementationcontract that has logics called by proxy + vaultImplementation = new Vault(); + + /// deploy the vault beacon that store the implementation contract address + vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); + proxyAdmin = new CustomProxyAdmin(); EndpointV2Mock clientChainLzEndpoint = new EndpointV2Mock(clientChainId); - Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint)); + Bootstrap bootstrapLogic = new Bootstrap( + address(clientChainLzEndpoint), + exocoreChainId, + address(vaultBeacon) + ); bootstrap = Bootstrap( payable(address( new TransparentUpgradeableProxy( @@ -127,7 +127,6 @@ contract DeployContracts is Script { vm.addr(contractDeployer), block.timestamp + 3 minutes, 1 seconds, - exocoreChainId, payable(exocoreValidatorSet), whitelistTokens, address(proxyAdmin) @@ -278,8 +277,6 @@ contract DeployContracts is Script { console.log("Tokens deployed"); deployContract(); console.log("Contract deployed"); - deployVaults(); - console.log("Vaults deployed"); approveAndDeposit(); console.log("Approved and deposited"); registerOperators(); diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index 3b0b0213..317d667a 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -18,13 +18,10 @@ abstract contract BaseRestakingController is { using OptionsBuilder for bytes; - event ClaimSucceeded(address token, address recipient, uint256 amount); - event MessageSent(Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); - receive() external payable {} modifier isTokenWhitelisted(address token) { - require(whitelistTokens[token], "BaseRestakingController: token is not whitelisted"); + require(isWhitelistedToken[token], "BaseRestakingController: token is not whitelisted"); _; } @@ -34,12 +31,12 @@ abstract contract BaseRestakingController is } modifier vaultExists(address token) { - require(address(tokenVaults[token]) != address(0), "BaseRestakingController: no vault added for this token"); + require(address(tokenToVault[token]) != address(0), "BaseRestakingController: no vault added for this token"); _; } - modifier isValidBech32Address(string memory operator) { - require(bytes(operator).length == 42, "BaseRestakingController: invalid bech32 address"); + modifier isValidBech32Address(string calldata exocoreAddress) { + require(exocoreAddressIsValid(exocoreAddress), "BaseRestakingController: invalid bech32 encoded Exocore address"); _; } @@ -103,4 +100,20 @@ abstract contract BaseRestakingController is _lzSend(exocoreChainId, payload, options, MessagingFee(fee.nativeFee, 0), exocoreValidatorSetAddress, false); emit MessageSent(act, receipt.guid, receipt.nonce, receipt.fee.nativeFee); } + + function exocoreAddressIsValid( + string calldata operatorExocoreAddress + ) public pure returns (bool) { + bytes memory stringBytes = bytes(operatorExocoreAddress); + if (stringBytes.length != 42) { + return false; + } + for (uint i = 0; i < EXO_ADDRESS_PREFIX.length; i++) { + if (stringBytes[i] != EXO_ADDRESS_PREFIX[i]) { + return false; + } + } + + return true; + } } diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index 5c275ed0..bb67285e 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -6,17 +6,19 @@ import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Ini import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/OwnableUpgradeable.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {ITransparentUpgradeableProxy} from "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; import {OAppCoreUpgradeable} from "../lzApp/OAppCoreUpgradeable.sol"; -import {IController} from "../interfaces/IController.sol"; +import {ILSTRestakingController} from "../interfaces/ILSTRestakingController.sol"; import {ICustomProxyAdmin} from "../interfaces/ICustomProxyAdmin.sol"; import {IOperatorRegistry} from "../interfaces/IOperatorRegistry.sol"; import {ITokenWhitelister} from "../interfaces/ITokenWhitelister.sol"; import {IVault} from "../interfaces/IVault.sol"; import {BootstrapLzReceiver} from "./BootstrapLzReceiver.sol"; -import {TSSReceiver} from "./TSSReceiver.sol"; +import {BootstrapStorage} from "../storage/BootstrapStorage.sol"; +import {Vault} from "./Vault.sol"; // ClientChainGateway differences: // replace IClientChainGateway with ITokenWhitelister (excludes only quote function). @@ -28,57 +30,62 @@ contract Bootstrap is PausableUpgradeable, OwnableUpgradeable, ITokenWhitelister, - IController, + ILSTRestakingController, IOperatorRegistry, - BootstrapLzReceiver, - TSSReceiver + BootstrapLzReceiver { bytes public constant EXO_ADDRESS_PREFIX = bytes("exo1"); - constructor(address _endpoint) OAppCoreUpgradeable(_endpoint) { + constructor( + address endpoint_, + uint32 exocoreChainId_, + address vaultBeacon_ + ) OAppCoreUpgradeable(endpoint_) BootstrapStorage(exocoreChainId_, vaultBeacon_) { _disableInitializers(); } function initialize( address owner, - uint256 _spawnTime, - uint256 _offsetDuration, - uint32 _exocoreChainId, - address payable _exocoreValidatorSetAddress, - address[] calldata _whitelistTokens, - address _customProxyAdmin + uint256 spawnTime_, + uint256 offsetDuration_, + address payable exocoreValidatorSetAddress_, + address[] calldata whitelistTokens_, + address customProxyAdmin_ ) external initializer { require(owner != address(0), "Bootstrap: owner should not be empty"); - require(_spawnTime > block.timestamp, "Bootstrap: spawn time should be in the future"); - require(_offsetDuration > 0, "Bootstrap: offset duration should be greater than 0"); + require(spawnTime_ > block.timestamp, "Bootstrap: spawn time should be in the future"); + require(offsetDuration_ > 0, "Bootstrap: offset duration should be greater than 0"); require( - _spawnTime > _offsetDuration, + spawnTime_ > offsetDuration_, "Bootstrap: spawn time should be greater than offset duration" ); - uint256 lockTime = _spawnTime - _offsetDuration; + uint256 lockTime = spawnTime_ - offsetDuration_; require( lockTime > block.timestamp, "Bootstrap: lock time should be in the future" ); - require(_exocoreChainId != 0, "Bootstrap: exocore chain id should not be empty"); - require(_exocoreValidatorSetAddress != address(0), + require(exocoreValidatorSetAddress_ != address(0), "Bootstrap: exocore validator set address should not be empty"); - require(_customProxyAdmin != address(0), + require(customProxyAdmin_ != address(0), "Bootstrap: custom proxy admin should not be empty"); - exocoreSpawnTime = _spawnTime; - offsetDuration = _offsetDuration; - exocoreChainId = _exocoreChainId; - exocoreValidatorSetAddress = _exocoreValidatorSetAddress; - for (uint256 i = 0; i < _whitelistTokens.length; i++) { - whitelistTokens[_whitelistTokens[i]] = true; - whitelistTokensArray.push(_whitelistTokens[i]); + exocoreSpawnTime = spawnTime_; + offsetDuration = offsetDuration_; + exocoreValidatorSetAddress = exocoreValidatorSetAddress_; + + for (uint256 i = 0; i < whitelistTokens_.length; i++) { + address underlyingToken = whitelistTokens_[i]; + whitelistTokens.push(underlyingToken); + isWhitelistedToken[underlyingToken] = true; + emit WhitelistTokenAdded(underlyingToken); + + _deployVault(underlyingToken); } - whiteListFunctionSelectors[Action.MARK_BOOTSTRAP] = + _whiteListFunctionSelectors[Action.MARK_BOOTSTRAP] = this.markBootstrapped.selector; - customProxyAdmin = _customProxyAdmin; + customProxyAdmin = customProxyAdmin_; bootstrapped = false; // msg.sender is not the proxy admin but the transparent proxy itself, and hence, @@ -177,11 +184,11 @@ contract Bootstrap is // anyway it would be pointless to add such tokens since other operations // cannot be performed. require( - !whitelistTokens[_token], + !isWhitelistedToken[_token], "Bootstrap: token should be not whitelisted before" ); - whitelistTokens[_token] = true; - whitelistTokensArray.push(_token); + whitelistTokens.push(_token); + isWhitelistedToken[_token] = true; emit WhitelistTokenAdded(_token); } @@ -191,14 +198,14 @@ contract Bootstrap is address _token ) external beforeLocked onlyOwner whenNotPaused { require( - whitelistTokens[_token], + isWhitelistedToken[_token], "Bootstrap: token should be already whitelisted" ); - whitelistTokens[_token] = false; - for(uint i = 0; i < whitelistTokensArray.length; i++) { - if (whitelistTokensArray[i] == _token) { - whitelistTokensArray[i] = whitelistTokensArray[whitelistTokensArray.length - 1]; - whitelistTokensArray.pop(); + isWhitelistedToken[_token] = false; + for(uint i = 0; i < whitelistTokens.length; i++) { + if (whitelistTokens[i] == _token) { + whitelistTokens[i] = whitelistTokens[whitelistTokens.length - 1]; + whitelistTokens.pop(); break; } } @@ -206,24 +213,6 @@ contract Bootstrap is emit WhitelistTokenRemoved(_token); } - // implementation of ITokenWhitelister - function addTokenVaults( - address[] calldata vaults - ) external beforeLocked onlyOwner whenNotPaused { - for (uint256 i = 0; i < vaults.length; i++) { - address underlyingToken = IVault(vaults[i]).getUnderlyingToken(); - if (!whitelistTokens[underlyingToken]) { - revert UnauthorizedToken(); - } - if (address(tokenVaults[underlyingToken]) != address(0)) { - revert VaultAlreadyAdded(); - } - tokenVaults[underlyingToken] = IVault(vaults[i]); - - emit VaultAdded(vaults[i]); - } - } - /** * @dev Validates the given Exocore address. * @param operatorExocoreAddress The Exocore address to validate. @@ -434,9 +423,9 @@ contract Bootstrap is function _validateAndGetVault( address token, uint256 amount ) view internal returns (IVault) { - require(whitelistTokens[token], "Bootstrap: token is not whitelisted"); + require(isWhitelistedToken[token], "Bootstrap: token is not whitelisted"); require(amount > 0, "Bootstrap: amount should be greater than zero"); - IVault vault = tokenVaults[token]; + IVault vault = tokenToVault[token]; if (address(vault) == address(0)) { revert VaultNotExist(); } @@ -514,14 +503,6 @@ contract Bootstrap is vault.withdraw(msg.sender, recipient, amount); } - // implementation of IController - // this function is not required before the network bootstrap. - function updateUsersBalances( - UserBalanceUpdateInfo[] calldata - ) view override external beforeLocked whenNotPaused { - revert NotYetSupported(); - } - // implementation of IController function delegateTo( string calldata operator, address token, uint256 amount @@ -662,7 +643,7 @@ contract Bootstrap is */ function getWhitelistedTokensCount( ) external view returns (uint256) { - return whitelistTokensArray.length; + return whitelistTokens.length; } /** @@ -672,8 +653,8 @@ contract Bootstrap is * @return A `TokenInfo` struct containing the token's name, symbol, address, decimals, total supply, and deposit amount. */ function getWhitelistedTokenAtIndex(uint256 index) public view returns (TokenInfo memory) { - require(index < whitelistTokensArray.length, "Index out of bounds"); - address tokenAddress = whitelistTokensArray[index]; + require(index < whitelistTokens.length, "Index out of bounds"); + address tokenAddress = whitelistTokens[index]; ERC20 token = ERC20(tokenAddress); return TokenInfo({ name: token.name(), @@ -684,4 +665,19 @@ contract Bootstrap is depositAmount: depositsByToken[tokenAddress] }); } + + function _deployVault(address underlyingToken) internal returns (IVault) { + Vault vault = Vault( + Create2.deploy( + 0, + bytes32(uint256(uint160(underlyingToken))), + // set the beacon address for beacon proxy + abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(vaultBeacon), "")) + ) + ); + vault.initialize(underlyingToken, address(this)); + emit VaultCreated(underlyingToken, address(vault)); + + tokenToVault[underlyingToken] = vault; + } } \ No newline at end of file diff --git a/src/core/BootstrapLzReceiver.sol b/src/core/BootstrapLzReceiver.sol index 61420628..ce6e1fbc 100644 --- a/src/core/BootstrapLzReceiver.sol +++ b/src/core/BootstrapLzReceiver.sol @@ -28,9 +28,8 @@ abstract contract BootstrapLzReceiver is _consumeInboundNonce(_origin.srcEid, _origin.sender, _origin.nonce); Action act = Action(uint8(payload[0])); require(act != Action.RESPOND, "BootstrapLzReceiver: invalid action"); - bytes4 selector_ = whiteListFunctionSelectors[act]; + bytes4 selector_ = _whiteListFunctionSelectors[act]; if (selector_ == bytes4(0)) { - emit UnsupportedRequestEvent(act); revert UnsupportedRequest(act); } (bool success, bytes memory reason) = diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index 5cbd94a0..51631323 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -7,9 +7,8 @@ import {OAppSenderUpgradeable, MessagingFee} from "../lzApp/OAppSenderUpgradeabl import {OAppReceiverUpgradeable} from "../lzApp/OAppReceiverUpgradeable.sol"; import {LSTRestakingController} from "./LSTRestakingController.sol"; import {NativeRestakingController} from "./NativeRestakingController.sol"; -import {ClientChainLzReceiver} from "./ClientChainLzReceiver.sol"; +import {ClientGatewayLzReceiver} from "./ClientGatewayLzReceiver.sol"; import {IClientChainGateway} from "../interfaces/IClientChainGateway.sol"; -import {TSSReceiver} from "./TSSReceiver.sol"; import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; import {Vault} from "./Vault.sol"; @@ -27,15 +26,10 @@ contract ClientChainGateway is IClientChainGateway, LSTRestakingController, NativeRestakingController, - TSSReceiver, - ClientChainLzReceiver + ClientGatewayLzReceiver { using OptionsBuilder for bytes; - event WhitelistTokenAdded(address _token); - event WhitelistTokenRemoved(address _token); - event VaultCreated(address _underlyingToken, address _vault); - /** * @notice This constructor initializes only immutable state variables * @param endpoint_ is the layerzero endpoint address deployed on this chain @@ -61,7 +55,7 @@ contract ClientChainGateway is // reinitializer(2) is used so that the ownable and oappcore functions can be called again. function initialize( address payable exocoreValidatorSetAddress_, - address[] calldata whitelistTokens_ + address[] calldata appendedWhitelistTokens_ ) external reinitializer(2) { clearBootstrapData(); @@ -69,16 +63,20 @@ contract ClientChainGateway is exocoreValidatorSetAddress = exocoreValidatorSetAddress_; - for (uint256 i = 0; i < whitelistTokens_.length; i++) { - address underlyingToken = whitelistTokens_[i]; - whitelistTokens[underlyingToken] = true; + for (uint256 i = 0; i < appendedWhitelistTokens_.length; i++) { + address underlyingToken = appendedWhitelistTokens_[i]; + require(!isWhitelistedToken[underlyingToken], "ClientChainGateway: token should not be whitelisted before"); + + whitelistTokens.push(underlyingToken); + isWhitelistedToken[underlyingToken] = true; emit WhitelistTokenAdded(underlyingToken); - _deployVault(underlyingToken); + // deploy the corresponding vault if not deployed before + if (address(tokenToVault[underlyingToken]) == address(0)) { + _deployVault(underlyingToken); + } } - _whiteListFunctionSelectors[Action.UPDATE_USERS_BALANCES] = this.updateUsersBalances.selector; - _registeredResponseHooks[Action.REQUEST_DEPOSIT] = this.afterReceiveDepositResponse.selector; _registeredResponseHooks[Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE] = this.afterReceiveWithdrawPrincipleResponse.selector; @@ -96,7 +94,7 @@ contract ClientChainGateway is function clearBootstrapData() internal { // mandatory to clear! - delete whiteListFunctionSelectors[Action.MARK_BOOTSTRAP]; + delete _whiteListFunctionSelectors[Action.MARK_BOOTSTRAP]; // the set below is recommended to clear, so that any possibilities of upgrades // can then be removed. delete customProxyAdmin; @@ -111,8 +109,8 @@ contract ClientChainGateway is // and have no utility after initialization. for(uint i = 0; i < depositors.length; i++) { address depositor = depositors[i]; - for(uint j = 0; j < whitelistTokensArray.length; j++) { - address token = whitelistTokensArray[j]; + for(uint j = 0; j < whitelistTokens.length; j++) { + address token = whitelistTokens[j]; delete totalDepositAmounts[depositor][token]; delete withdrawableAmounts[depositor][token]; for(uint k = 0; k < registeredOperators.length; k++) { @@ -129,19 +127,18 @@ contract ClientChainGateway is delete operators[exo]; delete commissionEdited[exo]; delete ethToExocoreAddress[eth]; - for(uint j = 0; j < whitelistTokensArray.length; j++) { - address token = whitelistTokensArray[j]; + for(uint j = 0; j < whitelistTokens.length; j++) { + address token = whitelistTokens[j]; delete delegationsByOperator[exo][token]; } } - for(uint j = 0; j < whitelistTokensArray.length; j++) { - address token = whitelistTokensArray[j]; + for(uint j = 0; j < whitelistTokens.length; j++) { + address token = whitelistTokens[j]; delete depositsByToken[token]; } // these should also be cleared - even if the loops are not used // cheap to clear and potentially large in size. delete depositors; - delete whitelistTokensArray; delete registeredOperators; } @@ -159,42 +156,32 @@ contract ClientChainGateway is _unpause(); } - function addWhitelistToken(address _token) external onlyOwner whenNotPaused { - require(!whitelistTokens[_token], "ClientChainGateway: token should not be whitelisted before"); - whitelistTokens[_token] = true; + function addWhitelistToken(address _token) public onlyOwner whenNotPaused { + require(!isWhitelistedToken[_token], "ClientChainGateway: token should not be whitelisted before"); + whitelistTokens.push(_token); + isWhitelistedToken[_token] = true; emit WhitelistTokenAdded(_token); // deploy the corresponding vault if not deployed before - if (address(tokenVaults[_token]) == address(0)) { + if (address(tokenToVault[_token]) == address(0)) { _deployVault(_token); } } function removeWhitelistToken(address _token) external onlyOwner whenNotPaused { - require(whitelistTokens[_token], "ClientChainGateway: token should be already whitelisted"); - whitelistTokens[_token] = false; - - emit WhitelistTokenRemoved(_token); - } - -<<<<<<< HEAD - function addTokenVaults(address[] calldata vaults) external onlyOwner whenNotPaused { - for (uint256 i = 0; i < vaults.length; i++) { - address underlyingToken = IVault(vaults[i]).getUnderlyingToken(); - if (!whitelistTokens[underlyingToken]) { - revert UnauthorizedToken(); - } - if (address(tokenVaults[underlyingToken]) != address(0)) { - revert VaultAlreadyAdded(); + require(isWhitelistedToken[_token], "ClientChainGateway: token should be already whitelisted"); + isWhitelistedToken[_token] = false; + for(uint i = 0; i < whitelistTokens.length; i++) { + if (whitelistTokens[i] == _token) { + whitelistTokens[i] = whitelistTokens[whitelistTokens.length - 1]; + whitelistTokens.pop(); + break; } - tokenVaults[underlyingToken] = IVault(vaults[i]); - - emit VaultAdded(vaults[i]); } + + emit WhitelistTokenRemoved(_token); } -======= ->>>>>>> ee404c5 (adapt to use beacon proxies and create2 for vaults and capsules creation) function quote(bytes memory _message) public view returns (uint256 nativeFee) { bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption( DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE @@ -218,94 +205,6 @@ contract ClientChainGateway is return (SENDER_VERSION, RECEIVER_VERSION); } -<<<<<<< HEAD - function afterReceiveDepositResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, address depositor, uint256 amount) = abi.decode(requestPayload, (address, address, uint256)); - - bool success = (uint8(bytes1(responsePayload[0])) == 1); - uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:])); - if (success) { - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - - vault.updatePrincipleBalance(depositor, lastlyUpdatedPrincipleBalance); - } - - emit DepositResult(success, token, depositor, amount); - } - - function afterReceiveWithdrawPrincipleResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, address withdrawer, uint256 unlockPrincipleAmount) = - abi.decode(requestPayload, (address, address, uint256)); - - bool success = (uint8(bytes1(responsePayload[0])) == 1); - uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:33])); - if (success) { - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - - vault.updatePrincipleBalance(withdrawer, lastlyUpdatedPrincipleBalance); - vault.updateWithdrawableBalance(withdrawer, unlockPrincipleAmount, 0); - } - - emit WithdrawPrincipleResult(success, token, withdrawer, unlockPrincipleAmount); - } - - function afterReceiveWithdrawRewardResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, address withdrawer, uint256 unlockRewardAmount) = - abi.decode(requestPayload, (address, address, uint256)); - - bool success = (uint8(bytes1(responsePayload[0])) == 1); - uint256 lastlyUpdatedRewardBalance = uint256(bytes32(responsePayload[1:33])); - if (success) { - IVault vault = tokenVaults[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - - vault.updateRewardBalance(withdrawer, lastlyUpdatedRewardBalance); - vault.updateWithdrawableBalance(withdrawer, 0, unlockRewardAmount); - } - - emit WithdrawRewardResult(success, token, withdrawer, unlockRewardAmount); - } - - function afterReceiveDelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, string memory operator, address delegator, uint256 amount) = - abi.decode(requestPayload, (address, string, address, uint256)); - - bool success = (uint8(bytes1(responsePayload[0])) == 1); - - emit DelegateResult(success, delegator, operator, token, amount); - } - - function afterReceiveUndelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, string memory operator, address undelegator, uint256 amount) = - abi.decode(requestPayload, (address, string, address, uint256)); - - bool success = (uint8(bytes1(responsePayload[0])) == 1); - - emit UndelegateResult(success, undelegator, operator, token, amount); -======= function _deployVault(address underlyingToken) internal returns (IVault) { Vault vault = Vault( Create2.deploy( @@ -318,7 +217,6 @@ contract ClientChainGateway is vault.initialize(underlyingToken, address(this)); emit VaultCreated(underlyingToken, address(vault)); - tokenVaults[underlyingToken] = vault; ->>>>>>> ee404c5 (adapt to use beacon proxies and create2 for vaults and capsules creation) + tokenToVault[underlyingToken] = vault; } } diff --git a/src/core/ClientChainLzReceiver.sol b/src/core/ClientChainLzReceiver.sol deleted file mode 100644 index 82225e61..00000000 --- a/src/core/ClientChainLzReceiver.sol +++ /dev/null @@ -1,181 +0,0 @@ -pragma solidity ^0.8.19; - -import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; -import {IVault} from "../interfaces/IVault.sol"; -import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; -import {OAppReceiverUpgradeable, Origin} from "../lzApp/OAppReceiverUpgradeable.sol"; - -import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; - -abstract contract ClientChainLzReceiver is PausableUpgradeable, OAppReceiverUpgradeable, ClientChainGatewayStorage { - event DepositResult(bool indexed success, address indexed token, address indexed depositor, uint256 amount); - event WithdrawPrincipleResult( - bool indexed success, address indexed token, address indexed withdrawer, uint256 amount - ); - event WithdrawRewardResult(bool indexed success, address indexed token, address indexed withdrawer, uint256 amount); - event DelegateResult( - bool indexed success, address indexed delegator, string delegatee, address token, uint256 amount - ); - event UndelegateResult( - bool indexed success, address indexed undelegator, string indexed undelegatee, address token, uint256 amount - ); - - error UnsupportedRequest(Action act); - error UnsupportedResponse(Action act); - error RequestOrResponseExecuteFailed(Action act, uint64 nonce, bytes reason); - error UnexpectedResponse(uint64 nonce); - error UnexpectedInboundNonce(uint64 expectedNonce, uint64 actualNonce); - error UnexpectedSourceChain(uint32 unexpectedSrcEndpointId); - error DepositShouldNotFailOnExocore(address token, address depositor); - - modifier onlyCalledFromThis() { - require(msg.sender == address(this), "ClientChainLzReceiver: could only be called from this contract itself with low level call"); - _; - } - - function _lzReceive(Origin calldata _origin, bytes calldata payload) internal virtual override whenNotPaused { - if (_origin.srcEid != exocoreChainId) { - revert UnexpectedSourceChain(_origin.srcEid); - } - - _consumeInboundNonce(_origin.srcEid, _origin.sender, _origin.nonce); - - Action act = Action(uint8(payload[0])); - if (act == Action.RESPOND) { - uint64 requestId = uint64(bytes8(payload[1:9])); - - Action requestAct = _registeredRequestActions[requestId]; - bytes4 hookSelector = _registeredResponseHooks[requestAct]; - if (hookSelector == bytes4(0)) { - revert UnsupportedResponse(act); - } - - bytes memory requestPayload = _registeredRequests[requestId]; - if (requestPayload.length == 0) { - revert UnexpectedResponse(requestId); - } - - (bool success, bytes memory reason) = - address(this).call(abi.encodePacked(hookSelector, abi.encode(requestPayload, payload[9:]))); - if (!success) { - revert RequestOrResponseExecuteFailed(act, _origin.nonce, reason); - } - - delete _registeredRequests[requestId]; - } else { - bytes4 selector_ = _whiteListFunctionSelectors[act]; - if (selector_ == bytes4(0)) { - revert UnsupportedRequest(act); - } - - (bool success, bytes memory reason) = - address(this).call(abi.encodePacked(selector_, abi.encode(payload[1:]))); - if (!success) { - revert RequestOrResponseExecuteFailed(act, _origin.nonce, reason); - } - } - } - - function nextNonce(uint32 srcEid, bytes32 sender) - public - view - virtual - override(OAppReceiverUpgradeable) - returns (uint64) - { - return inboundNonce[srcEid][sender] + 1; - } - - function _consumeInboundNonce(uint32 srcEid, bytes32 sender, uint64 nonce) internal { - inboundNonce[srcEid][sender] += 1; - if (nonce != inboundNonce[srcEid][sender]) { - revert UnexpectedInboundNonce(inboundNonce[srcEid][sender], nonce); - } - } - - function afterReceiveDepositResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, address depositor, uint256 amount) = abi.decode(requestPayload, (address, address, uint256)); - - bool success = (uint8(bytes1(responsePayload[0])) == 1); - uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:])); - - if (!success) { - revert DepositShouldNotFailOnExocore(token, depositor); - } - - if (token == VIRTUAL_STAKED_ETH_ADDRESS) { - IExoCapsule capsule = _getCapsule(depositor); - capsule.updatePrincipleBalance(lastlyUpdatedPrincipleBalance); - } else { - IVault vault = _getVault(token); - vault.updatePrincipleBalance(depositor, lastlyUpdatedPrincipleBalance); - } - - emit DepositResult(success, token, depositor, amount); - } - - function afterReceiveWithdrawPrincipleResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, address withdrawer, uint256 unlockPrincipleAmount) = - abi.decode(requestPayload, (address, address, uint256)); - - bool success = (uint8(bytes1(responsePayload[0])) == 1); - uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:33])); - if (success) { - IVault vault = _getVault(token); - - vault.updatePrincipleBalance(withdrawer, lastlyUpdatedPrincipleBalance); - vault.updateWithdrawableBalance(withdrawer, unlockPrincipleAmount, 0); - } - - emit WithdrawPrincipleResult(success, token, withdrawer, unlockPrincipleAmount); - } - - function afterReceiveWithdrawRewardResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, address withdrawer, uint256 unlockRewardAmount) = - abi.decode(requestPayload, (address, address, uint256)); - - bool success = (uint8(bytes1(responsePayload[0])) == 1); - uint256 lastlyUpdatedRewardBalance = uint256(bytes32(responsePayload[1:33])); - if (success) { - IVault vault = _getVault(token); - - vault.updateRewardBalance(withdrawer, lastlyUpdatedRewardBalance); - vault.updateWithdrawableBalance(withdrawer, 0, unlockRewardAmount); - } - - emit WithdrawRewardResult(success, token, withdrawer, unlockRewardAmount); - } - - function afterReceiveDelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, string memory operator, address delegator, uint256 amount) = - abi.decode(requestPayload, (address, string, address, uint256)); - - bool success = (uint8(bytes1(responsePayload[0])) == 1); - - emit DelegateResult(success, delegator, operator, token, amount); - } - - function afterReceiveUndelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, string memory operator, address undelegator, uint256 amount) = - abi.decode(requestPayload, (address, string, address, uint256)); - - bool success = (uint8(bytes1(responsePayload[0])) == 1); - - emit UndelegateResult(success, undelegator, operator, token, amount); - } -} diff --git a/src/core/ClientGatewayLzReceiver.sol b/src/core/ClientGatewayLzReceiver.sol index f0bfcf08..f25c4343 100644 --- a/src/core/ClientGatewayLzReceiver.sol +++ b/src/core/ClientGatewayLzReceiver.sol @@ -1,13 +1,23 @@ pragma solidity ^0.8.19; -import {BootstrapLzReceiver} from "./BootstrapLzReceiver.sol"; import {ClientChainGatewayStorage} from "../storage/ClientChainGatewayStorage.sol"; -import {Origin} from "../lzApp/OAppReceiverUpgradeable.sol"; +import {IVault} from "../interfaces/IVault.sol"; +import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; +import {OAppReceiverUpgradeable, Origin} from "../lzApp/OAppReceiverUpgradeable.sol"; -abstract contract ClientGatewayLzReceiver is BootstrapLzReceiver, ClientChainGatewayStorage { - function _lzReceive( - Origin calldata _origin, bytes calldata payload - ) internal virtual override(BootstrapLzReceiver) { +import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; + +abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUpgradeable, ClientChainGatewayStorage { + error UnsupportedResponse(Action act); + error UnexpectedResponse(uint64 nonce); + error DepositShouldNotFailOnExocore(address token, address depositor); + + modifier onlyCalledFromThis() { + require(msg.sender == address(this), "ClientChainLzReceiver: could only be called from this contract itself with low level call"); + _; + } + + function _lzReceive(Origin calldata _origin, bytes calldata payload) internal virtual override whenNotPaused { if (_origin.srcEid != exocoreChainId) { revert UnexpectedSourceChain(_origin.srcEid); } @@ -18,13 +28,13 @@ abstract contract ClientGatewayLzReceiver is BootstrapLzReceiver, ClientChainGat if (act == Action.RESPOND) { uint64 requestId = uint64(bytes8(payload[1:9])); - Action requestAct = registeredRequestActions[requestId]; - bytes4 hookSelector = registeredResponseHooks[requestAct]; + Action requestAct = _registeredRequestActions[requestId]; + bytes4 hookSelector = _registeredResponseHooks[requestAct]; if (hookSelector == bytes4(0)) { revert UnsupportedResponse(act); } - bytes memory requestPayload = registeredRequests[requestId]; + bytes memory requestPayload = _registeredRequests[requestId]; if (requestPayload.length == 0) { revert UnexpectedResponse(requestId); } @@ -35,11 +45,11 @@ abstract contract ClientGatewayLzReceiver is BootstrapLzReceiver, ClientChainGat revert RequestOrResponseExecuteFailed(act, _origin.nonce, reason); } - delete registeredRequests[requestId]; + delete _registeredRequestActions[requestId]; + delete _registeredRequests[requestId]; } else { - bytes4 selector_ = whiteListFunctionSelectors[act]; + bytes4 selector_ = _whiteListFunctionSelectors[act]; if (selector_ == bytes4(0)) { - emit UnsupportedRequestEvent(act); revert UnsupportedRequest(act); } @@ -50,4 +60,107 @@ abstract contract ClientGatewayLzReceiver is BootstrapLzReceiver, ClientChainGat } } } + + function nextNonce(uint32 srcEid, bytes32 sender) + public + view + virtual + override(OAppReceiverUpgradeable) + returns (uint64) + { + return inboundNonce[srcEid][sender] + 1; + } + + function _consumeInboundNonce(uint32 srcEid, bytes32 sender, uint64 nonce) internal { + inboundNonce[srcEid][sender] += 1; + if (nonce != inboundNonce[srcEid][sender]) { + revert UnexpectedInboundNonce(inboundNonce[srcEid][sender], nonce); + } + } + + function afterReceiveDepositResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, address depositor, uint256 amount) = abi.decode(requestPayload, (address, address, uint256)); + + bool success = (uint8(bytes1(responsePayload[0])) == 1); + uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:])); + + if (!success) { + revert DepositShouldNotFailOnExocore(token, depositor); + } + + if (token == VIRTUAL_STAKED_ETH_ADDRESS) { + IExoCapsule capsule = _getCapsule(depositor); + capsule.updatePrincipleBalance(lastlyUpdatedPrincipleBalance); + } else { + IVault vault = _getVault(token); + vault.updatePrincipleBalance(depositor, lastlyUpdatedPrincipleBalance); + } + + emit DepositResult(success, token, depositor, amount); + } + + function afterReceiveWithdrawPrincipleResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, address withdrawer, uint256 unlockPrincipleAmount) = + abi.decode(requestPayload, (address, address, uint256)); + + bool success = (uint8(bytes1(responsePayload[0])) == 1); + uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:33])); + if (success) { + IVault vault = _getVault(token); + + vault.updatePrincipleBalance(withdrawer, lastlyUpdatedPrincipleBalance); + vault.updateWithdrawableBalance(withdrawer, unlockPrincipleAmount, 0); + } + + emit WithdrawPrincipleResult(success, token, withdrawer, unlockPrincipleAmount); + } + + function afterReceiveWithdrawRewardResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, address withdrawer, uint256 unlockRewardAmount) = + abi.decode(requestPayload, (address, address, uint256)); + + bool success = (uint8(bytes1(responsePayload[0])) == 1); + uint256 lastlyUpdatedRewardBalance = uint256(bytes32(responsePayload[1:33])); + if (success) { + IVault vault = _getVault(token); + + vault.updateRewardBalance(withdrawer, lastlyUpdatedRewardBalance); + vault.updateWithdrawableBalance(withdrawer, 0, unlockRewardAmount); + } + + emit WithdrawRewardResult(success, token, withdrawer, unlockRewardAmount); + } + + function afterReceiveDelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, string memory operator, address delegator, uint256 amount) = + abi.decode(requestPayload, (address, string, address, uint256)); + + bool success = (uint8(bytes1(responsePayload[0])) == 1); + + emit DelegateResult(success, delegator, operator, token, amount); + } + + function afterReceiveUndelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, string memory operator, address undelegator, uint256 amount) = + abi.decode(requestPayload, (address, string, address, uint256)); + + bool success = (uint8(bytes1(responsePayload[0])) == 1); + + emit UndelegateResult(success, undelegator, operator, token, amount); + } } diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index ebee292e..7009b225 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -10,7 +10,8 @@ import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/Pau abstract contract LSTRestakingController is PausableUpgradeable, ILSTRestakingController, - BaseRestakingController { + BaseRestakingController +{ function deposit(address token, uint256 amount) external payable isTokenWhitelisted(token) isValidAmount(amount) whenNotPaused { IVault vault = _getVault(token); vault.deposit(msg.sender, amount); @@ -39,34 +40,4 @@ abstract contract LSTRestakingController is bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), rewardAmount); _sendMsgToExocore(Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, actionArgs); } - - function updateUsersBalances(UserBalanceUpdateInfo[] calldata info) public whenNotPaused { - require(msg.sender == address(this), "LSTRestakingController: caller must be client chain gateway itself"); - for (uint256 i = 0; i < info.length; i++) { - UserBalanceUpdateInfo memory userBalanceUpdate = info[i]; - for (uint256 j = 0; j < userBalanceUpdate.tokenBalances.length; j++) { - TokenBalanceUpdateInfo memory tokenBalanceUpdate = userBalanceUpdate.tokenBalances[j]; - require(whitelistTokens[tokenBalanceUpdate.token], "Controller: token is not whitelisted"); - IVault vault = _getVault(tokenBalanceUpdate.token); - - if (tokenBalanceUpdate.lastlyUpdatedPrincipleBalance > 0) { - vault.updatePrincipleBalance( - userBalanceUpdate.user, tokenBalanceUpdate.lastlyUpdatedPrincipleBalance - ); - } - - if (tokenBalanceUpdate.lastlyUpdatedRewardBalance > 0) { - vault.updateRewardBalance(userBalanceUpdate.user, tokenBalanceUpdate.lastlyUpdatedRewardBalance); - } - - if (tokenBalanceUpdate.unlockPrincipleAmount > 0 || tokenBalanceUpdate.unlockRewardAmount > 0) { - vault.updateWithdrawableBalance( - userBalanceUpdate.user, - tokenBalanceUpdate.unlockPrincipleAmount, - tokenBalanceUpdate.unlockRewardAmount - ); - } - } - } - } } diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 4c64e69f..197a00b3 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -16,9 +16,6 @@ abstract contract NativeRestakingController is { using ValidatorContainer for bytes32[]; - event CapsuleCreated(address owner, address capsule); - event StakedWithCapsule(address staker, address capsule); - function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable whenNotPaused { require(msg.value == 32 ether, "NativeRestakingController: stake value must be exactly 32 ether"); diff --git a/src/core/TSSReceiver.sol b/src/core/TSSReceiver.sol deleted file mode 100644 index 92db75e3..00000000 --- a/src/core/TSSReceiver.sol +++ /dev/null @@ -1,80 +0,0 @@ -pragma solidity ^0.8.19; - -import {BootstrapStorage} from "../storage/BootstrapStorage.sol"; -import {ITSSReceiver} from "../interfaces/ITSSReceiver.sol"; - -import {ECDSA} from "@openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol"; -import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; - -abstract contract TSSReceiver is PausableUpgradeable, ClientChainGatewayStorage, ITSSReceiver { - using ECDSA for bytes32; - - event MessageProcessed(uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload); - event MessageFailed(uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload, bytes _reason); - - error UnauthorizedSigner(); - - function receiveInterchainMsg(InterchainMsg calldata _msg, bytes calldata signature) external whenNotPaused { - require(_msg.nonce == ++lastMessageNonce, "TSSReceiver: message nonce is not expected"); - require(_msg.srcChainID == exocoreChainId, "TSSReceiver: source chain id is incorrect"); - require(keccak256(_msg.srcAddress) == keccak256(bytes("0x")), "TSSReceiver: source address is incorrect"); - require(_msg.dstChainID == block.chainid, "TSSReceiver: destination chain id is not matched with this chain"); - require( - keccak256(_msg.dstAddress) == keccak256(abi.encodePacked(address(this))), - "TSSReceiver: destination contract address is not matched with this contract" - ); - bool isValid = verifyInterchainMsg(_msg, signature); - if (!isValid) { - revert ITSSReceiver.UnauthorizedSigner(); - } - - Action act = Action(uint8(_msg.payload[0])); - bytes4 selector_ = whiteListFunctionSelectors[act]; - if (selector_ == bytes4(0)) { - revert UnsupportedRequest(act); - } - (bool success, bytes memory reason) = - address(this).call(abi.encodePacked(selector_, _msg.payload[1:])); - if (!success) { - emit ITSSReceiver.MessageFailed( - _msg.srcChainID, _msg.srcAddress, _msg.nonce, _msg.payload, reason - ); - } else { - emit ITSSReceiver.MessageProcessed( - _msg.srcChainID, _msg.srcAddress, _msg.nonce, _msg.payload - ); - } - } - - function verifyInterchainMsg(InterchainMsg calldata msg_, bytes calldata signature) - internal - view - returns (bool isValid) - { - bytes32 digest = keccak256( - abi.encodePacked( - msg_.srcChainID, msg_.srcAddress, msg_.dstChainID, msg_.dstAddress, msg_.nonce, msg_.payload - ) - ); - (uint8 v, bytes32 r, bytes32 s) = splitSignature(signature); - address signer = digest.recover(v, r, s); - if (signer == exocoreValidatorSetAddress) { - isValid = true; - } - } - - function splitSignature(bytes memory sig) internal pure returns (uint8 v, bytes32 r, bytes32 s) { - require(sig.length == 65); - - assembly { - // first 32 bytes, after the length prefix. - r := mload(add(sig, 32)) - // second 32 bytes. - s := mload(add(sig, 64)) - // final byte (first byte of the next 32 bytes). - v := byte(0, mload(add(sig, 96))) - } - - return (v, r, s); - } -} diff --git a/src/interfaces/IClientChainGateway.sol b/src/interfaces/IClientChainGateway.sol index d49caa1d..690c77c1 100644 --- a/src/interfaces/IClientChainGateway.sol +++ b/src/interfaces/IClientChainGateway.sol @@ -4,9 +4,8 @@ import {IOAppReceiver} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppR import {IOAppCore} from "@layerzero-v2/oapp/contracts/oapp/interfaces/IOAppCore.sol"; import {ILSTRestakingController} from "./ILSTRestakingController.sol"; import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; -import {ITSSReceiver} from "./ITSSReceiver.sol"; -interface IClientChainGateway is IOAppReceiver, IOAppCore, ILSTRestakingController, INativeRestakingController, ITSSReceiver { +interface IClientChainGateway is IOAppReceiver, IOAppCore, ILSTRestakingController, INativeRestakingController { function addWhitelistToken(address _token) external; function removeWhitelistToken(address _token) external; function quote(bytes memory _message) external view returns (uint256 nativeFee); diff --git a/src/interfaces/ILSTRestakingController.sol b/src/interfaces/ILSTRestakingController.sol index 042f01c9..09dd3a13 100644 --- a/src/interfaces/ILSTRestakingController.sol +++ b/src/interfaces/ILSTRestakingController.sol @@ -44,18 +44,4 @@ interface ILSTRestakingController is IBaseRestakingController { function withdrawPrincipleFromExocore(address token, uint256 principleAmount) external payable; function withdrawRewardFromExocore(address token, uint256 rewardAmount) external payable; - - /// *** function signatures for commands of Exocore validator set forwarded by Gateway *** - - /** - * @notice This should only be called by Exocore validator set through Gateway to update user's involved - * lastly updated token balance. - * @dev Only Exocore validato set could indirectly call this function through Gateway contract. - * @dev This function could be called in two scenaries: - * 1) Exocore validator set periodically calls this to update user principle and reward balance. - * 2) Exocore validator set sends reponse for the request of withdrawPrincipleFromExocore and unlock part of - * the vault assets and update user's withdrawable balance correspondingly. - * @param info - The info needed for updating users balance. - */ - function updateUsersBalances(UserBalanceUpdateInfo[] calldata info) external; } diff --git a/src/interfaces/ITSSReceiver.sol b/src/interfaces/ITSSReceiver.sol deleted file mode 100644 index d7fda1e9..00000000 --- a/src/interfaces/ITSSReceiver.sol +++ /dev/null @@ -1,31 +0,0 @@ -pragma solidity ^0.8.19; - -interface ITSSReceiver { - /** - * @dev the interchain message sent from client chain Gateway or received from Exocore validator set for cross-chain communication. - * @param dstChainID - testination chain ID. - * @param dstAddress - destination contract address that would receive the interchain message. - * @param payload - actual payload for receiver. - * @param refundAddress - address used for refundding. - * @param interchainFuelAddress - address that would pay for interchain costs. - * @param params - custom params for extension. - */ - struct InterchainMsg { - uint32 srcChainID; - bytes srcAddress; - uint32 dstChainID; - bytes dstAddress; - uint64 nonce; - bytes payload; - } - - function receiveInterchainMsg(InterchainMsg calldata _msg, bytes memory signature) external; - - error UnauthorizedSigner(); - event MessageProcessed( - uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload - ); - event MessageFailed( - uint32 _srcChainId, bytes _srcAddress, uint64 _nonce, bytes _payload, bytes _reason - ); -} diff --git a/src/interfaces/ITokenWhitelister.sol b/src/interfaces/ITokenWhitelister.sol index 73a1d010..7fb69d9a 100644 --- a/src/interfaces/ITokenWhitelister.sol +++ b/src/interfaces/ITokenWhitelister.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.19; interface ITokenWhitelister { function addWhitelistToken(address _token) external; function removeWhitelistToken(address _token) external; - function addTokenVaults(address[] calldata vaults) external; /** * @dev Emitted when a new token is added to the whitelist. @@ -18,15 +17,11 @@ interface ITokenWhitelister { event WhitelistTokenRemoved(address _token); /** - * @dev Emitted when a new vault is added to the mapping of token vaults. - * @param _vault The address of the vault that has been added. + * @dev Emitted when a new vault is created. + * @param vault The address of the vault that has been added. + * @param underlyingToken The underlying token of vault. */ - event VaultAdded(address _vault); - - /** - * @dev Indicates an operation failed because the specified vault already exists. - */ - error VaultAlreadyAdded(); + event VaultCreated(address underlyingToken, address vault); /** * @dev Indicates an operation was attempted with a token that is not authorized. diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 184cbd24..52ed8753 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; import "./Merkle.sol"; -import "../libraries/Endian.sol"; //Utility library for parsing and PHASE0 beacon chain block headers //SSZ Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index 81b277ae..86a45360 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -1,15 +1,19 @@ pragma solidity ^0.8.19; import {GatewayStorage} from "./GatewayStorage.sol"; - import {IOperatorRegistry} from "../interfaces/IOperatorRegistry.sol"; import {IVault} from "../interfaces/IVault.sol"; +import {IBeacon} from "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; // BootstrapStorage should inherit from GatewayStorage since it exists // prior to ClientChainGateway. ClientChainStorage should inherit from // BootstrapStorage to ensure overlap of positioning between the // members of each contract. contract BootstrapStorage is GatewayStorage { + /* -------------------------------------------------------------------------- */ + /* state variables exclusively owned by Bootstrap */ + /* -------------------------------------------------------------------------- */ + // time and duration /** * @notice A timestamp representing the scheduled spawn time of the Exocore chain, which @@ -34,29 +38,7 @@ contract BootstrapStorage is GatewayStorage { */ uint256 public offsetDuration; - // whitelisted tokens and their vaults, and total deposits of said tokens - /** - * @dev An array containing all the token addresses that have been added to the whitelist. - * @notice Use this array to iterate through all whitelisted tokens. - * This helps in operations like audits, UI display, or when removing tokens - * from the whitelist needs an indexed approach. - */ - address[] public whitelistTokensArray; - - /** - * @dev Stores a mapping of whitelisted token addresses to their status. - * @notice Use this to check if a token is allowed for processing. - * Each token address maps to a boolean indicating whether it is whitelisted. - */ - mapping(address token => bool whitelisted) public whitelistTokens; - - /** - * @dev Maps token addresses to their corresponding vault contracts. - * @notice Access the vault interface for a specific token using this mapping. - * Each token address maps to an IVault contract instance handling its operations. - */ - mapping(address token => IVault vault) public tokenVaults; - + // total deposits of said tokens /** * @dev A mapping of token addresses to the total amount of that token deposited into the * contract. @@ -192,13 +174,40 @@ contract BootstrapStorage is GatewayStorage { */ bytes clientChainInitializationData; + /* -------------------------------------------------------------------------- */ + /* shared state variables for Bootstrap and ClientChainGateway */ + /* -------------------------------------------------------------------------- */ + + // whitelisted tokens and their vaults, and total deposits of said tokens + /** + * @dev An array containing all the token addresses that have been added to the whitelist. + * @notice Use this array to iterate through all whitelisted tokens. + * This helps in operations like audits, UI display, or when removing tokens + * from the whitelist needs an indexed approach. + */ + address[] public whitelistTokens; + + /** + * @dev Stores a mapping of whitelisted token addresses to their status. + * @notice Use this to check if a token is allowed for processing. + * Each token address maps to a boolean indicating whether it is whitelisted. + */ + mapping(address token => bool whitelisted) public isWhitelistedToken; + + /** + * @dev Maps token addresses to their corresponding vault contracts. + * @notice Access the vault interface for a specific token using this mapping. + * Each token address maps to an IVault contract instance handling its operations. + */ + mapping(address token => IVault vault) public tokenToVault; + // cross-chain level information /** * @dev Stores the Layer Zero chain ID of the Exocore chain. * @notice Used to identify the specific Exocore chain this contract interacts with for * cross-chain functionalities. */ - uint32 public exocoreChainId; + uint32 public immutable exocoreChainId; /** * @dev A mapping of source chain id to source sender to the nonce of the last inbound @@ -216,6 +225,26 @@ contract BootstrapStorage is GatewayStorage { */ uint256 lastMessageNonce; + // the beacon that stores the Vault implementation contract address for proxy + /** + * @notice this stores the Vault implementation contract address for proxy, and it is + * shsared among all beacon proxies as an immutable. + */ + IBeacon public immutable vaultBeacon; + + /** + * @notice Stored code of type(BeaconProxy).creationCode + * @dev Maintained as a constant to solve an edge case - changes to OpenZeppelin's BeaconProxy code should not cause + * addresses of EigenPods that are pre-computed with Create2 to change, even upon upgrading this contract, changing compiler version, etc. + */ + bytes constant BEACON_PROXY_BYTECODE = + hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; + + + /* -------------------------------------------------------------------------- */ + /* Events */ + /* -------------------------------------------------------------------------- */ + /** * @notice Emitted when the spawn time of the Exocore chain is updated. * @@ -311,17 +340,9 @@ contract BootstrapStorage is GatewayStorage { */ event ClientChainGatewayLogicUpdated(address newLogic, bytes initializationData); - /** - * @notice Emitted when an unsupported LZ request is received by the contract. A request is - * defined as unsupported if there is no whitelisted function selector available for the - * Action. - * - * @dev This event is triggered when the contract receives an LZ request that is not yet - * whitelisted in the contract. - * - * @param act The action that was requested but not yet supported. - */ - event UnsupportedRequestEvent(Action act); + /* -------------------------------------------------------------------------- */ + /* Errors */ + /* -------------------------------------------------------------------------- */ /** * @dev Indicates an operation failed because the specified vault does not exist. @@ -366,4 +387,15 @@ contract BootstrapStorage is GatewayStorage { } uint256[40] private __gap; + + constructor( + uint32 exocoreChainId_, + address vaultBeacon_ + ) { + require(exocoreChainId_ != 0, "BootstrapStorage: exocore chain id should not be empty"); + require(vaultBeacon_ != address(0), "BootstrapStorage: the vaultBeacon address for beacon proxy should not be empty"); + + exocoreChainId = exocoreChainId_; + vaultBeacon = IBeacon(vaultBeacon_); + } } \ No newline at end of file diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 9f0a3592..034ec635 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -4,65 +4,73 @@ import {BootstrapStorage} from "./BootstrapStorage.sol"; import {IVault} from "../interfaces/IVault.sol"; import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; -import {GatewayStorage} from "./GatewayStorage.sol"; +import {BootstrapStorage} from "../storage/BootstrapStorage.sol"; import {IBeacon} from "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; -contract ClientChainGatewayStorage is GatewayStorage { - uint256 public lastMessageNonce; +contract ClientChainGatewayStorage is BootstrapStorage { + /* -------------------------------------------------------------------------- */ + /* state variables exclusively owned by ClientChainGateway */ + /* -------------------------------------------------------------------------- */ + uint64 public outboundNonce; - mapping(uint32 eid => mapping(bytes32 sender => uint64 nonce)) public inboundNonce; - mapping(address => bool) public whitelistTokens; - mapping(address => IVault) public tokenVaults; mapping(address => IExoCapsule) public ownerToCapsule; mapping(uint64 => bytes) _registeredRequests; mapping(uint64 => Action) _registeredRequestActions; mapping(Action => bytes4) _registeredResponseHooks; // immutable state variables - uint32 public immutable exocoreChainId; address public immutable beaconOracleAddress; IBeacon public immutable exoCapsuleBeacon; - IBeacon public immutable vaultBeacon; - + // constant state variables + bytes constant EXO_ADDRESS_PREFIX = bytes("exo1"); uint128 constant DESTINATION_GAS_LIMIT = 500000; uint128 constant DESTINATION_MSG_VALUE = 0; uint256 constant GWEI_TO_WEI = 1e9; address constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; IETHPOSDeposit constant ETH_POS = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); - /** - * @notice Stored code of type(BeaconProxy).creationCode - * @dev Maintained as a constant to solve an edge case - changes to OpenZeppelin's BeaconProxy code should not cause - * addresses of EigenPods that are pre-computed with Create2 to change, even upon upgrading this contract, changing compiler version, etc. - */ - bytes constant BEACON_PROXY_BYTECODE = - hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; - + uint256[40] private __gap; + /* -------------------------------------------------------------------------- */ + /* ClientChainGateway Events(besides inherited from BootstrapStorage) */ + /* -------------------------------------------------------------------------- */ + + /* ----------------- whitelist tokens and vaults management ----------------- */ + event WhitelistTokenAdded(address _token); + event WhitelistTokenRemoved(address _token); + event VaultCreated(address _underlyingToken, address _vault); + + /* ---------------------------- native restaking ---------------------------- */ + event CapsuleCreated(address owner, address capsule); + event StakedWithCapsule(address staker, address capsule); + + /* ----------------------------- restaking ----------------------------- */ + event ClaimSucceeded(address token, address recipient, uint256 amount); + event WithdrawRewardResult(bool indexed success, address indexed token, address indexed withdrawer, uint256 amount); + + /* -------------------------------------------------------------------------- */ + /* Errors */ + /* -------------------------------------------------------------------------- */ + error CapsuleNotExist(); - error VaultNotExist(); constructor( uint32 exocoreChainId_, address beaconOracleAddress_, address vaultBeacon_, address exoCapsuleBeacon_ - ) { - require(exocoreChainId_ != 0, "ClientChainGatewayStorage: exocore chain id should not be empty"); + ) BootstrapStorage(exocoreChainId_, vaultBeacon_) { require(beaconOracleAddress_ != address(0), "ClientChainGatewayStorage: beacon chain oracle address should not be empty"); - require(vaultBeacon_ != address(0), "ClientChainGatewayStorage: the vaultBeacon address for beacon proxy should not be empty"); require(exoCapsuleBeacon_ != address(0), "ClientChainGatewayStorage: the exoCapsuleBeacon address for beacon proxy should not be empty"); - exocoreChainId = exocoreChainId_; beaconOracleAddress = beaconOracleAddress_; exoCapsuleBeacon = IBeacon(exoCapsuleBeacon_); - vaultBeacon = IBeacon(vaultBeacon_); } function _getVault(address token) internal view returns (IVault) { - IVault vault = tokenVaults[token]; + IVault vault = tokenToVault[token]; if (address(vault) == address(0)) { revert VaultNotExist(); } diff --git a/src/storage/ExocoreGatewayStorage.sol b/src/storage/ExocoreGatewayStorage.sol index 1be25a64..b9e7a337 100644 --- a/src/storage/ExocoreGatewayStorage.sol +++ b/src/storage/ExocoreGatewayStorage.sol @@ -32,8 +32,6 @@ contract ExocoreGatewayStorage is GatewayStorage { error RequestExecuteFailed(Action act, uint64 nonce, bytes reason); error PrecompileCallFailed(bytes4 selector_, bytes reason); - error UnexpectedInboundNonce(uint64 expectedNonce, uint64 actualNonce); - error UnexpectedSourceChain(uint32 unexpectedSrcEndpointId); error InvalidRequestLength(Action act, uint256 expectedLength, uint256 actualLength); uint256[40] private __gap; diff --git a/test/foundry/Bootstrap.t.sol b/test/foundry/Bootstrap.t.sol index 1b37fc6b..eaae8fb7 100644 --- a/test/foundry/Bootstrap.t.sol +++ b/test/foundry/Bootstrap.t.sol @@ -15,10 +15,16 @@ import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; import {Origin} from "../../src/lzApp/OAppReceiverUpgradeable.sol"; import {GatewayStorage} from "../../src/storage/GatewayStorage.sol"; import {BootstrapStorage} from "../../src/storage/BootstrapStorage.sol"; -import {IController} from "../../src/interfaces/IController.sol"; +import {IBeacon} from "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; +import {UpgradeableBeacon} from "@openzeppelin-contracts/contracts/proxy/beacon/UpgradeableBeacon.sol"; +import {IVault} from "../../src/interfaces/IVault.sol"; +import "@openzeppelin/contracts/utils/Create2.sol"; +import "src/core/ExoCapsule.sol"; +import "src/storage/GatewayStorage.sol"; contract BootstrapTest is Test { MyToken myToken; + MyToken appendedToken; CustomProxyAdmin proxyAdmin; Bootstrap bootstrap; address[] addrs = new address[](6); @@ -36,12 +42,20 @@ contract BootstrapTest is Test { uint16 exocoreChainId = 1; uint16 clientChainId = 2; address[] whitelistTokens; - address[] vaults; + address[] appendedWhitelistTokensForUpgrade; NonShortCircuitEndpointV2Mock clientChainLzEndpoint; address exocoreValidatorSet = vm.addr(uint256(0x8)); address undeployedExocoreGateway = vm.addr(uint256(0x9)); address undeployedExocoreLzEndpoint = vm.addr(uint256(0xb)); + IVault vaultImplementation; + IExoCapsule capsuleImplementation; + IBeacon vaultBeacon; + IBeacon capsuleBeacon; + + bytes constant BEACON_PROXY_BYTECODE = + hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; + function setUp() public { addrs[0] = address(0x1); // Simulated OPERATOR1 address addrs[1] = address(0x2); // Simulated OPERATOR2 address @@ -54,13 +68,26 @@ contract BootstrapTest is Test { // first deploy the token myToken = new MyToken("MyToken", "MYT", 18, addrs, 1000 * 10 ** 18); whitelistTokens.push(address(myToken)); + appendedToken = new MyToken("MyToken2", "MYT2", 18, addrs, 1000 * 10 ** 18); + appendedWhitelistTokensForUpgrade.push(address(appendedToken)); + + /// deploy vault implementationcontract that has logics called by proxy + vaultImplementation = new Vault(); + + /// deploy the vault beacon that store the implementation contract address + vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); + // then the ProxyAdmin proxyAdmin = new CustomProxyAdmin(); // then the logic clientChainLzEndpoint = new NonShortCircuitEndpointV2Mock( clientChainId, exocoreValidatorSet ); - Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint)); + Bootstrap bootstrapLogic = new Bootstrap( + address(clientChainLzEndpoint), + exocoreChainId, + address(vaultBeacon) + ); // then the params + proxy spawnTime = block.timestamp + 1 hours; offsetDuration = 30 minutes; @@ -69,7 +96,7 @@ contract BootstrapTest is Test { new TransparentUpgradeableProxy( address(bootstrapLogic), address(proxyAdmin), abi.encodeCall(bootstrap.initialize, - (deployer, spawnTime, offsetDuration, exocoreChainId, + (deployer, spawnTime, offsetDuration, payable(exocoreValidatorSet), whitelistTokens, address(proxyAdmin)) ) @@ -77,36 +104,42 @@ contract BootstrapTest is Test { )) ); // validate the initialization - assertTrue(bootstrap.whitelistTokens(address(myToken))); - assertFalse(bootstrap.whitelistTokens(address(0xa))); + assertTrue(bootstrap.isWhitelistedToken(address(myToken))); + assertFalse(bootstrap.isWhitelistedToken(address(0xa))); assertTrue(bootstrap.getWhitelistedTokensCount() == 1); assertFalse(bootstrap.bootstrapped()); - assertTrue(bootstrap.whiteListFunctionSelectors(GatewayStorage.Action.MARK_BOOTSTRAP) != bytes4(0)); - // any one case - assertTrue(bootstrap.whiteListFunctionSelectors(GatewayStorage.Action.REQUEST_DEPOSIT) == bytes4(0)); proxyAdmin.initialize(address(bootstrap)); // deployer is the owner - Vault vaultLogic = new Vault(); - Vault vault = Vault(address(new TransparentUpgradeableProxy( - address(vaultLogic), address(proxyAdmin), "" - ))); - vault.initialize(address(myToken), address(bootstrap)); - vaults.push(address(vault)); - bootstrap.addTokenVaults(vaults); - assertTrue(address(bootstrap.tokenVaults(address(myToken))) == address(vault)); + address expectedVaultAddress = Create2.computeAddress( + bytes32(uint256(uint160(address(myToken)))), + keccak256(abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(vaultBeacon), ""))), + address(bootstrap) + ); + assertTrue(address(bootstrap.tokenToVault(address(myToken))) == expectedVaultAddress); // now set the gateway address for Exocore. clientChainLzEndpoint.setDestLzEndpoint( undeployedExocoreGateway, undeployedExocoreLzEndpoint ); bootstrap.setPeer(exocoreChainId, bytes32(bytes20(undeployedExocoreGateway))); // lastly set up the upgrade params + + // deploy capsule implementation contract that has logics called by proxy + capsuleImplementation = new ExoCapsule(); + + // deploy the capsule beacon that store the implementation contract address + capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); + ClientChainGateway clientGatewayLogic = new ClientChainGateway( - address(clientChainLzEndpoint) + address(clientChainLzEndpoint), + exocoreChainId, + address(0x1), + address(vaultBeacon), + address(capsuleBeacon) ); // uint256 tokenCount = bootstrap.getWhitelistedTokensCount(); // address[] memory tokensForCall = new address[](tokenCount); // for (uint256 i = 0; i < tokenCount; i++) { - // tokensForCall[i] = bootstrap.whitelistTokensArray(i); + // tokensForCall[i] = bootstrap.whitelistTokens(i); // } bytes memory initialization = abi.encodeCall( clientGatewayLogic.initialize, @@ -114,9 +147,8 @@ contract BootstrapTest is Test { // bootstrap.exocoreChainId(), // bootstrap.exocoreValidatorSetAddress(), // tokensForCall - exocoreChainId, payable(exocoreValidatorSet), - whitelistTokens + appendedWhitelistTokensForUpgrade ) ); bootstrap.setClientChainGatewayLogic( @@ -131,7 +163,7 @@ contract BootstrapTest is Test { MyToken myTokenClone = new MyToken("MyToken", "MYT", 18, addrs, 1000 * 10 ** 18); bootstrap.addWhitelistToken(address(myTokenClone)); vm.stopPrank(); - assertTrue(bootstrap.whitelistTokens(address(myTokenClone))); + assertTrue(bootstrap.isWhitelistedToken(address(myTokenClone))); assertTrue(bootstrap.getWhitelistedTokensCount() == 2); return myTokenClone; } @@ -158,7 +190,8 @@ contract BootstrapTest is Test { // Make deposits and check values for (uint256 i = 0; i < 6; i++) { vm.startPrank(addrs[i]); - myToken.approve(vaults[0], amounts[i]); + Vault vault = Vault(address(bootstrap.tokenToVault(address(myToken)))); + myToken.approve(address(vault), amounts[i]); uint256 prevDepositorsCount = bootstrap.getDepositorsCount(); bool prevIsDepositor = bootstrap.isDepositor(addrs[i]); uint256 prevBalance = myToken.balanceOf(addrs[i]); @@ -557,7 +590,7 @@ contract BootstrapTest is Test { bootstrap.addWhitelistToken(cloneAddress); vm.stopPrank(); // finally, check - bool isSupported = bootstrap.whitelistTokens(cloneAddress); + bool isSupported = bootstrap.isWhitelistedToken(cloneAddress); assertTrue(isSupported); } @@ -879,7 +912,7 @@ contract BootstrapTest is Test { ); uint256 prevTokenDeposit = bootstrap.depositsByToken(address(myToken)); uint256 prevVaultWithdrawable = Vault( - address(bootstrap.tokenVaults(address(myToken))) + address(bootstrap.tokenToVault(address(myToken))) ).withdrawableBalances(addrs[i]); bootstrap.withdrawPrincipleFromExocore(address(myToken), amounts[i]); uint256 postDeposit = bootstrap.totalDepositAmounts(addrs[i], address(myToken)); @@ -888,7 +921,7 @@ contract BootstrapTest is Test { ); uint256 postTokenDeposit = bootstrap.depositsByToken(address(myToken)); uint256 postVaultWithdrawable = Vault( - address(bootstrap.tokenVaults(address(myToken))) + address(bootstrap.tokenToVault(address(myToken))) ).withdrawableBalances(addrs[i]); assertTrue(postDeposit == prevDeposit - amounts[i]); assertTrue(postWithdrawable == prevWithdrawable - amounts[i]); @@ -980,16 +1013,13 @@ contract BootstrapTest is Test { function test12_MarkBootstrapped_AlreadyBootstrapped() public { test12_MarkBootstrapped(); - vm.startPrank(address(0x20)); - vm.expectEmit(address(bootstrap)); - emit BootstrapStorage.UnsupportedRequestEvent( - GatewayStorage.Action.MARK_BOOTSTRAP - ); - clientChainLzEndpoint.lzReceive( + vm.startPrank(address(clientChainLzEndpoint)); + vm.expectRevert(abi.encodeWithSelector(GatewayStorage.UnsupportedRequest.selector, GatewayStorage.Action.MARK_BOOTSTRAP)); + bootstrap.lzReceive( Origin(exocoreChainId, bytes32(bytes20(undeployedExocoreGateway)), uint64(2)), - address(bootstrap), generateUID(1), abi.encodePacked(GatewayStorage.Action.MARK_BOOTSTRAP, ""), + address(0), bytes("") ); vm.stopPrank(); @@ -1067,14 +1097,18 @@ contract BootstrapTest is Test { function test15_Initialize_OwnerZero() public { vm.startPrank(deployer); - Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint)); + Bootstrap bootstrapLogic = new Bootstrap( + address(clientChainLzEndpoint), + exocoreChainId, + address(vaultBeacon) + ); vm.expectRevert("Bootstrap: owner should not be empty"); Bootstrap( payable(address( new TransparentUpgradeableProxy( address(bootstrapLogic), address(proxyAdmin), abi.encodeCall(bootstrap.initialize, - (address(0x0), spawnTime, offsetDuration, exocoreChainId, + (address(0x0), spawnTime, offsetDuration, payable(exocoreValidatorSet), whitelistTokens, address(proxyAdmin)) ) @@ -1085,7 +1119,11 @@ contract BootstrapTest is Test { function test15_Initialize_SpawnTimeNotFuture() public { vm.startPrank(deployer); - Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint)); + Bootstrap bootstrapLogic = new Bootstrap( + address(clientChainLzEndpoint), + exocoreChainId, + address(vaultBeacon) + ); vm.warp(20); vm.expectRevert("Bootstrap: spawn time should be in the future"); Bootstrap( @@ -1093,7 +1131,7 @@ contract BootstrapTest is Test { new TransparentUpgradeableProxy( address(bootstrapLogic), address(proxyAdmin), abi.encodeCall(bootstrap.initialize, - (deployer, block.timestamp - 10, offsetDuration, exocoreChainId, + (deployer, block.timestamp - 10, offsetDuration, payable(exocoreValidatorSet), whitelistTokens, address(proxyAdmin)) ) @@ -1104,14 +1142,18 @@ contract BootstrapTest is Test { function test15_Initialize_OffsetDurationZero() public { vm.startPrank(deployer); - Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint)); + Bootstrap bootstrapLogic = new Bootstrap( + address(clientChainLzEndpoint), + exocoreChainId, + address(vaultBeacon) + ); vm.expectRevert("Bootstrap: offset duration should be greater than 0"); Bootstrap( payable(address( new TransparentUpgradeableProxy( address(bootstrapLogic), address(proxyAdmin), abi.encodeCall(bootstrap.initialize, - (deployer, spawnTime, 0, exocoreChainId, + (deployer, spawnTime, 0, payable(exocoreValidatorSet), whitelistTokens, address(proxyAdmin)) ) @@ -1122,7 +1164,11 @@ contract BootstrapTest is Test { function test15_Initialize_SpawnTimeLTOffsetDuration() public { vm.startPrank(deployer); - Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint)); + Bootstrap bootstrapLogic = new Bootstrap( + address(clientChainLzEndpoint), + exocoreChainId, + address(vaultBeacon) + ); vm.expectRevert("Bootstrap: spawn time should be greater than offset duration"); vm.warp(20); Bootstrap( @@ -1130,7 +1176,7 @@ contract BootstrapTest is Test { new TransparentUpgradeableProxy( address(bootstrapLogic), address(proxyAdmin), abi.encodeCall(bootstrap.initialize, - (deployer, 21, 22, exocoreChainId, + (deployer, 21, 22, payable(exocoreValidatorSet), whitelistTokens, address(proxyAdmin)) ) @@ -1141,7 +1187,11 @@ contract BootstrapTest is Test { function test15_Initialize_LockTimeNotFuture() public { vm.startPrank(deployer); - Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint)); + Bootstrap bootstrapLogic = new Bootstrap( + address(clientChainLzEndpoint), + exocoreChainId, + address(vaultBeacon) + ); vm.expectRevert("Bootstrap: lock time should be in the future"); vm.warp(20); Bootstrap( @@ -1149,25 +1199,7 @@ contract BootstrapTest is Test { new TransparentUpgradeableProxy( address(bootstrapLogic), address(proxyAdmin), abi.encodeCall(bootstrap.initialize, - (deployer, 21, 9, exocoreChainId, - payable(exocoreValidatorSet), whitelistTokens, - address(proxyAdmin)) - ) - ) - )) - ); - } - - function test15_Initialize_ExocoreChainIdZero() public { - vm.startPrank(deployer); - Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint)); - vm.expectRevert("Bootstrap: exocore chain id should not be empty"); - Bootstrap( - payable(address( - new TransparentUpgradeableProxy( - address(bootstrapLogic), address(proxyAdmin), - abi.encodeCall(bootstrap.initialize, - (deployer, spawnTime, offsetDuration, 0, + (deployer, 21, 9, payable(exocoreValidatorSet), whitelistTokens, address(proxyAdmin)) ) @@ -1178,14 +1210,18 @@ contract BootstrapTest is Test { function test15_Initialize_ExocoreValSetZero() public { vm.startPrank(deployer); - Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint)); + Bootstrap bootstrapLogic = new Bootstrap( + address(clientChainLzEndpoint), + exocoreChainId, + address(vaultBeacon) + ); vm.expectRevert("Bootstrap: exocore validator set address should not be empty"); Bootstrap( payable(address( new TransparentUpgradeableProxy( address(bootstrapLogic), address(proxyAdmin), abi.encodeCall(bootstrap.initialize, - (deployer, spawnTime, offsetDuration, exocoreChainId, + (deployer, spawnTime, offsetDuration, payable(address(0)), whitelistTokens, address(proxyAdmin)) ) @@ -1196,14 +1232,18 @@ contract BootstrapTest is Test { function test15_Initialize_CustomProxyAdminZero() public { vm.startPrank(deployer); - Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint)); + Bootstrap bootstrapLogic = new Bootstrap( + address(clientChainLzEndpoint), + exocoreChainId, + address(vaultBeacon) + ); vm.expectRevert("Bootstrap: custom proxy admin should not be empty"); Bootstrap( payable(address( new TransparentUpgradeableProxy( address(bootstrapLogic), address(proxyAdmin), abi.encodeCall(bootstrap.initialize, - (deployer, spawnTime, offsetDuration, exocoreChainId, + (deployer, spawnTime, offsetDuration, payable(exocoreValidatorSet), whitelistTokens, address(0x0)) ) @@ -1263,7 +1303,7 @@ contract BootstrapTest is Test { function test18_RemoveWhitelistToken() public { vm.startPrank(deployer); bootstrap.removeWhitelistToken(address(myToken)); - assertFalse(bootstrap.whitelistTokens(address(myToken))); + assertFalse(bootstrap.isWhitelistedToken(address(myToken))); assertTrue(bootstrap.getWhitelistedTokensCount() == 0); } @@ -1274,59 +1314,11 @@ contract BootstrapTest is Test { bootstrap.removeWhitelistToken(fakeToken); } - function test19_AddTokenVaults() public { - MyToken myTokenClone = test01_AddWhitelistToken(); - Vault vaultLogic = new Vault(); - Vault vault = Vault(address(new TransparentUpgradeableProxy( - address(vaultLogic), address(proxyAdmin), "" - ))); - vault.initialize(address(myTokenClone), address(bootstrap)); - address[] memory localVaults = new address[](1); - localVaults[0] = address(vault); - vm.startPrank(deployer); - bootstrap.addTokenVaults(localVaults); - assertTrue(address(bootstrap.tokenVaults(address(myTokenClone))) == address(vault)); - } - - function test19_AddTokenVaults_UnauthorizedToken() public { - vm.startPrank(deployer); - MyToken myTokenClone = new MyToken("MyToken", "MYT", 18, addrs, 1000 * 10 ** 18); - Vault vaultLogic = new Vault(); - Vault vault = Vault(address(new TransparentUpgradeableProxy( - address(vaultLogic), address(proxyAdmin), "" - ))); - vault.initialize(address(myTokenClone), address(bootstrap)); - address[] memory localVaults = new address[](1); - localVaults[0] = address(vault); - vm.expectRevert(abi.encodeWithSignature("UnauthorizedToken()")); - bootstrap.addTokenVaults(localVaults); - } - - function test19_AddTokenVaults_VaultAlreadyAdded() public { - vm.startPrank(deployer); - Vault vaultLogic = new Vault(); - Vault vault = Vault(address(new TransparentUpgradeableProxy( - address(vaultLogic), address(proxyAdmin), "" - ))); - vault.initialize(address(myToken), address(bootstrap)); - address[] memory localVaults = new address[](1); - localVaults[0] = address(vault); - vm.expectRevert(abi.encodeWithSignature("VaultAlreadyAdded()")); - bootstrap.addTokenVaults(localVaults); - } - function test20_WithdrawRewardFromExocore() public { vm.expectRevert(abi.encodeWithSignature("NotYetSupported()")); bootstrap.withdrawRewardFromExocore(address(0x0), 1); } - function test21_UpdateUsersBalances() public { - vm.expectRevert(abi.encodeWithSignature("NotYetSupported()")); - IController.UserBalanceUpdateInfo[] memory x = - new IController.UserBalanceUpdateInfo[](1); - bootstrap.updateUsersBalances(x); - } - function test22_Claim() public { test11_WithdrawPrincipleFromExocore(); for(uint256 i = 0; i < 6; i++) { diff --git a/test/foundry/ClientChainGateway.t.sol b/test/foundry/ClientChainGateway.t.sol index 45c58be5..06bfca75 100644 --- a/test/foundry/ClientChainGateway.t.sol +++ b/test/foundry/ClientChainGateway.t.sol @@ -17,7 +17,6 @@ import {EndpointV2Mock} from "../mocks/EndpointV2Mock.sol"; import "../../src/interfaces/precompiles/IDelegation.sol"; import "../../src/interfaces/precompiles/IDeposit.sol"; import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; -import "../../src/interfaces/ITSSReceiver.sol"; import "../../src/interfaces/IVault.sol"; import "../../src/interfaces/IExoCapsule.sol"; @@ -166,9 +165,5 @@ contract ClientChainGatewayTest is Test { vm.expectRevert(EnforcedPause.selector); clientGateway.undelegateFrom(operatorAddress, address(restakeToken), uint256(1)); - - vm.expectRevert(EnforcedPause.selector); - ITSSReceiver.InterchainMsg memory msg_; - clientGateway.receiveInterchainMsg(msg_, bytes("")); } } diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index 5aabc255..9088deef 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -4,7 +4,6 @@ import "./ExocoreDeployer.t.sol"; import "forge-std/Test.sol"; import "../../src/core/ExocoreGateway.sol"; import "../../src/storage/GatewayStorage.sol"; -import "../../src/interfaces/ITSSReceiver.sol"; import "../../src/core/ExoCapsule.sol"; import {ILSTRestakingController} from "../../src/interfaces/ILSTRestakingController.sol"; @@ -341,152 +340,6 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { vm.stopPrank(); } - function test_TSSReceiver() public { - Player memory depositor = players[0]; - Player memory relayer = players[1]; - uint256 depositAmount = 10000; - uint256 withdrawAmount = 100; - uint256 lastlyUpdatedPrincipleBalance; - - vm.startPrank(exocoreValidatorSet.addr); - restakeToken.transfer(depositor.addr, 1000000); - vm.stopPrank(); - deal(depositor.addr, 1e22); - deal(relayer.addr, 1e22); - deal(address(clientGateway), 1e22); - deal(address(exocoreGateway), 1e22); - - // -- deposit workflow test -- - - vm.startPrank(depositor.addr); - restakeToken.approve(address(vault), type(uint256).max); - - // first user call client chain gateway to deposit - - // estimate l0 relay fee that the user should pay - bytes memory depositRequestPayload = abi.encodePacked( - GatewayStorage.Action.REQUEST_DEPOSIT, - bytes32(bytes20(address(restakeToken))), - bytes32(bytes20(depositor.addr)), - depositAmount - ); - uint256 depositRequestNativeFee = clientGateway.quote(depositRequestPayload); - bytes32 depositRequestId = generateUID(1, true); - // depositor should transfer deposited token to vault - vm.expectEmit(true, true, false, true, address(restakeToken)); - emit Transfer(depositor.addr, address(vault), depositAmount); - // client chain layerzero endpoint should emit the message packet including deposit payload. - vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); - emit NewPacket( - exocoreChainId, - address(clientGateway), - address(exocoreGateway).toBytes32(), - uint64(1), - depositRequestPayload - ); - // client chain gateway should emit MessageSent event - vm.expectEmit(true, true, true, true, address(clientGateway)); - emit MessageSent(GatewayStorage.Action.REQUEST_DEPOSIT, depositRequestId, uint64(1), depositRequestNativeFee); - clientGateway.deposit{value: depositRequestNativeFee}(address(restakeToken), depositAmount); - - // second layerzero relayers should watch the request message packet and relay the message to destination endpoint - - // exocore gateway should return response message to exocore network layerzero endpoint - vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); - lastlyUpdatedPrincipleBalance = depositAmount; - bytes memory depositResponsePayload = - abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true, lastlyUpdatedPrincipleBalance); - uint256 depositResponseNativeFee = exocoreGateway.quote(clientChainId, depositResponsePayload); - bytes32 depositResponseId = generateUID(1, false); - emit NewPacket( - clientChainId, - address(exocoreGateway), - address(clientGateway).toBytes32(), - uint64(1), - depositResponsePayload - ); - // exocore gateway should emit MessageSent event - vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit MessageSent(GatewayStorage.Action.RESPOND, depositResponseId, uint64(1), depositResponseNativeFee); - exocoreLzEndpoint.lzReceive( - Origin(clientChainId, address(clientGateway).toBytes32(), uint64(1)), - address(exocoreGateway), - depositRequestId, - depositRequestPayload, - bytes("") - ); - - // third layerzero relayers should watch the response message packet and relay the message to source chain endpoint - - // client chain gateway should execute the response hook and emit depositResult event - vm.expectEmit(true, true, true, true, address(clientGateway)); - emit DepositResult(true, address(restakeToken), depositor.addr, depositAmount); - clientChainLzEndpoint.lzReceive( - Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(1)), - address(clientGateway), - depositResponseId, - depositResponsePayload, - bytes("") - ); - - assertUpdateBalances(relayer, depositor, depositAmount, withdrawAmount); - } - - function assertUpdateBalances( - Player memory relayer, - Player memory depositor, - uint256 depositAmount, - uint256 withdrawAmount - ) internal { - vm.chainId(clientChainId); - vm.startPrank(relayer.addr); - ILSTRestakingController.TokenBalanceUpdateInfo[] memory tokenBalances = new ILSTRestakingController.TokenBalanceUpdateInfo[](1); - tokenBalances[0] = ILSTRestakingController.TokenBalanceUpdateInfo({ - token: address(restakeToken), - lastlyUpdatedPrincipleBalance: depositAmount - withdrawAmount, - lastlyUpdatedRewardBalance: 0, - unlockPrincipleAmount: withdrawAmount, - unlockRewardAmount: 0 - }); - ILSTRestakingController.UserBalanceUpdateInfo[] memory userBalances = new ILSTRestakingController.UserBalanceUpdateInfo[](1); - userBalances[0] = - ILSTRestakingController.UserBalanceUpdateInfo({user: depositor.addr, updatedAt: 1, tokenBalances: tokenBalances}); - (ITSSReceiver.InterchainMsg memory _msg, bytes memory signature) = prepareEVSMsgAndSignature(userBalances); - - vm.expectEmit(false, false, false, true, address(clientGateway)); - emit MessageProcessed(exocoreChainId, bytes("0x"), 1, _msg.payload); - clientGateway.receiveInterchainMsg(_msg, signature); - assertEq(vault.withdrawableBalances(depositor.addr), withdrawAmount); - assertEq(vault.principleBalances(depositor.addr), depositAmount - withdrawAmount); - assertEq(vault.rewardBalances(depositor.addr), 0); - assertEq(vault.totalDepositedPrincipleAmount(depositor.addr), depositAmount); - assertEq(vault.totalUnlockPrincipleAmount(depositor.addr), withdrawAmount); - } - - function prepareEVSMsgAndSignature(ILSTRestakingController.UserBalanceUpdateInfo[] memory userBalances) - internal - view - returns (ITSSReceiver.InterchainMsg memory _msg, bytes memory signature) - { - bytes memory args = abi.encode(userBalances); - bytes memory payload = abi.encodePacked(GatewayStorage.Action.UPDATE_USERS_BALANCES, args); - _msg = ITSSReceiver.InterchainMsg({ - srcChainID: exocoreChainId, - srcAddress: bytes("0x"), - dstChainID: clientChainId, - dstAddress: abi.encodePacked(bytes20(address(clientGateway))), - nonce: 1, - payload: payload - }); - bytes32 digest = keccak256( - abi.encodePacked( - _msg.srcChainID, _msg.srcAddress, _msg.dstChainID, _msg.dstAddress, _msg.nonce, _msg.payload - ) - ); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(exocoreValidatorSet.privateKey, digest); - signature = abi.encodePacked(r, s, v); - } - function generateUID(uint64 nonce, bool fromClientChainToExocore) internal view returns (bytes32 uid) { if (fromClientChainToExocore) { uid = GUID.generate( diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index 51a683ca..7f8689ec 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -167,7 +167,7 @@ contract ExocoreDeployer is Test { ); // find vault according to uderlying token address - vault = Vault(address(clientGateway.tokenVaults(address(restakeToken)))); + vault = Vault(address(clientGateway.tokenToVault(address(restakeToken)))); // deploy Exocore network contracts exocoreGatewayLogic = new ExocoreGateway(address(exocoreLzEndpoint)); diff --git a/test/foundry/ExocoreGateway.t.sol b/test/foundry/ExocoreGateway.t.sol index eb815183..7b7a89aa 100644 --- a/test/foundry/ExocoreGateway.t.sol +++ b/test/foundry/ExocoreGateway.t.sol @@ -12,7 +12,6 @@ import "forge-std/Test.sol"; import "../../src/interfaces/precompiles/IDelegation.sol"; import "../../src/interfaces/precompiles/IDeposit.sol"; import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; -import "../../src/interfaces/ITSSReceiver.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; contract ExocoreGatewayTest is Test { diff --git a/test/foundry/WithdrawReward.t.sol b/test/foundry/WithdrawReward.t.sol index b9c5a2d4..9db86028 100644 --- a/test/foundry/WithdrawReward.t.sol +++ b/test/foundry/WithdrawReward.t.sol @@ -4,7 +4,6 @@ import "./ExocoreDeployer.t.sol"; import "forge-std/Test.sol"; import "../../src/core/ExocoreGateway.sol"; import "../../src/storage/GatewayStorage.sol"; -import "../../src/interfaces/ITSSReceiver.sol"; import "forge-std/console.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; From 2ef02eb1edfc5b17c245122d5364ffe8ab1a306f Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 10 May 2024 18:24:43 +0800 Subject: [PATCH 32/93] reuse _getVault and rename exocoreAddressIsValid --- src/core/BaseRestakingController.sol | 4 ++-- src/core/Bootstrap.sol | 7 ++----- src/storage/BootstrapStorage.sol | 8 ++++++++ src/storage/ClientChainGatewayStorage.sol | 8 -------- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index 317d667a..2d643d06 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -36,7 +36,7 @@ abstract contract BaseRestakingController is } modifier isValidBech32Address(string calldata exocoreAddress) { - require(exocoreAddressIsValid(exocoreAddress), "BaseRestakingController: invalid bech32 encoded Exocore address"); + require(isValidExocoreAddress(exocoreAddress), "BaseRestakingController: invalid bech32 encoded Exocore address"); _; } @@ -101,7 +101,7 @@ abstract contract BaseRestakingController is emit MessageSent(act, receipt.guid, receipt.nonce, receipt.fee.nativeFee); } - function exocoreAddressIsValid( + function isValidExocoreAddress( string calldata operatorExocoreAddress ) public pure returns (bool) { bytes memory stringBytes = bytes(operatorExocoreAddress); diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index bb67285e..4204ed85 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -425,11 +425,8 @@ contract Bootstrap is ) view internal returns (IVault) { require(isWhitelistedToken[token], "Bootstrap: token is not whitelisted"); require(amount > 0, "Bootstrap: amount should be greater than zero"); - IVault vault = tokenToVault[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - return vault; + + return _getVault(token); } // implementation of IController diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index 86a45360..24a5d787 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -398,4 +398,12 @@ contract BootstrapStorage is GatewayStorage { exocoreChainId = exocoreChainId_; vaultBeacon = IBeacon(vaultBeacon_); } + + function _getVault(address token) internal view returns (IVault) { + IVault vault = tokenToVault[token]; + if (address(vault) == address(0)) { + revert VaultNotExist(); + } + return vault; + } } \ No newline at end of file diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 034ec635..349182e5 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -69,14 +69,6 @@ contract ClientChainGatewayStorage is BootstrapStorage { exoCapsuleBeacon = IBeacon(exoCapsuleBeacon_); } - function _getVault(address token) internal view returns (IVault) { - IVault vault = tokenToVault[token]; - if (address(vault) == address(0)) { - revert VaultNotExist(); - } - return vault; - } - function _getCapsule(address owner) internal view returns (IExoCapsule) { IExoCapsule capsule = ownerToCapsule[owner]; if (address(capsule) == address(0)) { From bd0aea8e174c7b04f7bb99f445de9b0b4965e8b6 Mon Sep 17 00:00:00 2001 From: adu Date: Sat, 11 May 2024 09:20:50 +0800 Subject: [PATCH 33/93] reuse modifiers --- src/core/BaseRestakingController.sol | 36 ----------------------- src/core/ClientChainGateway.sol | 3 +- src/storage/ClientChainGatewayStorage.sol | 36 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index 2d643d06..3a53486d 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -20,26 +20,6 @@ abstract contract BaseRestakingController is receive() external payable {} - modifier isTokenWhitelisted(address token) { - require(isWhitelistedToken[token], "BaseRestakingController: token is not whitelisted"); - _; - } - - modifier isValidAmount(uint256 amount) { - require(amount > 0, "BaseRestakingController: amount should be greater than zero"); - _; - } - - modifier vaultExists(address token) { - require(address(tokenToVault[token]) != address(0), "BaseRestakingController: no vault added for this token"); - _; - } - - modifier isValidBech32Address(string calldata exocoreAddress) { - require(isValidExocoreAddress(exocoreAddress), "BaseRestakingController: invalid bech32 encoded Exocore address"); - _; - } - function claim(address token, uint256 amount, address recipient) external isTokenWhitelisted(token) @@ -100,20 +80,4 @@ abstract contract BaseRestakingController is _lzSend(exocoreChainId, payload, options, MessagingFee(fee.nativeFee, 0), exocoreValidatorSetAddress, false); emit MessageSent(act, receipt.guid, receipt.nonce, receipt.fee.nativeFee); } - - function isValidExocoreAddress( - string calldata operatorExocoreAddress - ) public pure returns (bool) { - bytes memory stringBytes = bytes(operatorExocoreAddress); - if (stringBytes.length != 42) { - return false; - } - for (uint i = 0; i < EXO_ADDRESS_PREFIX.length; i++) { - if (stringBytes[i] != EXO_ADDRESS_PREFIX[i]) { - return false; - } - } - - return true; - } } diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index 51631323..f6049efc 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -168,8 +168,7 @@ contract ClientChainGateway is } } - function removeWhitelistToken(address _token) external onlyOwner whenNotPaused { - require(isWhitelistedToken[_token], "ClientChainGateway: token should be already whitelisted"); + function removeWhitelistToken(address _token) external isTokenWhitelisted(_token) onlyOwner whenNotPaused { isWhitelistedToken[_token] = false; for(uint i = 0; i < whitelistTokens.length; i++) { if (whitelistTokens[i] == _token) { diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 349182e5..904e7106 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -56,6 +56,26 @@ contract ClientChainGatewayStorage is BootstrapStorage { error CapsuleNotExist(); + modifier isTokenWhitelisted(address token) { + require(isWhitelistedToken[token], "BaseRestakingController: token is not whitelisted"); + _; + } + + modifier isValidAmount(uint256 amount) { + require(amount > 0, "BaseRestakingController: amount should be greater than zero"); + _; + } + + modifier vaultExists(address token) { + require(address(tokenToVault[token]) != address(0), "BaseRestakingController: no vault added for this token"); + _; + } + + modifier isValidBech32Address(string calldata exocoreAddress) { + require(_isValidExocoreAddress(exocoreAddress), "BaseRestakingController: invalid bech32 encoded Exocore address"); + _; + } + constructor( uint32 exocoreChainId_, address beaconOracleAddress_, @@ -69,6 +89,22 @@ contract ClientChainGatewayStorage is BootstrapStorage { exoCapsuleBeacon = IBeacon(exoCapsuleBeacon_); } + function _isValidExocoreAddress( + string calldata operatorExocoreAddress + ) public pure returns (bool) { + bytes memory stringBytes = bytes(operatorExocoreAddress); + if (stringBytes.length != 42) { + return false; + } + for (uint i = 0; i < EXO_ADDRESS_PREFIX.length; i++) { + if (stringBytes[i] != EXO_ADDRESS_PREFIX[i]) { + return false; + } + } + + return true; + } + function _getCapsule(address owner) internal view returns (IExoCapsule) { IExoCapsule capsule = ownerToCapsule[owner]; if (address(capsule) == address(0)) { From c28f379390cffeec48aff5e204912b14df02a15c Mon Sep 17 00:00:00 2001 From: bwhour Date: Mon, 13 May 2024 15:10:50 +0800 Subject: [PATCH 34/93] fix some warnings and reuse some code --- src/core/BaseRestakingController.sol | 66 ++++++++++++++++++-------- src/core/Bootstrap.sol | 1 + src/core/ClientChainGateway.sol | 1 + src/core/ExoCapsule.sol | 2 +- src/core/LSTRestakingController.sol | 20 ++------ src/core/NativeRestakingController.sol | 9 +--- 6 files changed, 53 insertions(+), 46 deletions(-) diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index 3a53486d..1ca4345a 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -44,13 +44,8 @@ abstract contract BaseRestakingController is isValidAmount(amount) isValidBech32Address(operator) whenNotPaused { - _getVault(token); - _registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); - _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DELEGATE_TO; + _processRequest(token, msg.sender, amount, Action.REQUEST_DELEGATE_TO, operator); - bytes memory actionArgs = - abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), bytes(operator), amount); - _sendMsgToExocore(Action.REQUEST_DELEGATE_TO, actionArgs); } function undelegateFrom(string calldata operator, address token, uint256 amount) @@ -59,25 +54,56 @@ abstract contract BaseRestakingController is isValidAmount(amount) isValidBech32Address(operator) whenNotPaused { - _getVault(token); - _registeredRequests[outboundNonce + 1] = abi.encode(token, operator, msg.sender, amount); - _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_UNDELEGATE_FROM; + _processRequest(token, msg.sender, amount, Action.REQUEST_UNDELEGATE_FROM, operator); - bytes memory actionArgs = - abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), bytes(operator), amount); - _sendMsgToExocore(Action.REQUEST_UNDELEGATE_FROM, actionArgs); } - function _sendMsgToExocore(Action act, bytes memory actionArgs) internal { + function _processRequest( + address token, + address sender, + uint256 amount, + Action action, + string memory operator // Optional parameter, you can pass an empty string if you don't need it. + ) internal { + if (token != VIRTUAL_STAKED_ETH_ADDRESS) { + IVault vault = _getVault(token); + // Logic specific to the REQUEST_DEPOSIT action + if (action == Action.REQUEST_DEPOSIT && bytes(operator).length == 0) { + vault.deposit(sender, amount); + } + } outboundNonce++; - bytes memory payload = abi.encodePacked(act, actionArgs); - bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption( - DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE - ).addExecutorOrderedExecutionOption(); + // Determine how to code _registeredRequests based on whether or not an operator is provided + if (bytes(operator).length > 0) { + _registeredRequests[outboundNonce] = abi.encode(token, operator, sender, amount); + } else { + _registeredRequests[outboundNonce] = abi.encode(token, sender, amount); + } + _registeredRequestActions[outboundNonce] = action; + // Consider whether operator is empty when building actionArgs + bytes memory actionArgs; + if (bytes(operator).length > 0) { + actionArgs = abi.encodePacked( + bytes32(bytes20(token)), + bytes32(bytes20(sender)), + bytes(operator), + amount + ); + } else { + actionArgs = abi.encodePacked( + bytes32(bytes20(token)), + bytes32(bytes20(sender)), + amount + ); + } + _sendMsgToExocore(action, actionArgs); + } + function _sendMsgToExocore(Action action, bytes memory actionArgs) internal { + bytes memory payload = abi.encodePacked(action, actionArgs); + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption(DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE).addExecutorOrderedExecutionOption(); MessagingFee memory fee = _quote(exocoreChainId, payload, options, false); - MessagingReceipt memory receipt = - _lzSend(exocoreChainId, payload, options, MessagingFee(fee.nativeFee, 0), exocoreValidatorSetAddress, false); - emit MessageSent(act, receipt.guid, receipt.nonce, receipt.fee.nativeFee); + MessagingReceipt memory receipt = _lzSend(exocoreChainId, payload, options, MessagingFee(fee.nativeFee, 0), exocoreValidatorSetAddress, false); + emit MessageSent(action, receipt.guid, receipt.nonce, receipt.fee.nativeFee); } } diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index 4204ed85..5b263918 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -676,5 +676,6 @@ contract Bootstrap is emit VaultCreated(underlyingToken, address(vault)); tokenToVault[underlyingToken] = vault; + return vault; } } \ No newline at end of file diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index f6049efc..aff27e9d 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -217,5 +217,6 @@ contract ClientChainGateway is emit VaultCreated(underlyingToken, address(vault)); tokenToVault[underlyingToken] = vault; + return vault; } } diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 8faae6a8..9b3f66b0 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -102,7 +102,7 @@ contract ExoCapsule is ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof - ) external onlyGateway { + ) external view onlyGateway { bytes32 validatorPubkey = validatorContainer.getPubkey(); uint64 withdrawableEpoch = validatorContainer.getWithdrawableEpoch(); diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index 7009b225..58702ec8 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -13,31 +13,17 @@ abstract contract LSTRestakingController is BaseRestakingController { function deposit(address token, uint256 amount) external payable isTokenWhitelisted(token) isValidAmount(amount) whenNotPaused { - IVault vault = _getVault(token); - vault.deposit(msg.sender, amount); - _registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, amount); - _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DEPOSIT; + _processRequest(token, msg.sender, amount, Action.REQUEST_DEPOSIT,""); - bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), amount); - _sendMsgToExocore(Action.REQUEST_DEPOSIT, actionArgs); } function withdrawPrincipleFromExocore(address token, uint256 principleAmount) external payable isTokenWhitelisted(token) isValidAmount(principleAmount) whenNotPaused { - _getVault(token); - _registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, principleAmount); - _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE; + _processRequest(token, msg.sender, principleAmount, Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE,""); - bytes memory actionArgs = - abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), principleAmount); - _sendMsgToExocore(Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, actionArgs); } function withdrawRewardFromExocore(address token, uint256 rewardAmount) external payable isTokenWhitelisted(token) isValidAmount(rewardAmount) whenNotPaused { - _getVault(token); - _registeredRequests[outboundNonce + 1] = abi.encode(token, msg.sender, rewardAmount); - _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE; + _processRequest(token, msg.sender, rewardAmount, Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE,""); - bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(msg.sender)), rewardAmount); - _sendMsgToExocore(Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, actionArgs); } } diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 197a00b3..03903875 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -54,16 +54,9 @@ abstract contract NativeRestakingController is capsule.verifyDepositProof(validatorContainer, proof); uint256 depositValue = uint256(validatorContainer.getEffectiveBalance()) * GWEI_TO_WEI; - _registeredRequests[outboundNonce + 1] = abi.encode(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, depositValue); - _registeredRequestActions[outboundNonce + 1] = Action.REQUEST_DEPOSIT; + _processRequest(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, depositValue, Action.REQUEST_DEPOSIT, ""); - bytes memory actionArgs = abi.encodePacked( - bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), - bytes32(bytes20(msg.sender)), - depositValue - ); - _sendMsgToExocore(Action.REQUEST_DEPOSIT, actionArgs); } function processBeaconChainPartialWithdrawal( From eba19b62ddbc47d17c73b510f682bd2b666b0e30 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 13 May 2024 14:02:43 -0400 Subject: [PATCH 35/93] fix: add prettier for audit, implement non beacon chain ETH withdraw workflow --- .prettierrc | 17 +++++ package-lock.json | 44 +++++++++++- package.json | 5 +- src/core/ExoCapsule.sol | 48 ++++++++++--- src/core/NativeRestakingController.sol | 23 ++++--- src/interfaces/IExoCapsule.sol | 9 ++- test/foundry/DepositWithdrawPrinciple.t.sol | 69 +++++++++++++------ test/foundry/ExoCapsule.t.sol | 76 +++++++++++++++------ 8 files changed, 222 insertions(+), 69 deletions(-) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..d05d9551 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,17 @@ +{ + "plugins": ["prettier-plugin-solidity"], + "printWidth": 120, + "tabWidth": 2, + "overrides": [ + { + "files": "*.sol", + "options": { + "printWidth": 120, + "tabWidth": 4, + "useTabs": false, + "singleQuote": false, + "bracketSpacing": false + } + } + ] +} diff --git a/package-lock.json b/package-lock.json index bdd0cb26..ef039303 100644 --- a/package-lock.json +++ b/package-lock.json @@ -376,7 +376,8 @@ "@nomicfoundation/hardhat-foundry": "^1.1.1", "@nomicfoundation/hardhat-toolbox": "^4.0.0", "hardhat": "^2.19.3", - "mocha": "^10.2.0" + "mocha": "^10.2.0", + "prettier-plugin-solidity": "^1.3.1" }, "optionalDependencies": { "fsevents": "^2.3.3" @@ -5392,6 +5393,41 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-solidity": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.3.1.tgz", + "integrity": "sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA==", + "dev": true, + "dependencies": { + "@solidity-parser/parser": "^0.17.0", + "semver": "^7.5.4", + "solidity-comments-extractor": "^0.0.8" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "prettier": ">=2.3.0" + } + }, + "node_modules/prettier-plugin-solidity/node_modules/@solidity-parser/parser": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.17.0.tgz", + "integrity": "sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw==", + "dev": true + }, + "node_modules/prettier-plugin-solidity/node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6018,6 +6054,12 @@ "semver": "bin/semver" } }, + "node_modules/solidity-comments-extractor": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/solidity-comments-extractor/-/solidity-comments-extractor-0.0.8.tgz", + "integrity": "sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g==", + "dev": true + }, "node_modules/solidity-coverage": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.5.tgz", diff --git a/package.json b/package.json index e0890ef9..023321a0 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "death": "^1.1.0", "debug": "^4.3.4", "decamelize": "^4.0.0", - "decimal.js":"10.4.3", + "decimal.js": "10.4.3", "deep-eql": "^4.1.3", "deep-extend": "^0.6.0", "deep-is": "^0.1.4", @@ -379,7 +379,8 @@ "@nomicfoundation/hardhat-foundry": "^1.1.1", "@nomicfoundation/hardhat-toolbox": "^4.0.0", "hardhat": "^2.19.3", - "mocha": "^10.2.0" + "mocha": "^10.2.0", + "prettier-plugin-solidity": "^1.3.1" }, "scripts": { "test": "mocha" diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 9b3f66b0..1464846a 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -11,11 +11,7 @@ import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -contract ExoCapsule is - Initializable, - ExoCapsuleStorage, - IExoCapsule -{ +contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { using BeaconChainProofs for bytes32; using ValidatorContainer for bytes32[]; using WithdrawalContainer for bytes32[]; @@ -23,6 +19,10 @@ contract ExoCapsule is event PrincipleBalanceUpdated(address, uint256); event WithdrawableBalanceUpdated(address, uint256); event WithdrawalSuccess(address, address, uint256); + /// @notice Emitted when ETH is received via the `receive` fallback + event NonBeaconChainETHReceived(uint256 amountReceived); + /// @notice Emitted when ETH that was previously received via the `receive` fallback is withdrawn + event NonBeaconChainETHWithdrawn(address indexed recipient, uint256 amountWithdrawn); error InvalidValidatorContainer(bytes32 pubkey); error InvalidWithdrawalContainer(uint64 validatorIndex); @@ -38,6 +38,9 @@ contract ExoCapsule is error InactiveValidatorContainer(bytes32 pubkey); error InvalidGateway(address, address); + /// @notice This variable tracks any ETH deposited into this contract via the `receive` fallback function + uint256 public nonBeaconChainETHBalance; + modifier onlyGateway() { if (msg.sender != address(gateway)) { revert InvalidGateway(address(gateway), msg.sender); @@ -49,6 +52,11 @@ contract ExoCapsule is _disableInitializers(); } + receive() external payable { + nonBeaconChainETHBalance += msg.value; + emit NonBeaconChainETHReceived(msg.value); + } + function initialize(address gateway_, address capsuleOwner_, address beaconOracle_) external initializer { require(gateway_ != address(0), "ExoCapsuleStorage: gateway address can not be empty"); require(capsuleOwner_ != address(0), "ExoCapsule: capsule owner address can not be empty"); @@ -169,6 +177,16 @@ contract ExoCapsule is emit WithdrawalSuccess(capsuleOwner, recipient, amount); } + /// @notice Called by the capsule owner to withdraw the nonBeaconChainETHBalance + function withdrawNonBeaconChainETHBalance(address recipient, uint256 amountToWithdraw) external onlyGateway { + require( + amountToWithdraw <= nonBeaconChainETHBalance, + "ExoCapsule.withdrawNonBeaconChainETHBalance: amountToWithdraw is greater than nonBeaconChainETHBalance" + ); + nonBeaconChainETHBalance -= amountToWithdraw; + emit NonBeaconChainETHWithdrawn(recipient, amountToWithdraw); + } + function updatePrincipleBalance(uint256 lastlyUpdatedPrincipleBalance) external onlyGateway { principleBalance = lastlyUpdatedPrincipleBalance; @@ -200,7 +218,10 @@ contract ExoCapsule is return root; } - function _verifyValidatorContainer(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) internal view { + function _verifyValidatorContainer( + bytes32[] calldata validatorContainer, + ValidatorContainerProof calldata proof + ) internal view { bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); bytes32 validatorContainerRoot = validatorContainer.merklelizeValidatorContainer(); bool valid = validatorContainerRoot.isValidValidatorContainerRoot( @@ -215,7 +236,10 @@ contract ExoCapsule is } } - function _verifyWithdrawalContainer(bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata proof) internal view { + function _verifyWithdrawalContainer( + bytes32[] calldata withdrawalContainer, + WithdrawalContainerProof calldata proof + ) internal view { bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); bytes32 withdrawalContainerRoot = withdrawalContainer.merklelizeWithdrawalContainer(); bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot( @@ -230,7 +254,10 @@ contract ExoCapsule is } } - function _isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint256 atTimestamp) internal pure returns (bool) { + function _isActivatedAtEpoch( + bytes32[] calldata validatorContainer, + uint256 atTimestamp + ) internal pure returns (bool) { uint64 atEpoch = _timestampToEpoch(atTimestamp); uint64 activationEpoch = validatorContainer.getActivationEpoch(); uint64 exitEpoch = validatorContainer.getExitEpoch(); @@ -264,7 +291,10 @@ contract ExoCapsule is * reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md */ function _timestampToEpoch(uint256 timestamp) internal pure returns (uint64) { - require(timestamp >= BEACON_CHAIN_GENESIS_TIME, "timestamp should be greater than beacon chain genesis timestamp"); + require( + timestamp >= BEACON_CHAIN_GENESIS_TIME, + "timestamp should be greater than beacon chain genesis timestamp" + ); return uint64((timestamp - BEACON_CHAIN_GENESIS_TIME) / BeaconChainProofs.SECONDS_PER_EPOCH); } } diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 03903875..df5bd278 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -16,7 +16,11 @@ abstract contract NativeRestakingController is { using ValidatorContainer for bytes32[]; - function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable whenNotPaused { + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable whenNotPaused { require(msg.value == 32 ether, "NativeRestakingController: stake value must be exactly 32 ether"); IExoCapsule capsule = ownerToCapsule[msg.sender]; @@ -29,8 +33,11 @@ abstract contract NativeRestakingController is } function createExoCapsule() public whenNotPaused returns (address) { - require(address(ownerToCapsule[msg.sender]) == address(0), "NativeRestakingController: message sender has already created the capsule"); - ExoCapsule capsule = ExoCapsule( + require( + address(ownerToCapsule[msg.sender]) == address(0), + "NativeRestakingController: message sender has already created the capsule" + ); + IExoCapsule capsule = IExoCapsule( Create2.deploy( 0, bytes32(uint256(uint160(msg.sender))), @@ -55,8 +62,6 @@ abstract contract NativeRestakingController is uint256 depositValue = uint256(validatorContainer.getEffectiveBalance()) * GWEI_TO_WEI; _processRequest(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, depositValue, Action.REQUEST_DEPOSIT, ""); - - } function processBeaconChainPartialWithdrawal( @@ -64,16 +69,12 @@ abstract contract NativeRestakingController is IExoCapsule.ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, IExoCapsule.WithdrawalContainerProof calldata withdrawalProof - ) external payable whenNotPaused { - - } + ) external payable whenNotPaused {} function processBeaconChainFullWithdrawal( bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, IExoCapsule.WithdrawalContainerProof calldata withdrawalProof - ) external payable whenNotPaused { - - } + ) external payable whenNotPaused {} } diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 608b08b1..69dd1e3d 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -18,10 +18,9 @@ interface IExoCapsule { uint256 withdrawalIndex; } - function verifyDepositProof( - bytes32[] calldata validatorContainer, - ValidatorContainerProof calldata proof - ) external; + function initialize(address gateway, address capsuleOwner, address beaconOracle) external; + + function verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) external; function verifyPartialWithdrawalProof( bytes32[] calldata validatorContainer, @@ -44,4 +43,4 @@ interface IExoCapsule { function updateWithdrawableBalance(uint256 unlockPrincipleAmount) external; function capsuleWithdrawalCredentials() external view returns (bytes memory); -} \ No newline at end of file +} diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index 9088deef..51c7346e 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "../../src/core/ExocoreGateway.sol"; import "../../src/storage/GatewayStorage.sol"; import "../../src/core/ExoCapsule.sol"; +import {IExoCapsule} from "../../src/interfaces/IExoCapsule.sol"; import {ILSTRestakingController} from "../../src/interfaces/ILSTRestakingController.sol"; import "forge-std/console.sol"; @@ -18,7 +19,10 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { event DepositResult(bool indexed success, address indexed token, address indexed depositor, uint256 amount); event WithdrawPrincipleResult( - bool indexed success, address indexed token, address indexed withdrawer, uint256 amount + bool indexed success, + address indexed token, + address indexed withdrawer, + uint256 amount ); event Transfer(address indexed from, address indexed to, uint256 amount); event MessageSent(GatewayStorage.Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); @@ -81,8 +85,12 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // exocore gateway should return response message to exocore network layerzero endpoint vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); lastlyUpdatedPrincipleBalance = depositAmount; - bytes memory depositResponsePayload = - abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true, lastlyUpdatedPrincipleBalance); + bytes memory depositResponsePayload = abi.encodePacked( + GatewayStorage.Action.RESPOND, + uint64(1), + true, + lastlyUpdatedPrincipleBalance + ); uint256 depositResponseNativeFee = exocoreGateway.quote(clientChainId, depositResponsePayload); bytes32 depositResponseId = generateUID(1, false); emit NewPacket( @@ -149,7 +157,8 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { withdrawRequestNativeFee ); clientGateway.withdrawPrincipleFromExocore{value: withdrawRequestNativeFee}( - address(restakeToken), withdrawAmount + address(restakeToken), + withdrawAmount ); // second layerzero relayers should watch the request message packet and relay the message to destination endpoint @@ -157,8 +166,12 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // exocore gateway should return response message to exocore network layerzero endpoint vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); lastlyUpdatedPrincipleBalance -= withdrawAmount; - bytes memory withdrawResponsePayload = - abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(2), true, lastlyUpdatedPrincipleBalance); + bytes memory withdrawResponsePayload = abi.encodePacked( + GatewayStorage.Action.RESPOND, + uint64(2), + true, + lastlyUpdatedPrincipleBalance + ); uint256 withdrawResponseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload); bytes32 withdrawResponseId = generateUID(2, false); emit NewPacket( @@ -207,7 +220,9 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // before native stake and deposit, we simulate proper block environment states to make proof valid /// 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 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; validatorProof.beaconBlockTimestamp = mockProofTimestamp; @@ -223,11 +238,13 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { ); // 1. firstly depositor should stake to beacon chain by depositing 32 ETH to ETHPOS contract - ExoCapsule expectedCapsule = ExoCapsule(Create2.computeAddress( - bytes32(uint256(uint160(depositor.addr))), - keccak256(abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(capsuleBeacon), ""))), - address(clientGateway) - )); + IExoCapsule expectedCapsule = IExoCapsule( + Create2.computeAddress( + bytes32(uint256(uint160(depositor.addr))), + keccak256(abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(capsuleBeacon), ""))), + address(clientGateway) + ) + ); vm.expectEmit(true, true, true, true, address(clientGateway)); emit CapsuleCreated(depositor.addr, address(expectedCapsule)); emit StakedWithCapsule(depositor.addr, address(expectedCapsule)); @@ -240,17 +257,13 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // because capsule address is expected to be compatible with validator container withdrawal credentails address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); vm.etch(capsuleAddress, address(expectedCapsule).code); - capsule = ExoCapsule(capsuleAddress); + 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(clientGatewayLogic)) - .sig("ownerToCapsule(address)") - .with_key(depositor.addr) - .find() + stdstore.target(address(clientGatewayLogic)).sig("ownerToCapsule(address)").with_key(depositor.addr).find() ); vm.store(address(clientGateway), capsuleSlotInGateway, bytes32(uint256(uint160(address(capsule))))); assertEq(address(clientGateway.ownerToCapsule(depositor.addr)), address(capsule)); @@ -293,8 +306,12 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { /// exocore gateway should return response message to exocore network layerzero endpoint uint256 lastlyUpdatedPrincipleBalance = depositAmount; - bytes memory depositResponsePayload = - abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true, lastlyUpdatedPrincipleBalance); + bytes memory depositResponsePayload = abi.encodePacked( + GatewayStorage.Action.RESPOND, + uint64(1), + true, + lastlyUpdatedPrincipleBalance + ); uint256 depositResponseNativeFee = exocoreGateway.quote(clientChainId, depositResponsePayload); bytes32 depositResponseId = generateUID(1, false); @@ -343,11 +360,19 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { function generateUID(uint64 nonce, bool fromClientChainToExocore) internal view returns (bytes32 uid) { if (fromClientChainToExocore) { uid = GUID.generate( - nonce, clientChainId, address(clientGateway), exocoreChainId, address(exocoreGateway).toBytes32() + nonce, + clientChainId, + address(clientGateway), + exocoreChainId, + address(exocoreGateway).toBytes32() ); } else { uid = GUID.generate( - nonce, exocoreChainId, address(exocoreGateway), clientChainId, address(clientGateway).toBytes32() + nonce, + exocoreChainId, + address(exocoreGateway), + clientChainId, + address(clientGateway).toBytes32() ); } } diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 73455a6d..39656cda 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -38,7 +38,7 @@ contract SetUp is Test { uint64 internal constant SLOTS_PER_EPOCH = 32; /// @notice The number of seconds in a slot in the beacon chain uint64 internal constant SECONDS_PER_SLOT = 12; - /// @notice Number of seconds per epoch: 384 == 32 slots/epoch * 12 seconds/slot + /// @notice Number of seconds per epoch: 384 == 32 slots/epoch * 12 seconds/slot uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; @@ -53,9 +53,15 @@ contract SetUp is Test { validatorProof.stateRoot = stdJson.readBytes32(validatorInfo, ".beaconStateRoot"); require(validatorProof.stateRoot != bytes32(0), "state root should not be empty"); - validatorProof.stateRootProof = stdJson.readBytes32Array(validatorInfo, ".StateRootAgainstLatestBlockHeaderProof"); + validatorProof.stateRootProof = stdJson.readBytes32Array( + validatorInfo, + ".StateRootAgainstLatestBlockHeaderProof" + ); require(validatorProof.stateRootProof.length == 3, "state root proof should have 3 nodes"); - validatorProof.validatorContainerRootProof = stdJson.readBytes32Array(validatorInfo, ".WithdrawalCredentialProof"); + 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"); @@ -72,13 +78,15 @@ contract SetUp is Test { address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); vm.etch(capsuleAddress, address(phantomCapsule).code); - capsule = ExoCapsule(capsuleAddress); + capsule = ExoCapsule(payable(capsuleAddress)); stdstore.target(capsuleAddress).sig("gateway()").checked_write(bytes32(uint256(uint160(address(this))))); stdstore.target(capsuleAddress).sig("capsuleOwner()").checked_write(bytes32(uint256(uint160(capsuleOwner)))); - stdstore.target(capsuleAddress).sig("beaconOracle()").checked_write(bytes32(uint256(uint160(address(beaconOracle))))); + stdstore.target(capsuleAddress).sig("beaconOracle()").checked_write( + bytes32(uint256(uint160(address(beaconOracle)))) + ); } function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { @@ -107,7 +115,9 @@ contract VerifyDepositProof is SetUp { using stdStorage for StdStorage; function test_verifyDepositProof() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -123,7 +133,9 @@ contract VerifyDepositProof is SetUp { } function test_verifyDepositProof_revert_validatorAlreadyDeposited() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -138,12 +150,16 @@ contract VerifyDepositProof is SetUp { capsule.verifyDepositProof(validatorContainer, validatorProof); // deposit again should revert - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.DoubleDepositedValidator.selector, _getPubkey(validatorContainer))); + vm.expectRevert( + abi.encodeWithSelector(ExoCapsule.DoubleDepositedValidator.selector, _getPubkey(validatorContainer)) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); } function test_verifyDepositProof_revert_staleProof() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp + 1 hours; mockCurrentBlockTimestamp = mockProofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS + 1 seconds; vm.warp(mockCurrentBlockTimestamp); @@ -156,12 +172,20 @@ contract VerifyDepositProof is SetUp { ); // deposit should revert because of proof is stale - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.StaleValidatorContainer.selector, _getPubkey(validatorContainer), mockProofTimestamp)); + vm.expectRevert( + abi.encodeWithSelector( + ExoCapsule.StaleValidatorContainer.selector, + _getPubkey(validatorContainer), + mockProofTimestamp + ) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); } function test_verifyDepositProof_revert_malformedValidatorContainer() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -177,18 +201,24 @@ contract VerifyDepositProof is SetUp { // construct malformed validator container that has extra fields validatorContainer.push(bytes32(uint256(123))); - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer))); + vm.expectRevert( + abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer)) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); vm.revertTo(snapshot); // construct malformed validator container that misses fields validatorContainer.pop(); - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer))); + vm.expectRevert( + abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer)) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); } function test_verifyDepositProof_revert_inactiveValidatorContainer() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; vm.mockCall( address(beaconOracle), @@ -201,12 +231,16 @@ contract VerifyDepositProof is SetUp { mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); validatorProof.beaconBlockTimestamp = mockProofTimestamp; - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InactiveValidatorContainer.selector, _getPubkey(validatorContainer))); + vm.expectRevert( + abi.encodeWithSelector(ExoCapsule.InactiveValidatorContainer.selector, _getPubkey(validatorContainer)) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); } function test_verifyDepositProof_revert_mismatchWithdrawalCredentials() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -235,7 +269,9 @@ contract VerifyDepositProof is SetUp { } function test_verifyDepositProof_revert_proofNotMatchWithBeaconRoot() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -249,7 +285,9 @@ contract VerifyDepositProof is SetUp { ); // verify proof against mismatch beacon block root - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer))); + vm.expectRevert( + abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer)) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); } -} \ No newline at end of file +} From 9e25adcd797da4fb93ebe41937ddd18bfc0dbaee Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 13 May 2024 22:48:52 -0400 Subject: [PATCH 36/93] fix: move variable to ExoCapsuleStorage --- src/core/ExoCapsule.sol | 3 --- src/storage/ExoCapsuleStorage.sol | 5 ++++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 1464846a..b8f1f43e 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -38,9 +38,6 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { error InactiveValidatorContainer(bytes32 pubkey); error InvalidGateway(address, address); - /// @notice This variable tracks any ETH deposited into this contract via the `receive` fallback function - uint256 public nonBeaconChainETHBalance; - modifier onlyGateway() { if (msg.sender != address(gateway)) { revert InvalidGateway(address(gateway), msg.sender); diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 1dc7a243..38e03b4b 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -29,7 +29,10 @@ contract ExoCapsuleStorage { uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; uint256 public principleBalance; + /// @notice the amount of execution layer ETH in this contract that is staked in(i.e. withdrawn from the Beacon Chain but not from Exocore) uint256 public withdrawableBalance; + /// @notice This variable tracks any ETH deposited into this contract via the `receive` fallback function + uint256 public nonBeaconChainETHBalance; address public capsuleOwner; INativeRestakingController public gateway; IBeaconChainOracle public beaconOracle; @@ -38,4 +41,4 @@ contract ExoCapsuleStorage { mapping(uint256 index => bytes32 pubkey) _capsuleValidatorsByIndex; uint256[40] private __gap; -} \ No newline at end of file +} From f89cb94f78849dff526c6159f978ba3797019572 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 14 May 2024 10:28:49 -0400 Subject: [PATCH 37/93] feat: consolidiate partial and full withdraw workflow in a single function --- src/core/ExoCapsule.sol | 94 ++++++++++++------- src/core/NativeRestakingController.sol | 29 ++++-- src/interfaces/IExoCapsule.sol | 11 +-- src/interfaces/INativeRestakingController.sol | 29 +++--- src/libraries/WithdrawalContainer.sol | 7 +- src/storage/ClientChainGatewayStorage.sol | 27 ++++-- src/storage/ExoCapsuleStorage.sol | 4 + 7 files changed, 119 insertions(+), 82 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index b8f1f43e..5c6bc6cd 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -19,6 +19,20 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { event PrincipleBalanceUpdated(address, uint256); event WithdrawableBalanceUpdated(address, uint256); event WithdrawalSuccess(address, address, uint256); + /// @notice Emitted when a partial withdrawal claim is successfully redeemed + event PartialWithdrawalRedeemed( + bytes32 pubkey, + uint256 withdrawalTimestamp, + address indexed recipient, + uint64 partialWithdrawalAmountGwei + ); + /// @notice Emitted when an ETH validator is prove to have fully withdrawn from the beacon chain + event FullWithdrawalRedeemed( + bytes32 pubkey, + uint256 withdrawalTimestamp, + address indexed recipient, + uint64 withdrawalAmountGwei + ); /// @notice Emitted when ETH is received via the `receive` fallback event NonBeaconChainETHReceived(uint256 amountReceived); /// @notice Emitted when ETH that was previously received via the `receive` fallback is withdrawn @@ -29,6 +43,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { error DoubleDepositedValidator(bytes32 pubkey); error StaleValidatorContainer(bytes32 pubkey, uint256 timestamp); error UnregisteredOrWithdrawnValidatorContainer(bytes32 pubkey); + error WithdrawalAlreadyProven(bytes32 pubkey, uint256 timestamp); error FullyWithdrawnValidatorContainer(bytes32 pubkey); error UnmatchedValidatorAndWithdrawal(bytes32 pubkey); error NotPartialWithdrawal(bytes32 pubkey); @@ -102,20 +117,16 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { _capsuleValidatorsByIndex[proof.validatorIndex] = validatorPubkey; } - function verifyPartialWithdrawalProof( + function verifyWithdrawalProof( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof - ) external view onlyGateway { + ) external onlyGateway returns (bool partialWithdrawal) { bytes32 validatorPubkey = validatorContainer.getPubkey(); uint64 withdrawableEpoch = validatorContainer.getWithdrawableEpoch(); - - bool partialWithdrawal = _timestampToEpoch(validatorProof.beaconBlockTimestamp) < withdrawableEpoch; - - if (!partialWithdrawal) { - revert NotPartialWithdrawal(validatorPubkey); - } + Validator storage validator = _capsuleValidators[validatorPubkey]; + partialWithdrawal = _timestampToEpoch(validatorProof.beaconBlockTimestamp) < withdrawableEpoch; if (validatorProof.beaconBlockTimestamp != withdrawalProof.beaconBlockTimestamp) { revert UnmatchedValidatorAndWithdrawal(validatorPubkey); @@ -125,38 +136,45 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { revert InvalidValidatorContainer(validatorPubkey); } - _verifyValidatorContainer(validatorContainer, validatorProof); - _verifyWithdrawalContainer(withdrawalContainer, withdrawalProof); - } - - function verifyFullWithdrawalProof( - bytes32[] calldata validatorContainer, - ValidatorContainerProof calldata validatorProof, - bytes32[] calldata withdrawalContainer, - WithdrawalContainerProof calldata withdrawalProof - ) external onlyGateway { - bytes32 validatorPubkey = validatorContainer.getPubkey(); - uint64 withdrawableEpoch = validatorContainer.getWithdrawableEpoch(); - - Validator storage validator = _capsuleValidators[validatorPubkey]; - bool fullyWithdrawal = _timestampToEpoch(validatorProof.beaconBlockTimestamp) > withdrawableEpoch; - - if (!fullyWithdrawal) { - revert NotPartialWithdrawal(validatorPubkey); + if (validator.status == VALIDATOR_STATUS.UNREGISTERED) { + revert UnregisteredOrWithdrawnValidatorContainer(validatorPubkey); } - if (validatorProof.beaconBlockTimestamp != withdrawalProof.beaconBlockTimestamp) { - revert UnmatchedValidatorAndWithdrawal(validatorPubkey); + if (provenWithdrawal[validatorPubkey][withdrawalProof.beaconBlockTimestamp]) { + revert WithdrawalAlreadyProven(validatorPubkey, withdrawalProof.beaconBlockTimestamp); } - if (!validatorContainer.verifyValidatorContainerBasic()) { - revert InvalidValidatorContainer(validatorPubkey); - } + provenWithdrawal[validatorPubkey][withdrawalProof.beaconBlockTimestamp] = true; _verifyValidatorContainer(validatorContainer, validatorProof); _verifyWithdrawalContainer(withdrawalContainer, withdrawalProof); - validator.status = VALIDATOR_STATUS.WITHDRAWN; + uint64 withdrawalAmountGwei = withdrawalContainer.getAmount(); + + if (partialWithdrawal) { + // Immediately send ETH without sending request to Exocore side + emit PartialWithdrawalRedeemed( + validatorPubkey, + withdrawalProof.beaconBlockTimestamp, + capsuleOwner, + withdrawalAmountGwei + ); + _sendETH(capsuleOwner, withdrawalAmountGwei * GWEI_TO_WEI); + } else { + // Full withdrawal + validator.status = VALIDATOR_STATUS.WITHDRAWN; + validator.restakedBalanceGwei = 0; + // If over MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32 * 1e9, then send remaining amount immediately + emit FullWithdrawalRedeemed( + validatorPubkey, + withdrawalProof.beaconBlockTimestamp, + capsuleOwner, + withdrawalAmountGwei + ); + if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { + _sendETH(capsuleOwner, (withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * GWEI_TO_WEI); + } + } } function withdraw(uint256 amount, address recipient) external onlyGateway { @@ -166,10 +184,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { ); withdrawableBalance -= amount; - (bool sent, ) = recipient.call{value: amount}(""); - if (!sent) { - revert WithdrawalFailure(capsuleOwner, recipient, amount); - } + _sendETH(recipient, amount); emit WithdrawalSuccess(capsuleOwner, recipient, amount); } @@ -215,6 +230,13 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { return root; } + function _sendETH(address recipient, uint256 amountWei) internal { + (bool sent, ) = recipient.call{value: amountWei}(""); + if (!sent) { + revert WithdrawalFailure(capsuleOwner, recipient, amountWei); + } + } + function _verifyValidatorContainer( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index df5bd278..63962e52 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -64,17 +64,28 @@ abstract contract NativeRestakingController is _processRequest(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, depositValue, Action.REQUEST_DEPOSIT, ""); } - function processBeaconChainPartialWithdrawal( + function processBeaconChainWithdrawal( bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, IExoCapsule.WithdrawalContainerProof calldata withdrawalProof - ) external payable whenNotPaused {} - - function processBeaconChainFullWithdrawal( - bytes32[] calldata validatorContainer, - IExoCapsule.ValidatorContainerProof calldata validatorProof, - bytes32[] calldata withdrawalContainer, - IExoCapsule.WithdrawalContainerProof calldata withdrawalProof - ) external payable whenNotPaused {} + ) external payable whenNotPaused { + IExoCapsule capsule = _getCapsule(msg.sender); + bool partialWithdrawal = capsule.verifyWithdrawalProof( + validatorContainer, + validatorProof, + withdrawalContainer, + withdrawalProof + ); + if (!partialWithdrawal) { + // request full withdraw + _processRequest( + VIRTUAL_STAKED_ETH_ADDRESS, + msg.sender, + 32 ether, + Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, + "" + ); + } + } } diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 69dd1e3d..88548af9 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -22,19 +22,12 @@ interface IExoCapsule { function verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) external; - function verifyPartialWithdrawalProof( + function verifyWithdrawalProof( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof - ) external; - - function verifyFullWithdrawalProof( - bytes32[] calldata validatorContainer, - ValidatorContainerProof calldata validatorProof, - bytes32[] calldata withdrawalContainer, - WithdrawalContainerProof calldata withdrawalProof - ) external; + ) external returns (bool partialWithdrawal); function withdraw(uint256 amount, address recipient) external; diff --git a/src/interfaces/INativeRestakingController.sol b/src/interfaces/INativeRestakingController.sol index 709d585a..3e8f600c 100644 --- a/src/interfaces/INativeRestakingController.sol +++ b/src/interfaces/INativeRestakingController.sol @@ -7,11 +7,11 @@ interface INativeRestakingController is IBaseRestakingController { /// *** function signatures for staker operations *** /** - * @notice Stakers call this function to deposit to beacon chain validator, and point withdrawal_credentials of + * @notice Stakers call this function to deposit to beacon chain validator, and point withdrawal_credentials of * beacon chain validator to staker's ExoCapsule contract address. An ExoCapsule contract owned by staker would * be created if it does not exist. * @param pubkey the BLS pubkey of beacon chain validator - * @param signature the BLS signature + * @param signature the BLS signature * @param depositDataRoot The SHA-256 hash of the SSZ-encoded DepositData object. * Used as a protection against malformed input. */ @@ -26,13 +26,16 @@ interface INativeRestakingController is IBaseRestakingController { * @notice This is called to deposit ETH that is staked on Ethereum beacon chain to Exocore network to be restaked in future * @dev Before deposit, staker should have created the ExoCapsule that it owns and point the validator's withdrawal crendentials * to the ExoCapsule owned by staker. The effective balance of `validatorContainer` would be credited as deposited value by Exocore network. - * @ param + * @ param */ - function depositBeaconChainValidator(bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata proof) payable external; + function depositBeaconChainValidator( + bytes32[] calldata validatorContainer, + IExoCapsule.ValidatorContainerProof calldata proof + ) external payable; /** - * @notice When a beacon chain partial withdrawal to an ExoCapsule contract happens(the withdrawal time is less than validator's withdrawable_epoch), - * this function could be called with `validatorContainer`, `withdrawalContainer` and corresponding proofs to prove this partial withdrawal + * @notice When a beacon chain partial withdrawal to an ExoCapsule contract happens(the withdrawal time is less than validator's withdrawable_epoch), + * this function could be called with `validatorContainer`, `withdrawalContainer` and corresponding proofs to prove this partial withdrawal * from beacon chain is done and unlock withdrawn ETH to be claimable for ExoCapsule owner. * @param validatorContainer is the data structure included in `BeaconState` of `BeaconBlock` that contains beacon chain validator information, * refer to: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator @@ -42,16 +45,10 @@ interface INativeRestakingController is IBaseRestakingController { * https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#withdrawal * @param withdrawalProof is the merkle proof needed for verifying that `withdrawalContainer` is included in some beacon block root. */ - function processBeaconChainPartialWithdrawal( - bytes32[] calldata validatorContainer, - IExoCapsule.ValidatorContainerProof calldata validatorProof, - bytes32[] calldata withdrawalContainer, - IExoCapsule.WithdrawalContainerProof calldata withdrawalProof - ) payable external; /** - * @notice When a beacon chain full withdrawal to this capsule contract happens(the withdrawal time is euqal to or greater than - * validator's withdrawable_epoch), this function could be called with `validatorContainer`, `withdrawalContainer` and corresponding + * @notice When a beacon chain full withdrawal to this capsule contract happens(the withdrawal time is euqal to or greater than + * validator's withdrawable_epoch), this function could be called with `validatorContainer`, `withdrawalContainer` and corresponding * proofs to prove this full withdrawal from beacon chain is done, send withdrawal request to Exocore network to be processed. * After Exocore network finishs dealing with withdrawal request and sending back the response, ExoCapsule would unlock corresponding ETH * in response to be cliamable for ExoCapsule owner. @@ -63,10 +60,10 @@ interface INativeRestakingController is IBaseRestakingController { * https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#withdrawal * @param withdrawalProof is the merkle proof needed for verifying that `withdrawalContainer` is included in some beacon block root. */ - function processBeaconChainFullWithdrawal( + function processBeaconChainWithdrawal( bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, IExoCapsule.WithdrawalContainerProof calldata withdrawalProof - ) payable external; + ) external payable; } diff --git a/src/libraries/WithdrawalContainer.sol b/src/libraries/WithdrawalContainer.sol index a5254671..e45afd63 100644 --- a/src/libraries/WithdrawalContainer.sol +++ b/src/libraries/WithdrawalContainer.sol @@ -14,7 +14,7 @@ library WithdrawalContainer { uint256 internal constant VALID_LENGTH = 4; uint256 internal constant MERKLE_TREE_HEIGHT = 2; - + function verifyWithdrawalContainerBasic(bytes32[] calldata withdrawalContainer) internal pure returns (bool) { return withdrawalContainer.length == VALID_LENGTH; } @@ -31,6 +31,9 @@ library WithdrawalContainer { return address(bytes20(withdrawalContainer[2])); } + /** + * @dev Retrieves a withdrawal's withdrawal amount (in gwei) + */ function getAmount(bytes32[] calldata withdrawalContainer) internal pure returns (uint64) { return withdrawalContainer[3].fromLittleEndianUint64(); } @@ -47,4 +50,4 @@ library WithdrawalContainer { return leaves[0]; } -} \ No newline at end of file +} diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 904e7106..9f4e4c0f 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -22,7 +22,7 @@ contract ClientChainGatewayStorage is BootstrapStorage { // immutable state variables address public immutable beaconOracleAddress; IBeacon public immutable exoCapsuleBeacon; - + // constant state variables bytes constant EXO_ADDRESS_PREFIX = bytes("exo1"); uint128 constant DESTINATION_GAS_LIMIT = 500000; @@ -30,7 +30,7 @@ contract ClientChainGatewayStorage is BootstrapStorage { uint256 constant GWEI_TO_WEI = 1e9; address constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; IETHPOSDeposit constant ETH_POS = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); - + uint256[40] private __gap; /* -------------------------------------------------------------------------- */ @@ -72,26 +72,33 @@ contract ClientChainGatewayStorage is BootstrapStorage { } modifier isValidBech32Address(string calldata exocoreAddress) { - require(_isValidExocoreAddress(exocoreAddress), "BaseRestakingController: invalid bech32 encoded Exocore address"); + require( + _isValidExocoreAddress(exocoreAddress), + "BaseRestakingController: invalid bech32 encoded Exocore address" + ); _; } constructor( - uint32 exocoreChainId_, - address beaconOracleAddress_, + uint32 exocoreChainId_, + address beaconOracleAddress_, address vaultBeacon_, address exoCapsuleBeacon_ ) BootstrapStorage(exocoreChainId_, vaultBeacon_) { - require(beaconOracleAddress_ != address(0), "ClientChainGatewayStorage: beacon chain oracle address should not be empty"); - require(exoCapsuleBeacon_ != address(0), "ClientChainGatewayStorage: the exoCapsuleBeacon address for beacon proxy should not be empty"); + require( + beaconOracleAddress_ != address(0), + "ClientChainGatewayStorage: beacon chain oracle address should not be empty" + ); + require( + exoCapsuleBeacon_ != address(0), + "ClientChainGatewayStorage: the exoCapsuleBeacon address for beacon proxy should not be empty" + ); beaconOracleAddress = beaconOracleAddress_; exoCapsuleBeacon = IBeacon(exoCapsuleBeacon_); } - function _isValidExocoreAddress( - string calldata operatorExocoreAddress - ) public pure returns (bool) { + function _isValidExocoreAddress(string calldata operatorExocoreAddress) public pure returns (bool) { bytes memory stringBytes = bytes(operatorExocoreAddress); if (stringBytes.length != 42) { return false; diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 38e03b4b..147dce4a 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -27,6 +27,8 @@ contract ExoCapsuleStorage { address public constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; uint256 public constant BEACON_CHAIN_GENESIS_TIME = 1606824023; uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; + uint256 constant GWEI_TO_WEI = 1e9; + uint64 constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; uint256 public principleBalance; /// @notice the amount of execution layer ETH in this contract that is staked in(i.e. withdrawn from the Beacon Chain but not from Exocore) @@ -39,6 +41,8 @@ contract ExoCapsuleStorage { mapping(bytes32 pubkey => Validator validator) _capsuleValidators; mapping(uint256 index => bytes32 pubkey) _capsuleValidatorsByIndex; + /// @notice This is a mapping of validatorPubkeyHash to timestamp to whether or not they have proven a withdrawal for that timestamp + mapping(bytes32 => mapping(uint256 => bool)) public provenWithdrawal; uint256[40] private __gap; } From 555446c6f2bdd8ad8a78fb875efac880db7dc2f0 Mon Sep 17 00:00:00 2001 From: adu Date: Thu, 16 May 2024 11:45:52 +0800 Subject: [PATCH 38/93] fix ExocoreGateway.requestUndelegateFrom and add undeletion integration test --- src/core/ExocoreGateway.sol | 2 +- test/foundry/Delegation.t.sol | 188 +++++++++++++++++++++++++++++----- test/mocks/DelegationMock.sol | 10 +- 3 files changed, 175 insertions(+), 25 deletions(-) diff --git a/src/core/ExocoreGateway.sol b/src/core/ExocoreGateway.sol index 3d9f39e6..64b8a863 100644 --- a/src/core/ExocoreGateway.sol +++ b/src/core/ExocoreGateway.sol @@ -224,7 +224,7 @@ contract ExocoreGateway is revert InvalidRequestLength(Action.REQUEST_UNDELEGATE_FROM, UNDELEGATE_REQUEST_LENGTH, payload.length); } - bytes memory token = payload[1:32]; + bytes memory token = payload[:32]; bytes memory delegator = payload[32:64]; bytes memory operator = payload[64:106]; uint256 amount = uint256(bytes32(payload[106:138])); diff --git a/test/foundry/Delegation.t.sol b/test/foundry/Delegation.t.sol index 38753eac..472825bd 100644 --- a/test/foundry/Delegation.t.sol +++ b/test/foundry/Delegation.t.sol @@ -5,6 +5,7 @@ import "forge-std/Test.sol"; import "../../src/core/ExocoreGateway.sol"; import "../../src/storage/GatewayStorage.sol"; import "../../src/interfaces/precompiles/IDelegation.sol"; +import "../mocks/DelegationMock.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; @@ -20,6 +21,9 @@ contract DelegateTest is ExocoreDeployer { event DelegateResult( bool indexed success, address indexed delegator, string delegatee, address token, uint256 amount + ); + event UndelegateResult( + bool indexed success, address indexed undelegator, string indexed undelegatee, address token, uint256 amount ); event DelegateRequestProcessed( uint32 clientChainLzId, @@ -40,30 +44,47 @@ contract DelegateTest is ExocoreDeployer { function test_Delegation() public { Player memory delegator = players[0]; + Player memory relayer = players[1]; + string memory operatorAddress = "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"; + + deal(delegator.addr, 1e22); + deal(address(exocoreGateway), 1e22); + uint256 delegateAmount = 10000; + + _testDelegate(delegator.addr, relayer.addr, operatorAddress, delegateAmount); + } + + function test_Undelegation() public { + Player memory delegator = players[0]; + Player memory relayer = players[1]; string memory operatorAddress = "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac"; deal(delegator.addr, 1e22); - deal(address(clientGateway), 1e22); deal(address(exocoreGateway), 1e22); uint256 delegateAmount = 10000; + uint256 undelegateAmount = 5000; - // -- delegate workflow test -- + _testDelegate(delegator.addr, relayer.addr, operatorAddress, delegateAmount); + _testUndelegate(delegator.addr, relayer.addr, operatorAddress, undelegateAmount); + } - vm.startPrank(delegator.addr); + function _testDelegate(address delegator, address relayer, string memory operator, uint256 delegateAmount) internal { + /* ------------------------- delegate workflow test ------------------------- */ - // first user call client chain gateway to delegate + // 1. first user call client chain gateway to delegate - // estimate l0 relay fee that would be charged from user + /// estimate the messaging fee that would be charged from user bytes memory delegateRequestPayload = abi.encodePacked( GatewayStorage.Action.REQUEST_DELEGATE_TO, abi.encodePacked(bytes32(bytes20(address(restakeToken)))), - abi.encodePacked(bytes32(bytes20(delegator.addr))), - bytes(operatorAddress), + abi.encodePacked(bytes32(bytes20(delegator))), + bytes(operator), delegateAmount ); uint256 requestNativeFee = clientGateway.quote(delegateRequestPayload); - bytes32 requestId = generateUID(1, true); - // client chain layerzero endpoint should emit the message packet including delegate payload. + bytes32 requestId = generateUID(uint64(1), true); + + /// layerzero endpoint should emit the message packet including delegate payload. vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); emit NewPacket( exocoreChainId, @@ -72,28 +93,35 @@ contract DelegateTest is ExocoreDeployer { uint64(1), delegateRequestPayload ); - // client chain gateway should emit MessageSent event + + /// clientGateway should emit MessageSent event vm.expectEmit(true, true, true, true, address(clientGateway)); emit MessageSent(GatewayStorage.Action.REQUEST_DELEGATE_TO, requestId, uint64(1), requestNativeFee); - clientGateway.delegateTo{value: requestNativeFee}(operatorAddress, address(restakeToken), delegateAmount); - // second layerzero relayers should watch the request message packet and relay the message to destination endpoint + /// delegator call clientGateway to send delegation request + vm.startPrank(delegator); + clientGateway.delegateTo{value: requestNativeFee}(operator, address(restakeToken), delegateAmount); + vm.stopPrank(); + + // 2. second layerzero relayers should watch the request message packet and relay the message to destination endpoint - // DelegationMock contract function should receive correct params + bytes memory delegateResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true); + uint256 responseNativeFee = exocoreGateway.quote(clientChainId, delegateResponsePayload); + bytes32 responseId = generateUID(uint64(1), false); + + /// DelegationMock contract should receive correct message payload vm.expectEmit(true, true, true, true, DELEGATION_PRECOMPILE_ADDRESS); emit DelegateRequestProcessed( uint16(clientChainId), uint64(1), abi.encodePacked(bytes32(bytes20(address(restakeToken)))), - abi.encodePacked(bytes32(bytes20(delegator.addr))), - operatorAddress, + abi.encodePacked(bytes32(bytes20(delegator))), + operator, delegateAmount ); - // exocore gateway should return response message to exocore network layerzero endpoint + + /// layerzero endpoint should emit the message packet including delegation response payload. vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); - bytes memory delegateResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true); - uint256 responseNativeFee = exocoreGateway.quote(clientChainId, delegateResponsePayload); - bytes32 responseId = generateUID(1, false); emit NewPacket( clientChainId, address(exocoreGateway), @@ -101,9 +129,13 @@ contract DelegateTest is ExocoreDeployer { uint64(1), delegateResponsePayload ); - // exocore gateway should emit MessageSent event + + /// exocoreGateway should emit MessageSent event after finishing sending response vm.expectEmit(true, true, true, true, address(exocoreGateway)); emit MessageSent(GatewayStorage.Action.RESPOND, responseId, uint64(1), responseNativeFee); + + /// relayer call layerzero endpoint to deliver request messages and generate response message + vm.startPrank(relayer); exocoreLzEndpoint.lzReceive( Origin(clientChainId, address(clientGateway).toBytes32(), uint64(1)), address(exocoreGateway), @@ -111,12 +143,20 @@ contract DelegateTest is ExocoreDeployer { delegateRequestPayload, bytes("") ); + vm.stopPrank(); + + /// assert that DelegationMock contract should have recorded the delegate + uint256 actualDelegateAmount = DelegationMock(DELEGATION_PRECOMPILE_ADDRESS).getDelegateAmount(delegator, operator, clientChainId, address(restakeToken)); + assertEq(actualDelegateAmount, delegateAmount); - // third layerzero relayers should watch the response message packet and relay the message to source chain endpoint + // 3. third layerzero relayers should watch the response message packet and relay the message to source chain endpoint - // client chain gateway should execute the response hook and emit depositResult event + /// after relayer relay the response message back to client chain, clientGateway should emit DelegateResult event vm.expectEmit(true, true, true, true, address(clientGateway)); - emit DelegateResult(true, delegator.addr, operatorAddress, address(restakeToken), delegateAmount); + emit DelegateResult(true, delegator, operator, address(restakeToken), delegateAmount); + + /// relayer should watch the response message and relay it back to client chain + vm.startPrank(relayer); clientChainLzEndpoint.lzReceive( Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(1)), address(clientGateway), @@ -124,6 +164,108 @@ contract DelegateTest is ExocoreDeployer { delegateResponsePayload, bytes("") ); + vm.stopPrank(); + } + + function _testUndelegate(address delegator, address relayer, string memory operator, uint256 undelegateAmount) internal { + /* ------------------------- undelegate workflow test ------------------------- */ + uint256 totalDelegate = DelegationMock(DELEGATION_PRECOMPILE_ADDRESS).getDelegateAmount(delegator, operator, clientChainId, address(restakeToken)); + require(undelegateAmount <= totalDelegate, "undelegate amount overflow"); + + // 1. first user call client chain gateway to undelegate + + /// estimate the messaging fee that would be charged from user + bytes memory undelegateRequestPayload = abi.encodePacked( + GatewayStorage.Action.REQUEST_UNDELEGATE_FROM, + abi.encodePacked(bytes32(bytes20(address(restakeToken)))), + abi.encodePacked(bytes32(bytes20(delegator))), + bytes(operator), + undelegateAmount + ); + uint256 requestNativeFee = clientGateway.quote(undelegateRequestPayload); + bytes32 requestId = generateUID(2, true); + + /// layerzero endpoint should emit the message packet including undelegate payload. + vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); + emit NewPacket( + exocoreChainId, + address(clientGateway), + address(exocoreGateway).toBytes32(), + uint64(2), + undelegateRequestPayload + ); + + /// clientGateway should emit MessageSent event + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit MessageSent(GatewayStorage.Action.REQUEST_UNDELEGATE_FROM, requestId, uint64(2), requestNativeFee); + + /// delegator call clientGateway to send undelegation request + vm.startPrank(delegator); + clientGateway.undelegateFrom{value: requestNativeFee}(operator, address(restakeToken), undelegateAmount); + vm.stopPrank(); + + // 2. second layerzero relayers should watch the request message packet and relay the message to destination endpoint + + bytes memory undelegateResponsePayload = abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(2), true); + uint256 responseNativeFee = exocoreGateway.quote(clientChainId, undelegateResponsePayload); + bytes32 responseId = generateUID(2, false); + + /// DelegationMock contract should receive correct message payload + vm.expectEmit(true, true, true, true, DELEGATION_PRECOMPILE_ADDRESS); + emit UndelegateRequestProcessed( + uint16(clientChainId), + uint64(2), + abi.encodePacked(bytes32(bytes20(address(restakeToken)))), + abi.encodePacked(bytes32(bytes20(delegator))), + operator, + undelegateAmount + ); + + /// layerzero endpoint should emit the message packet including undelegation response payload. + vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); + emit NewPacket( + clientChainId, + address(exocoreGateway), + address(clientGateway).toBytes32(), + uint64(2), + undelegateResponsePayload + ); + + /// exocoreGateway should emit MessageSent event after finishing sending response + vm.expectEmit(true, true, true, true, address(exocoreGateway)); + emit MessageSent(GatewayStorage.Action.RESPOND, responseId, uint64(2), responseNativeFee); + + /// relayer call layerzero endpoint to deliver request messages and generate response message + vm.startPrank(relayer); + exocoreLzEndpoint.lzReceive( + Origin(clientChainId, address(clientGateway).toBytes32(), uint64(2)), + address(exocoreGateway), + requestId, + undelegateRequestPayload, + bytes("") + ); + vm.stopPrank(); + + /// assert that DelegationMock contract should have recorded the undelegation + uint256 actualDelegateAmount = DelegationMock(DELEGATION_PRECOMPILE_ADDRESS).getDelegateAmount(delegator, operator, clientChainId, address(restakeToken)); + assertEq(actualDelegateAmount, totalDelegate - undelegateAmount); + + // 3. third layerzero relayers should watch the response message packet and relay the message to source chain endpoint + + /// after relayer relay the response message back to client chain, clientGateway should emit UndelegateResult event + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit UndelegateResult(true, delegator, operator, address(restakeToken), undelegateAmount); + + /// relayer should watch the response message and relay it back to client chain + vm.startPrank(relayer); + clientChainLzEndpoint.lzReceive( + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), uint64(2)), + address(clientGateway), + responseId, + undelegateResponsePayload, + bytes("") + ); + vm.stopPrank(); } function generateUID(uint64 nonce, bool fromClientChainToExocore) internal view returns (bytes32 uid) { diff --git a/test/mocks/DelegationMock.sol b/test/mocks/DelegationMock.sol index 74756355..46d1e0a5 100644 --- a/test/mocks/DelegationMock.sol +++ b/test/mocks/DelegationMock.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.19; import {IDelegation} from "../../src/interfaces/precompiles/IDelegation.sol"; contract DelegationMock is IDelegation { - mapping(bytes => mapping(bytes => mapping(uint32 => mapping(bytes => uint256)))) delegateTo; + mapping(bytes => mapping(bytes => mapping(uint32 => mapping(bytes => uint256)))) public delegateTo; event DelegateRequestProcessed( uint32 clientChainLzId, @@ -56,4 +56,12 @@ contract DelegationMock is IDelegation { clientChainLzId, lzNonce, assetsAddress, stakerAddress, string(operatorAddr), opAmount ); } + + function getDelegateAmount(address delegator, string memory operator, uint32 clientChainLzId, address token) public view returns (uint256) { + return delegateTo[_addressToBytes(delegator)][bytes(operator)][clientChainLzId][_addressToBytes(token)]; + } + + function _addressToBytes(address addr) internal pure returns (bytes memory) { + return abi.encodePacked(bytes32(bytes20(addr))); + } } From 0fa6f75934cb74f88692001c634dd547a61d9ab2 Mon Sep 17 00:00:00 2001 From: bwhour Date: Fri, 17 May 2024 12:23:38 +0800 Subject: [PATCH 39/93] optmize the _processRequest to make it simple and gas saving --- .gitignore | 4 +- src/core/BaseRestakingController.sol | 44 ++++++++------------- src/core/LSTRestakingController.sol | 5 +-- src/interfaces/IBaseRestakingController.sol | 2 +- 4 files changed, 21 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index 9e3329f4..f886c552 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ node_modules ## Hardhat **/artifacts -**/cache_hardhat \ No newline at end of file +**/cache_hardhat + +.idea diff --git a/src/core/BaseRestakingController.sol b/src/core/BaseRestakingController.sol index 1ca4345a..96b144f8 100644 --- a/src/core/BaseRestakingController.sol +++ b/src/core/BaseRestakingController.sol @@ -45,7 +45,6 @@ abstract contract BaseRestakingController is isValidBech32Address(operator) whenNotPaused { _processRequest(token, msg.sender, amount, Action.REQUEST_DELEGATE_TO, operator); - } function undelegateFrom(string calldata operator, address token, uint256 amount) @@ -55,7 +54,6 @@ abstract contract BaseRestakingController is isValidBech32Address(operator) whenNotPaused { _processRequest(token, msg.sender, amount, Action.REQUEST_UNDELEGATE_FROM, operator); - } function _processRequest( @@ -63,39 +61,29 @@ abstract contract BaseRestakingController is address sender, uint256 amount, Action action, - string memory operator // Optional parameter, you can pass an empty string if you don't need it. + string memory operator // Optional parameter, empty string when not needed. ) internal { if (token != VIRTUAL_STAKED_ETH_ADDRESS) { IVault vault = _getVault(token); - // Logic specific to the REQUEST_DEPOSIT action - if (action == Action.REQUEST_DEPOSIT && bytes(operator).length == 0) { - vault.deposit(sender, amount); + if (action == Action.REQUEST_DEPOSIT) { + vault.deposit(sender, amount); // Logic specific to the REQUEST_DEPOSIT action. } } outboundNonce++; - // Determine how to code _registeredRequests based on whether or not an operator is provided - if (bytes(operator).length > 0) { - _registeredRequests[outboundNonce] = abi.encode(token, operator, sender, amount); - } else { - _registeredRequests[outboundNonce] = abi.encode(token, sender, amount); - } + bool hasOperator = bytes(operator).length > 0; + + // Use a single abi.encode call via ternary operators to handle both cases. + _registeredRequests[outboundNonce] = hasOperator + ? abi.encode(token, operator, sender, amount) + : abi.encode(token, sender, amount); + _registeredRequestActions[outboundNonce] = action; - // Consider whether operator is empty when building actionArgs - bytes memory actionArgs; - if (bytes(operator).length > 0) { - actionArgs = abi.encodePacked( - bytes32(bytes20(token)), - bytes32(bytes20(sender)), - bytes(operator), - amount - ); - } else { - actionArgs = abi.encodePacked( - bytes32(bytes20(token)), - bytes32(bytes20(sender)), - amount - ); - } + + // Use a single abi.encodePacked call via ternary operators to handle both cases. + bytes memory actionArgs = hasOperator + ? abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(sender)), bytes(operator), amount) + : abi.encodePacked(bytes32(bytes20(token)), bytes32(bytes20(sender)), amount); + _sendMsgToExocore(action, actionArgs); } function _sendMsgToExocore(Action action, bytes memory actionArgs) internal { diff --git a/src/core/LSTRestakingController.sol b/src/core/LSTRestakingController.sol index 58702ec8..3d5f39ed 100644 --- a/src/core/LSTRestakingController.sol +++ b/src/core/LSTRestakingController.sol @@ -10,20 +10,17 @@ import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/Pau abstract contract LSTRestakingController is PausableUpgradeable, ILSTRestakingController, - BaseRestakingController + BaseRestakingController { function deposit(address token, uint256 amount) external payable isTokenWhitelisted(token) isValidAmount(amount) whenNotPaused { _processRequest(token, msg.sender, amount, Action.REQUEST_DEPOSIT,""); - } function withdrawPrincipleFromExocore(address token, uint256 principleAmount) external payable isTokenWhitelisted(token) isValidAmount(principleAmount) whenNotPaused { _processRequest(token, msg.sender, principleAmount, Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE,""); - } function withdrawRewardFromExocore(address token, uint256 rewardAmount) external payable isTokenWhitelisted(token) isValidAmount(rewardAmount) whenNotPaused { _processRequest(token, msg.sender, rewardAmount, Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE,""); - } } diff --git a/src/interfaces/IBaseRestakingController.sol b/src/interfaces/IBaseRestakingController.sol index 14d79b02..6075c94d 100644 --- a/src/interfaces/IBaseRestakingController.sol +++ b/src/interfaces/IBaseRestakingController.sol @@ -12,7 +12,7 @@ interface IBaseRestakingController { */ function delegateTo(string calldata operator, address token, uint256 amount) external payable; - function undelegateFrom(string calldata, address token, uint256 amount) external payable; + function undelegateFrom(string calldata operator, address token, uint256 amount) external payable; /** * @notice Client chain users call to claim their unlocked assets from the vault. From aae7dad21e071b8f66ca71026fa54388208896ea Mon Sep 17 00:00:00 2001 From: adu Date: Fri, 17 May 2024 16:57:38 +0800 Subject: [PATCH 40/93] store BEACON_PROXY_BYTECODE in a separate contract to fix code size too big issue and deploy contracts on sepolia for testing --- script/1_Prerequisities.s.sol | 22 ----- script/2_DeployBoth.s.sol | 39 +++++++- script/6_CreateExoCapsule.s.sol | 86 +++++++++++++++++ script/BaseScript.sol | 4 +- script/capsule.json | 4 + script/deployBeaconOracle.s.sol | 45 +++++++++ script/deployedContracts.json | 18 +++- script/integration/1_DeployBootstrap.s.sol | 9 +- script/prerequisitContracts.json | 7 +- src/core/BeaconProxyBytecode.sol | 15 +++ src/core/Bootstrap.sol | 8 +- src/core/ClientChainGateway.sol | 7 +- src/core/NativeRestakingController.sol | 2 +- src/storage/BootstrapStorage.sol | 15 ++- src/storage/ClientChainGatewayStorage.sol | 5 +- test/foundry/Bootstrap.t.sol | 32 +++++-- test/foundry/ClientChainGateway.t.sol | 9 +- test/foundry/ExoCapsule.t.sol | 6 +- test/foundry/ExocoreDeployer.t.sol | 8 +- .../validator_container_proof_8955769.json | 68 ++++++++++++++ test/mocks/DepositWithdrawMock.sol | 10 +- test/mocks/ExocoreGatewayMock.sol | 94 ++++++++++++++----- 22 files changed, 420 insertions(+), 93 deletions(-) create mode 100644 script/6_CreateExoCapsule.s.sol create mode 100644 script/capsule.json create mode 100644 script/deployBeaconOracle.s.sol create mode 100644 src/core/BeaconProxyBytecode.sol create mode 100644 test/foundry/test-data/validator_container_proof_8955769.json diff --git a/script/1_Prerequisities.s.sol b/script/1_Prerequisities.s.sol index 717a941f..31bda538 100644 --- a/script/1_Prerequisities.s.sol +++ b/script/1_Prerequisities.s.sol @@ -56,9 +56,6 @@ contract PrerequisitiesScript is BaseScript { // use deployed ERC20 token as restake token restakeToken = ERC20PresetFixedSupply(erc20TokenAddress); - // deploy beacon chain oracle - beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); - string memory deployedContracts = "deployedContracts"; string memory clientChainContracts = "clientChainContracts"; string memory exocoreContracts = "exocoreContracts"; @@ -82,23 +79,4 @@ contract PrerequisitiesScript is BaseScript { vm.writeJson(finalJson, "script/prerequisitContracts.json"); } - - function _deployBeaconOracle() internal returns (address) { - uint256 GENESIS_BLOCK_TIMESTAMP; - - if (block.chainid == 1) { - GENESIS_BLOCK_TIMESTAMP = 1606824023; - } else if (block.chainid == 5) { - GENESIS_BLOCK_TIMESTAMP = 1616508000; - } else if (block.chainid == 11155111) { - GENESIS_BLOCK_TIMESTAMP = 1655733600; - } else if (block.chainid == 17000) { - GENESIS_BLOCK_TIMESTAMP = 1695902400; - } else { - revert("Unsupported chainId."); - } - - EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); - return address(oracle); - } } diff --git a/script/2_DeployBoth.s.sol b/script/2_DeployBoth.s.sol index c6fa7f99..ee1c5cc8 100644 --- a/script/2_DeployBoth.s.sol +++ b/script/2_DeployBoth.s.sol @@ -5,6 +5,7 @@ import {Vault} from "../src/core/Vault.sol"; import "../src/core/ExocoreGateway.sol"; import "../test/mocks/ExocoreGatewayMock.sol"; import "../src/core/ExoCapsule.sol"; +import "../src/core/BeaconProxyBytecode.sol"; import "forge-std/Script.sol"; import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; @@ -24,9 +25,6 @@ contract DeployScript is BaseScript { clientChainLzEndpoint = ILayerZeroEndpointV2(stdJson.readAddress(prerequisities, ".clientChain.lzEndpoint")); require(address(clientChainLzEndpoint) != address(0), "client chain l0 endpoint should not be empty"); - beaconOracle = IBeaconChainOracle(stdJson.readAddress(prerequisities, ".clientChain.beaconOracle")); - require(address(beaconOracle) != address(0), "client chain beacon oracle should not be empty"); - restakeToken = ERC20PresetFixedSupply(stdJson.readAddress(prerequisities, ".clientChain.erc20Token")); require(address(restakeToken) != address(0), "restake token address should not be empty"); @@ -62,6 +60,9 @@ contract DeployScript is BaseScript { // deploy clientchaingateway on client chain via rpc vm.selectFork(clientChain); vm.startBroadcast(deployer.privateKey); + + // deploy beacon chain oracle + beaconOracle = _deployBeaconOracle(); /// deploy vault implementation contract and capsule implementation contract /// that has logics called by proxy @@ -72,16 +73,20 @@ contract DeployScript is BaseScript { vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); - /// deploy client chain gateway + // deploy BeaconProxyBytecode to store BeaconProxyBytecode + beaconProxyBytecode = new BeaconProxyBytecode(); + whitelistTokens.push(address(restakeToken)); + /// deploy client chain gateway ProxyAdmin clientChainProxyAdmin = new ProxyAdmin(); ClientChainGateway clientGatewayLogic = new ClientChainGateway( address(clientChainLzEndpoint), exocoreChainId, address(beaconOracle), address(vaultBeacon), - address(capsuleBeacon) + address(capsuleBeacon), + address(beaconProxyBytecode) ); clientGateway = ClientChainGateway( payable( @@ -99,11 +104,15 @@ contract DeployScript is BaseScript { ) ); + // find vault according to uderlying token address + vault = Vault(address(ClientChainGateway(payable(address(clientGateway))).tokenToVault(address(restakeToken)))); + vm.stopBroadcast(); // deploy on Exocore via rpc vm.selectFork(exocore); vm.startBroadcast(deployer.privateKey); + // deploy Exocore network contracts ProxyAdmin exocoreProxyAdmin = new ProxyAdmin(); @@ -153,6 +162,7 @@ contract DeployScript is BaseScript { vm.serializeAddress(clientChainContracts, "erc20Token", address(restakeToken)); vm.serializeAddress(clientChainContracts, "vaultBeacon", address(vaultBeacon)); vm.serializeAddress(clientChainContracts, "capsuleBeacon", address(capsuleBeacon)); + vm.serializeAddress(clientChainContracts, "beaconProxyBytecode", address(beaconProxyBytecode)); string memory clientChainContractsOutput = vm.serializeAddress(clientChainContracts, "proxyAdmin", address(clientChainProxyAdmin)); @@ -174,4 +184,23 @@ contract DeployScript is BaseScript { vm.writeJson(finalJson, "script/deployedContracts.json"); } + + function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { + uint256 GENESIS_BLOCK_TIMESTAMP; + + if (block.chainid == 1) { + GENESIS_BLOCK_TIMESTAMP = 1606824023; + } else if (block.chainid == 5) { + GENESIS_BLOCK_TIMESTAMP = 1616508000; + } else if (block.chainid == 11155111) { + GENESIS_BLOCK_TIMESTAMP = 1655733600; + } else if (block.chainid == 17000) { + GENESIS_BLOCK_TIMESTAMP = 1695902400; + } else { + revert("Unsupported chainId."); + } + + EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); + return oracle; + } } diff --git a/script/6_CreateExoCapsule.s.sol b/script/6_CreateExoCapsule.s.sol new file mode 100644 index 00000000..57f310e6 --- /dev/null +++ b/script/6_CreateExoCapsule.s.sol @@ -0,0 +1,86 @@ +pragma solidity ^0.8.19; + +import "forge-std/Script.sol"; +import {ERC20PresetFixedSupply} from "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import "../src/interfaces/IClientChainGateway.sol"; +import "../src/interfaces/IVault.sol"; +import "../src/interfaces/IExocoreGateway.sol"; +import "../src/interfaces/precompiles/IDelegation.sol"; +import "../src/interfaces/precompiles/IDeposit.sol"; +import "../src/interfaces/precompiles/IWithdrawPrinciple.sol"; +import "../src/interfaces/precompiles/IClaimReward.sol"; +import "../src/storage/GatewayStorage.sol"; +import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; +import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; +import {BaseScript} from "./BaseScript.sol"; +import "forge-std/StdJson.sol"; + +contract DepositScript is BaseScript { + using AddressCast for address; + + function setUp() public virtual override { + super.setUp(); + + string memory deployedContracts = vm.readFile("script/deployedContracts.json"); + + clientGateway = + IClientChainGateway(payable(stdJson.readAddress(deployedContracts, ".clientChain.clientChainGateway"))); + require(address(clientGateway) != address(0), "clientGateway address should not be empty"); + + if (!useExocorePrecompileMock) { + // bind precompile mock contracts code to constant precompile address so that local simulation could pass + bytes memory DepositMockCode = vm.getDeployedCode("DepositWithdrawMock.sol"); + vm.etch(DEPOSIT_PRECOMPILE_ADDRESS, DepositMockCode); + + bytes memory DelegationMockCode = vm.getDeployedCode("DelegationMock.sol"); + vm.etch(DELEGATION_PRECOMPILE_ADDRESS, DelegationMockCode); + + bytes memory WithdrawPrincipleMockCode = vm.getDeployedCode("DepositWithdrawMock.sol"); + vm.etch(WITHDRAW_PRECOMPILE_ADDRESS, WithdrawPrincipleMockCode); + + bytes memory WithdrawRewardMockCode = vm.getDeployedCode("ClaimRewardMock.sol"); + vm.etch(CLAIM_REWARD_PRECOMPILE_ADDRESS, WithdrawRewardMockCode); + } + + // transfer some gas fee to depositor, relayer and exocore gateway + clientChain = vm.createSelectFork(clientChainRPCURL); + vm.startBroadcast(deployer.privateKey); + if (depositor.addr.balance < 0.2 ether) { + (bool sent,) = depositor.addr.call{value: 0.2 ether}(""); + require(sent, "Failed to send Ether"); + } + vm.stopBroadcast(); + + exocore = vm.createSelectFork(exocoreRPCURL); + vm.startBroadcast(exocoreGenesis.privateKey); + if (depositor.addr.balance < 2 ether) { + (bool sent,) = depositor.addr.call{value: 2 ether}(""); + require(sent, "Failed to send Ether"); + } + if (relayer.addr.balance < 2 ether) { + (bool sent,) = relayer.addr.call{value: 2 ether}(""); + require(sent, "Failed to send Ether"); + } + if (address(exocoreGateway).balance < 2 ether) { + (bool sent,) = address(exocoreGateway).call{value: 2 ether}(""); + require(sent, "Failed to send Ether"); + } + vm.stopBroadcast(); + } + + function run() public { + vm.selectFork(clientChain); + vm.startBroadcast(depositor.privateKey); + + address capsule = clientGateway.createExoCapsule(); + + vm.stopBroadcast(); + + string memory capsulesJson = "capsule1"; + vm.serializeAddress(capsulesJson, "owner", depositor.addr); + string memory capsulesOutput = vm.serializeAddress(capsulesJson, "capsule", capsule); + + vm.writeJson(capsulesOutput, "script/capsule.json"); + } +} diff --git a/script/BaseScript.sol b/script/BaseScript.sol index 37d471e7..2e1e22a3 100644 --- a/script/BaseScript.sol +++ b/script/BaseScript.sol @@ -4,6 +4,7 @@ import "../src/interfaces/IClientChainGateway.sol"; import "../src/interfaces/IVault.sol"; import "../src/interfaces/IExocoreGateway.sol"; import "../src/interfaces/IExoCapsule.sol"; +import "../src/core/BeaconProxyBytecode.sol"; import {ERC20PresetFixedSupply} from "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; @@ -34,12 +35,13 @@ contract BaseScript is Script { IExocoreGateway exocoreGateway; ILayerZeroEndpointV2 clientChainLzEndpoint; ILayerZeroEndpointV2 exocoreLzEndpoint; - IBeaconChainOracle beaconOracle; + EigenLayerBeaconOracle beaconOracle; ERC20PresetFixedSupply restakeToken; IVault vaultImplementation; IExoCapsule capsuleImplementation; IBeacon vaultBeacon; IBeacon capsuleBeacon; + BeaconProxyBytecode beaconProxyBytecode; address delegationMock; address depositMock; diff --git a/script/capsule.json b/script/capsule.json new file mode 100644 index 00000000..c75d730f --- /dev/null +++ b/script/capsule.json @@ -0,0 +1,4 @@ +{ + "capsule": "0xFCC9a210747182a5FC16520e46081E8Ca6C93c39", + "owner": "0xA1dfab3234f49e02e04E6C56a021F1a497CD0f82" +} \ No newline at end of file diff --git a/script/deployBeaconOracle.s.sol b/script/deployBeaconOracle.s.sol new file mode 100644 index 00000000..9d005dea --- /dev/null +++ b/script/deployBeaconOracle.s.sol @@ -0,0 +1,45 @@ +pragma solidity ^0.8.19; + +import "forge-std/Script.sol"; +import {ERC20PresetFixedSupply} from "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; +import {NonShortCircuitEndpointV2Mock} from "test/mocks/NonShortCircuitEndpointV2Mock.sol"; +import "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import "test/mocks/ClaimRewardMock.sol"; +import "test/mocks/DelegationMock.sol"; +import "test/mocks/DepositWithdrawMock.sol"; +import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; +import "./BaseScript.sol"; + +contract PrerequisitiesScript is BaseScript { + function setUp() public virtual override { + super.setUp(); + } + + function run() public { + clientChain = vm.createSelectFork(clientChainRPCURL); + + vm.startBroadcast(deployer.privateKey); + beaconOracle = EigenLayerBeaconOracle(0xd3D285cd1516038dAED61B8BF7Ae2daD63662492); + (bool success,) = address(beaconOracle).call(abi.encodeWithSelector(beaconOracle.addTimestamp.selector, 1715918948)); + vm.stopPrank(); + } + + function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { + uint256 GENESIS_BLOCK_TIMESTAMP; + + if (block.chainid == 1) { + GENESIS_BLOCK_TIMESTAMP = 1606824023; + } else if (block.chainid == 5) { + GENESIS_BLOCK_TIMESTAMP = 1616508000; + } else if (block.chainid == 11155111) { + GENESIS_BLOCK_TIMESTAMP = 1655733600; + } else if (block.chainid == 17000) { + GENESIS_BLOCK_TIMESTAMP = 1695902400; + } else { + revert("Unsupported chainId."); + } + + EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); + return oracle; + } +} diff --git a/script/deployedContracts.json b/script/deployedContracts.json index 12e6e428..47fff070 100644 --- a/script/deployedContracts.json +++ b/script/deployedContracts.json @@ -1,14 +1,22 @@ { "clientChain": { - "clientChainGateway": "0x2A1440F140275Cb5D4CC057f70e66bE17302d34f", + "beaconOracle": "0x811099C615C2aEc0D19411E3e24cf84C91A4EB69", + "beaconProxyBytecode": "0x6E9968a8338b52db9D0f898074F8F3177AC536fb", + "capsuleBeacon": "0xa670D5D7be192036bddA4E1cC437078C0Eb2F5C7", + "clientChainGateway": "0xd9995bf3B8FF84565B14F5DeA6726c8836b40EBC", "erc20Token": "0x83E6850591425e3C1E263c054f4466838B9Bd9e4", "lzEndpoint": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "proxyAdmin": "0xae8E02Fd6a434Bc50Cb6195008DFb15f9a369238", - "resVault": "0x21a106936F2C794731aBf690923d24D35D451bDB" + "proxyAdmin": "0xCceA3b7430681Ee7424aF896F876F61994241AD6", + "resVault": "0x17A627CF8FD3b606e95168d74Ff4c6A725F3D44F", + "vaultBeacon": "0x82E7254Da852b41b38211dB59E0531c0e393D343" }, "exocore": { - "exocoreGateway": "0x7BEC5b65eB5455baD82b5372333453973b476FFE", + "claimRewardPrecompileMock": "0xF74ebB6772D74C92F39a5ef188166d7E664203fA", + "delegationPrecompileMock": "0x8700Cb31b3A29Fd0035F21b26B2F21EfDD9AF8Ab", + "depositPrecompileMock": "0x00486175f0E82ef26022C308FfE43753c4045FF3", + "exocoreGateway": "0x27a312Ac654F95420BF2D35881e5a1Fc2b4B31F3", "lzEndpoint": "0x6EDCE65403992e310A62460808c4b910D972f10f", - "proxyAdmin": "0x838169697D3f829554D6E7B4cBA8A7614E2829c7" + "proxyAdmin": "0x41C61Ec91199E52B088F47c0208ef987eF23866F", + "withdrawPrecompileMock": "0x00486175f0E82ef26022C308FfE43753c4045FF3" } } \ No newline at end of file diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index 5a26b63e..d7725086 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -16,7 +16,7 @@ import {CustomProxyAdmin} from "../../src/core/CustomProxyAdmin.sol"; import {MyToken} from "../../test/foundry/MyToken.sol"; import {Vault} from "../../src/core/Vault.sol"; import {IVault} from "../../src/interfaces/IVault.sol"; - +import "../../src/core/BeaconProxyBytecode.sol"; // Technically this is used for testing but it is marked as a script // because it is a script that is used to deploy the contracts on Anvil @@ -50,6 +50,7 @@ contract DeployContracts is Script { IVault vaultImplementation; IBeacon vaultBeacon; + BeaconProxyBytecode beaconProxyBytecode; function setUp() private { // these are default values for Anvil's usual mnemonic. @@ -111,12 +112,16 @@ contract DeployContracts is Script { /// deploy the vault beacon that store the implementation contract address vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); + // deploy BeaconProxyBytecode to store BeaconProxyBytecode + beaconProxyBytecode = new BeaconProxyBytecode(); + proxyAdmin = new CustomProxyAdmin(); EndpointV2Mock clientChainLzEndpoint = new EndpointV2Mock(clientChainId); Bootstrap bootstrapLogic = new Bootstrap( address(clientChainLzEndpoint), exocoreChainId, - address(vaultBeacon) + address(vaultBeacon), + address(beaconProxyBytecode) ); bootstrap = Bootstrap( payable(address( diff --git a/script/prerequisitContracts.json b/script/prerequisitContracts.json index eb4ea5a5..c0ace1a1 100644 --- a/script/prerequisitContracts.json +++ b/script/prerequisitContracts.json @@ -1,9 +1,14 @@ { "clientChain": { + "beaconOracle": "0x0000000000000000000000000000000000000000", "erc20Token": "0x83E6850591425e3C1E263c054f4466838B9Bd9e4", "lzEndpoint": "0x6EDCE65403992e310A62460808c4b910D972f10f" }, "exocore": { - "lzEndpoint": "0x6EDCE65403992e310A62460808c4b910D972f10f" + "claimRewardPrecompileMock": "0xF74ebB6772D74C92F39a5ef188166d7E664203fA", + "delegationPrecompileMock": "0x8700Cb31b3A29Fd0035F21b26B2F21EfDD9AF8Ab", + "depositPrecompileMock": "0x00486175f0E82ef26022C308FfE43753c4045FF3", + "lzEndpoint": "0x6EDCE65403992e310A62460808c4b910D972f10f", + "withdrawPrecompileMock": "0x00486175f0E82ef26022C308FfE43753c4045FF3" } } \ No newline at end of file diff --git a/src/core/BeaconProxyBytecode.sol b/src/core/BeaconProxyBytecode.sol new file mode 100644 index 00000000..fb4b3200 --- /dev/null +++ b/src/core/BeaconProxyBytecode.sol @@ -0,0 +1,15 @@ +pragma solidity ^0.8.19; + +contract BeaconProxyBytecode { + /** + * @notice Stored code of type(BeaconProxy).creationCode + * @dev Maintained as a constant to solve an edge case - changes to OpenZeppelin's BeaconProxy code should not cause + * addresses of EigenPods that are pre-computed with Create2 to change, even upon upgrading this contract, changing compiler version, etc. + */ + bytes constant BEACON_PROXY_BYTECODE = + hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; + + function getBytecode() external pure returns (bytes memory) { + return BEACON_PROXY_BYTECODE; + } +} diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index 5b263918..f34e60bb 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -19,6 +19,7 @@ import {IVault} from "../interfaces/IVault.sol"; import {BootstrapLzReceiver} from "./BootstrapLzReceiver.sol"; import {BootstrapStorage} from "../storage/BootstrapStorage.sol"; import {Vault} from "./Vault.sol"; +import {BeaconProxyBytecode} from "./BeaconProxyBytecode.sol"; // ClientChainGateway differences: // replace IClientChainGateway with ITokenWhitelister (excludes only quote function). @@ -39,8 +40,9 @@ contract Bootstrap is constructor( address endpoint_, uint32 exocoreChainId_, - address vaultBeacon_ - ) OAppCoreUpgradeable(endpoint_) BootstrapStorage(exocoreChainId_, vaultBeacon_) { + address vaultBeacon_, + address beaconProxyBytecode_ + ) OAppCoreUpgradeable(endpoint_) BootstrapStorage(exocoreChainId_, vaultBeacon_, beaconProxyBytecode_) { _disableInitializers(); } @@ -669,7 +671,7 @@ contract Bootstrap is 0, bytes32(uint256(uint160(underlyingToken))), // set the beacon address for beacon proxy - abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(vaultBeacon), "")) + abi.encodePacked(beaconProxyBytecode.getBytecode(), abi.encode(address(vaultBeacon), "")) ) ); vault.initialize(underlyingToken, address(this)); diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index aff27e9d..1eaf6511 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -43,10 +43,11 @@ contract ClientChainGateway is uint32 exocoreChainId_, address beaconOracleAddress_, address vaultBeacon_, - address exoCapsuleBeacon_ + address exoCapsuleBeacon_, + address beaconProxyBytecode_ ) OAppCoreUpgradeable(endpoint_) - ClientChainGatewayStorage(exocoreChainId_, beaconOracleAddress_, vaultBeacon_, exoCapsuleBeacon_) + ClientChainGatewayStorage(exocoreChainId_, beaconOracleAddress_, vaultBeacon_, exoCapsuleBeacon_, beaconProxyBytecode_) { _disableInitializers(); } @@ -210,7 +211,7 @@ contract ClientChainGateway is 0, bytes32(uint256(uint160(underlyingToken))), // set the beacon address for beacon proxy - abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(vaultBeacon), "")) + abi.encodePacked(beaconProxyBytecode.getBytecode(), abi.encode(address(vaultBeacon), "")) ) ); vault.initialize(underlyingToken, address(this)); diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 03903875..245d709d 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -35,7 +35,7 @@ abstract contract NativeRestakingController is 0, bytes32(uint256(uint160(msg.sender))), // set the beacon address for beacon proxy - abi.encodePacked(BEACON_PROXY_BYTECODE, abi.encode(address(exoCapsuleBeacon), "")) + abi.encodePacked(beaconProxyBytecode.getBytecode(), abi.encode(address(exoCapsuleBeacon), "")) ) ); capsule.initialize(address(this), msg.sender, beaconOracleAddress); diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index 24a5d787..420b805d 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -4,6 +4,7 @@ import {GatewayStorage} from "./GatewayStorage.sol"; import {IOperatorRegistry} from "../interfaces/IOperatorRegistry.sol"; import {IVault} from "../interfaces/IVault.sol"; import {IBeacon} from "@openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol"; +import {BeaconProxyBytecode} from "../core/BeaconProxyBytecode.sol"; // BootstrapStorage should inherit from GatewayStorage since it exists // prior to ClientChainGateway. ClientChainStorage should inherit from @@ -232,14 +233,7 @@ contract BootstrapStorage is GatewayStorage { */ IBeacon public immutable vaultBeacon; - /** - * @notice Stored code of type(BeaconProxy).creationCode - * @dev Maintained as a constant to solve an edge case - changes to OpenZeppelin's BeaconProxy code should not cause - * addresses of EigenPods that are pre-computed with Create2 to change, even upon upgrading this contract, changing compiler version, etc. - */ - bytes constant BEACON_PROXY_BYTECODE = - hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; - + BeaconProxyBytecode public immutable beaconProxyBytecode; /* -------------------------------------------------------------------------- */ /* Events */ @@ -390,13 +384,16 @@ contract BootstrapStorage is GatewayStorage { constructor( uint32 exocoreChainId_, - address vaultBeacon_ + address vaultBeacon_, + address beaconProxyBytecode_ ) { require(exocoreChainId_ != 0, "BootstrapStorage: exocore chain id should not be empty"); require(vaultBeacon_ != address(0), "BootstrapStorage: the vaultBeacon address for beacon proxy should not be empty"); + require(beaconProxyBytecode_ != address(0), "BootstrapStorage: the beaconProxyBytecode address should not be empty"); exocoreChainId = exocoreChainId_; vaultBeacon = IBeacon(vaultBeacon_); + beaconProxyBytecode = BeaconProxyBytecode(beaconProxyBytecode_); } function _getVault(address token) internal view returns (IVault) { diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index 904e7106..aa5cb496 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -80,8 +80,9 @@ contract ClientChainGatewayStorage is BootstrapStorage { uint32 exocoreChainId_, address beaconOracleAddress_, address vaultBeacon_, - address exoCapsuleBeacon_ - ) BootstrapStorage(exocoreChainId_, vaultBeacon_) { + address exoCapsuleBeacon_, + address beaconProxyBytecode_ + ) BootstrapStorage(exocoreChainId_, vaultBeacon_, beaconProxyBytecode_) { require(beaconOracleAddress_ != address(0), "ClientChainGatewayStorage: beacon chain oracle address should not be empty"); require(exoCapsuleBeacon_ != address(0), "ClientChainGatewayStorage: the exoCapsuleBeacon address for beacon proxy should not be empty"); diff --git a/test/foundry/Bootstrap.t.sol b/test/foundry/Bootstrap.t.sol index eaae8fb7..713330a5 100644 --- a/test/foundry/Bootstrap.t.sol +++ b/test/foundry/Bootstrap.t.sol @@ -21,6 +21,7 @@ import {IVault} from "../../src/interfaces/IVault.sol"; import "@openzeppelin/contracts/utils/Create2.sol"; import "src/core/ExoCapsule.sol"; import "src/storage/GatewayStorage.sol"; +import "src/core/BeaconProxyBytecode.sol"; contract BootstrapTest is Test { MyToken myToken; @@ -52,6 +53,7 @@ contract BootstrapTest is Test { IExoCapsule capsuleImplementation; IBeacon vaultBeacon; IBeacon capsuleBeacon; + BeaconProxyBytecode beaconProxyBytecode; bytes constant BEACON_PROXY_BYTECODE = hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; @@ -77,6 +79,9 @@ contract BootstrapTest is Test { /// deploy the vault beacon that store the implementation contract address vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); + // deploy BeaconProxyBytecode to store BeaconProxyBytecode + beaconProxyBytecode = new BeaconProxyBytecode(); + // then the ProxyAdmin proxyAdmin = new CustomProxyAdmin(); // then the logic @@ -86,7 +91,8 @@ contract BootstrapTest is Test { Bootstrap bootstrapLogic = new Bootstrap( address(clientChainLzEndpoint), exocoreChainId, - address(vaultBeacon) + address(vaultBeacon), + address(beaconProxyBytecode) ); // then the params + proxy spawnTime = block.timestamp + 1 hours; @@ -134,7 +140,8 @@ contract BootstrapTest is Test { exocoreChainId, address(0x1), address(vaultBeacon), - address(capsuleBeacon) + address(capsuleBeacon), + address(beaconProxyBytecode) ); // uint256 tokenCount = bootstrap.getWhitelistedTokensCount(); // address[] memory tokensForCall = new address[](tokenCount); @@ -1100,7 +1107,8 @@ contract BootstrapTest is Test { Bootstrap bootstrapLogic = new Bootstrap( address(clientChainLzEndpoint), exocoreChainId, - address(vaultBeacon) + address(vaultBeacon), + address(beaconProxyBytecode) ); vm.expectRevert("Bootstrap: owner should not be empty"); Bootstrap( @@ -1122,7 +1130,8 @@ contract BootstrapTest is Test { Bootstrap bootstrapLogic = new Bootstrap( address(clientChainLzEndpoint), exocoreChainId, - address(vaultBeacon) + address(vaultBeacon), + address(beaconProxyBytecode) ); vm.warp(20); vm.expectRevert("Bootstrap: spawn time should be in the future"); @@ -1145,7 +1154,8 @@ contract BootstrapTest is Test { Bootstrap bootstrapLogic = new Bootstrap( address(clientChainLzEndpoint), exocoreChainId, - address(vaultBeacon) + address(vaultBeacon), + address(beaconProxyBytecode) ); vm.expectRevert("Bootstrap: offset duration should be greater than 0"); Bootstrap( @@ -1167,7 +1177,8 @@ contract BootstrapTest is Test { Bootstrap bootstrapLogic = new Bootstrap( address(clientChainLzEndpoint), exocoreChainId, - address(vaultBeacon) + address(vaultBeacon), + address(beaconProxyBytecode) ); vm.expectRevert("Bootstrap: spawn time should be greater than offset duration"); vm.warp(20); @@ -1190,7 +1201,8 @@ contract BootstrapTest is Test { Bootstrap bootstrapLogic = new Bootstrap( address(clientChainLzEndpoint), exocoreChainId, - address(vaultBeacon) + address(vaultBeacon), + address(beaconProxyBytecode) ); vm.expectRevert("Bootstrap: lock time should be in the future"); vm.warp(20); @@ -1213,7 +1225,8 @@ contract BootstrapTest is Test { Bootstrap bootstrapLogic = new Bootstrap( address(clientChainLzEndpoint), exocoreChainId, - address(vaultBeacon) + address(vaultBeacon), + address(beaconProxyBytecode) ); vm.expectRevert("Bootstrap: exocore validator set address should not be empty"); Bootstrap( @@ -1235,7 +1248,8 @@ contract BootstrapTest is Test { Bootstrap bootstrapLogic = new Bootstrap( address(clientChainLzEndpoint), exocoreChainId, - address(vaultBeacon) + address(vaultBeacon), + address(beaconProxyBytecode) ); vm.expectRevert("Bootstrap: custom proxy admin should not be empty"); Bootstrap( diff --git a/test/foundry/ClientChainGateway.t.sol b/test/foundry/ClientChainGateway.t.sol index 06bfca75..b6433ec0 100644 --- a/test/foundry/ClientChainGateway.t.sol +++ b/test/foundry/ClientChainGateway.t.sol @@ -19,7 +19,7 @@ import "../../src/interfaces/precompiles/IDeposit.sol"; import "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; import "../../src/interfaces/IVault.sol"; import "../../src/interfaces/IExoCapsule.sol"; - +import "src/core/BeaconProxyBytecode.sol"; contract ClientChainGatewayTest is Test { Player[] players; @@ -38,6 +38,7 @@ contract ClientChainGatewayTest is Test { IExoCapsule capsuleImplementation; IBeacon vaultBeacon; IBeacon capsuleBeacon; + BeaconProxyBytecode beaconProxyBytecode; string operatorAddress = "exo1v4s6vtjpmxwu9rlhqms5urzrc3tc2ae2gnuqhc"; uint16 exocoreChainId = 2; @@ -75,6 +76,9 @@ contract ClientChainGatewayTest is Test { vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); + + // deploy BeaconProxyBytecode to store BeaconProxyBytecode + beaconProxyBytecode = new BeaconProxyBytecode(); restakeToken = new ERC20PresetFixedSupply("rest", "rest", 1e16, exocoreValidatorSet.addr); whitelistTokens.push(address(restakeToken)); @@ -86,7 +90,8 @@ contract ClientChainGatewayTest is Test { exocoreChainId, address(beaconOracle), address(vaultBeacon), - address(capsuleBeacon) + address(capsuleBeacon), + address(beaconProxyBytecode) ); clientGateway = ClientChainGateway( payable(address(new TransparentUpgradeableProxy(address(clientGatewayLogic), address(proxyAdmin), ""))) diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 73455a6d..a2dfe6ad 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -46,7 +46,7 @@ contract SetUp is Test { uint256 mockCurrentBlockTimestamp; function setUp() public { - string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); + string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_8955769.json"); validatorContainer = stdJson.readBytes32Array(validatorInfo, ".ValidatorFields"); require(validatorContainer.length > 0, "validator container should not be empty"); @@ -71,8 +71,10 @@ contract SetUp is Test { ExoCapsule phantomCapsule = new ExoCapsule(); address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); + vm.etch(capsuleAddress, address(phantomCapsule).code); capsule = ExoCapsule(capsuleAddress); + assertEq(bytes32(capsule.capsuleWithdrawalCredentials()), _getWithdrawalCredentials(validatorContainer)); stdstore.target(capsuleAddress).sig("gateway()").checked_write(bytes32(uint256(uint160(address(this))))); @@ -106,7 +108,7 @@ contract VerifyDepositProof is SetUp { using BeaconChainProofs for bytes32; using stdStorage for StdStorage; - function test_verifyDepositProof() public { + function test_verifyDepositProof_success() public { uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index 7f8689ec..8a94a937 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -24,6 +24,7 @@ import "../../src/interfaces/precompiles/IClaimReward.sol"; import "test/mocks/ETHPOSDepositMock.sol"; import "src/libraries/Endian.sol"; import "src/core/ExoCapsule.sol"; +import "src/core/BeaconProxyBytecode.sol"; contract ExocoreDeployer is Test { using AddressCast for address; @@ -48,6 +49,7 @@ contract ExocoreDeployer is Test { IExoCapsule capsuleImplementation; IBeacon vaultBeacon; IBeacon capsuleBeacon; + BeaconProxyBytecode beaconProxyBytecode; uint256 constant BEACON_CHAIN_GENESIS_TIME = 1606824023; /// @notice The number of slots each epoch in the beacon chain @@ -135,6 +137,9 @@ contract ExocoreDeployer is Test { vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); capsuleBeacon = new UpgradeableBeacon(address(capsuleImplementation)); + // deploy BeaconProxyBytecode to store BeaconProxyBytecode + beaconProxyBytecode = new BeaconProxyBytecode(); + // attach ETHPOSDepositMock contract code to constant address ETHPOSDepositMock ethPOSDepositMock = new ETHPOSDepositMock(); vm.etch(address(ETH_POS), address(ethPOSDepositMock).code); @@ -148,7 +153,8 @@ contract ExocoreDeployer is Test { exocoreChainId, address(beaconOracle), address(vaultBeacon), - address(capsuleBeacon) + address(capsuleBeacon), + address(beaconProxyBytecode) ); clientGateway = ClientChainGateway( payable( diff --git a/test/foundry/test-data/validator_container_proof_8955769.json b/test/foundry/test-data/validator_container_proof_8955769.json new file mode 100644 index 00000000..2acf04e0 --- /dev/null +++ b/test/foundry/test-data/validator_container_proof_8955769.json @@ -0,0 +1,68 @@ +{ + "latestBlockHeaderRoot": "0xa757c2c667004a32911f098816ab7b83a884e8288e7032bd2c6d08a0e84011cf", + "StateRootAgainstLatestBlockHeaderProof": [ + "0x04d41931448bd9435b49b5b64cd08a10c0eddd58ee01d18085e3c4777bc3c6a6", + "0xffea50b94a33ab1fbf96897669e07b068322089fa3d29c7ccb489f0bf902e4e0", + "0xa97fa9a4a943377a9dbcaffc0a86bbbd4baa111d0ad90d6fdbd09863a0480667" + ], + "beaconStateRoot": "0x797b3790cbcc171dba1ea3794b045169fcfcc49c6a01b288952808bca47352d3", + "validatorIndex": 1154920, + "WithdrawalCredentialProof": [ + "0x771ad07c2cc810cbefa17ccd563aa857878311f2a11839a14e07bf0742e97dd4", + "0xe94d946474e6f2e7d0bb7199c84edc975b40451375b496e200dfc4f000967f55", + "0x4530b7bd066535cf30ce3cdb0fbd37df1050963d9ccaac2a4063eca29de53742", + "0xe27fcabe5cf40eb9b6413de028edad47eb1892cc1a5ccde8aa5819c705dfb562", + "0x1cf399c5081a51a6ff8a4a2d0109cfc31c9dece6a40598b7fdf71bf1b31a1db4", + "0x4e320eee0f9356efdcaeeae7a0bce7423606fb6028fff7a6b5a80a201aaf9be0", + "0x5f4bf6e7d157fe389b944b139e477a6603a95606bc88f3fbd21c725eab9b23bb", + "0xc87dad2e0b98961331f401eff268444ba62d765c5660edd4e89e4a9212bd3c97", + "0xa823ec200d3ad44f80e705a29321d46fb1dd772c3374d762dee8859818671e7c", + "0xa89ae389e365d7fbe77993b4cd22421f00fd59f3753e9185f6a0b8d949f98e47", + "0x4ed9832a3ff17db34343ecfe6f8f05a1cd8f6dc29b2a88d9f1b2249e28dc85fe", + "0xa40e898238a592d5178f6136d07079de245c139f1390c884d99b283211e0635d", + "0x8ecf54fc93b1d8f6031c43dd88d6b4d23508ab8b1279831947d3dc6374214caa", + "0x17c02e90cc9fde733a1611b92511cfc0932d2232235d99c23c6d50a5e8ae42da", + "0xa3a2ce643211405afffb01977d4cc4c8b38a87a33e6be8a968b36695bfb443f5", + "0x5801f863b9dcd6b1fc732ef9913b9834e9f860b163362a2862f41ea276804100", + "0x7d9d035872b0b3a4aa36fc38ec22f8844356db1b4df3f7ed6685471e248f4b3a", + "0x345ff18c389f4b012539810c4b38ac0c30486e066dc1b73838c766c0ac60489d", + "0xe40de22dafb71d941531beed9425d1f01b58b16d132aecc521fa76c12d7f98ab", + "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0xb29740bb6e642d4c802524042dc87dd9106de14a985b3ecf97a306bf37ae4294", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", + "0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff", + "0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5", + "0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d", + "0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c", + "0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327", + "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", + "0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76", + "0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f", + "0x3b11150000000000000000000000000000000000000000000000000000000000", + "0x1c40160000000000000000000000000000000000000000000000000000000000", + "0xffb5d7ff01773ed2dd0bdf42b256210d378ba39978109a7c88293eb84f3c45f4", + "0x0d599e5da1b4f53ef05b0a142ffb31458150e87dc8a3a85d638a82bd4c27fdb3", + "0x9edc0d52ad5c2a175be71107d8c1354ea09560d940cf5767c454c8ec7c2c01a7", + "0x8a8580a725016e90c04458cf5e9647c6371e08e4bca680d272a2749fe807ee85" + ], + "ValidatorFields": [ + "0x6559ea8a926160a0681fb62b44c307aa96227bcd640c1bae49dd6d5bf49735ad", + "0x010000000000000000000000b9d7934878b5fb9610b3fe8a5e441e8fad7e293f", + "0x0040597307000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x23f4030000000000000000000000000000000000000000000000000000000000", + "0x66f4030000000000000000000000000000000000000000000000000000000000", + "0xffffffffffffffff000000000000000000000000000000000000000000000000", + "0xffffffffffffffff000000000000000000000000000000000000000000000000" + ] +} \ No newline at end of file diff --git a/test/mocks/DepositWithdrawMock.sol b/test/mocks/DepositWithdrawMock.sol index 16dfe1df..c01d77c6 100644 --- a/test/mocks/DepositWithdrawMock.sol +++ b/test/mocks/DepositWithdrawMock.sol @@ -4,7 +4,7 @@ import {IDeposit} from "../../src/interfaces/precompiles/IDeposit.sol"; import {IWithdraw} from "../../src/interfaces/precompiles/IWithdrawPrinciple.sol"; contract DepositWithdrawMock is IDeposit, IWithdraw { - mapping(uint32 => mapping(bytes => mapping(bytes => uint256))) principleBalances; + mapping(uint32 => mapping(bytes => mapping(bytes => uint256))) public principleBalances; function depositTo(uint32 clientChainLzId, bytes memory assetsAddress, bytes memory stakerAddress, uint256 opAmount) external @@ -30,4 +30,12 @@ contract DepositWithdrawMock is IDeposit, IWithdraw { return (success, principleBalances[clientChainLzId][assetsAddress][withdrawer]); } + + function getPrincipleBalance(uint32 clientChainLzId, address token, address staker) public view returns (uint256) { + return principleBalances[clientChainLzId][_addressToBytes(token)][_addressToBytes(staker)]; + } + + function _addressToBytes(address addr) internal pure returns (bytes memory) { + return abi.encodePacked(bytes32(bytes20(addr))); + } } diff --git a/test/mocks/ExocoreGatewayMock.sol b/test/mocks/ExocoreGatewayMock.sol index 56d9f2aa..974c807f 100644 --- a/test/mocks/ExocoreGatewayMock.sol +++ b/test/mocks/ExocoreGatewayMock.sol @@ -9,6 +9,7 @@ import {OwnableUpgradeable} from "@openzeppelin-upgradeable/contracts/access/Own import {OptionsBuilder} from "@layerzero-v2/oapp/contracts/oapp/libs/OptionsBuilder.sol"; import {IExocoreGateway} from "src/interfaces/IExocoreGateway.sol"; import {ILayerZeroReceiver} from "@layerzero-v2/protocol/contracts/interfaces/ILayerZeroReceiver.sol"; +import {IClientChains} from "src/interfaces/precompiles/IClientChains.sol"; contract ExocoreGatewayMock is Initializable, @@ -26,7 +27,8 @@ contract ExocoreGatewayMock is REQUEST_DELEGATE_TO, REQUEST_UNDELEGATE_FROM, RESPOND, - UPDATE_USERS_BALANCES + UPDATE_USERS_BALANCES, + MARK_BOOTSTRAP } mapping(Action => bytes4) public whiteListFunctionSelectors; @@ -36,6 +38,7 @@ contract ExocoreGatewayMock is address immutable DELEGATION_PRECOMPILE_MOCK_ADDRESS; address immutable WITHDRAW_PRINCIPLE_PRECOMPILE_MOCK_ADDRESS; address immutable CLAIM_REWARD_PRECOMPILE_MOCK_ADDRESS; + address immutable CLIENT_CHAINS_PRECOMPILE_MOCK_ADDRESS; bytes4 constant DEPOSIT_FUNCTION_SELECTOR = bytes4(keccak256("depositTo(uint32,bytes,bytes,uint256)")); bytes4 constant DELEGATE_TO_THROUGH_CLIENT_CHAIN_FUNCTION_SELECTOR = @@ -46,10 +49,17 @@ contract ExocoreGatewayMock is bytes4(keccak256("withdrawPrinciple(uint32,bytes,bytes,uint256)")); bytes4 constant CLAIM_REWARD_FUNCTION_SELECTOR = bytes4(keccak256("claimReward(uint32,bytes,bytes,uint256)")); + uint256 constant DEPOSIT_REQUEST_LENGTH = 96; + uint256 constant DELEGATE_REQUEST_LENGTH = 138; + uint256 constant UNDELEGATE_REQUEST_LENGTH = 138; + uint256 constant WITHDRAW_PRINCIPLE_REQUEST_LENGTH = 96; + uint256 constant CLAIM_REWARD_REQUEST_LENGTH = 96; + uint128 constant DESTINATION_GAS_LIMIT = 500000; uint128 constant DESTINATION_MSG_VALUE = 0; mapping(uint32 eid => mapping(bytes32 sender => uint64 nonce)) inboundNonce; + mapping(uint16 id => bool) chainToBootstrapped; event MessageSent(Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); @@ -58,6 +68,7 @@ contract ExocoreGatewayMock is error PrecompileCallFailed(bytes4 selector_, bytes reason); error UnexpectedInboundNonce(uint64 expectedNonce, uint64 actualNonce); error UnexpectedSourceChain(uint32 unexpectedSrcEndpointId); + error InvalidRequestLength(Action act, uint256 expectedLength, uint256 actualLength); uint256[40] private __gap; @@ -77,6 +88,7 @@ contract ExocoreGatewayMock is DELEGATION_PRECOMPILE_MOCK_ADDRESS = delegationPrecompileMockAddress; WITHDRAW_PRINCIPLE_PRECOMPILE_MOCK_ADDRESS = withdrawPrinciplePrecompileMockAddress; CLAIM_REWARD_PRECOMPILE_MOCK_ADDRESS = ClaimRewardPrecompileMockAddress; + CLIENT_CHAINS_PRECOMPILE_MOCK_ADDRESS = address(0); _disableInitializers(); } @@ -100,24 +112,40 @@ contract ExocoreGatewayMock is __Pausable_init_unchained(); } + // TODO: call this function automatically, either within the initializer (which requires + // setPeer) or be triggered by Golang after the contract is deployed. + // For manual calls, this function should be called immediately after deployment and + // then never needs to be called again. + function markBootstrapOnAllChains() public { + (bool success, uint16[] memory clientChainIds) = + IClientChains(CLIENT_CHAINS_PRECOMPILE_MOCK_ADDRESS).getClientChains(); + require(success, "ExocoreGateway: failed to get client chain ids"); + + for (uint256 i = 0; i < clientChainIds.length; i++) { + uint16 clientChainId = clientChainIds[i]; + if (!chainToBootstrapped[clientChainId]) { + _sendInterchainMsg(uint32(clientChainId), Action.MARK_BOOTSTRAP, ""); + // TODO: should this be marked only when receiving a response? + chainToBootstrapped[clientChainId] = true; + } + } + } + function pause() external { require( - msg.sender == exocoreValidatorSetAddress, "only Exocore validator set aggregated address could call this" + msg.sender == exocoreValidatorSetAddress, "ExocoreGateway: caller is not Exocore validator set aggregated address" ); _pause(); } function unpause() external { require( - msg.sender == exocoreValidatorSetAddress, "only Exocore validator set aggregated address could call this" + msg.sender == exocoreValidatorSetAddress, "ExocoreGateway: caller is not Exocore validator set aggregated address" ); _unpause(); } function _lzReceive(Origin calldata _origin, bytes calldata payload) internal virtual override whenNotPaused { - // TODO: current exocore precompiles take srcChainId as uint16, so this check should be removed after exocore network fixes it - require(_origin.srcEid <= type(uint16).max, "source chain endpoint id should not exceed uint16.max"); - _consumeInboundNonce(_origin.srcEid, _origin.sender, _origin.nonce); Action act = Action(uint8(payload[0])); @@ -134,6 +162,10 @@ contract ExocoreGatewayMock is } function requestDeposit(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) public onlyCalledFromThis { + if (payload.length != DEPOSIT_REQUEST_LENGTH) { + revert InvalidRequestLength(Action.REQUEST_DEPOSIT, DEPOSIT_REQUEST_LENGTH, payload.length); + } + bytes calldata token = payload[:32]; bytes calldata depositor = payload[32:64]; uint256 amount = uint256(bytes32(payload[64:96])); @@ -141,7 +173,7 @@ contract ExocoreGatewayMock is (bool success, bytes memory responseOrReason) = DEPOSIT_PRECOMPILE_MOCK_ADDRESS.call( abi.encodeWithSelector( DEPOSIT_FUNCTION_SELECTOR, - srcChainId, // TODO: Casting srcChainId from uint32 to uint16 should be fixed after exocore network fix source chain id type + srcChainId, token, depositor, amount @@ -161,6 +193,10 @@ contract ExocoreGatewayMock is public onlyCalledFromThis { + if (payload.length != WITHDRAW_PRINCIPLE_REQUEST_LENGTH) { + revert InvalidRequestLength(Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, WITHDRAW_PRINCIPLE_REQUEST_LENGTH, payload.length); + } + bytes calldata token = payload[:32]; bytes calldata withdrawer = payload[32:64]; uint256 amount = uint256(bytes32(payload[64:96])); @@ -168,7 +204,7 @@ contract ExocoreGatewayMock is (bool success, bytes memory responseOrReason) = WITHDRAW_PRINCIPLE_PRECOMPILE_MOCK_ADDRESS.call( abi.encodeWithSelector( WITHDRAW_PRINCIPLE_FUNCTION_SELECTOR, - srcChainId, // TODO: Casting srcChainId from uint32 to uint16 should be fixed after exocore network fix source chain id type + srcChainId, token, withdrawer, amount @@ -188,6 +224,10 @@ contract ExocoreGatewayMock is public onlyCalledFromThis { + if (payload.length != CLAIM_REWARD_REQUEST_LENGTH) { + revert InvalidRequestLength(Action.REQUEST_WITHDRAW_REWARD_FROM_EXOCORE, CLAIM_REWARD_REQUEST_LENGTH, payload.length); + } + bytes calldata token = payload[:32]; bytes calldata withdrawer = payload[32:64]; uint256 amount = uint256(bytes32(payload[64:96])); @@ -195,7 +235,7 @@ contract ExocoreGatewayMock is (bool success, bytes memory responseOrReason) = CLAIM_REWARD_PRECOMPILE_MOCK_ADDRESS.call( abi.encodeWithSelector( CLAIM_REWARD_FUNCTION_SELECTOR, - srcChainId, // TODO: Casting srcChainId from uint32 to uint16 should be fixed after exocore network fix source chain id type + srcChainId, token, withdrawer, amount @@ -210,15 +250,19 @@ contract ExocoreGatewayMock is } function requestDelegateTo(uint32 srcChainId, uint64 lzNonce, bytes calldata payload) public onlyCalledFromThis { + if (payload.length != DELEGATE_REQUEST_LENGTH) { + revert InvalidRequestLength(Action.REQUEST_DELEGATE_TO, DELEGATE_REQUEST_LENGTH, payload.length); + } + bytes calldata token = payload[:32]; bytes calldata delegator = payload[32:64]; - bytes calldata operator = payload[64:108]; - uint256 amount = uint256(bytes32(payload[108:140])); + bytes calldata operator = payload[64:106]; + uint256 amount = uint256(bytes32(payload[106:138])); (bool success,) = DELEGATION_PRECOMPILE_MOCK_ADDRESS.call( abi.encodeWithSelector( DELEGATE_TO_THROUGH_CLIENT_CHAIN_FUNCTION_SELECTOR, - srcChainId, // TODO: Casting srcChainId from uint32 to uint16 should be fixed after exocore network fix source chain id type + srcChainId, lzNonce, token, delegator, @@ -233,15 +277,19 @@ contract ExocoreGatewayMock is public onlyCalledFromThis { - bytes memory token = payload[1:32]; + if (payload.length != UNDELEGATE_REQUEST_LENGTH) { + revert InvalidRequestLength(Action.REQUEST_UNDELEGATE_FROM, UNDELEGATE_REQUEST_LENGTH, payload.length); + } + + bytes memory token = payload[:32]; bytes memory delegator = payload[32:64]; - bytes memory operator = payload[64:108]; - uint256 amount = uint256(bytes32(payload[108:140])); + bytes memory operator = payload[64:106]; + uint256 amount = uint256(bytes32(payload[106:138])); (bool success,) = DELEGATION_PRECOMPILE_MOCK_ADDRESS.call( abi.encodeWithSelector( UNDELEGATE_FROM_THROUGH_CLIENT_CHAIN_FUNCTION_SELECTOR, - srcChainId, // TODO: Casting srcChainId from uint32 to uint16 should be fixed after exocore network fix source chain id type + srcChainId, lzNonce, token, delegator, @@ -254,8 +302,9 @@ contract ExocoreGatewayMock is function _sendInterchainMsg(uint32 srcChainId, Action act, bytes memory actionArgs) internal whenNotPaused { bytes memory payload = abi.encodePacked(act, actionArgs); - bytes memory options = - OptionsBuilder.newOptions().addExecutorLzReceiveOption(DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE); + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption( + DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE + ).addExecutorOrderedExecutionOption(); MessagingFee memory fee = _quote(srcChainId, payload, options, false); MessagingReceipt memory receipt = @@ -264,8 +313,9 @@ contract ExocoreGatewayMock is } function quote(uint32 srcChainid, bytes memory _message) public view returns (uint256 nativeFee) { - bytes memory options = - OptionsBuilder.newOptions().addExecutorLzReceiveOption(DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE); + bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption( + DESTINATION_GAS_LIMIT, DESTINATION_MSG_VALUE + ).addExecutorOrderedExecutionOption(); MessagingFee memory fee = _quote(srcChainid, _message, options, false); return fee.nativeFee; } @@ -280,10 +330,6 @@ contract ExocoreGatewayMock is return inboundNonce[srcEid][sender] + 1; } - function getInboundNonce(uint32 srcEid, bytes32 sender) public view returns (uint64) { - return inboundNonce[srcEid][sender]; - } - function _consumeInboundNonce(uint32 srcEid, bytes32 sender, uint64 nonce) internal { inboundNonce[srcEid][sender] += 1; if (nonce != inboundNonce[srcEid][sender]) { From 8c64426cab149e0923ce99dfd9ad4e8a1284ee89 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 20 May 2024 10:20:26 +0800 Subject: [PATCH 41/93] remove unused file and comment on chainid --- error.txt | 1 - script/2_DeployBoth.s.sol | 4 ++++ test/foundry/ClientChainGateway.t.sol | 8 ++++++-- test/foundry/ExocoreDeployer.t.sol | 8 ++++++-- 4 files changed, 16 insertions(+), 5 deletions(-) delete mode 100644 error.txt diff --git a/error.txt b/error.txt deleted file mode 100644 index 65140093..00000000 --- a/error.txt +++ /dev/null @@ -1 +0,0 @@ -2024-04-30T12:14:44.163331Z ERROR foundry_compilers::resolver: failed to resolve versions diff --git a/script/2_DeployBoth.s.sol b/script/2_DeployBoth.s.sol index ee1c5cc8..51935a0d 100644 --- a/script/2_DeployBoth.s.sol +++ b/script/2_DeployBoth.s.sol @@ -188,12 +188,16 @@ contract DeployScript is BaseScript { function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { uint256 GENESIS_BLOCK_TIMESTAMP; + // mainnet if (block.chainid == 1) { GENESIS_BLOCK_TIMESTAMP = 1606824023; + // goerli } else if (block.chainid == 5) { GENESIS_BLOCK_TIMESTAMP = 1616508000; + // sepolia } else if (block.chainid == 11155111) { GENESIS_BLOCK_TIMESTAMP = 1655733600; + // holesky } else if (block.chainid == 17000) { GENESIS_BLOCK_TIMESTAMP = 1695902400; } else { diff --git a/test/foundry/ClientChainGateway.t.sol b/test/foundry/ClientChainGateway.t.sol index b6433ec0..7d043294 100644 --- a/test/foundry/ClientChainGateway.t.sol +++ b/test/foundry/ClientChainGateway.t.sol @@ -105,15 +105,19 @@ contract ClientChainGatewayTest is Test { vm.stopPrank(); } - function _deployBeaconOracle() internal returns (address) { + function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { uint256 GENESIS_BLOCK_TIMESTAMP; + // mainnet if (block.chainid == 1) { GENESIS_BLOCK_TIMESTAMP = 1606824023; + // goerli } else if (block.chainid == 5) { GENESIS_BLOCK_TIMESTAMP = 1616508000; + // sepolia } else if (block.chainid == 11155111) { GENESIS_BLOCK_TIMESTAMP = 1655733600; + // holesky } else if (block.chainid == 17000) { GENESIS_BLOCK_TIMESTAMP = 1695902400; } else { @@ -121,7 +125,7 @@ contract ClientChainGatewayTest is Test { } EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); - return address(oracle); + return oracle; } function test_PauseClientChainGateway() public { diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index 8a94a937..a922ecb0 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -221,15 +221,19 @@ contract ExocoreDeployer is Test { vm.etch(CLAIM_REWARD_PRECOMPILE_ADDRESS, WithdrawRewardMockCode); } - function _deployBeaconOracle() internal returns (address) { + function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { uint256 GENESIS_BLOCK_TIMESTAMP; + // mainnet if (block.chainid == 1) { GENESIS_BLOCK_TIMESTAMP = 1606824023; + // goerli } else if (block.chainid == 5) { GENESIS_BLOCK_TIMESTAMP = 1616508000; + // sepolia } else if (block.chainid == 11155111) { GENESIS_BLOCK_TIMESTAMP = 1655733600; + // holesky } else if (block.chainid == 17000) { GENESIS_BLOCK_TIMESTAMP = 1695902400; } else { @@ -237,7 +241,7 @@ contract ExocoreDeployer is Test { } EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); - return address(oracle); + return oracle; } function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { From 741ca19d3cf14b55f628be007a136d58316dd23d Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 20 May 2024 10:24:27 +0800 Subject: [PATCH 42/93] fix name typo prerequisite --- script/1_Prerequisities.s.sol | 2 +- script/2_DeployBoth.s.sol | 2 +- .../{prerequisitContracts.json => prerequisiteContracts.json} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename script/{prerequisitContracts.json => prerequisiteContracts.json} (100%) diff --git a/script/1_Prerequisities.s.sol b/script/1_Prerequisities.s.sol index 31bda538..4cd56796 100644 --- a/script/1_Prerequisities.s.sol +++ b/script/1_Prerequisities.s.sol @@ -77,6 +77,6 @@ contract PrerequisitiesScript is BaseScript { vm.serializeString(deployedContracts, "clientChain", clientChainContractsOutput); string memory finalJson = vm.serializeString(deployedContracts, "exocore", exocoreContractsOutput); - vm.writeJson(finalJson, "script/prerequisitContracts.json"); + vm.writeJson(finalJson, "script/prerequisiteContracts.json"); } } diff --git a/script/2_DeployBoth.s.sol b/script/2_DeployBoth.s.sol index 51935a0d..e1cc096f 100644 --- a/script/2_DeployBoth.s.sol +++ b/script/2_DeployBoth.s.sol @@ -20,7 +20,7 @@ contract DeployScript is BaseScript { function setUp() public virtual override { super.setUp(); - string memory prerequisities = vm.readFile("script/prerequisitContracts.json"); + string memory prerequisities = vm.readFile("script/prerequisiteContracts.json"); clientChainLzEndpoint = ILayerZeroEndpointV2(stdJson.readAddress(prerequisities, ".clientChain.lzEndpoint")); require(address(clientChainLzEndpoint) != address(0), "client chain l0 endpoint should not be empty"); diff --git a/script/prerequisitContracts.json b/script/prerequisiteContracts.json similarity index 100% rename from script/prerequisitContracts.json rename to script/prerequisiteContracts.json From 8c3b1e04cfad22d1b2663d5be77823748f8b8d62 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 20 May 2024 10:55:04 +0800 Subject: [PATCH 43/93] deploy vault when adding whitelist token --- src/core/Bootstrap.sol | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index f34e60bb..fa112428 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -192,6 +192,11 @@ contract Bootstrap is whitelistTokens.push(_token); isWhitelistedToken[_token] = true; + // deploy the corresponding vault if not deployed before + if (address(tokenToVault[_token]) == address(0)) { + _deployVault(_token); + } + emit WhitelistTokenAdded(_token); } @@ -204,6 +209,8 @@ contract Bootstrap is "Bootstrap: token should be already whitelisted" ); isWhitelistedToken[_token] = false; + // the implicit assumption here is that the _token must be included in whitelistTokens + // if isWhitelistedToken[_token] is true for(uint i = 0; i < whitelistTokens.length; i++) { if (whitelistTokens[i] == _token) { whitelistTokens[i] = whitelistTokens[whitelistTokens.length - 1]; From eb7b9a0fd253e6c617dc834a1ae12b32162ad035 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 20 May 2024 10:55:24 +0800 Subject: [PATCH 44/93] remove license infos --- src/libraries/BeaconChainProofs.sol | 2 -- src/libraries/Endian.sol | 1 - src/libraries/Merkle.sol | 2 -- 3 files changed, 5 deletions(-) diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 52ed8753..8f570165 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -1,5 +1,3 @@ -// SPDX-License-Identifier: BUSL-1.1 - pragma solidity ^0.8.0; import "./Merkle.sol"; diff --git a/src/libraries/Endian.sol b/src/libraries/Endian.sol index ac996ce3..f2642ffd 100644 --- a/src/libraries/Endian.sol +++ b/src/libraries/Endian.sol @@ -1,4 +1,3 @@ -// SPDX-License-Identifier: BUSL-1.1 pragma solidity ^0.8.0; library Endian { diff --git a/src/libraries/Merkle.sol b/src/libraries/Merkle.sol index 52630da4..6340e8ca 100644 --- a/src/libraries/Merkle.sol +++ b/src/libraries/Merkle.sol @@ -1,6 +1,4 @@ -// SPDX-License-Identifier: BUSL-1.1 // Adapted from OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) - pragma solidity ^0.8.0; /** From fbcfa8616fb013c1e635b8b5ea2c60e262ea2171 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 20 May 2024 11:00:06 +0800 Subject: [PATCH 45/93] remove unused codes in Merkle.sol --- src/libraries/Merkle.sol | 59 ---------------------------------------- 1 file changed, 59 deletions(-) diff --git a/src/libraries/Merkle.sol b/src/libraries/Merkle.sol index 6340e8ca..11046729 100644 --- a/src/libraries/Merkle.sol +++ b/src/libraries/Merkle.sol @@ -16,65 +16,6 @@ pragma solidity ^0.8.0; * against this attack out of the box. */ library Merkle { - /** - * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up - * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt - * hash matches the root of the tree. The tree is built assuming `leaf` is - * the 0 indexed `index`'th leaf from the bottom left of the tree. - * - * Note this is for a Merkle tree using the keccak/sha3 hash function - */ - function verifyInclusionKeccak( - bytes memory proof, - bytes32 root, - bytes32 leaf, - uint256 index - ) internal pure returns (bool) { - return processInclusionProofKeccak(proof, leaf, index) == root; - } - - /** - * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up - * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt - * hash matches the root of the tree. The tree is built assuming `leaf` is - * the 0 indexed `index`'th leaf from the bottom left of the tree. - * - * _Available since v4.4._ - * - * Note this is for a Merkle tree using the keccak/sha3 hash function - */ - function processInclusionProofKeccak( - bytes memory proof, - bytes32 leaf, - uint256 index - ) internal pure returns (bytes32) { - require( - proof.length != 0 && proof.length % 32 == 0, - "Merkle.processInclusionProofKeccak: proof length should be a non-zero multiple of 32" - ); - bytes32 computedHash = leaf; - for (uint256 i = 32; i <= proof.length; i += 32) { - if (index % 2 == 0) { - // if ith bit of index is 0, then computedHash is a left sibling - assembly { - mstore(0x00, computedHash) - mstore(0x20, mload(add(proof, i))) - computedHash := keccak256(0x00, 0x40) - index := div(index, 2) - } - } else { - // if ith bit of index is 1, then computedHash is a right sibling - assembly { - mstore(0x00, mload(add(proof, i))) - mstore(0x20, computedHash) - computedHash := keccak256(0x00, 0x40) - index := div(index, 2) - } - } - } - return computedHash; - } - /** * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt From 2a800489adc7e4ae79f5da532f356071bde585f8 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 20 May 2024 11:01:15 +0800 Subject: [PATCH 46/93] clearBootstrapData => _clearBootstrapData --- src/core/ClientChainGateway.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/ClientChainGateway.sol b/src/core/ClientChainGateway.sol index 1eaf6511..14efb5a4 100644 --- a/src/core/ClientChainGateway.sol +++ b/src/core/ClientChainGateway.sol @@ -58,7 +58,7 @@ contract ClientChainGateway is address payable exocoreValidatorSetAddress_, address[] calldata appendedWhitelistTokens_ ) external reinitializer(2) { - clearBootstrapData(); + _clearBootstrapData(); require(exocoreValidatorSetAddress_ != address(0), "ClientChainGateway: exocore validator set address should not be empty"); @@ -93,7 +93,7 @@ contract ClientChainGateway is __Pausable_init_unchained(); } - function clearBootstrapData() internal { + function _clearBootstrapData() internal { // mandatory to clear! delete _whiteListFunctionSelectors[Action.MARK_BOOTSTRAP]; // the set below is recommended to clear, so that any possibilities of upgrades From fa80b66b87e906016c617b0ec040ec7c2a2719c9 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 20 May 2024 11:09:46 +0800 Subject: [PATCH 47/93] fix ExoCapsule according Max's review --- src/core/ExoCapsule.sol | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 9b3f66b0..165e5f1f 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -50,9 +50,9 @@ contract ExoCapsule is } function initialize(address gateway_, address capsuleOwner_, address beaconOracle_) external initializer { - require(gateway_ != address(0), "ExoCapsuleStorage: gateway address can not be empty"); + require(gateway_ != address(0), "ExoCapsule: gateway address can not be empty"); require(capsuleOwner_ != address(0), "ExoCapsule: capsule owner address can not be empty"); - require(beaconOracle_ != address(0), "ExoCapsuleStorage: beacon chain oracle address should not be empty"); + require(beaconOracle_ != address(0), "ExoCapsule: beacon chain oracle address should not be empty"); gateway = INativeRestakingController(gateway_); beaconOracle = IBeaconChainOracle(beaconOracle_); @@ -67,6 +67,10 @@ contract ExoCapsule is bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); Validator storage validator = _capsuleValidators[validatorPubkey]; + if (!validatorContainer.verifyValidatorContainerBasic()) { + revert InvalidValidatorContainer(validatorPubkey); + } + if (validator.status != VALIDATOR_STATUS.UNREGISTERED) { revert DoubleDepositedValidator(validatorPubkey); } @@ -75,10 +79,6 @@ contract ExoCapsule is revert StaleValidatorContainer(validatorPubkey, proof.beaconBlockTimestamp); } - if (!validatorContainer.verifyValidatorContainerBasic()) { - revert InvalidValidatorContainer(validatorPubkey); - } - if (!_isActivatedAtEpoch(validatorContainer, proof.beaconBlockTimestamp)) { revert InactiveValidatorContainer(validatorPubkey); } @@ -108,6 +108,10 @@ contract ExoCapsule is bool partialWithdrawal = _timestampToEpoch(validatorProof.beaconBlockTimestamp) < withdrawableEpoch; + if (!validatorContainer.verifyValidatorContainerBasic()) { + revert InvalidValidatorContainer(validatorPubkey); + } + if (!partialWithdrawal) { revert NotPartialWithdrawal(validatorPubkey); } @@ -116,10 +120,6 @@ contract ExoCapsule is revert UnmatchedValidatorAndWithdrawal(validatorPubkey); } - if (!validatorContainer.verifyValidatorContainerBasic()) { - revert InvalidValidatorContainer(validatorPubkey); - } - _verifyValidatorContainer(validatorContainer, validatorProof); _verifyWithdrawalContainer(withdrawalContainer, withdrawalProof); } @@ -136,6 +136,10 @@ contract ExoCapsule is Validator storage validator = _capsuleValidators[validatorPubkey]; bool fullyWithdrawal = _timestampToEpoch(validatorProof.beaconBlockTimestamp) > withdrawableEpoch; + if (!validatorContainer.verifyValidatorContainerBasic()) { + revert InvalidValidatorContainer(validatorPubkey); + } + if (!fullyWithdrawal) { revert NotPartialWithdrawal(validatorPubkey); } @@ -144,10 +148,6 @@ contract ExoCapsule is revert UnmatchedValidatorAndWithdrawal(validatorPubkey); } - if (!validatorContainer.verifyValidatorContainerBasic()) { - revert InvalidValidatorContainer(validatorPubkey); - } - _verifyValidatorContainer(validatorContainer, validatorProof); _verifyWithdrawalContainer(withdrawalContainer, withdrawalProof); From 8644d222ff346525af0194c87b6c7ac695e8dc28 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 20 May 2024 11:25:11 +0800 Subject: [PATCH 48/93] optimize _isStaleProof and _hasFullyWithdrawn --- src/core/ExoCapsule.sol | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 165e5f1f..2c773621 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -239,23 +239,11 @@ contract ExoCapsule is } function _isStaleProof(Validator storage validator, uint256 proofTimestamp) internal view returns (bool) { - if (proofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS >= block.timestamp) { - if (proofTimestamp > validator.mostRecentBalanceUpdateTimestamp) { - return false; - } - } - - return true; + return proofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS < block.timestamp || proofTimestamp <= validator.mostRecentBalanceUpdateTimestamp; } function _hasFullyWithdrawn(bytes32[] calldata validatorContainer) internal view returns (bool) { - if (validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(block.timestamp)) { - if (validatorContainer.getEffectiveBalance() == 0) { - return true; - } - } - - return false; + return validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(block.timestamp) && validatorContainer.getEffectiveBalance() == 0 } /** From 7a9f17e689dd777d05b1797e7fff9fa19214085e Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 20 May 2024 11:53:27 +0800 Subject: [PATCH 49/93] fix bootstrap unit test --- src/core/ExoCapsule.sol | 2 +- test/foundry/Bootstrap.t.sol | 34 +++++++++------------------------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 2c773621..60dbd760 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -243,7 +243,7 @@ contract ExoCapsule is } function _hasFullyWithdrawn(bytes32[] calldata validatorContainer) internal view returns (bool) { - return validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(block.timestamp) && validatorContainer.getEffectiveBalance() == 0 + return validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(block.timestamp) && validatorContainer.getEffectiveBalance() == 0; } /** diff --git a/test/foundry/Bootstrap.t.sol b/test/foundry/Bootstrap.t.sol index 713330a5..c371c618 100644 --- a/test/foundry/Bootstrap.t.sol +++ b/test/foundry/Bootstrap.t.sol @@ -271,7 +271,7 @@ contract BootstrapTest is Test { vm.stopPrank(); } - function test02_Deposit_VaultNotExist() public { + function test02_Deposit_Success() public { address cloneDeployer = address(0xdebd); // Deploy a new token vm.startPrank(cloneDeployer); @@ -287,10 +287,12 @@ contract BootstrapTest is Test { // now add it to the whitelist vm.startPrank(deployer); bootstrap.addWhitelistToken(cloneAddress); + vm.stopPrank(); // now try to deposit - myToken.approve(address(bootstrap), amounts[0]); - vm.expectRevert(abi.encodeWithSignature("VaultNotExist()")); + vm.startPrank(addrs[0]); + IVault vault = bootstrap.tokenToVault(cloneAddress); + myTokenClone.approve(address(vault), amounts[0]); bootstrap.deposit(cloneAddress, amounts[0]); vm.stopPrank(); } @@ -731,13 +733,13 @@ contract BootstrapTest is Test { ); } - function test09_DelegateTo_VaultNotExist() public { + function test09_DelegateTo_NotEnoughBlance() public { test03_RegisterOperator(); vm.startPrank(deployer); bootstrap.addWhitelistToken(address(0xa)); vm.stopPrank(); vm.startPrank(addrs[0]); - vm.expectRevert(abi.encodeWithSignature("VaultNotExist()")); + vm.expectRevert(bytes("Bootstrap: insufficient withdrawable balance")); bootstrap.delegateTo( "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac", address(0xa), amounts[0] ); @@ -867,13 +869,13 @@ contract BootstrapTest is Test { ); } - function test10_UndelegateFrom_VaultNotExist() public { + function test10_UndelegateFrom_NotEnoughBalance() public { test03_RegisterOperator(); vm.startPrank(deployer); bootstrap.addWhitelistToken(address(0xa)); vm.stopPrank(); vm.startPrank(addrs[0]); - vm.expectRevert(abi.encodeWithSignature("VaultNotExist()")); + vm.expectRevert(bytes("Bootstrap: insufficient delegated balance")); bootstrap.undelegateFrom( "exo13hasr43vvq8v44xpzh0l6yuym4kca98f87j7ac", address(0xa), amounts[0] ); @@ -976,15 +978,6 @@ contract BootstrapTest is Test { vm.stopPrank(); } - function test11_WithdrawPrincipleFromExocore_VaultNotExist() public { - vm.startPrank(deployer); - bootstrap.addWhitelistToken(address(0xa)); - vm.stopPrank(); - vm.startPrank(addrs[0]); - vm.expectRevert(abi.encodeWithSignature("VaultNotExist()")); - bootstrap.withdrawPrincipleFromExocore(address(0xa), 5); - } - function test12_MarkBootstrapped() public { vm.warp(spawnTime + 1); vm.startPrank(address(0x20)); @@ -1365,13 +1358,4 @@ contract BootstrapTest is Test { ); bootstrap.claim(address(myToken), amounts[0] + 5, addrs[0]); } - - function test22_Claim_VaultNotExist() public { - vm.startPrank(deployer); - bootstrap.addWhitelistToken(address(0xa)); - vm.stopPrank(); - vm.startPrank(addrs[0]); - vm.expectRevert(abi.encodeWithSignature("VaultNotExist()")); - bootstrap.claim(address(0xa), 5, addrs[0]); - } } From d46ca9f72c9352f83bcaef1c84128250af16d038 Mon Sep 17 00:00:00 2001 From: adu Date: Mon, 20 May 2024 12:05:36 +0800 Subject: [PATCH 50/93] add comments for request lenght --- src/storage/ExocoreGatewayStorage.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/storage/ExocoreGatewayStorage.sol b/src/storage/ExocoreGatewayStorage.sol index b9e7a337..ad5a4b51 100644 --- a/src/storage/ExocoreGatewayStorage.sol +++ b/src/storage/ExocoreGatewayStorage.sol @@ -18,10 +18,15 @@ contract ExocoreGatewayStorage is GatewayStorage { bytes4(keccak256("withdrawPrinciple(uint32,bytes,bytes,uint256)")); bytes4 constant CLAIM_REWARD_FUNCTION_SELECTOR = bytes4(keccak256("claimReward(uint32,bytes,bytes,uint256)")); + // bytes32 token + bytes32 depositor + uint256 amount uint256 constant DEPOSIT_REQUEST_LENGTH = 96; + // bytes32 token + bytes32 delegator + bytes(42) operator + uint256 amount uint256 constant DELEGATE_REQUEST_LENGTH = 138; + // bytes32 token + bytes32 delegator + bytes(42) operator + uint256 amount uint256 constant UNDELEGATE_REQUEST_LENGTH = 138; + // bytes32 token + bytes32 withdrawer + uint256 amount uint256 constant WITHDRAW_PRINCIPLE_REQUEST_LENGTH = 96; + // bytes32 token + bytes32 withdrawer + uint256 amount uint256 constant CLAIM_REWARD_REQUEST_LENGTH = 96; uint128 constant DESTINATION_GAS_LIMIT = 500000; From 140b7a1688971df588e5bb3034cc08c6bd77d133 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 21 May 2024 12:25:29 -0400 Subject: [PATCH 51/93] fix: merge conflict --- src/core/ExoCapsule.sol | 66 +++++++++++++++++-------------- src/storage/ExoCapsuleStorage.sol | 9 +++++ 2 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 4ce75b31..e369b986 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -11,11 +11,7 @@ import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -contract ExoCapsule is - Initializable, - ExoCapsuleStorage, - IExoCapsule -{ +contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { using BeaconChainProofs for bytes32; using ValidatorContainer for bytes32[]; using WithdrawalContainer for bytes32[]; @@ -134,14 +130,14 @@ contract ExoCapsule is Validator storage validator = _capsuleValidators[validatorPubkey]; partialWithdrawal = _timestampToEpoch(validatorProof.beaconBlockTimestamp) < withdrawableEpoch; - if (validatorProof.beaconBlockTimestamp != withdrawalProof.beaconBlockTimestamp) { - revert UnmatchedValidatorAndWithdrawal(validatorPubkey); - } - if (!validatorContainer.verifyValidatorContainerBasic()) { revert InvalidValidatorContainer(validatorPubkey); } + if (validatorProof.beaconBlockTimestamp != withdrawalProof.beaconBlockTimestamp) { + revert UnmatchedValidatorAndWithdrawal(validatorPubkey); + } + if (validator.status == VALIDATOR_STATUS.UNREGISTERED) { revert UnregisteredOrWithdrawnValidatorContainer(validatorPubkey); } @@ -205,6 +201,11 @@ contract ExoCapsule is emit NonBeaconChainETHWithdrawn(recipient, amountToWithdraw); } + /// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false + function withdrawBeforeRestaking() external onlyGateway hasNeverRestaked { + _processWithdrawalBeforeRestaking(podOwner); + } + function updatePrincipleBalance(uint256 lastlyUpdatedPrincipleBalance) external onlyGateway { principleBalance = lastlyUpdatedPrincipleBalance; @@ -236,19 +237,7 @@ contract ExoCapsule is return root; } - function _sendETH(address recipient, uint256 amountWei) internal { - (bool sent, ) = recipient.call{value: amountWei}(""); - if (!sent) { - revert WithdrawalFailure(capsuleOwner, recipient, amountWei); - } - } - - function _verifyValidatorContainer( - bytes32[] calldata validatorContainer, - ValidatorContainerProof calldata proof - ) internal view { - - function getRegisteredValidatorByPubkey(bytes32 pubkey) public view returns(Validator memory) { + function getRegisteredValidatorByPubkey(bytes32 pubkey) public view returns (Validator memory) { Validator memory validator = _capsuleValidators[pubkey]; if (validator.status == VALIDATOR_STATUS.UNREGISTERED) { revert UnregisteredValidator(pubkey); @@ -257,7 +246,7 @@ contract ExoCapsule is return validator; } - function getRegisteredValidatorByIndex(uint256 index) public view returns(Validator memory) { + function getRegisteredValidatorByIndex(uint256 index) public view returns (Validator memory) { Validator memory validator = _capsuleValidators[_capsuleValidatorsByIndex[index]]; if (validator.status == VALIDATOR_STATUS.UNREGISTERED) { revert UnregisteredValidator(_capsuleValidatorsByIndex[index]); @@ -266,7 +255,17 @@ contract ExoCapsule is return validator; } - function _verifyValidatorContainer(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) internal view { + function _sendETH(address recipient, uint256 amountWei) internal { + (bool sent, ) = recipient.call{value: amountWei}(""); + if (!sent) { + revert WithdrawalFailure(capsuleOwner, recipient, amountWei); + } + } + + function _verifyValidatorContainer( + bytes32[] calldata validatorContainer, + ValidatorContainerProof calldata proof + ) internal view { bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); bytes32 validatorContainerRoot = validatorContainer.merklelizeValidatorContainer(); bool valid = validatorContainerRoot.isValidValidatorContainerRoot( @@ -299,8 +298,10 @@ contract ExoCapsule is } } - - function _isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint256 atTimestamp) internal pure returns (bool) { + function _isActivatedAtEpoch( + bytes32[] calldata validatorContainer, + uint256 atTimestamp + ) internal pure returns (bool) { uint64 atEpoch = _timestampToEpoch(atTimestamp); uint64 activationEpoch = validatorContainer.getActivationEpoch(); uint64 exitEpoch = validatorContainer.getExitEpoch(); @@ -309,11 +310,15 @@ contract ExoCapsule is } function _isStaleProof(Validator storage validator, uint256 proofTimestamp) internal view returns (bool) { - return proofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS < block.timestamp || proofTimestamp <= validator.mostRecentBalanceUpdateTimestamp; + return + proofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS < block.timestamp || + proofTimestamp <= validator.mostRecentBalanceUpdateTimestamp; } function _hasFullyWithdrawn(bytes32[] calldata validatorContainer) internal view returns (bool) { - return validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(block.timestamp) && validatorContainer.getEffectiveBalance() == 0; + return + validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(block.timestamp) && + validatorContainer.getEffectiveBalance() == 0; } /** @@ -322,7 +327,10 @@ contract ExoCapsule is * reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md */ function _timestampToEpoch(uint256 timestamp) internal pure returns (uint64) { - require(timestamp >= BEACON_CHAIN_GENESIS_TIME, "timestamp should be greater than beacon chain genesis timestamp"); + require( + timestamp >= BEACON_CHAIN_GENESIS_TIME, + "timestamp should be greater than beacon chain genesis timestamp" + ); return uint64((timestamp - BEACON_CHAIN_GENESIS_TIME) / BeaconChainProofs.SECONDS_PER_EPOCH); } } diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 147dce4a..d0294836 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -30,6 +30,15 @@ contract ExoCapsuleStorage { uint256 constant GWEI_TO_WEI = 1e9; uint64 constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; + /** + * @notice The latest timestamp at which the capsule owner withdrew the balance of the capsule, via calling `withdrawBeforeRestaking`. + * @dev This variable is only updated when the `withdrawBeforeRestaking` function is called, which can only occur before `hasRestaked` is set to true for this capsule. + * Proofs for this capsule are only valid against Beacon Chain state roots corresponding to timestamps after the stored `mostRecentWithdrawalTimestamp`. + */ + uint64 public mostRecentWithdrawalTimestamp; + /// @notice an indicator of whether or not the capsule owner has ever "fully restaked" by successfully calling `verifyCorrectWithdrawalCredentials`. + bool public hasRestaked; + uint256 public principleBalance; /// @notice the amount of execution layer ETH in this contract that is staked in(i.e. withdrawn from the Beacon Chain but not from Exocore) uint256 public withdrawableBalance; From bc80017879616e3131409b6cef33236e7d4f177f Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 27 May 2024 12:41:06 -0400 Subject: [PATCH 52/93] fix: update principle and withdraw balance after request --- src/core/ClientGatewayLzReceiver.sol | 103 ++++++++++++++++----------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/src/core/ClientGatewayLzReceiver.sol b/src/core/ClientGatewayLzReceiver.sol index f25c4343..d0cb1039 100644 --- a/src/core/ClientGatewayLzReceiver.sol +++ b/src/core/ClientGatewayLzReceiver.sol @@ -11,9 +11,13 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp error UnsupportedResponse(Action act); error UnexpectedResponse(uint64 nonce); error DepositShouldNotFailOnExocore(address token, address depositor); + error WithdrawShouldNotFailOnExocore(address token, address withdrawer); modifier onlyCalledFromThis() { - require(msg.sender == address(this), "ClientChainLzReceiver: could only be called from this contract itself with low level call"); + require( + msg.sender == address(this), + "ClientChainLzReceiver: could only be called from this contract itself with low level call" + ); _; } @@ -39,8 +43,9 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp revert UnexpectedResponse(requestId); } - (bool success, bytes memory reason) = - address(this).call(abi.encodePacked(hookSelector, abi.encode(requestPayload, payload[9:]))); + (bool success, bytes memory reason) = address(this).call( + abi.encodePacked(hookSelector, abi.encode(requestPayload, payload[9:])) + ); if (!success) { revert RequestOrResponseExecuteFailed(act, _origin.nonce, reason); } @@ -53,21 +58,19 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp revert UnsupportedRequest(act); } - (bool success, bytes memory reason) = - address(this).call(abi.encodePacked(selector_, abi.encode(payload[1:]))); + (bool success, bytes memory reason) = address(this).call( + abi.encodePacked(selector_, abi.encode(payload[1:])) + ); if (!success) { revert RequestOrResponseExecuteFailed(act, _origin.nonce, reason); } } } - function nextNonce(uint32 srcEid, bytes32 sender) - public - view - virtual - override(OAppReceiverUpgradeable) - returns (uint64) - { + function nextNonce( + uint32 srcEid, + bytes32 sender + ) public view virtual override(OAppReceiverUpgradeable) returns (uint64) { return inboundNonce[srcEid][sender] + 1; } @@ -78,10 +81,10 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp } } - function afterReceiveDepositResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { + function afterReceiveDepositResponse( + bytes memory requestPayload, + bytes calldata responsePayload + ) public onlyCalledFromThis { (address token, address depositor, uint256 amount) = abi.decode(requestPayload, (address, address, uint256)); bool success = (uint8(bytes1(responsePayload[0])) == 1); @@ -102,16 +105,28 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp emit DepositResult(success, token, depositor, amount); } - function afterReceiveWithdrawPrincipleResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, address withdrawer, uint256 unlockPrincipleAmount) = - abi.decode(requestPayload, (address, address, uint256)); + function afterReceiveWithdrawPrincipleResponse( + bytes memory requestPayload, + bytes calldata responsePayload + ) public onlyCalledFromThis { + (address token, address withdrawer, uint256 unlockPrincipleAmount) = abi.decode( + requestPayload, + (address, address, uint256) + ); bool success = (uint8(bytes1(responsePayload[0])) == 1); uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:33])); - if (success) { + + if (!success) { + revert WithdrawShouldNotFailOnExocore(token, withdrawer); + } + + if (token == VIRTUAL_STAKED_ETH_ADDRESS) { + IExoCapsule capsule = _getCapsule(withdrawer); + + capsule.updatePrincipleBalance(lastlyUpdatedPrincipleBalance); + capsule.updateWithdrawableBalance(unlockPrincipleAmount); + } else { IVault vault = _getVault(token); vault.updatePrincipleBalance(withdrawer, lastlyUpdatedPrincipleBalance); @@ -121,12 +136,14 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp emit WithdrawPrincipleResult(success, token, withdrawer, unlockPrincipleAmount); } - function afterReceiveWithdrawRewardResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, address withdrawer, uint256 unlockRewardAmount) = - abi.decode(requestPayload, (address, address, uint256)); + function afterReceiveWithdrawRewardResponse( + bytes memory requestPayload, + bytes calldata responsePayload + ) public onlyCalledFromThis { + (address token, address withdrawer, uint256 unlockRewardAmount) = abi.decode( + requestPayload, + (address, address, uint256) + ); bool success = (uint8(bytes1(responsePayload[0])) == 1); uint256 lastlyUpdatedRewardBalance = uint256(bytes32(responsePayload[1:33])); @@ -140,24 +157,28 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp emit WithdrawRewardResult(success, token, withdrawer, unlockRewardAmount); } - function afterReceiveDelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, string memory operator, address delegator, uint256 amount) = - abi.decode(requestPayload, (address, string, address, uint256)); + function afterReceiveDelegateResponse( + bytes memory requestPayload, + bytes calldata responsePayload + ) public onlyCalledFromThis { + (address token, string memory operator, address delegator, uint256 amount) = abi.decode( + requestPayload, + (address, string, address, uint256) + ); bool success = (uint8(bytes1(responsePayload[0])) == 1); emit DelegateResult(success, delegator, operator, token, amount); } - function afterReceiveUndelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) - public - onlyCalledFromThis - { - (address token, string memory operator, address undelegator, uint256 amount) = - abi.decode(requestPayload, (address, string, address, uint256)); + function afterReceiveUndelegateResponse( + bytes memory requestPayload, + bytes calldata responsePayload + ) public onlyCalledFromThis { + (address token, string memory operator, address undelegator, uint256 amount) = abi.decode( + requestPayload, + (address, string, address, uint256) + ); bool success = (uint8(bytes1(responsePayload[0])) == 1); From d8dc3b301db0fa3c832113bccb43a85bc84bceff Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 27 May 2024 13:29:53 -0400 Subject: [PATCH 53/93] fix: update withdraw modifiers --- src/core/ExoCapsule.sol | 50 +++++++++++++++++++++++--- src/core/NativeRestakingController.sol | 10 ++++-- src/interfaces/IExoCapsule.sol | 2 +- src/storage/ExoCapsuleStorage.sol | 2 +- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index e369b986..d962ef2b 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -33,6 +33,8 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { address indexed recipient, uint64 withdrawalAmountGwei ); + /// @notice Emitted when capsuleOwner enables restaking + event RestakingActivated(address indexed capsuleOwner); /// @notice Emitted when ETH is received via the `receive` fallback event NonBeaconChainETHReceived(uint256 amountReceived); /// @notice Emitted when ETH that was previously received via the `receive` fallback is withdrawn @@ -42,7 +44,6 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { error InvalidWithdrawalContainer(uint64 validatorIndex); error DoubleDepositedValidator(bytes32 pubkey); error StaleValidatorContainer(bytes32 pubkey, uint256 timestamp); - error UnregisteredOrWithdrawnValidatorContainer(bytes32 pubkey); error WithdrawalAlreadyProven(bytes32 pubkey, uint256 timestamp); error UnregisteredValidator(bytes32 pubkey); error UnregisteredOrWithdrawnValidatorContainer(bytes32 pubkey); @@ -62,6 +63,21 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { _; } + modifier hasNeverRestaked() { + require(!hasRestaked, "Restaking is enabled"); + _; + } + + /// @notice Checks that `timestamp` is greater than or equal to the value stored in `mostRecentWithdrawalTimestamp` + /// @notice All partial/full withdrawal timestamps should be greater than `mostRecentWithdrawalTimestamp` + modifier proofIsForValidTimestamp(uint256 timestamp) { + require( + timestamp >= mostRecentWithdrawalTimestamp, + "proofIsForValidTimestamp: beacon chain proof must be at or after mostRecentWithdrawalTimestamp" + ); + _; + } + constructor() { _disableInitializers(); } @@ -124,7 +140,12 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof - ) external onlyGateway returns (bool partialWithdrawal) { + ) + external + onlyGateway + proofIsForValidTimestamp(withdrawalProof.beaconBlockTimestamp) + returns (bool partialWithdrawal, uint256 withdrawalAmount) + { bytes32 validatorPubkey = validatorContainer.getPubkey(); uint64 withdrawableEpoch = validatorContainer.getWithdrawableEpoch(); Validator storage validator = _capsuleValidators[validatorPubkey]; @@ -174,7 +195,8 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { withdrawalAmountGwei ); if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { - _sendETH(capsuleOwner, (withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * GWEI_TO_WEI); + withdrawalAmount = (withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * GWEI_TO_WEI; + _sendETH(capsuleOwner, withdrawalAmount); } } } @@ -201,9 +223,21 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { emit NonBeaconChainETHWithdrawn(recipient, amountToWithdraw); } - /// @notice Called by the pod owner to withdraw the balance of the pod when `hasRestaked` is set to false + /** + * @notice Called by the capsule owner to activate restaking by withdrawing + * all existing ETH from the capsule and preventing further withdrawals via + * "withdrawBeforeRestaking()" + */ + function activateRestaking() external onlyGateway hasNeverRestaked { + hasRestaked = true; + _processWithdrawalBeforeRestaking(capsuleOwner); + + emit RestakingActivated(capsuleOwner); + } + + /// @notice Called by the capsule owner to withdraw the balance of the capsule when `hasRestaked` is set to false function withdrawBeforeRestaking() external onlyGateway hasNeverRestaked { - _processWithdrawalBeforeRestaking(podOwner); + _processWithdrawalBeforeRestaking(capsuleOwner); } function updatePrincipleBalance(uint256 lastlyUpdatedPrincipleBalance) external onlyGateway { @@ -255,6 +289,12 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { return validator; } + function _processWithdrawalBeforeRestaking(address _capsuleOwner) internal { + mostRecentWithdrawalTimestamp = block.timestamp; + nonBeaconChainETHBalance = 0; + _sendETH(_capsuleOwner, address(this).balance); + } + function _sendETH(address recipient, uint256 amountWei) internal { (bool sent, ) = recipient.call{value: amountWei}(""); if (!sent) { diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 6df5ca93..02469f50 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -16,7 +16,11 @@ abstract contract NativeRestakingController is { using ValidatorContainer for bytes32[]; - function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) external payable whenNotPaused { + function stake( + bytes calldata pubkey, + bytes calldata signature, + bytes32 depositDataRoot + ) external payable whenNotPaused { require(msg.value == 32 ether, "NativeRestakingController: stake value must be exactly 32 ether"); IExoCapsule capsule = ownerToCapsule[msg.sender]; @@ -67,7 +71,7 @@ abstract contract NativeRestakingController is IExoCapsule.WithdrawalContainerProof calldata withdrawalProof ) external payable whenNotPaused { IExoCapsule capsule = _getCapsule(msg.sender); - bool partialWithdrawal = capsule.verifyWithdrawalProof( + (bool partialWithdrawal, uint256 withdrawalAmount) = capsule.verifyWithdrawalProof( validatorContainer, validatorProof, withdrawalContainer, @@ -78,7 +82,7 @@ abstract contract NativeRestakingController is _processRequest( VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, - 32 ether, + withdrawalAmount, Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, "" ); diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 88548af9..d5f3c55f 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -27,7 +27,7 @@ interface IExoCapsule { ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof - ) external returns (bool partialWithdrawal); + ) external returns (bool partialWithdrawal, uint256 withdrawalAmount); function withdraw(uint256 amount, address recipient) external; diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index d0294836..a5584094 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -35,7 +35,7 @@ contract ExoCapsuleStorage { * @dev This variable is only updated when the `withdrawBeforeRestaking` function is called, which can only occur before `hasRestaked` is set to true for this capsule. * Proofs for this capsule are only valid against Beacon Chain state roots corresponding to timestamps after the stored `mostRecentWithdrawalTimestamp`. */ - uint64 public mostRecentWithdrawalTimestamp; + uint256 public mostRecentWithdrawalTimestamp; /// @notice an indicator of whether or not the capsule owner has ever "fully restaked" by successfully calling `verifyCorrectWithdrawalCredentials`. bool public hasRestaked; From 0851432b178f9210f968d82990c1789d0d2f71cc Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 28 May 2024 00:20:02 -0400 Subject: [PATCH 54/93] fix: isValidWCRootAgainstExecutionPayloadRoot check logic updated with proper withdrawal tree height --- src/core/ExoCapsule.sol | 11 +++ src/interfaces/IExoCapsule.sol | 4 + src/libraries/BeaconChainProofs.sol | 132 +++++++++++++++++++--------- test/foundry/ExoCapsule.t.sol | 78 ++++++++++++---- 4 files changed, 163 insertions(+), 62 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index d962ef2b..3eb62da6 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -42,6 +42,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { error InvalidValidatorContainer(bytes32 pubkey); error InvalidWithdrawalContainer(uint64 validatorIndex); + error InvalidHistoricalSummaries(uint64 validatorIndex); error DoubleDepositedValidator(bytes32 pubkey); error StaleValidatorContainer(bytes32 pubkey, uint256 timestamp); error WithdrawalAlreadyProven(bytes32 pubkey, uint256 timestamp); @@ -336,6 +337,16 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { if (!valid) { revert InvalidWithdrawalContainer(withdrawalContainer.getValidatorIndex()); } + // Verify historical summaries + bool validHistoricalSummaries = beaconBlockRoot.isValidHistoricalSummaryRoot( + proof.historicalSummaryBlockRootProof, + proof.historicalSummaryIndex, + proof.blockRoot, + proof.blockRootIndex + ); + if (!validHistoricalSummaries) { + revert InvalidHistoricalSummaries(withdrawalContainer.getValidatorIndex()); + } } function _isActivatedAtEpoch( diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index d5f3c55f..79153c1b 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -15,6 +15,10 @@ interface IExoCapsule { bytes32 executionPayloadRoot; bytes32[] executionPayloadRootProof; bytes32[] withdrawalContainerRootProof; + bytes32[] historicalSummaryBlockRootProof; + uint256 historicalSummaryIndex; + bytes32 blockRoot; + uint256 blockRootIndex; uint256 withdrawalIndex; } diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 8f570165..0b7dbb86 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -24,7 +24,7 @@ library BeaconChainProofs { uint256 internal constant VALIDATOR_FIELD_TREE_HEIGHT = 3; uint256 internal constant NUM_EXECUTION_PAYLOAD_HEADER_FIELDS = 15; - uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT = 4; + uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT = 5; // After deneb hard fork, it's increased from 4 to 5 uint256 internal constant NUM_EXECUTION_PAYLOAD_FIELDS = 15; uint256 internal constant EXECUTION_PAYLOAD_FIELD_TREE_HEIGHT = 4; @@ -103,7 +103,7 @@ library BeaconChainProofs { /// @notice The number of seconds in a slot in the beacon chain uint64 internal constant SECONDS_PER_SLOT = 12; - /// @notice Number of seconds per epoch: 384 == 32 slots/epoch * 12 seconds/slot + /// @notice Number of seconds per epoch: 384 == 32 slots/epoch * 12 seconds/slot uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; bytes8 internal constant UINT64_MASK = 0xffffffffffffffff; @@ -139,7 +139,12 @@ library BeaconChainProofs { bytes32[] calldata stateRootProof ) internal view returns (bool valid) { bool validStateRoot = isValidStateRoot(stateRoot, beaconBlockRoot, stateRootProof); - bool validVCRootAgainstStateRoot = isValidVCRootAgainstStateRoot(validatorContainerRoot, stateRoot, validatorContainerRootProof, validatorIndex); + bool validVCRootAgainstStateRoot = isValidVCRootAgainstStateRoot( + validatorContainerRoot, + stateRoot, + validatorContainerRootProof, + validatorIndex + ); if (validStateRoot && validVCRootAgainstStateRoot) { valid = true; } @@ -150,17 +155,15 @@ library BeaconChainProofs { bytes32 beaconBlockRoot, bytes32[] calldata stateRootProof ) internal view returns (bool) { - require( - stateRootProof.length == BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT, - "state root proof should have 3 nodes" - ); - - return Merkle.verifyInclusionSha256({ - proof: stateRootProof, - root: beaconBlockRoot, - leaf: stateRoot, - index: STATE_ROOT_INDEX - }); + require(stateRootProof.length == BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT, "state root proof should have 3 nodes"); + + return + Merkle.verifyInclusionSha256({ + proof: stateRootProof, + root: beaconBlockRoot, + leaf: stateRoot, + index: STATE_ROOT_INDEX + }); } function isValidVCRootAgainstStateRoot( @@ -176,12 +179,13 @@ library BeaconChainProofs { uint256 leafIndex = (VALIDATOR_TREE_ROOT_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); - return Merkle.verifyInclusionSha256({ - proof: validatorContainerRootProof, - root: stateRoot, - leaf: validatorContainerRoot, - index: leafIndex - }); + return + Merkle.verifyInclusionSha256({ + proof: validatorContainerRootProof, + root: stateRoot, + leaf: validatorContainerRoot, + index: leafIndex + }); } function isValidWithdrawalContainerRoot( @@ -192,13 +196,18 @@ library BeaconChainProofs { bytes32 executionPayloadRoot, bytes32[] calldata executionPayloadRootProof ) internal view returns (bool valid) { - bool validExecutionPayloadRoot = isValidExecutionPayloadRoot(executionPayloadRoot, beaconBlockRoot, executionPayloadRootProof); + bool validExecutionPayloadRoot = isValidExecutionPayloadRoot( + executionPayloadRoot, + beaconBlockRoot, + executionPayloadRootProof + ); bool validWCRootAgainstExecutionPayloadRoot = isValidWCRootAgainstExecutionPayloadRoot( - withdrawalContainerRoot, - executionPayloadRoot, - withdrawalContainerRootProof, + withdrawalContainerRoot, + executionPayloadRoot, + withdrawalContainerRootProof, withdrawalIndex ); + if (validExecutionPayloadRoot && validWCRootAgainstExecutionPayloadRoot) { valid = true; } @@ -210,19 +219,20 @@ library BeaconChainProofs { bytes32[] calldata executionPayloadRootProof ) internal view returns (bool) { require( - executionPayloadRootProof.length == BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT, + executionPayloadRootProof.length == + BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT, "state root proof should have 3 nodes" ); - uint256 leafIndex = (BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT)) | - EXECUTION_PAYLOAD_INDEX; + uint256 leafIndex = (BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT)) | EXECUTION_PAYLOAD_INDEX; - return Merkle.verifyInclusionSha256({ - proof: executionPayloadRootProof, - root: beaconBlockRoot, - leaf: executionPayloadRoot, - index: leafIndex - }); + return + Merkle.verifyInclusionSha256({ + proof: executionPayloadRootProof, + root: beaconBlockRoot, + leaf: executionPayloadRoot, + index: leafIndex + }); } function isValidWCRootAgainstExecutionPayloadRoot( @@ -232,18 +242,54 @@ library BeaconChainProofs { uint256 withdrawalIndex ) internal view returns (bool) { require( - withdrawalContainerRootProof.length == (VALIDATOR_TREE_HEIGHT + 1) + BEACON_STATE_FIELD_TREE_HEIGHT, - "validator container root proof should have 46 nodes" + withdrawalContainerRootProof.length == + (EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT + WITHDRAWALS_TREE_HEIGHT + 1), + "withdrawalProof has incorrect length" ); - uint256 leafIndex = (WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1)) | - uint256(withdrawalIndex); + uint256 leafIndex = (WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1)) | uint256(withdrawalIndex); + + return + Merkle.verifyInclusionSha256({ + proof: withdrawalContainerRootProof, + root: executionPayloadRoot, + leaf: withdrawalContainerRoot, + index: leafIndex + }); + } - return Merkle.verifyInclusionSha256({ - proof: withdrawalContainerRootProof, - root: executionPayloadRoot, - leaf: withdrawalContainerRoot, - index: leafIndex - }); + function isValidHistoricalSummaryRoot( + bytes32 beaconBlockRoot, + bytes32[] calldata historicalSummaryBlockRootProof, + uint256 historicalSummaryIndex, + bytes32 blockRoot, + uint256 blockRootIndex + ) internal view returns (bool) { + require( + historicalSummaryBlockRootProof.length == + (BEACON_STATE_FIELD_TREE_HEIGHT + + (HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + + 1 + + (BLOCK_ROOTS_TREE_HEIGHT)), + "historicalSummaryBlockRootProof has incorrect length" + ); + /** + * Note: Here, the "1" in "1 + (BLOCK_ROOTS_TREE_HEIGHT)" signifies that extra step of choosing the "block_root_summary" within the individual + * "historical_summary". Everywhere else it signifies merkelize_with_mixin, where the length of an array is hashed with the root of the array, + * but not here. + */ + uint256 historicalBlockHeaderIndex = (HISTORICAL_SUMMARIES_INDEX << + ((HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + 1 + (BLOCK_ROOTS_TREE_HEIGHT))) | + (historicalSummaryIndex << (1 + (BLOCK_ROOTS_TREE_HEIGHT))) | + (BLOCK_SUMMARY_ROOT_INDEX << (BLOCK_ROOTS_TREE_HEIGHT)) | + blockRootIndex; + + return + Merkle.verifyInclusionSha256({ + proof: historicalSummaryBlockRootProof, + root: beaconBlockRoot, + leaf: blockRoot, + index: historicalBlockHeaderIndex + }); } } diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 62216245..ccffe894 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -39,7 +39,7 @@ contract SetUp is Test { uint64 internal constant SLOTS_PER_EPOCH = 32; /// @notice The number of seconds in a slot in the beacon chain uint64 internal constant SECONDS_PER_SLOT = 12; - /// @notice Number of seconds per epoch: 384 == 32 slots/epoch * 12 seconds/slot + /// @notice Number of seconds per epoch: 384 == 32 slots/epoch * 12 seconds/slot uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; @@ -54,9 +54,15 @@ contract SetUp is Test { validatorProof.stateRoot = stdJson.readBytes32(validatorInfo, ".beaconStateRoot"); require(validatorProof.stateRoot != bytes32(0), "state root should not be empty"); - validatorProof.stateRootProof = stdJson.readBytes32Array(validatorInfo, ".StateRootAgainstLatestBlockHeaderProof"); + validatorProof.stateRootProof = stdJson.readBytes32Array( + validatorInfo, + ".StateRootAgainstLatestBlockHeaderProof" + ); require(validatorProof.stateRootProof.length == 3, "state root proof should have 3 nodes"); - validatorProof.validatorContainerRootProof = stdJson.readBytes32Array(validatorInfo, ".WithdrawalCredentialProof"); + 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"); @@ -73,14 +79,16 @@ contract SetUp is Test { address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); vm.etch(capsuleAddress, address(phantomCapsule).code); - capsule = ExoCapsule(capsuleAddress); + capsule = ExoCapsule(payable(capsuleAddress)); assertEq(bytes32(capsule.capsuleWithdrawalCredentials()), _getWithdrawalCredentials(validatorContainer)); stdstore.target(capsuleAddress).sig("gateway()").checked_write(bytes32(uint256(uint160(address(this))))); stdstore.target(capsuleAddress).sig("capsuleOwner()").checked_write(bytes32(uint256(uint160(capsuleOwner)))); - stdstore.target(capsuleAddress).sig("beaconOracle()").checked_write(bytes32(uint256(uint160(address(beaconOracle))))); + stdstore.target(capsuleAddress).sig("beaconOracle()").checked_write( + bytes32(uint256(uint160(address(beaconOracle)))) + ); } function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { @@ -113,7 +121,9 @@ contract VerifyDepositProof is SetUp { using stdStorage for StdStorage; function test_verifyDepositProof_success() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -127,7 +137,9 @@ contract VerifyDepositProof is SetUp { capsule.verifyDepositProof(validatorContainer, validatorProof); - ExoCapsuleStorage.Validator memory validator = capsule.getRegisteredValidatorByPubkey(_getPubkey(validatorContainer)); + ExoCapsuleStorage.Validator memory validator = capsule.getRegisteredValidatorByPubkey( + _getPubkey(validatorContainer) + ); assertEq(uint8(validator.status), uint8(ExoCapsuleStorage.VALIDATOR_STATUS.REGISTERED)); assertEq(validator.validatorIndex, validatorProof.validatorIndex); assertEq(validator.mostRecentBalanceUpdateTimestamp, validatorProof.beaconBlockTimestamp); @@ -135,7 +147,9 @@ contract VerifyDepositProof is SetUp { } function test_verifyDepositProof_revert_validatorAlreadyDeposited() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -150,12 +164,16 @@ contract VerifyDepositProof is SetUp { capsule.verifyDepositProof(validatorContainer, validatorProof); // deposit again should revert - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.DoubleDepositedValidator.selector, _getPubkey(validatorContainer))); + vm.expectRevert( + abi.encodeWithSelector(ExoCapsule.DoubleDepositedValidator.selector, _getPubkey(validatorContainer)) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); } function test_verifyDepositProof_revert_staleProof() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp + 1 hours; mockCurrentBlockTimestamp = mockProofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS + 1 seconds; vm.warp(mockCurrentBlockTimestamp); @@ -168,12 +186,20 @@ contract VerifyDepositProof is SetUp { ); // deposit should revert because of proof is stale - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.StaleValidatorContainer.selector, _getPubkey(validatorContainer), mockProofTimestamp)); + vm.expectRevert( + abi.encodeWithSelector( + ExoCapsule.StaleValidatorContainer.selector, + _getPubkey(validatorContainer), + mockProofTimestamp + ) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); } function test_verifyDepositProof_revert_malformedValidatorContainer() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -189,18 +215,24 @@ contract VerifyDepositProof is SetUp { // construct malformed validator container that has extra fields validatorContainer.push(bytes32(uint256(123))); - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer))); + vm.expectRevert( + abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer)) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); vm.revertTo(snapshot); // construct malformed validator container that misses fields validatorContainer.pop(); - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer))); + vm.expectRevert( + abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer)) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); } function test_verifyDepositProof_revert_inactiveValidatorContainer() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; vm.mockCall( address(beaconOracle), @@ -213,12 +245,16 @@ contract VerifyDepositProof is SetUp { mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); validatorProof.beaconBlockTimestamp = mockProofTimestamp; - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InactiveValidatorContainer.selector, _getPubkey(validatorContainer))); + vm.expectRevert( + abi.encodeWithSelector(ExoCapsule.InactiveValidatorContainer.selector, _getPubkey(validatorContainer)) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); } function test_verifyDepositProof_revert_mismatchWithdrawalCredentials() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -247,7 +283,9 @@ contract VerifyDepositProof is SetUp { } function test_verifyDepositProof_revert_proofNotMatchWithBeaconRoot() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -261,7 +299,9 @@ contract VerifyDepositProof is SetUp { ); // verify proof against mismatch beacon block root - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer))); + vm.expectRevert( + abi.encodeWithSelector(ExoCapsule.InvalidValidatorContainer.selector, _getPubkey(validatorContainer)) + ); capsule.verifyDepositProof(validatorContainer, validatorProof); } } From c095a377ed388f99e219e936b19ed036cf6a938a Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 28 May 2024 12:40:55 -0400 Subject: [PATCH 55/93] feat: update withdrawal test setup contract --- src/core/ExoCapsule.sol | 16 +- test/foundry/ExoCapsule.t.sol | 203 +++++++++++++++++- .../test-data/full_withdrawal_proof.json | 159 ++++++++++++++ .../test-data/partial_withdrawal_proof.json | 159 ++++++++++++++ 4 files changed, 534 insertions(+), 3 deletions(-) create mode 100644 test/foundry/test-data/full_withdrawal_proof.json create mode 100644 test/foundry/test-data/partial_withdrawal_proof.json diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 3eb62da6..5ba04ec1 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -69,6 +69,12 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { _; } + /// @notice checks that hasRestaked is set to true by calling activateRestaking() + modifier hasEnabledRestaking() { + require(hasRestaked, "restaking is not enabled"); + _; + } + /// @notice Checks that `timestamp` is greater than or equal to the value stored in `mostRecentWithdrawalTimestamp` /// @notice All partial/full withdrawal timestamps should be greater than `mostRecentWithdrawalTimestamp` modifier proofIsForValidTimestamp(uint256 timestamp) { @@ -96,12 +102,19 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { gateway = INativeRestakingController(gateway_); beaconOracle = IBeaconChainOracle(beaconOracle_); capsuleOwner = capsuleOwner_; + hasRestaked = true; } function verifyDepositProof( bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof - ) external onlyGateway { + ) + external + onlyGateway + proofIsForValidTimestamp(proof.beaconBlockTimestamp) + // ensure that caller has previously enabled restaking by calling `activateRestaking()` + hasEnabledRestaking + { bytes32 validatorPubkey = validatorContainer.getPubkey(); bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); Validator storage validator = _capsuleValidators[validatorPubkey]; @@ -221,6 +234,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { "ExoCapsule.withdrawNonBeaconChainETHBalance: amountToWithdraw is greater than nonBeaconChainETHBalance" ); nonBeaconChainETHBalance -= amountToWithdraw; + _sendETH(recipient, amountToWithdraw); emit NonBeaconChainETHWithdrawn(recipient, amountToWithdraw); } diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index ccffe894..c2b60edd 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -13,7 +13,7 @@ import {ExoCapsuleStorage} from "src/storage/ExoCapsuleStorage.sol"; import "src/libraries/BeaconChainProofs.sol"; import "src/libraries/Endian.sol"; -contract SetUp is Test { +contract DepositSetup is Test { using stdStorage for StdStorage; using Endian for bytes32; @@ -89,6 +89,8 @@ contract SetUp is Test { stdstore.target(capsuleAddress).sig("beaconOracle()").checked_write( bytes32(uint256(uint160(address(beaconOracle)))) ); + + stdstore.target(capsuleAddress).sig("hasRestaked()").checked_write(true); } function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { @@ -116,7 +118,7 @@ contract SetUp is Test { } } -contract VerifyDepositProof is SetUp { +contract VerifyDepositProof is DepositSetup { using BeaconChainProofs for bytes32; using stdStorage for StdStorage; @@ -278,6 +280,9 @@ contract VerifyDepositProof is SetUp { bytes32 beaconOraclerSlot = bytes32(stdstore.target(address(anotherCapsule)).sig("beaconOracle()").find()); vm.store(address(anotherCapsule), beaconOraclerSlot, bytes32(uint256(uint160(address(beaconOracle))))); + bytes32 hasRestakedSlot = bytes32(stdstore.target(address(anotherCapsule)).sig("hasRestaked()").find()); + vm.store(address(anotherCapsule), hasRestakedSlot, bytes32(uint256(1))); + vm.expectRevert(abi.encodeWithSelector(ExoCapsule.WithdrawalCredentialsNotMatch.selector)); anotherCapsule.verifyDepositProof(validatorContainer, validatorProof); } @@ -305,3 +310,197 @@ contract VerifyDepositProof is SetUp { capsule.verifyDepositProof(validatorContainer, validatorProof); } } + +contract WithdrawalSetup is Test { + using stdStorage for StdStorage; + using Endian for bytes32; + + bytes32[] validatorContainer; + /** + struct ValidatorContainerProof { + uint256 beaconBlockTimestamp; + bytes32 stateRoot; + bytes32[] stateRootProof; + bytes32[] validatorContainerRootProof; + uint256 validatorIndex; + } + */ + IExoCapsule.ValidatorContainerProof validatorProof; + + bytes32[] withdrawalContainer; + /** + struct WithdrawalContainerProof { + uint256 beaconBlockTimestamp; + bytes32 executionPayloadRoot; + bytes32[] executionPayloadRootProof; + bytes32[] withdrawalContainerRootProof; + bytes32[] historicalSummaryBlockRootProof; + uint256 historicalSummaryIndex; + bytes32 blockRoot; + uint256 blockRootIndex; + uint256 withdrawalIndex; + } + */ + IExoCapsule.WithdrawalContainerProof withdrawalProof; + bytes32 beaconBlockRoot; + + ExoCapsule capsule; + IBeaconChainOracle beaconOracle; + address capsuleOwner; + + uint256 constant BEACON_CHAIN_GENESIS_TIME = 1606824023; + /// @notice The number of slots each epoch in the beacon chain + uint64 internal constant SLOTS_PER_EPOCH = 32; + /// @notice The number of seconds in a slot in the beacon chain + uint64 internal constant SECONDS_PER_SLOT = 12; + /// @notice Number of seconds per epoch: 384 == 32 slots/epoch * 12 seconds/slot + uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; + uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; + + uint256 mockProofTimestamp; + uint256 mockCurrentBlockTimestamp; + + function setUp() public { + string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); + + validatorContainer = stdJson.readBytes32Array(withdrawalInfo, ".ValidatorFields"); + require(validatorContainer.length > 0, "validator container should not be empty"); + + validatorProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot"); + require(validatorProof.stateRoot != bytes32(0), "state root should not be empty"); + validatorProof.stateRootProof = stdJson.readBytes32Array( + withdrawalInfo, + ".StateRootAgainstLatestBlockHeaderProof" + ); + require(validatorProof.stateRootProof.length == 3, "state root proof should have 3 nodes"); + validatorProof.validatorContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ValidatorProof"); + require(validatorProof.validatorContainerRootProof.length == 46, "validator root proof should have 46 nodes"); + validatorProof.validatorIndex = stdJson.readUint(withdrawalInfo, ".validatorIndex"); + require(validatorProof.validatorIndex != 0, "validator root index should not be 0"); + + beaconBlockRoot = stdJson.readBytes32(withdrawalInfo, ".latestBlockHeaderRoot"); + require(beaconBlockRoot != bytes32(0), "beacon block root should not be empty"); + + withdrawalContainer = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalFields"); + require(withdrawalContainer.length > 0, "validator container should not be empty"); + + withdrawalProof.blockRoot = stdJson.readBytes32(withdrawalInfo, ".blockHeaderRoot"); + require(withdrawalProof.blockRoot != bytes32(0), "block header root should not be empty"); + + withdrawalProof.blockRootIndex = stdJson.readUint(withdrawalInfo, ".blockHeaderRootIndex"); + require(withdrawalProof.blockRootIndex != 0, "block header root index should not be 0"); + + withdrawalProof.withdrawalIndex = stdJson.readUint(withdrawalInfo, ".withdrawalIndex"); + + withdrawalProof.historicalSummaryIndex = stdJson.readUint(withdrawalInfo, ".historicalSummaryIndex"); + require(withdrawalProof.historicalSummaryIndex != 0, "historical summary index should not be 0"); + + withdrawalProof.historicalSummaryBlockRootProof = stdJson.readBytes32Array( + withdrawalInfo, + ".HistoricalSummaryProof" + ); + withdrawalProof.withdrawalContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalProof"); + withdrawalProof.executionPayloadRoot = stdJson.readBytes32(withdrawalInfo, ".executionPayloadRoot"); + withdrawalProof.executionPayloadRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ExecutionPayloadProof"); + + beaconOracle = IBeaconChainOracle(address(0x123)); + vm.etch(address(beaconOracle), bytes("aabb")); + + capsuleOwner = address(0x125); + + ExoCapsule phantomCapsule = new ExoCapsule(); + + address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); + vm.etch(capsuleAddress, address(phantomCapsule).code); + capsule = ExoCapsule(payable(capsuleAddress)); + assertEq(bytes32(capsule.capsuleWithdrawalCredentials()), _getWithdrawalCredentials(validatorContainer)); + + stdstore.target(capsuleAddress).sig("gateway()").checked_write(bytes32(uint256(uint160(address(this))))); + + stdstore.target(capsuleAddress).sig("capsuleOwner()").checked_write(bytes32(uint256(uint160(capsuleOwner)))); + + stdstore.target(capsuleAddress).sig("beaconOracle()").checked_write( + bytes32(uint256(uint160(address(beaconOracle)))) + ); + + stdstore.target(capsuleAddress).sig("hasRestaked()").checked_write(true); + } + + 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 _getEffectiveBalance(bytes32[] storage vc) internal view returns (uint64) { + return vc[2].fromLittleEndianUint64(); + } + + 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(); + } +} + +contract VerifyWithdrawalProof is WithdrawalSetup { + using BeaconChainProofs for bytes32; + using stdStorage for StdStorage; + + function test_NonBeaconChainETHWithdraw() public { + assertEq(capsule.nonBeaconChainETHBalance(), 0); + address sender = vm.addr(1); + vm.startPrank(sender); + vm.deal(sender, 1 ether); + (bool sent, ) = address(capsule).call{value: 0.5 ether}(""); + assertEq(sent, true); + assertEq(capsule.nonBeaconChainETHBalance(), 0.5 ether); + vm.stopPrank(); + + address recipient = vm.addr(2); + capsule.withdrawNonBeaconChainETHBalance(recipient, 0.2 ether); + assertEq(recipient.balance, 0.2 ether); + assertEq(capsule.nonBeaconChainETHBalance(), 0.3 ether); + + vm.expectRevert( + bytes( + "ExoCapsule.withdrawNonBeaconChainETHBalance: amountToWithdraw is greater than nonBeaconChainETHBalance" + ) + ); + capsule.withdrawNonBeaconChainETHBalance(recipient, 0.5 ether); + } + + function test_verifyWithdrawalProof_success() public { + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; + mockProofTimestamp = activationTimestamp; + mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(beaconBlockRoot) + ); + + capsule.verifyDepositProof(validatorContainer, validatorProof); + + ExoCapsuleStorage.Validator memory validator = capsule.getRegisteredValidatorByPubkey( + _getPubkey(validatorContainer) + ); + assertEq(uint8(validator.status), uint8(ExoCapsuleStorage.VALIDATOR_STATUS.REGISTERED)); + assertEq(validator.validatorIndex, validatorProof.validatorIndex); + assertEq(validator.mostRecentBalanceUpdateTimestamp, validatorProof.beaconBlockTimestamp); + assertEq(validator.restakedBalanceGwei, _getEffectiveBalance(validatorContainer)); + } +} diff --git a/test/foundry/test-data/full_withdrawal_proof.json b/test/foundry/test-data/full_withdrawal_proof.json new file mode 100644 index 00000000..3e47b263 --- /dev/null +++ b/test/foundry/test-data/full_withdrawal_proof.json @@ -0,0 +1,159 @@ +{ + "slot": 6397852, + "validatorIndex": 302913, + "historicalSummaryIndex": 146, + "withdrawalIndex": 0, + "blockHeaderRootIndex": 8092, + "beaconStateRoot": "0xe562b064fa5f17412bf13cd3b650f9d84b912f127c3f4c09879864d51a8b4daf", + "slotRoot": "0x9c9f610000000000000000000000000000000000000000000000000000000000", + "timestampRoot": "0xb06fed6400000000000000000000000000000000000000000000000000000000", + "blockHeaderRoot": "0x8b036996f94e940c80c5c692fd0e25467a5d55f1cf92b7808f92090fc7be1d17", + "executionPayloadRoot": "0xe628472355543b53917635e60c1f924f111f7a3cd58f2d947e8631b9d9924cb1", + "latestBlockHeaderRoot": "0xa81fa0ec796b5f84e6435745245f6d24279a11a74e29666560355507c441332d", + "SlotProof": [ + "0x89c5010000000000000000000000000000000000000000000000000000000000", + "0xab4a015ca78ff722e478d047b19650dc6fc92a4270c6cd34401523d3d6a1d9f2", + "0xf4e65df697eb85f3ab176ac93b6ad4d96bd6b04bdddcc4f6c98f0fa94effc553" + ], + "WithdrawalProof": [ + "0xa3d843f57c18ee3dac0eb263e446fe5d0110059137807d3cae4a2e60ccca013f", + "0x87441da495942a4af734cbca4dbcf0b96b2d83137ce595c9f29495aae6a8d99e", + "0xae0dc609ecbfb26abc191227a76efb332aaea29725253756f2cad136ef5837a6", + "0x765bcd075991ecad96203020d1576fdb9b45b41dad3b5adde11263ab9f6f56b8", + "0x1000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x9d9b56c23faa6a8abf0a49105eb12bbdfdf198a9c6c616b8f24f6e91ad79de92", + "0xac5e32ea973e990d191039e91c7d9fd9830599b8208675f159c3df128228e729", + "0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471" + ], + "ValidatorProof": [ + "0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e", + "0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee", + "0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1", + "0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1", + "0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac", + "0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537", + "0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38", + "0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748", + "0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a", + "0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c", + "0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b", + "0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e", + "0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee", + "0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9", + "0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74", + "0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3", + "0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f", + "0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6", + "0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528", + "0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3", + "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", + "0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff", + "0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5", + "0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d", + "0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c", + "0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327", + "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", + "0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76", + "0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f", + "0x846b080000000000000000000000000000000000000000000000000000000000", + "0x5a6c050000000000000000000000000000000000000000000000000000000000", + "0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e", + "0xce3d1b002aa817e3718132b7ffe6cea677f81b1b3b690b8052732d8e1a70d06b", + "0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b", + "0xba25997a12576cc460c39cfc6cc87d7dc215b237a0f3c4dc8cb7473125b2e138" + ], + "TimestampProof": [ + "0x28a2c80000000000000000000000000000000000000000000000000000000000", + "0xa749df3368741198702798435eea361b1b1946aa9456587a2be377c8472ea2df", + "0xb1c03097a5a24f46cdb684df37e3ac0536a76428be1a6c6d6450378701ab1f3d", + "0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471" + ], + "ExecutionPayloadProof": [ + "0xb6a435ffd17014d1dad214ba466aaa7fba5aa247945d2c29fd53e90d554f4474", + "0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0x5ec9aaf0a3571602f4704d4471b9af564caf17e4d22a7c31017293cb95949053", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0xc1f7cb289e44e9f711d7d7c05b67b84b8c6dd0394b688922a490cfd3fe216db1" + ], + "ValidatorFields": [ + "0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d", + "0x0100000000000000000000008e35f095545c56b07c942a4f3b055ef1ec4cb148", + "0x0040597307000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xea65010000000000000000000000000000000000000000000000000000000000", + "0xf265010000000000000000000000000000000000000000000000000000000000", + "0xffffffffffffffff000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000" + ], + "WithdrawalFields": [ + "0x45cee50000000000000000000000000000000000000000000000000000000000", + "0x419f040000000000000000000000000000000000000000000000000000000000", + "0x59b0d71688da01057c08e4c1baa8faa629819c2a000000000000000000000000", + "0xe5015b7307000000000000000000000000000000000000000000000000000000" + ], + "StateRootAgainstLatestBlockHeaderProof": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71" + ], + "HistoricalSummaryProof": [ + "0x050b5923fe2e470849a7d467e4cbed4803d3a46d516b84552567976960ff4ccc", + "0x6ab9b5fc357c793cc5339398016c28ea21f0be4d56a68b554a29766dd312eeeb", + "0xb8c4c9f1dec537f4c4652e6bf519bac768e85b2590b7683e392aab698b50d529", + "0x1cae4e7facb6883024359eb1e9212d36e68a106a7733dc1c099017a74e5f465a", + "0x616439c1379e10fc6ad2fa192a2e49c2d5a7155fdde95911f37fcfb75952fcb2", + "0x301ab0d5d5ada4bd2e859658fc31743b5a502b26bc9b8b162e5a161e21218048", + "0x9d2bc97fffd61659313009e67a4c729a10274f2af26914a53bc7af6717da211e", + "0x4bdcbe543f9ef7348855aac43d6b6286f9c0c7be53de8a1300bea1ba5ba0758e", + "0xb6631640d626ea9523ae619a42633072614326cc9220462dffdeb63e804ef05f", + "0xf19a76e33ca189a8682ece523c2afda138db575955b7af31a427c9b8adb41e15", + "0x221b43ad87d7410624842cad296fc48360b5bf4e835f6ff610db736774d2f2d3", + "0x297c51f4ff236db943bebeb35538e207c8de6330d26aa8138a9ca206f42154bf", + "0x129a0644f33b9ee4e9a36a11dd59d1dedc64012fbb7a79263d07f82d647ffba8", + "0x763794c2042b9c8381ac7c4d7f4d38b0abb38069b2810b522f87951f25d9d2be", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xee0639a2ada97368e8b3493f9d2141e16c3cd9fe54e13691bb7a2c376c56c7c8", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0xba2bc704559c23541f0c9efa0b522454e8cd06cd504d0e45724709cf5672640f", + "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0xff857f4c17c9fb2e544e496685ebd8e2258c761e4636cfb031ba73a4430061c7", + "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x9300000000000000000000000000000000000000000000000000000000000000", + "0xd9ed050000000000000000000000000000000000000000000000000000000000", + "0xd824a89a9d5dd329a069b394ddc6618c70ed784982061959ac77d58d48e9d7c8", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0x8dc0b81ebe27fb91663a74713252d2eae5deb3b983374afc3c2d2f6b254128f1", + "0x09c7a863a253b4163a10916db2781eb35f58c016f0a24b3331ed84af3822c341" + ] +} diff --git a/test/foundry/test-data/partial_withdrawal_proof.json b/test/foundry/test-data/partial_withdrawal_proof.json new file mode 100644 index 00000000..0cea88f5 --- /dev/null +++ b/test/foundry/test-data/partial_withdrawal_proof.json @@ -0,0 +1,159 @@ +{ + "slot": 6397852, + "validatorIndex": 302913, + "historicalSummaryIndex": 146, + "withdrawalIndex": 0, + "blockHeaderRootIndex": 8092, + "beaconStateRoot": "0xc4ea7f435356d2de29784712dc1fb5597d7d36ec705ddebcbef8bdc4cb4ecaf0", + "slotRoot": "0x9c9f610000000000000000000000000000000000000000000000000000000000", + "timestampRoot": "0xb06fed6400000000000000000000000000000000000000000000000000000000", + "blockHeaderRoot": "0x8d9698a822c4f57d890f9465c046ae8799301ff01daca55fb2e8e347d547ec6d", + "executionPayloadRoot": "0xf6e0315572785590a6ee69adab31e5c1e9879264aeead5b7762a384ae7d938d0", + "latestBlockHeaderRoot": "0x34e6f6ae9370c1eacc38aad8c5a887983979b79a35c57f09570afd80a879fd69", + "SlotProof": [ + "0x89c5010000000000000000000000000000000000000000000000000000000000", + "0xab4a015ca78ff722e478d047b19650dc6fc92a4270c6cd34401523d3d6a1d9f2", + "0xe01b2b111680b6b01268c23ee3d7d4d145e98809dd5894cc8d1944bdc19e346e" + ], + "WithdrawalProof": [ + "0xa3d843f57c18ee3dac0eb263e446fe5d0110059137807d3cae4a2e60ccca013f", + "0x87441da495942a4af734cbca4dbcf0b96b2d83137ce595c9f29495aae6a8d99e", + "0xae0dc609ecbfb26abc191227a76efb332aaea29725253756f2cad136ef5837a6", + "0x765bcd075991ecad96203020d1576fdb9b45b41dad3b5adde11263ab9f6f56b8", + "0x1000000000000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x9d9b56c23faa6a8abf0a49105eb12bbdfdf198a9c6c616b8f24f6e91ad79de92", + "0xac5e32ea973e990d191039e91c7d9fd9830599b8208675f159c3df128228e729", + "0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471" + ], + "ValidatorProof": [ + "0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e", + "0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee", + "0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1", + "0x9b9adf5d31a74f30ae86ce7f7a8bfe89d2bdf2bd799d0c6896174b4f15878bf1", + "0x17d22cd18156b4bcbefbcfa3ed820c14cc5af90cb7c02c373cc476bc947ba4ac", + "0x22c1a00da80f2c5c8a11fdd629af774b9dde698305735d63b19aed6a70310537", + "0x949da2d82acf86e064a7022c5d5e69528ad6d3dd5b9cdf7fb9b736f0d925fc38", + "0x1920215f3c8c349a04f6d29263f495416e58d85c885b7d356dd4d335427d2748", + "0x7f12746ac9a3cc418594ab2c25838fdaf9ef43050a12f38f0c25ad7f976d889a", + "0x451a649946a59a90f56035d1eccdfcaa99ac8bb74b87c403653bdc5bc0055e2c", + "0x00ab86a6644a7694fa7bc0da3a8730404ea7e26da981b169316f7acdbbe8c79b", + "0x0d500027bb8983acbec0993a3d063f5a1f4b9a5b5893016bc9eec28e5633867e", + "0x2ba5cbed64a0202199a181c8612a8c5dad2512ad0ec6aa7f0c079392e16008ee", + "0xab8576644897391ddc0773ac95d072de29d05982f39201a5e0630b81563e91e9", + "0xc6e90f3f46f28faea3476837d2ec58ad5fa171c1f04446f2aa40aa433523ec74", + "0xb86e491b234c25dc5fa17b43c11ef04a7b3e89f99b2bb7d8daf87ee6dc6f3ae3", + "0xdb41e006a5111a4f2620a004e207d2a63fc5324d7f528409e779a34066a9b67f", + "0xe2356c743f98d89213868108ad08074ca35f685077e487077cef8a55917736c6", + "0xf7552771443e29ebcc7a4aad87e308783559a0b4ff696a0e49f81fb2736fe528", + "0x3d3aabf6c36de4242fef4b6e49441c24451ccf0e8e184a33bece69d3e3d40ac3", + "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x31206fa80a50bb6abe29085058f16212212a60eec8f049fecb92d8c8e0a84bc0", + "0x21352bfecbeddde993839f614c3dac0a3ee37543f9b412b16199dc158e23b544", + "0x619e312724bb6d7c3153ed9de791d764a366b389af13c58bf8a8d90481a46765", + "0x7cdd2986268250628d0c10e385c58c6191e6fbe05191bcc04f133f2cea72c1c4", + "0x848930bd7ba8cac54661072113fb278869e07bb8587f91392933374d017bcbe1", + "0x8869ff2c22b28cc10510d9853292803328be4fb0e80495e8bb8d271f5b889636", + "0xb5fe28e79f1b850f8658246ce9b6a1e7b49fc06db7143e8fe0b4f2b0c5523a5c", + "0x985e929f70af28d0bdd1a90a808f977f597c7c778c489e98d3bd8910d31ac0f7", + "0xc6f67e02e6e4e1bdefb994c6098953f34636ba2b6ca20a4721d2b26a886722ff", + "0x1c9a7e5ff1cf48b4ad1582d3f4e4a1004f3b20d8c5a2b71387a4254ad933ebc5", + "0x2f075ae229646b6f6aed19a5e372cf295081401eb893ff599b3f9acc0c0d3e7d", + "0x328921deb59612076801e8cd61592107b5c67c79b846595cc6320c395b46362c", + "0xbfb909fdb236ad2411b4e4883810a074b840464689986c3f8a8091827e17c327", + "0x55d8fb3687ba3ba49f342c77f5a1f89bec83d811446e1a467139213d640b6a74", + "0xf7210d4f8e7e1039790e7bf4efa207555a10a6db1dd4b95da313aaa88b88fe76", + "0xad21b516cbc645ffe34ab5de1c8aef8cd4e7f8d2b51e8e1456adc7563cda206f", + "0x846b080000000000000000000000000000000000000000000000000000000000", + "0x5a6c050000000000000000000000000000000000000000000000000000000000", + "0x47314ebc95c63f3fd909af6442ed250d823b2ee8a6e48d8167d4dfdab96c8b5e", + "0xce3d1b002aa817e3718132b7ffe6cea677f81b1b3b690b8052732d8e1a70d06b", + "0x4715bf9a259680cd06827b30bddb27ad445506e9edeb72a3eab904d94dea816b", + "0x673e299c82563e700195d7d66981bdf521b9609a3b6d22be78f069b359e553e5" + ], + "TimestampProof": [ + "0x28a2c80000000000000000000000000000000000000000000000000000000000", + "0xa749df3368741198702798435eea361b1b1946aa9456587a2be377c8472ea2df", + "0x568268c732e7471701bcaedcae302de5fe87b60cf9e6518696719f9732078b97", + "0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471" + ], + "ExecutionPayloadProof": [ + "0xb6a435ffd17014d1dad214ba466aaa7fba5aa247945d2c29fd53e90d554f4474", + "0x336488033fe5f3ef4ccc12af07b9370b92e553e35ecb4a337a1b1c0e4afe1e0e", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0x5ec9aaf0a3571602f4704d4471b9af564caf17e4d22a7c31017293cb95949053", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0xc1f7cb289e44e9f711d7d7c05b67b84b8c6dd0394b688922a490cfd3fe216db1" + ], + "ValidatorFields": [ + "0xe36689b7b39ee895a754ba878afac2aa5d83349143a8b23d371823dd9ed3435d", + "0x0100000000000000000000008e35f095545c56b07c942a4f3b055ef1ec4cb148", + "0x0040597307000000000000000000000000000000000000000000000000000000", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xea65010000000000000000000000000000000000000000000000000000000000", + "0xf265010000000000000000000000000000000000000000000000000000000000", + "0xffffffffffffffff000000000000000000000000000000000000000000000000", + "0xffffffffffffffff000000000000000000000000000000000000000000000000" + ], + "WithdrawalFields": [ + "0x45cee50000000000000000000000000000000000000000000000000000000000", + "0x419f040000000000000000000000000000000000000000000000000000000000", + "0x59b0d71688da01057c08e4c1baa8faa629819c2a000000000000000000000000", + "0xbd56200000000000000000000000000000000000000000000000000000000000" + ], + "StateRootAgainstLatestBlockHeaderProof": [ + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xf5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71" + ], + "HistoricalSummaryProof": [ + "0x050b5923fe2e470849a7d467e4cbed4803d3a46d516b84552567976960ff4ccc", + "0x6ab9b5fc357c793cc5339398016c28ea21f0be4d56a68b554a29766dd312eeeb", + "0xb8c4c9f1dec537f4c4652e6bf519bac768e85b2590b7683e392aab698b50d529", + "0x1cae4e7facb6883024359eb1e9212d36e68a106a7733dc1c099017a74e5f465a", + "0x616439c1379e10fc6ad2fa192a2e49c2d5a7155fdde95911f37fcfb75952fcb2", + "0x301ab0d5d5ada4bd2e859658fc31743b5a502b26bc9b8b162e5a161e21218048", + "0x9d2bc97fffd61659313009e67a4c729a10274f2af26914a53bc7af6717da211e", + "0x4bdcbe543f9ef7348855aac43d6b6286f9c0c7be53de8a1300bea1ba5ba0758e", + "0xb6631640d626ea9523ae619a42633072614326cc9220462dffdeb63e804ef05f", + "0xf19a76e33ca189a8682ece523c2afda138db575955b7af31a427c9b8adb41e15", + "0x221b43ad87d7410624842cad296fc48360b5bf4e835f6ff610db736774d2f2d3", + "0x297c51f4ff236db943bebeb35538e207c8de6330d26aa8138a9ca206f42154bf", + "0x129a0644f33b9ee4e9a36a11dd59d1dedc64012fbb7a79263d07f82d647ffba8", + "0x763794c2042b9c8381ac7c4d7f4d38b0abb38069b2810b522f87951f25d9d2be", + "0x0000000000000000000000000000000000000000000000000000000000000000", + "0xee0639a2ada97368e8b3493f9d2141e16c3cd9fe54e13691bb7a2c376c56c7c8", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0xc78009fdf07fc56a11f122370658a353aaa542ed63e44c4bc15ff4cd105ab33c", + "0xba2bc704559c23541f0c9efa0b522454e8cd06cd504d0e45724709cf5672640f", + "0x9efde052aa15429fae05bad4d0b1d7c64da64d03d7a1854a588c2cb8430c0d30", + "0xd88ddfeed400a8755596b21942c1497e114c302e6118290f91e6772976041fa1", + "0xff857f4c17c9fb2e544e496685ebd8e2258c761e4636cfb031ba73a4430061c7", + "0x26846476fd5fc54a5d43385167c95144f2643f533cc85bb9d16b782f8d7db193", + "0x506d86582d252405b840018792cad2bf1259f1ef5aa5f887e13cb2f0094f51e1", + "0xffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b", + "0x6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220", + "0xb7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f", + "0xdf6af5f5bbdb6be9ef8aa618e4bf8073960867171e29676f8b284dea6a08a85e", + "0xb58d900f5e182e3c50ef74969ea16c7726c549757cc23523c369587da7293784", + "0xd49a7502ffcfb0340b1d7885688500ca308161a7f96b62df9d083b71fcc8f2bb", + "0x8fe6b1689256c0d385f42f5bbe2027a22c1996e110ba97c171d3e5948de92beb", + "0x8d0d63c39ebade8509e0ae3c9c3876fb5fa112be18f905ecacfecb92057603ab", + "0x95eec8b2e541cad4e91de38385f2e046619f54496c2382cb6cacd5b98c26f5a4", + "0xf893e908917775b62bff23294dbbe3a1cd8e6cc1c35b4801887b646a6f81f17f", + "0xcddba7b592e3133393c16194fac7431abf2f5485ed711db282183c819e08ebaa", + "0x8a8d7fe3af8caa085a7639a832001457dfb9128a8061142ad0335629ff23ff9c", + "0xfeb3c337d7a51a6fbf00b9e34c52e1c9195c969bd4e7a0bfd51d5c5bed9c1167", + "0xe71f0aa83cc32edfbefa9f4d3e0174ca85182eec9f3a09f6a6c0df6377a510d7", + "0x9300000000000000000000000000000000000000000000000000000000000000", + "0xd9ed050000000000000000000000000000000000000000000000000000000000", + "0xd824a89a9d5dd329a069b394ddc6618c70ed784982061959ac77d58d48e9d7c8", + "0xdb56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71", + "0x8dc0b81ebe27fb91663a74713252d2eae5deb3b983374afc3c2d2f6b254128f1", + "0xe237bc62b6b5269da5f4093c292d0f3bf2cf4d2eb93b4f366dba675c4df9cc62" + ] +} From 78d576b0805eabbc31c82d1876efe205f4c5b437 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 28 May 2024 12:59:29 -0400 Subject: [PATCH 56/93] feat: refactor validator container set using internal setter functions --- test/foundry/ExoCapsule.t.sol | 109 ++++++++++-------- .../test-data/full_withdrawal_proof.json | 2 +- 2 files changed, 60 insertions(+), 51 deletions(-) diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index c2b60edd..4361c8f6 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -361,8 +361,58 @@ contract WithdrawalSetup is Test { uint256 mockCurrentBlockTimestamp; function setUp() public { - string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); + string memory withdrawalInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); + // string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); + _setValidatorContainer(withdrawalInfo); + + beaconOracle = IBeaconChainOracle(address(0x123)); + vm.etch(address(beaconOracle), bytes("aabb")); + + capsuleOwner = address(0x125); + + ExoCapsule phantomCapsule = new ExoCapsule(); + + address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); + vm.etch(capsuleAddress, address(phantomCapsule).code); + capsule = ExoCapsule(payable(capsuleAddress)); + assertEq(bytes32(capsule.capsuleWithdrawalCredentials()), _getWithdrawalCredentials(validatorContainer)); + stdstore.target(capsuleAddress).sig("gateway()").checked_write(bytes32(uint256(uint160(address(this))))); + + stdstore.target(capsuleAddress).sig("capsuleOwner()").checked_write(bytes32(uint256(uint160(capsuleOwner)))); + + stdstore.target(capsuleAddress).sig("beaconOracle()").checked_write( + bytes32(uint256(uint160(address(beaconOracle)))) + ); + + stdstore.target(capsuleAddress).sig("hasRestaked()").checked_write(true); + + uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + + _getActivationEpoch(validatorContainer) * + SECONDS_PER_EPOCH; + mockProofTimestamp = activationTimestamp; + mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(beaconBlockRoot) + ); + + capsule.verifyDepositProof(validatorContainer, validatorProof); + + ExoCapsuleStorage.Validator memory validator = capsule.getRegisteredValidatorByPubkey( + _getPubkey(validatorContainer) + ); + assertEq(uint8(validator.status), uint8(ExoCapsuleStorage.VALIDATOR_STATUS.REGISTERED)); + assertEq(validator.validatorIndex, validatorProof.validatorIndex); + assertEq(validator.mostRecentBalanceUpdateTimestamp, validatorProof.beaconBlockTimestamp); + assertEq(validator.restakedBalanceGwei, _getEffectiveBalance(validatorContainer)); + } + + function _setValidatorContainer(string memory withdrawalInfo) internal { validatorContainer = stdJson.readBytes32Array(withdrawalInfo, ".ValidatorFields"); require(validatorContainer.length > 0, "validator container should not be empty"); @@ -373,13 +423,20 @@ contract WithdrawalSetup is Test { ".StateRootAgainstLatestBlockHeaderProof" ); require(validatorProof.stateRootProof.length == 3, "state root proof should have 3 nodes"); - validatorProof.validatorContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ValidatorProof"); + validatorProof.validatorContainerRootProof = stdJson.readBytes32Array( + withdrawalInfo, + ".WithdrawalCredentialProof" + ); require(validatorProof.validatorContainerRootProof.length == 46, "validator root proof should have 46 nodes"); validatorProof.validatorIndex = stdJson.readUint(withdrawalInfo, ".validatorIndex"); require(validatorProof.validatorIndex != 0, "validator root index should not be 0"); beaconBlockRoot = stdJson.readBytes32(withdrawalInfo, ".latestBlockHeaderRoot"); require(beaconBlockRoot != bytes32(0), "beacon block root should not be empty"); + } + + function _setWithdrawalContainer() internal { + string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); withdrawalContainer = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalFields"); require(withdrawalContainer.length > 0, "validator container should not be empty"); @@ -402,28 +459,6 @@ contract WithdrawalSetup is Test { withdrawalProof.withdrawalContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalProof"); withdrawalProof.executionPayloadRoot = stdJson.readBytes32(withdrawalInfo, ".executionPayloadRoot"); withdrawalProof.executionPayloadRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ExecutionPayloadProof"); - - beaconOracle = IBeaconChainOracle(address(0x123)); - vm.etch(address(beaconOracle), bytes("aabb")); - - capsuleOwner = address(0x125); - - ExoCapsule phantomCapsule = new ExoCapsule(); - - address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); - vm.etch(capsuleAddress, address(phantomCapsule).code); - capsule = ExoCapsule(payable(capsuleAddress)); - assertEq(bytes32(capsule.capsuleWithdrawalCredentials()), _getWithdrawalCredentials(validatorContainer)); - - stdstore.target(capsuleAddress).sig("gateway()").checked_write(bytes32(uint256(uint160(address(this))))); - - stdstore.target(capsuleAddress).sig("capsuleOwner()").checked_write(bytes32(uint256(uint160(capsuleOwner)))); - - stdstore.target(capsuleAddress).sig("beaconOracle()").checked_write( - bytes32(uint256(uint160(address(beaconOracle)))) - ); - - stdstore.target(capsuleAddress).sig("hasRestaked()").checked_write(true); } function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { @@ -477,30 +512,4 @@ contract VerifyWithdrawalProof is WithdrawalSetup { ); capsule.withdrawNonBeaconChainETHBalance(recipient, 0.5 ether); } - - function test_verifyWithdrawalProof_success() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + - _getActivationEpoch(validatorContainer) * - SECONDS_PER_EPOCH; - mockProofTimestamp = activationTimestamp; - mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; - vm.warp(mockCurrentBlockTimestamp); - validatorProof.beaconBlockTimestamp = mockProofTimestamp; - - vm.mockCall( - address(beaconOracle), - abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), - abi.encode(beaconBlockRoot) - ); - - capsule.verifyDepositProof(validatorContainer, validatorProof); - - ExoCapsuleStorage.Validator memory validator = capsule.getRegisteredValidatorByPubkey( - _getPubkey(validatorContainer) - ); - assertEq(uint8(validator.status), uint8(ExoCapsuleStorage.VALIDATOR_STATUS.REGISTERED)); - assertEq(validator.validatorIndex, validatorProof.validatorIndex); - assertEq(validator.mostRecentBalanceUpdateTimestamp, validatorProof.beaconBlockTimestamp); - assertEq(validator.restakedBalanceGwei, _getEffectiveBalance(validatorContainer)); - } } diff --git a/test/foundry/test-data/full_withdrawal_proof.json b/test/foundry/test-data/full_withdrawal_proof.json index 3e47b263..bf4392ac 100644 --- a/test/foundry/test-data/full_withdrawal_proof.json +++ b/test/foundry/test-data/full_withdrawal_proof.json @@ -26,7 +26,7 @@ "0xac5e32ea973e990d191039e91c7d9fd9830599b8208675f159c3df128228e729", "0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471" ], - "ValidatorProof": [ + "WithdrawalCredentialProof": [ "0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e", "0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee", "0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1", From 7459af1006a71a689b47b6731f31327a4b9c9f8d Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 28 May 2024 14:07:06 -0400 Subject: [PATCH 57/93] fix: withdrwal proof generation test --- src/core/ExoCapsule.sol | 2 +- test/foundry/ExoCapsule.t.sol | 28 +++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 5ba04ec1..0518471d 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -7,7 +7,7 @@ import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; - +import "forge-std/console.sol"; import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 4361c8f6..14f069f3 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -359,11 +359,13 @@ contract WithdrawalSetup is Test { uint256 mockProofTimestamp; uint256 mockCurrentBlockTimestamp; + uint256 activationTimestamp; function setUp() public { string memory withdrawalInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); // string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); _setValidatorContainer(withdrawalInfo); + _setWithdrawalContainer(); beaconOracle = IBeaconChainOracle(address(0x123)); vm.etch(address(beaconOracle), bytes("aabb")); @@ -387,9 +389,7 @@ contract WithdrawalSetup is Test { stdstore.target(capsuleAddress).sig("hasRestaked()").checked_write(true); - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + - _getActivationEpoch(validatorContainer) * - SECONDS_PER_EPOCH; + activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -512,4 +512,26 @@ contract VerifyWithdrawalProof is WithdrawalSetup { ); capsule.withdrawNonBeaconChainETHBalance(recipient, 0.5 ether); } + + function test_processFullWithdrawal_revert_AlreadyProcessed() public setValidatorContainerAndTimestamp { + capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); + } + + modifier setValidatorContainerAndTimestamp() { + string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); + _setValidatorContainer(withdrawalInfo); + + vm.warp(mockCurrentBlockTimestamp); + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(beaconBlockRoot) + ); + + validatorProof.beaconBlockTimestamp = activationTimestamp; + withdrawalProof.beaconBlockTimestamp = activationTimestamp; + _; + } } From c47efd3bfafc5214e5a33f39ec5e7f9bf23f39b5 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 28 May 2024 20:58:31 -0400 Subject: [PATCH 58/93] fix: historial summaries verification pass with beacon state root --- src/core/ExoCapsule.sol | 8 ++++---- src/interfaces/IExoCapsule.sol | 1 + src/libraries/BeaconChainProofs.sol | 32 +++++++++++++++++++---------- test/foundry/ExoCapsule.t.sol | 3 +++ 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 0518471d..d9d46332 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -339,20 +339,20 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata proof ) internal view { - bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); bytes32 withdrawalContainerRoot = withdrawalContainer.merklelizeWithdrawalContainer(); bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot( proof.withdrawalContainerRootProof, proof.withdrawalIndex, - beaconBlockRoot, + proof.blockRoot, proof.executionPayloadRoot, - proof.executionPayloadRootProof + proof.executionPayloadRootProof, + proof.beaconBlockTimestamp ); if (!valid) { revert InvalidWithdrawalContainer(withdrawalContainer.getValidatorIndex()); } // Verify historical summaries - bool validHistoricalSummaries = beaconBlockRoot.isValidHistoricalSummaryRoot( + bool validHistoricalSummaries = proof.stateRoot.isValidHistoricalSummaryRoot( proof.historicalSummaryBlockRootProof, proof.historicalSummaryIndex, proof.blockRoot, diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 79153c1b..bac4c023 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -12,6 +12,7 @@ interface IExoCapsule { struct WithdrawalContainerProof { uint256 beaconBlockTimestamp; + bytes32 stateRoot; bytes32 executionPayloadRoot; bytes32[] executionPayloadRootProof; bytes32[] withdrawalContainerRootProof; diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 0b7dbb86..f31e3d08 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -24,7 +24,9 @@ library BeaconChainProofs { uint256 internal constant VALIDATOR_FIELD_TREE_HEIGHT = 3; uint256 internal constant NUM_EXECUTION_PAYLOAD_HEADER_FIELDS = 15; - uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT = 5; // After deneb hard fork, it's increased from 4 to 5 + uint256 internal constant DENEB_FORK_TIMESTAMP = 1710338135; + uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_CAPELLA = 4; + uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_DENEB = 5; // After deneb hard fork, it's increased from 4 to 5 uint256 internal constant NUM_EXECUTION_PAYLOAD_FIELDS = 15; uint256 internal constant EXECUTION_PAYLOAD_FIELD_TREE_HEIGHT = 4; @@ -192,20 +194,23 @@ library BeaconChainProofs { bytes32 withdrawalContainerRoot, bytes32[] calldata withdrawalContainerRootProof, uint256 withdrawalIndex, - bytes32 beaconBlockRoot, + bytes32 blockRoot, bytes32 executionPayloadRoot, - bytes32[] calldata executionPayloadRootProof + bytes32[] calldata executionPayloadRootProof, + uint256 beaconBlockTimestamp ) internal view returns (bool valid) { bool validExecutionPayloadRoot = isValidExecutionPayloadRoot( executionPayloadRoot, - beaconBlockRoot, + blockRoot, executionPayloadRootProof ); + bool validWCRootAgainstExecutionPayloadRoot = isValidWCRootAgainstExecutionPayloadRoot( withdrawalContainerRoot, executionPayloadRoot, withdrawalContainerRootProof, - withdrawalIndex + withdrawalIndex, + beaconBlockTimestamp ); if (validExecutionPayloadRoot && validWCRootAgainstExecutionPayloadRoot) { @@ -215,7 +220,7 @@ library BeaconChainProofs { function isValidExecutionPayloadRoot( bytes32 executionPayloadRoot, - bytes32 beaconBlockRoot, + bytes32 blockRoot, bytes32[] calldata executionPayloadRootProof ) internal view returns (bool) { require( @@ -229,7 +234,7 @@ library BeaconChainProofs { return Merkle.verifyInclusionSha256({ proof: executionPayloadRootProof, - root: beaconBlockRoot, + root: blockRoot, leaf: executionPayloadRoot, index: leafIndex }); @@ -239,11 +244,16 @@ library BeaconChainProofs { bytes32 withdrawalContainerRoot, bytes32 executionPayloadRoot, bytes32[] calldata withdrawalContainerRootProof, - uint256 withdrawalIndex + uint256 withdrawalIndex, + uint256 beaconBlockTimestamp ) internal view returns (bool) { + uint256 executionPayloadHeaderFieldTreeHeight = (beaconBlockTimestamp < DENEB_FORK_TIMESTAMP) + ? EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_CAPELLA + : EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_DENEB; + require( withdrawalContainerRootProof.length == - (EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT + WITHDRAWALS_TREE_HEIGHT + 1), + (executionPayloadHeaderFieldTreeHeight + WITHDRAWALS_TREE_HEIGHT + 1), "withdrawalProof has incorrect length" ); @@ -259,7 +269,7 @@ library BeaconChainProofs { } function isValidHistoricalSummaryRoot( - bytes32 beaconBlockRoot, + bytes32 beaconStateRoot, bytes32[] calldata historicalSummaryBlockRootProof, uint256 historicalSummaryIndex, bytes32 blockRoot, @@ -287,7 +297,7 @@ library BeaconChainProofs { return Merkle.verifyInclusionSha256({ proof: historicalSummaryBlockRootProof, - root: beaconBlockRoot, + root: beaconStateRoot, leaf: blockRoot, index: historicalBlockHeaderIndex }); diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 14f069f3..3b655459 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -441,6 +441,9 @@ contract WithdrawalSetup is Test { withdrawalContainer = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalFields"); require(withdrawalContainer.length > 0, "validator container should not be empty"); + withdrawalProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot"); + require(withdrawalProof.stateRoot != bytes32(0), "state root should not be empty"); + withdrawalProof.blockRoot = stdJson.readBytes32(withdrawalInfo, ".blockHeaderRoot"); require(withdrawalProof.blockRoot != bytes32(0), "block header root should not be empty"); From 57fca492dfdad11bdbbc40f79f68d0eaaf7cfa8d Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 11 Jun 2024 13:01:08 -0400 Subject: [PATCH 59/93] feat: update has restaking logic --- src/core/ExoCapsule.sol | 1 - src/core/NativeRestakingController.sol | 7 +++++++ src/interfaces/IExoCapsule.sol | 4 ++++ src/interfaces/INativeRestakingController.sol | 20 +++++++++++++------ test/foundry/ExoCapsule.t.sol | 6 ++---- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index d9d46332..d4b0719a 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -102,7 +102,6 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { gateway = INativeRestakingController(gateway_); beaconOracle = IBeaconChainOracle(beaconOracle_); capsuleOwner = capsuleOwner_; - hasRestaked = true; } function verifyDepositProof( diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 02469f50..fba9bbac 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -53,11 +53,18 @@ abstract contract NativeRestakingController is return address(capsule); } + function withdrawBeforeRestaking() external whenNotPaused { + IExoCapsule capsule = _getCapsule(msg.sender); + capsule.withdrawBeforeRestaking(); + } + function depositBeaconChainValidator( bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata proof ) external payable whenNotPaused { IExoCapsule capsule = _getCapsule(msg.sender); + // Before verifying deposit proof, we need to activate restaking and withdraw all potential ETH withdrawan to the exocapsule back to the owner. This amount won't be counted as staked balance + capsule.activateRestaking(); capsule.verifyDepositProof(validatorContainer, proof); uint256 depositValue = uint256(validatorContainer.getEffectiveBalance()) * GWEI_TO_WEI; diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index bac4c023..88a516f2 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -25,6 +25,10 @@ interface IExoCapsule { function initialize(address gateway, address capsuleOwner, address beaconOracle) external; + function withdrawBeforeRestaking() external; + + function activateRestaking() external; + function verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) external; function verifyWithdrawalProof( diff --git a/src/interfaces/INativeRestakingController.sol b/src/interfaces/INativeRestakingController.sol index 8bb33ee9..b9233f7a 100644 --- a/src/interfaces/INativeRestakingController.sol +++ b/src/interfaces/INativeRestakingController.sol @@ -7,11 +7,11 @@ interface INativeRestakingController is IBaseRestakingController { /// *** function signatures for staker operations *** /** - * @notice Stakers call this function to deposit to beacon chain validator, and point withdrawal_credentials of + * @notice Stakers call this function to deposit to beacon chain validator, and point withdrawal_credentials of * beacon chain validator to staker's ExoCapsule contract address. An ExoCapsule contract owned by staker would * be created if it does not exist. * @param pubkey the BLS pubkey of beacon chain validator - * @param signature the BLS signature + * @param signature the BLS signature * @param depositDataRoot The SHA-256 hash of the SSZ-encoded DepositData object. * Used as a protection against malformed input. */ @@ -22,17 +22,25 @@ interface INativeRestakingController is IBaseRestakingController { */ function createExoCapsule() external returns (address capsule); + /** + * @notice Before verifying deposit proof, validator containers can still set withdraw address to the ExoCapsule. In this case, we need to withdraw current balance, but only if restaking is not enabled yet + */ + function withdrawBeforeRestaking() external; + /** * @notice This is called to deposit ETH that is staked on Ethereum beacon chain to Exocore network to be restaked in future * @dev Before deposit, staker should have created the ExoCapsule that it owns and point the validator's withdrawal crendentials * to the ExoCapsule owned by staker. The effective balance of `validatorContainer` would be credited as deposited value by Exocore network. - * @ param + * @ param */ - function depositBeaconChainValidator(bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata proof) payable external; + function depositBeaconChainValidator( + bytes32[] calldata validatorContainer, + IExoCapsule.ValidatorContainerProof calldata proof + ) external payable; /** - * @notice When a beacon chain partial withdrawal to an ExoCapsule contract happens(the withdrawal time is less than validator's withdrawable_epoch), - * this function could be called with `validatorContainer`, `withdrawalContainer` and corresponding proofs to prove this partial withdrawal + * @notice When a beacon chain partial withdrawal to an ExoCapsule contract happens(the withdrawal time is less than validator's withdrawable_epoch), + * this function could be called with `validatorContainer`, `withdrawalContainer` and corresponding proofs to prove this partial withdrawal * from beacon chain is done and unlock withdrawn ETH to be claimable for ExoCapsule owner. * @param validatorContainer is the data structure included in `BeaconState` of `BeaconBlock` that contains beacon chain validator information, * refer to: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 3b655459..0fe6ce37 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -363,7 +363,7 @@ contract WithdrawalSetup is Test { function setUp() public { string memory withdrawalInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); - // string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); + _setValidatorContainer(withdrawalInfo); _setWithdrawalContainer(); @@ -524,9 +524,7 @@ contract VerifyWithdrawalProof is WithdrawalSetup { string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); _setValidatorContainer(withdrawalInfo); - vm.warp(mockCurrentBlockTimestamp); - validatorProof.beaconBlockTimestamp = mockProofTimestamp; - + // vm.warp(mockCurrentBlockTimestamp); vm.mockCall( address(beaconOracle), abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), From 33076b313076471afe44ee0af396acf4ac2225e4 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 11 Jun 2024 13:17:17 -0400 Subject: [PATCH 60/93] fix: remove prettier config and stick with forge fmt --- .prettierrc | 17 ----------------- package-lock.json | 44 +------------------------------------------- package.json | 3 +-- 3 files changed, 2 insertions(+), 62 deletions(-) delete mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index d05d9551..00000000 --- a/.prettierrc +++ /dev/null @@ -1,17 +0,0 @@ -{ - "plugins": ["prettier-plugin-solidity"], - "printWidth": 120, - "tabWidth": 2, - "overrides": [ - { - "files": "*.sol", - "options": { - "printWidth": 120, - "tabWidth": 4, - "useTabs": false, - "singleQuote": false, - "bracketSpacing": false - } - } - ] -} diff --git a/package-lock.json b/package-lock.json index ef039303..bdd0cb26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -376,8 +376,7 @@ "@nomicfoundation/hardhat-foundry": "^1.1.1", "@nomicfoundation/hardhat-toolbox": "^4.0.0", "hardhat": "^2.19.3", - "mocha": "^10.2.0", - "prettier-plugin-solidity": "^1.3.1" + "mocha": "^10.2.0" }, "optionalDependencies": { "fsevents": "^2.3.3" @@ -5393,41 +5392,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-plugin-solidity": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.3.1.tgz", - "integrity": "sha512-MN4OP5I2gHAzHZG1wcuJl0FsLS3c4Cc5494bbg+6oQWBPuEamjwDvmGfFMZ6NFzsh3Efd9UUxeT7ImgjNH4ozA==", - "dev": true, - "dependencies": { - "@solidity-parser/parser": "^0.17.0", - "semver": "^7.5.4", - "solidity-comments-extractor": "^0.0.8" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "prettier": ">=2.3.0" - } - }, - "node_modules/prettier-plugin-solidity/node_modules/@solidity-parser/parser": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.17.0.tgz", - "integrity": "sha512-Nko8R0/kUo391jsEHHxrGM07QFdnPGvlmox4rmH0kNiNAashItAilhy4Mv4pK5gQmW5f4sXAF58fwJbmlkGcVw==", - "dev": true - }, - "node_modules/prettier-plugin-solidity/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -6054,12 +6018,6 @@ "semver": "bin/semver" } }, - "node_modules/solidity-comments-extractor": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/solidity-comments-extractor/-/solidity-comments-extractor-0.0.8.tgz", - "integrity": "sha512-htM7Vn6LhHreR+EglVMd2s+sZhcXAirB1Zlyrv5zBuTxieCvjfnRpd7iZk75m/u6NOlEyQ94C6TWbBn2cY7w8g==", - "dev": true - }, "node_modules/solidity-coverage": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.5.tgz", diff --git a/package.json b/package.json index 023321a0..2589c133 100644 --- a/package.json +++ b/package.json @@ -379,8 +379,7 @@ "@nomicfoundation/hardhat-foundry": "^1.1.1", "@nomicfoundation/hardhat-toolbox": "^4.0.0", "hardhat": "^2.19.3", - "mocha": "^10.2.0", - "prettier-plugin-solidity": "^1.3.1" + "mocha": "^10.2.0" }, "scripts": { "test": "mocha" From 15e5ab57cf7cae4ccaeee333db24717d3cb6df5b Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 11 Jun 2024 13:31:53 -0400 Subject: [PATCH 61/93] fix: forge fmt without prettier conflict --- src/core/Bootstrap.sol | 1 + src/core/ClientGatewayLzReceiver.sol | 85 ++++++------ src/core/ExoCapsule.sol | 71 ++++------ src/core/NativeRestakingController.sol | 22 ++- src/interfaces/IExoCapsule.sol | 4 +- src/interfaces/INativeRestakingController.sol | 37 +++-- src/libraries/BeaconChainProofs.sol | 123 ++++++++--------- src/libraries/WithdrawalContainer.sol | 2 + src/storage/ExoCapsuleStorage.sol | 20 ++- test/foundry/DepositWithdrawPrinciple.t.sol | 36 ++--- test/foundry/ExoCapsule.t.sol | 128 ++++++++---------- 11 files changed, 239 insertions(+), 290 deletions(-) diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index 9c760c07..bd805fa6 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -529,4 +529,5 @@ contract Bootstrap is depositAmount: depositsByToken[tokenAddress] }); } + } diff --git a/src/core/ClientGatewayLzReceiver.sol b/src/core/ClientGatewayLzReceiver.sol index 8a559506..18ef04d1 100644 --- a/src/core/ClientGatewayLzReceiver.sol +++ b/src/core/ClientGatewayLzReceiver.sol @@ -44,9 +44,8 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp revert UnexpectedResponse(requestId); } - (bool success, bytes memory reason) = address(this).call( - abi.encodePacked(hookSelector, abi.encode(requestPayload, payload[9:])) - ); + (bool success, bytes memory reason) = + address(this).call(abi.encodePacked(hookSelector, abi.encode(requestPayload, payload[9:]))); if (!success) { revert RequestOrResponseExecuteFailed(act, _origin.nonce, reason); } @@ -59,19 +58,21 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp revert UnsupportedRequest(act); } - (bool success, bytes memory reason) = address(this).call( - abi.encodePacked(selector_, abi.encode(payload[1:])) - ); + (bool success, bytes memory reason) = + address(this).call(abi.encodePacked(selector_, abi.encode(payload[1:]))); if (!success) { revert RequestOrResponseExecuteFailed(act, _origin.nonce, reason); } } } - function nextNonce( - uint32 srcEid, - bytes32 sender - ) public view virtual override(OAppReceiverUpgradeable) returns (uint64) { + function nextNonce(uint32 srcEid, bytes32 sender) + public + view + virtual + override(OAppReceiverUpgradeable) + returns (uint64) + { return inboundNonce[srcEid][sender] + 1; } @@ -82,10 +83,10 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp } } - function afterReceiveDepositResponse( - bytes memory requestPayload, - bytes calldata responsePayload - ) public onlyCalledFromThis { + function afterReceiveDepositResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { (address token, address depositor, uint256 amount) = abi.decode(requestPayload, (address, address, uint256)); bool success = (uint8(bytes1(responsePayload[0])) == 1); @@ -106,14 +107,12 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp emit DepositResult(success, token, depositor, amount); } - function afterReceiveWithdrawPrincipleResponse( - bytes memory requestPayload, - bytes calldata responsePayload - ) public onlyCalledFromThis { - (address token, address withdrawer, uint256 unlockPrincipleAmount) = abi.decode( - requestPayload, - (address, address, uint256) - ); + function afterReceiveWithdrawPrincipleResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, address withdrawer, uint256 unlockPrincipleAmount) = + abi.decode(requestPayload, (address, address, uint256)); bool success = (uint8(bytes1(responsePayload[0])) == 1); uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:33])); @@ -137,14 +136,12 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp emit WithdrawPrincipleResult(success, token, withdrawer, unlockPrincipleAmount); } - function afterReceiveWithdrawRewardResponse( - bytes memory requestPayload, - bytes calldata responsePayload - ) public onlyCalledFromThis { - (address token, address withdrawer, uint256 unlockRewardAmount) = abi.decode( - requestPayload, - (address, address, uint256) - ); + function afterReceiveWithdrawRewardResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, address withdrawer, uint256 unlockRewardAmount) = + abi.decode(requestPayload, (address, address, uint256)); bool success = (uint8(bytes1(responsePayload[0])) == 1); uint256 lastlyUpdatedRewardBalance = uint256(bytes32(responsePayload[1:33])); @@ -158,28 +155,24 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp emit WithdrawRewardResult(success, token, withdrawer, unlockRewardAmount); } - function afterReceiveDelegateResponse( - bytes memory requestPayload, - bytes calldata responsePayload - ) public onlyCalledFromThis { - (address token, string memory operator, address delegator, uint256 amount) = abi.decode( - requestPayload, - (address, string, address, uint256) - ); + function afterReceiveDelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, string memory operator, address delegator, uint256 amount) = + abi.decode(requestPayload, (address, string, address, uint256)); bool success = (uint8(bytes1(responsePayload[0])) == 1); emit DelegateResult(success, delegator, operator, token, amount); } - function afterReceiveUndelegateResponse( - bytes memory requestPayload, - bytes calldata responsePayload - ) public onlyCalledFromThis { - (address token, string memory operator, address undelegator, uint256 amount) = abi.decode( - requestPayload, - (address, string, address, uint256) - ); + function afterReceiveUndelegateResponse(bytes memory requestPayload, bytes calldata responsePayload) + public + onlyCalledFromThis + { + (address token, string memory operator, address undelegator, uint256 amount) = + abi.decode(requestPayload, (address, string, address, uint256)); bool success = (uint8(bytes1(responsePayload[0])) == 1); diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index e34273ca..49fea559 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -12,6 +12,7 @@ import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracl import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { + using BeaconChainProofs for bytes32; using ValidatorContainer for bytes32[]; using WithdrawalContainer for bytes32[]; @@ -21,17 +22,11 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { event WithdrawalSuccess(address, address, uint256); /// @notice Emitted when a partial withdrawal claim is successfully redeemed event PartialWithdrawalRedeemed( - bytes32 pubkey, - uint256 withdrawalTimestamp, - address indexed recipient, - uint64 partialWithdrawalAmountGwei + bytes32 pubkey, uint256 withdrawalTimestamp, address indexed recipient, uint64 partialWithdrawalAmountGwei ); /// @notice Emitted when an ETH validator is prove to have fully withdrawn from the beacon chain event FullWithdrawalRedeemed( - bytes32 pubkey, - uint256 withdrawalTimestamp, - address indexed recipient, - uint64 withdrawalAmountGwei + bytes32 pubkey, uint256 withdrawalTimestamp, address indexed recipient, uint64 withdrawalAmountGwei ); /// @notice Emitted when capsuleOwner enables restaking event RestakingActivated(address indexed capsuleOwner); @@ -104,10 +99,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { capsuleOwner = capsuleOwner_; } - function verifyDepositProof( - bytes32[] calldata validatorContainer, - ValidatorContainerProof calldata proof - ) + function verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) external onlyGateway proofIsForValidTimestamp(proof.beaconBlockTimestamp) @@ -190,10 +182,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { if (partialWithdrawal) { // Immediately send ETH without sending request to Exocore side emit PartialWithdrawalRedeemed( - validatorPubkey, - withdrawalProof.beaconBlockTimestamp, - capsuleOwner, - withdrawalAmountGwei + validatorPubkey, withdrawalProof.beaconBlockTimestamp, capsuleOwner, withdrawalAmountGwei ); _sendETH(capsuleOwner, withdrawalAmountGwei * GWEI_TO_WEI); } else { @@ -202,10 +191,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { validator.restakedBalanceGwei = 0; // If over MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32 * 1e9, then send remaining amount immediately emit FullWithdrawalRedeemed( - validatorPubkey, - withdrawalProof.beaconBlockTimestamp, - capsuleOwner, - withdrawalAmountGwei + validatorPubkey, withdrawalProof.beaconBlockTimestamp, capsuleOwner, withdrawalAmountGwei ); if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { withdrawalAmount = (withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * GWEI_TO_WEI; @@ -309,16 +295,16 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { } function _sendETH(address recipient, uint256 amountWei) internal { - (bool sent, ) = recipient.call{value: amountWei}(""); + (bool sent,) = recipient.call{value: amountWei}(""); if (!sent) { revert WithdrawalFailure(capsuleOwner, recipient, amountWei); } } - function _verifyValidatorContainer( - bytes32[] calldata validatorContainer, - ValidatorContainerProof calldata proof - ) internal view { + function _verifyValidatorContainer(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) + internal + view + { bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); bytes32 validatorContainerRoot = validatorContainer.merklelizeValidatorContainer(); bool valid = validatorContainerRoot.isValidValidatorContainerRoot( @@ -333,10 +319,10 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { } } - function _verifyWithdrawalContainer( - bytes32[] calldata withdrawalContainer, - WithdrawalContainerProof calldata proof - ) internal view { + function _verifyWithdrawalContainer(bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata proof) + internal + view + { bytes32 withdrawalContainerRoot = withdrawalContainer.merklelizeWithdrawalContainer(); bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot( proof.withdrawalContainerRootProof, @@ -351,20 +337,18 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { } // Verify historical summaries bool validHistoricalSummaries = proof.stateRoot.isValidHistoricalSummaryRoot( - proof.historicalSummaryBlockRootProof, - proof.historicalSummaryIndex, - proof.blockRoot, - proof.blockRootIndex + proof.historicalSummaryBlockRootProof, proof.historicalSummaryIndex, proof.blockRoot, proof.blockRootIndex ); if (!validHistoricalSummaries) { revert InvalidHistoricalSummaries(withdrawalContainer.getValidatorIndex()); } } - function _isActivatedAtEpoch( - bytes32[] calldata validatorContainer, - uint256 atTimestamp - ) internal pure returns (bool) { + function _isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint256 atTimestamp) + internal + pure + returns (bool) + { uint64 atEpoch = _timestampToEpoch(atTimestamp); uint64 activationEpoch = validatorContainer.getActivationEpoch(); uint64 exitEpoch = validatorContainer.getExitEpoch(); @@ -373,15 +357,13 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { } function _isStaleProof(Validator storage validator, uint256 proofTimestamp) internal view returns (bool) { - return - proofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS < block.timestamp || - proofTimestamp <= validator.mostRecentBalanceUpdateTimestamp; + return proofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS < block.timestamp + || proofTimestamp <= validator.mostRecentBalanceUpdateTimestamp; } function _hasFullyWithdrawn(bytes32[] calldata validatorContainer) internal view returns (bool) { - return - validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(block.timestamp) && - validatorContainer.getEffectiveBalance() == 0; + return validatorContainer.getWithdrawableEpoch() <= _timestampToEpoch(block.timestamp) + && validatorContainer.getEffectiveBalance() == 0; } /** @@ -391,8 +373,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { */ function _timestampToEpoch(uint256 timestamp) internal pure returns (uint64) { require( - timestamp >= BEACON_CHAIN_GENESIS_TIME, - "timestamp should be greater than beacon chain genesis timestamp" + timestamp >= BEACON_CHAIN_GENESIS_TIME, "timestamp should be greater than beacon chain genesis timestamp" ); return uint64((timestamp - BEACON_CHAIN_GENESIS_TIME) / BeaconChainProofs.SECONDS_PER_EPOCH); } diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 7d126bac..bfceb08b 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -18,11 +18,11 @@ abstract contract NativeRestakingController is using ValidatorContainer for bytes32[]; - function stake( - bytes calldata pubkey, - bytes calldata signature, - bytes32 depositDataRoot - ) external payable whenNotPaused { + function stake(bytes calldata pubkey, bytes calldata signature, bytes32 depositDataRoot) + external + payable + whenNotPaused + { require(msg.value == 32 ether, "NativeRestakingController: stake value must be exactly 32 ether"); IExoCapsule capsule = ownerToCapsule[msg.sender]; @@ -65,7 +65,8 @@ abstract contract NativeRestakingController is IExoCapsule.ValidatorContainerProof calldata proof ) external payable whenNotPaused { IExoCapsule capsule = _getCapsule(msg.sender); - // Before verifying deposit proof, we need to activate restaking and withdraw all potential ETH withdrawan to the exocapsule back to the owner. This amount won't be counted as staked balance + // Before verifying deposit proof, we need to activate restaking and withdraw all potential ETH withdrawan to + // the exocapsule back to the owner. This amount won't be counted as staked balance capsule.activateRestaking(); capsule.verifyDepositProof(validatorContainer, proof); @@ -80,12 +81,8 @@ abstract contract NativeRestakingController is IExoCapsule.WithdrawalContainerProof calldata withdrawalProof ) external payable whenNotPaused { IExoCapsule capsule = _getCapsule(msg.sender); - (bool partialWithdrawal, uint256 withdrawalAmount) = capsule.verifyWithdrawalProof( - validatorContainer, - validatorProof, - withdrawalContainer, - withdrawalProof - ); + (bool partialWithdrawal, uint256 withdrawalAmount) = + capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); if (!partialWithdrawal) { // request full withdraw _processRequest( @@ -97,4 +94,5 @@ abstract contract NativeRestakingController is ); } } + } diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index db8d4a48..2a39245c 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -30,7 +30,8 @@ interface IExoCapsule { function activateRestaking() external; - function verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) external; + function verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) + external; function verifyWithdrawalProof( bytes32[] calldata validatorContainer, @@ -46,4 +47,5 @@ interface IExoCapsule { function updateWithdrawableBalance(uint256 unlockPrincipleAmount) external; function capsuleWithdrawalCredentials() external view returns (bytes memory); + } diff --git a/src/interfaces/INativeRestakingController.sol b/src/interfaces/INativeRestakingController.sol index 8dd1ff90..af4cfb95 100644 --- a/src/interfaces/INativeRestakingController.sol +++ b/src/interfaces/INativeRestakingController.sol @@ -25,14 +25,18 @@ interface INativeRestakingController is IBaseRestakingController { function createExoCapsule() external returns (address capsule); /** - * @notice Before verifying deposit proof, validator containers can still set withdraw address to the ExoCapsule. In this case, we need to withdraw current balance, but only if restaking is not enabled yet + * @notice Before verifying deposit proof, validator containers can still set withdraw address to the ExoCapsule. In + * this case, we need to withdraw current balance, but only if restaking is not enabled yet */ function withdrawBeforeRestaking() external; /** - * @notice This is called to deposit ETH that is staked on Ethereum beacon chain to Exocore network to be restaked in future - * @dev Before deposit, staker should have created the ExoCapsule that it owns and point the validator's withdrawal crendentials - * to the ExoCapsule owned by staker. The effective balance of `validatorContainer` would be credited as deposited value by Exocore network. + * @notice This is called to deposit ETH that is staked on Ethereum beacon chain to Exocore network to be restaked + * in future + * @dev Before deposit, staker should have created the ExoCapsule that it owns and point the validator's withdrawal + * crendentials + * to the ExoCapsule owned by staker. The effective balance of `validatorContainer` would be credited as deposited + * value by Exocore network. * @ param */ function depositBeaconChainValidator( @@ -41,10 +45,13 @@ interface INativeRestakingController is IBaseRestakingController { ) external payable; /** - * @notice When a beacon chain partial withdrawal to an ExoCapsule contract happens(the withdrawal time is less than validator's withdrawable_epoch), - * this function could be called with `validatorContainer`, `withdrawalContainer` and corresponding proofs to prove this partial withdrawal + * @notice When a beacon chain partial withdrawal to an ExoCapsule contract happens(the withdrawal time is less than + * validator's withdrawable_epoch), + * this function could be called with `validatorContainer`, `withdrawalContainer` and corresponding proofs to prove + * this partial withdrawal * from beacon chain is done and unlock withdrawn ETH to be claimable for ExoCapsule owner. - * @param validatorContainer is the data structure included in `BeaconState` of `BeaconBlock` that contains beacon chain validator information, + * @param validatorContainer is the data structure included in `BeaconState` of `BeaconBlock` that contains beacon + * chain validator information, * refer to: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator * @param validatorProof is the merkle proof needed for verifying that `validatorContainer` is included in some * beacon block root. @@ -56,12 +63,17 @@ interface INativeRestakingController is IBaseRestakingController { */ /** - * @notice When a beacon chain full withdrawal to this capsule contract happens(the withdrawal time is euqal to or greater than - * validator's withdrawable_epoch), this function could be called with `validatorContainer`, `withdrawalContainer` and corresponding - * proofs to prove this full withdrawal from beacon chain is done, send withdrawal request to Exocore network to be processed. - * After Exocore network finishs dealing with withdrawal request and sending back the response, ExoCapsule would unlock corresponding ETH + * @notice When a beacon chain full withdrawal to this capsule contract happens(the withdrawal time is euqal to or + * greater than + * validator's withdrawable_epoch), this function could be called with `validatorContainer`, `withdrawalContainer` + * and corresponding + * proofs to prove this full withdrawal from beacon chain is done, send withdrawal request to Exocore network to be + * processed. + * After Exocore network finishs dealing with withdrawal request and sending back the response, ExoCapsule would + * unlock corresponding ETH * in response to be cliamable for ExoCapsule owner. - * @param validatorContainer is the data structure included in `BeaconState` of `BeaconBlock` that contains beacon chain validator information, + * @param validatorContainer is the data structure included in `BeaconState` of `BeaconBlock` that contains beacon + * chain validator information, * refer to: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#validator * @param validatorProof is the merkle proof needed for verifying that `validatorContainer` is included in some * beacon block root. @@ -77,4 +89,5 @@ interface INativeRestakingController is IBaseRestakingController { bytes32[] calldata withdrawalContainer, IExoCapsule.WithdrawalContainerProof calldata withdrawalProof ) external payable; + } diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 82d0a540..c32975e2 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -29,9 +29,10 @@ library BeaconChainProofs { uint256 internal constant VALIDATOR_FIELD_TREE_HEIGHT = 3; uint256 internal constant NUM_EXECUTION_PAYLOAD_HEADER_FIELDS = 15; - uint256 internal constant DENEB_FORK_TIMESTAMP = 1710338135; + uint256 internal constant DENEB_FORK_TIMESTAMP = 1_710_338_135; uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_CAPELLA = 4; - uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_DENEB = 5; // After deneb hard fork, it's increased from 4 to 5 + uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_DENEB = 5; // After deneb hard fork, it's + // increased from 4 to 5 uint256 internal constant NUM_EXECUTION_PAYLOAD_FIELDS = 15; uint256 internal constant EXECUTION_PAYLOAD_FIELD_TREE_HEIGHT = 4; @@ -151,30 +152,26 @@ library BeaconChainProofs { ) internal view returns (bool valid) { bool validStateRoot = isValidStateRoot(stateRoot, beaconBlockRoot, stateRootProof); bool validVCRootAgainstStateRoot = isValidVCRootAgainstStateRoot( - validatorContainerRoot, - stateRoot, - validatorContainerRootProof, - validatorIndex + validatorContainerRoot, stateRoot, validatorContainerRootProof, validatorIndex ); if (validStateRoot && validVCRootAgainstStateRoot) { valid = true; } } - function isValidStateRoot( - bytes32 stateRoot, - bytes32 beaconBlockRoot, - bytes32[] calldata stateRootProof - ) internal view returns (bool) { + function isValidStateRoot(bytes32 stateRoot, bytes32 beaconBlockRoot, bytes32[] calldata stateRootProof) + internal + view + returns (bool) + { require(stateRootProof.length == BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT, "state root proof should have 3 nodes"); - return - Merkle.verifyInclusionSha256({ - proof: stateRootProof, - root: beaconBlockRoot, - leaf: stateRoot, - index: STATE_ROOT_INDEX - }); + return Merkle.verifyInclusionSha256({ + proof: stateRootProof, + root: beaconBlockRoot, + leaf: stateRoot, + index: STATE_ROOT_INDEX + }); } function isValidVCRootAgainstStateRoot( @@ -190,13 +187,12 @@ library BeaconChainProofs { uint256 leafIndex = (VALIDATOR_TREE_ROOT_INDEX << (VALIDATOR_TREE_HEIGHT + 1)) | uint256(validatorIndex); - return - Merkle.verifyInclusionSha256({ - proof: validatorContainerRootProof, - root: stateRoot, - leaf: validatorContainerRoot, - index: leafIndex - }); + return Merkle.verifyInclusionSha256({ + proof: validatorContainerRootProof, + root: stateRoot, + leaf: validatorContainerRoot, + index: leafIndex + }); } function isValidWithdrawalContainerRoot( @@ -208,11 +204,8 @@ library BeaconChainProofs { bytes32[] calldata executionPayloadRootProof, uint256 beaconBlockTimestamp ) internal view returns (bool valid) { - bool validExecutionPayloadRoot = isValidExecutionPayloadRoot( - executionPayloadRoot, - blockRoot, - executionPayloadRootProof - ); + bool validExecutionPayloadRoot = + isValidExecutionPayloadRoot(executionPayloadRoot, blockRoot, executionPayloadRootProof); bool validWCRootAgainstExecutionPayloadRoot = isValidWCRootAgainstExecutionPayloadRoot( withdrawalContainerRoot, @@ -233,20 +226,19 @@ library BeaconChainProofs { bytes32[] calldata executionPayloadRootProof ) internal view returns (bool) { require( - executionPayloadRootProof.length == - BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT, + executionPayloadRootProof.length + == BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT, "state root proof should have 3 nodes" ); uint256 leafIndex = (BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT)) | EXECUTION_PAYLOAD_INDEX; - return - Merkle.verifyInclusionSha256({ - proof: executionPayloadRootProof, - root: blockRoot, - leaf: executionPayloadRoot, - index: leafIndex - }); + return Merkle.verifyInclusionSha256({ + proof: executionPayloadRootProof, + root: blockRoot, + leaf: executionPayloadRoot, + index: leafIndex + }); } function isValidWCRootAgainstExecutionPayloadRoot( @@ -261,20 +253,18 @@ library BeaconChainProofs { : EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_DENEB; require( - withdrawalContainerRootProof.length == - (executionPayloadHeaderFieldTreeHeight + WITHDRAWALS_TREE_HEIGHT + 1), + withdrawalContainerRootProof.length == (executionPayloadHeaderFieldTreeHeight + WITHDRAWALS_TREE_HEIGHT + 1), "withdrawalProof has incorrect length" ); uint256 leafIndex = (WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1)) | uint256(withdrawalIndex); - return - Merkle.verifyInclusionSha256({ - proof: withdrawalContainerRootProof, - root: executionPayloadRoot, - leaf: withdrawalContainerRoot, - index: leafIndex - }); + return Merkle.verifyInclusionSha256({ + proof: withdrawalContainerRootProof, + root: executionPayloadRoot, + leaf: withdrawalContainerRoot, + index: leafIndex + }); } function isValidHistoricalSummaryRoot( @@ -285,31 +275,28 @@ library BeaconChainProofs { uint256 blockRootIndex ) internal view returns (bool) { require( - historicalSummaryBlockRootProof.length == - (BEACON_STATE_FIELD_TREE_HEIGHT + - (HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + - 1 + - (BLOCK_ROOTS_TREE_HEIGHT)), + historicalSummaryBlockRootProof.length + == (BEACON_STATE_FIELD_TREE_HEIGHT + (HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + 1 + (BLOCK_ROOTS_TREE_HEIGHT)), "historicalSummaryBlockRootProof has incorrect length" ); /** - * Note: Here, the "1" in "1 + (BLOCK_ROOTS_TREE_HEIGHT)" signifies that extra step of choosing the "block_root_summary" within the individual - * "historical_summary". Everywhere else it signifies merkelize_with_mixin, where the length of an array is hashed with the root of the array, + * Note: Here, the "1" in "1 + (BLOCK_ROOTS_TREE_HEIGHT)" signifies that extra step of choosing the + * "block_root_summary" within the individual + * "historical_summary". Everywhere else it signifies merkelize_with_mixin, where the length of an array is + * hashed with the root of the array, * but not here. */ - uint256 historicalBlockHeaderIndex = (HISTORICAL_SUMMARIES_INDEX << - ((HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + 1 + (BLOCK_ROOTS_TREE_HEIGHT))) | - (historicalSummaryIndex << (1 + (BLOCK_ROOTS_TREE_HEIGHT))) | - (BLOCK_SUMMARY_ROOT_INDEX << (BLOCK_ROOTS_TREE_HEIGHT)) | - blockRootIndex; - - return - Merkle.verifyInclusionSha256({ - proof: historicalSummaryBlockRootProof, - root: beaconStateRoot, - leaf: blockRoot, - index: historicalBlockHeaderIndex - }); + uint256 historicalBlockHeaderIndex = ( + HISTORICAL_SUMMARIES_INDEX << ((HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + 1 + (BLOCK_ROOTS_TREE_HEIGHT)) + ) | (historicalSummaryIndex << (1 + (BLOCK_ROOTS_TREE_HEIGHT))) + | (BLOCK_SUMMARY_ROOT_INDEX << (BLOCK_ROOTS_TREE_HEIGHT)) | blockRootIndex; + + return Merkle.verifyInclusionSha256({ + proof: historicalSummaryBlockRootProof, + root: beaconStateRoot, + leaf: blockRoot, + index: historicalBlockHeaderIndex + }); } } diff --git a/src/libraries/WithdrawalContainer.sol b/src/libraries/WithdrawalContainer.sol index 2863dd99..e6d26b83 100644 --- a/src/libraries/WithdrawalContainer.sol +++ b/src/libraries/WithdrawalContainer.sol @@ -15,6 +15,7 @@ library WithdrawalContainer { uint256 internal constant VALID_LENGTH = 4; uint256 internal constant MERKLE_TREE_HEIGHT = 2; + function verifyWithdrawalContainerBasic(bytes32[] calldata withdrawalContainer) internal pure returns (bool) { return withdrawalContainer.length == VALID_LENGTH; } @@ -50,4 +51,5 @@ library WithdrawalContainer { return leaves[0]; } + } diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index bc2dea10..72e8fa01 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -11,7 +11,6 @@ contract ExoCapsuleStorage { UNREGISTERED, // the validator has not been registered in this ExoCapsule REGISTERED, // staked on ethpos and withdrawal credentials are pointed to the ExoCapsule WITHDRAWN // withdrawn from the Beacon Chain - } struct Validator { @@ -33,16 +32,21 @@ contract ExoCapsuleStorage { uint64 constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; /** - * @notice The latest timestamp at which the capsule owner withdrew the balance of the capsule, via calling `withdrawBeforeRestaking`. - * @dev This variable is only updated when the `withdrawBeforeRestaking` function is called, which can only occur before `hasRestaked` is set to true for this capsule. - * Proofs for this capsule are only valid against Beacon Chain state roots corresponding to timestamps after the stored `mostRecentWithdrawalTimestamp`. + * @notice The latest timestamp at which the capsule owner withdrew the balance of the capsule, via calling + * `withdrawBeforeRestaking`. + * @dev This variable is only updated when the `withdrawBeforeRestaking` function is called, which can only occur + * before `hasRestaked` is set to true for this capsule. + * Proofs for this capsule are only valid against Beacon Chain state roots corresponding to timestamps after the + * stored `mostRecentWithdrawalTimestamp`. */ uint256 public mostRecentWithdrawalTimestamp; - /// @notice an indicator of whether or not the capsule owner has ever "fully restaked" by successfully calling `verifyCorrectWithdrawalCredentials`. + /// @notice an indicator of whether or not the capsule owner has ever "fully restaked" by successfully calling + /// `verifyCorrectWithdrawalCredentials`. bool public hasRestaked; uint256 public principleBalance; - /// @notice the amount of execution layer ETH in this contract that is staked in(i.e. withdrawn from the Beacon Chain but not from Exocore) + /// @notice the amount of execution layer ETH in this contract that is staked in(i.e. withdrawn from the Beacon + /// Chain but not from Exocore) uint256 public withdrawableBalance; /// @notice This variable tracks any ETH deposited into this contract via the `receive` fallback function uint256 public nonBeaconChainETHBalance; @@ -52,8 +56,10 @@ contract ExoCapsuleStorage { mapping(bytes32 pubkey => Validator validator) internal _capsuleValidators; mapping(uint256 index => bytes32 pubkey) internal _capsuleValidatorsByIndex; - /// @notice This is a mapping of validatorPubkeyHash to timestamp to whether or not they have proven a withdrawal for that timestamp + /// @notice This is a mapping of validatorPubkeyHash to timestamp to whether or not they have proven a withdrawal + /// for that timestamp mapping(bytes32 => mapping(uint256 => bool)) public provenWithdrawal; uint256[40] private __gap; + } diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index 9c220da7..795722d7 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -20,10 +20,7 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { event DepositResult(bool indexed success, address indexed token, address indexed depositor, uint256 amount); event WithdrawPrincipleResult( - bool indexed success, - address indexed token, - address indexed withdrawer, - uint256 amount + bool indexed success, address indexed token, address indexed withdrawer, uint256 amount ); event Transfer(address indexed from, address indexed to, uint256 amount); event MessageSent(GatewayStorage.Action indexed act, bytes32 packetId, uint64 nonce, uint256 nativeFee); @@ -88,12 +85,8 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // exocore gateway should return response message to exocore network layerzero endpoint vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); lastlyUpdatedPrincipleBalance = depositAmount; - bytes memory depositResponsePayload = abi.encodePacked( - GatewayStorage.Action.RESPOND, - uint64(1), - true, - lastlyUpdatedPrincipleBalance - ); + bytes memory depositResponsePayload = + abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(1), true, lastlyUpdatedPrincipleBalance); uint256 depositResponseNativeFee = exocoreGateway.quote(clientChainId, depositResponsePayload); bytes32 depositResponseId = generateUID(1, false); emit NewPacket( @@ -161,8 +154,7 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { withdrawRequestNativeFee ); clientGateway.withdrawPrincipleFromExocore{value: withdrawRequestNativeFee}( - address(restakeToken), - withdrawAmount + address(restakeToken), withdrawAmount ); // second layerzero relayers should watch the request message packet and relay the message to destination @@ -171,12 +163,8 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { // exocore gateway should return response message to exocore network layerzero endpoint vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); lastlyUpdatedPrincipleBalance -= withdrawAmount; - bytes memory withdrawResponsePayload = abi.encodePacked( - GatewayStorage.Action.RESPOND, - uint64(2), - true, - lastlyUpdatedPrincipleBalance - ); + bytes memory withdrawResponsePayload = + abi.encodePacked(GatewayStorage.Action.RESPOND, uint64(2), true, lastlyUpdatedPrincipleBalance); uint256 withdrawResponseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload); bytes32 withdrawResponseId = generateUID(2, false); emit NewPacket( @@ -366,19 +354,11 @@ contract DepositWithdrawPrincipleTest is ExocoreDeployer { function generateUID(uint64 nonce, bool fromClientChainToExocore) internal view returns (bytes32 uid) { if (fromClientChainToExocore) { uid = GUID.generate( - nonce, - clientChainId, - address(clientGateway), - exocoreChainId, - address(exocoreGateway).toBytes32() + nonce, clientChainId, address(clientGateway), exocoreChainId, address(exocoreGateway).toBytes32() ); } else { uid = GUID.generate( - nonce, - exocoreChainId, - address(exocoreGateway), - clientChainId, - address(clientGateway).toBytes32() + nonce, exocoreChainId, address(exocoreGateway), clientChainId, address(clientGateway).toBytes32() ); } } diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 9b3bcce9..76101ac1 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -15,6 +15,7 @@ import "src/libraries/Endian.sol"; import {ExoCapsuleStorage} from "src/storage/ExoCapsuleStorage.sol"; contract DepositSetup is Test { + using stdStorage for StdStorage; using Endian for bytes32; @@ -55,15 +56,11 @@ contract DepositSetup is Test { validatorProof.stateRoot = stdJson.readBytes32(validatorInfo, ".beaconStateRoot"); require(validatorProof.stateRoot != bytes32(0), "state root should not be empty"); - validatorProof.stateRootProof = stdJson.readBytes32Array( - validatorInfo, - ".StateRootAgainstLatestBlockHeaderProof" - ); + validatorProof.stateRootProof = + stdJson.readBytes32Array(validatorInfo, ".StateRootAgainstLatestBlockHeaderProof"); require(validatorProof.stateRootProof.length == 3, "state root proof should have 3 nodes"); - validatorProof.validatorContainerRootProof = stdJson.readBytes32Array( - validatorInfo, - ".WithdrawalCredentialProof" - ); + 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"); @@ -119,13 +116,13 @@ contract DepositSetup is Test { } contract VerifyDepositProof is DepositSetup { + using BeaconChainProofs for bytes32; using stdStorage for StdStorage; function test_verifyDepositProof_success() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + - _getActivationEpoch(validatorContainer) * - SECONDS_PER_EPOCH; + uint256 activationTimestamp = + BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -139,9 +136,8 @@ contract VerifyDepositProof is DepositSetup { capsule.verifyDepositProof(validatorContainer, validatorProof); - ExoCapsuleStorage.Validator memory validator = capsule.getRegisteredValidatorByPubkey( - _getPubkey(validatorContainer) - ); + ExoCapsuleStorage.Validator memory validator = + capsule.getRegisteredValidatorByPubkey(_getPubkey(validatorContainer)); assertEq(uint8(validator.status), uint8(ExoCapsuleStorage.VALIDATOR_STATUS.REGISTERED)); assertEq(validator.validatorIndex, validatorProof.validatorIndex); assertEq(validator.mostRecentBalanceUpdateTimestamp, validatorProof.beaconBlockTimestamp); @@ -149,9 +145,8 @@ contract VerifyDepositProof is DepositSetup { } function test_verifyDepositProof_revert_validatorAlreadyDeposited() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + - _getActivationEpoch(validatorContainer) * - SECONDS_PER_EPOCH; + uint256 activationTimestamp = + BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -173,9 +168,8 @@ contract VerifyDepositProof is DepositSetup { } function test_verifyDepositProof_revert_staleProof() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + - _getActivationEpoch(validatorContainer) * - SECONDS_PER_EPOCH; + uint256 activationTimestamp = + BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp + 1 hours; mockCurrentBlockTimestamp = mockProofTimestamp + VERIFY_BALANCE_UPDATE_WINDOW_SECONDS + 1 seconds; vm.warp(mockCurrentBlockTimestamp); @@ -190,18 +184,15 @@ contract VerifyDepositProof is DepositSetup { // deposit should revert because of proof is stale vm.expectRevert( abi.encodeWithSelector( - ExoCapsule.StaleValidatorContainer.selector, - _getPubkey(validatorContainer), - mockProofTimestamp + ExoCapsule.StaleValidatorContainer.selector, _getPubkey(validatorContainer), mockProofTimestamp ) ); capsule.verifyDepositProof(validatorContainer, validatorProof); } function test_verifyDepositProof_revert_malformedValidatorContainer() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + - _getActivationEpoch(validatorContainer) * - SECONDS_PER_EPOCH; + uint256 activationTimestamp = + BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -232,9 +223,8 @@ contract VerifyDepositProof is DepositSetup { } function test_verifyDepositProof_revert_inactiveValidatorContainer() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + - _getActivationEpoch(validatorContainer) * - SECONDS_PER_EPOCH; + uint256 activationTimestamp = + BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; vm.mockCall( address(beaconOracle), @@ -254,9 +244,8 @@ contract VerifyDepositProof is DepositSetup { } function test_verifyDepositProof_revert_mismatchWithdrawalCredentials() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + - _getActivationEpoch(validatorContainer) * - SECONDS_PER_EPOCH; + uint256 activationTimestamp = + BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -288,9 +277,8 @@ contract VerifyDepositProof is DepositSetup { } function test_verifyDepositProof_revert_proofNotMatchWithBeaconRoot() public { - uint256 activationTimestamp = BEACON_CHAIN_GENESIS_TIME + - _getActivationEpoch(validatorContainer) * - SECONDS_PER_EPOCH; + uint256 activationTimestamp = + BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); @@ -309,37 +297,39 @@ contract VerifyDepositProof is DepositSetup { ); capsule.verifyDepositProof(validatorContainer, validatorProof); } + } contract WithdrawalSetup is Test { + using stdStorage for StdStorage; using Endian for bytes32; bytes32[] validatorContainer; /** - struct ValidatorContainerProof { - uint256 beaconBlockTimestamp; - bytes32 stateRoot; - bytes32[] stateRootProof; - bytes32[] validatorContainerRootProof; - uint256 validatorIndex; - } - */ + * struct ValidatorContainerProof { + * uint256 beaconBlockTimestamp; + * bytes32 stateRoot; + * bytes32[] stateRootProof; + * bytes32[] validatorContainerRootProof; + * uint256 validatorIndex; + * } + */ IExoCapsule.ValidatorContainerProof validatorProof; bytes32[] withdrawalContainer; /** - struct WithdrawalContainerProof { - uint256 beaconBlockTimestamp; - bytes32 executionPayloadRoot; - bytes32[] executionPayloadRootProof; - bytes32[] withdrawalContainerRootProof; - bytes32[] historicalSummaryBlockRootProof; - uint256 historicalSummaryIndex; - bytes32 blockRoot; - uint256 blockRootIndex; - uint256 withdrawalIndex; - } + * struct WithdrawalContainerProof { + * uint256 beaconBlockTimestamp; + * bytes32 executionPayloadRoot; + * bytes32[] executionPayloadRootProof; + * bytes32[] withdrawalContainerRootProof; + * bytes32[] historicalSummaryBlockRootProof; + * uint256 historicalSummaryIndex; + * bytes32 blockRoot; + * uint256 blockRootIndex; + * uint256 withdrawalIndex; + * } */ IExoCapsule.WithdrawalContainerProof withdrawalProof; bytes32 beaconBlockRoot; @@ -348,7 +338,7 @@ contract WithdrawalSetup is Test { IBeaconChainOracle beaconOracle; address capsuleOwner; - uint256 constant BEACON_CHAIN_GENESIS_TIME = 1606824023; + uint256 constant BEACON_CHAIN_GENESIS_TIME = 1_606_824_023; /// @notice The number of slots each epoch in the beacon chain uint64 internal constant SLOTS_PER_EPOCH = 32; /// @notice The number of seconds in a slot in the beacon chain @@ -403,9 +393,8 @@ contract WithdrawalSetup is Test { capsule.verifyDepositProof(validatorContainer, validatorProof); - ExoCapsuleStorage.Validator memory validator = capsule.getRegisteredValidatorByPubkey( - _getPubkey(validatorContainer) - ); + ExoCapsuleStorage.Validator memory validator = + capsule.getRegisteredValidatorByPubkey(_getPubkey(validatorContainer)); assertEq(uint8(validator.status), uint8(ExoCapsuleStorage.VALIDATOR_STATUS.REGISTERED)); assertEq(validator.validatorIndex, validatorProof.validatorIndex); assertEq(validator.mostRecentBalanceUpdateTimestamp, validatorProof.beaconBlockTimestamp); @@ -418,15 +407,11 @@ contract WithdrawalSetup is Test { validatorProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot"); require(validatorProof.stateRoot != bytes32(0), "state root should not be empty"); - validatorProof.stateRootProof = stdJson.readBytes32Array( - withdrawalInfo, - ".StateRootAgainstLatestBlockHeaderProof" - ); + validatorProof.stateRootProof = + stdJson.readBytes32Array(withdrawalInfo, ".StateRootAgainstLatestBlockHeaderProof"); require(validatorProof.stateRootProof.length == 3, "state root proof should have 3 nodes"); - validatorProof.validatorContainerRootProof = stdJson.readBytes32Array( - withdrawalInfo, - ".WithdrawalCredentialProof" - ); + validatorProof.validatorContainerRootProof = + stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalCredentialProof"); require(validatorProof.validatorContainerRootProof.length == 46, "validator root proof should have 46 nodes"); validatorProof.validatorIndex = stdJson.readUint(withdrawalInfo, ".validatorIndex"); require(validatorProof.validatorIndex != 0, "validator root index should not be 0"); @@ -455,10 +440,8 @@ contract WithdrawalSetup is Test { withdrawalProof.historicalSummaryIndex = stdJson.readUint(withdrawalInfo, ".historicalSummaryIndex"); require(withdrawalProof.historicalSummaryIndex != 0, "historical summary index should not be 0"); - withdrawalProof.historicalSummaryBlockRootProof = stdJson.readBytes32Array( - withdrawalInfo, - ".HistoricalSummaryProof" - ); + withdrawalProof.historicalSummaryBlockRootProof = + stdJson.readBytes32Array(withdrawalInfo, ".HistoricalSummaryProof"); withdrawalProof.withdrawalContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalProof"); withdrawalProof.executionPayloadRoot = stdJson.readBytes32(withdrawalInfo, ".executionPayloadRoot"); withdrawalProof.executionPayloadRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ExecutionPayloadProof"); @@ -487,9 +470,11 @@ contract WithdrawalSetup is Test { function _getExitEpoch(bytes32[] storage vc) internal view returns (uint64) { return vc[6].fromLittleEndianUint64(); } + } contract VerifyWithdrawalProof is WithdrawalSetup { + using BeaconChainProofs for bytes32; using stdStorage for StdStorage; @@ -498,7 +483,7 @@ contract VerifyWithdrawalProof is WithdrawalSetup { address sender = vm.addr(1); vm.startPrank(sender); vm.deal(sender, 1 ether); - (bool sent, ) = address(capsule).call{value: 0.5 ether}(""); + (bool sent,) = address(capsule).call{value: 0.5 ether}(""); assertEq(sent, true); assertEq(capsule.nonBeaconChainETHBalance(), 0.5 ether); vm.stopPrank(); @@ -535,4 +520,5 @@ contract VerifyWithdrawalProof is WithdrawalSetup { withdrawalProof.beaconBlockTimestamp = activationTimestamp; _; } + } From b3b5fb1018eecad8e09238fac18c05f06c289819 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 11 Jun 2024 13:35:15 -0400 Subject: [PATCH 62/93] feat: add vscode extension settings --- .vscode/settings.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..dbff0926 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[solidity]": { + "editor.defaultFormatter": "JuanBlanco.solidity" + }, + "solidity.formatter": "forge" +} From ee6f10974381f7a389d2a713338af1b5b12cf7f2 Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 13 Jun 2024 21:55:12 -0400 Subject: [PATCH 63/93] feat: full withdraw test improved --- test/foundry/ExoCapsule.t.sol | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 76101ac1..1ba567c3 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -87,6 +87,8 @@ contract DepositSetup is Test { stdstore.target(capsuleAddress).sig("beaconOracle()").checked_write( bytes32(uint256(uint160(address(beaconOracle)))) ); + + stdstore.target(capsuleAddress).sig("hasRestaked()").checked_write(true); } function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { @@ -399,6 +401,8 @@ contract WithdrawalSetup is Test { assertEq(validator.validatorIndex, validatorProof.validatorIndex); assertEq(validator.mostRecentBalanceUpdateTimestamp, validatorProof.beaconBlockTimestamp); assertEq(validator.restakedBalanceGwei, _getEffectiveBalance(validatorContainer)); + + vm.deal(address(capsule), 1 ether); // Deposit 1 ether to handle excess amount withdraw } function _setValidatorContainer(string memory withdrawalInfo) internal { @@ -501,8 +505,21 @@ contract VerifyWithdrawalProof is WithdrawalSetup { capsule.withdrawNonBeaconChainETHBalance(recipient, 0.5 ether); } + function test_processFullWithdrawal_success() public setValidatorContainerAndTimestamp { + capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); + } + function test_processFullWithdrawal_revert_AlreadyProcessed() public setValidatorContainerAndTimestamp { capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); + + vm.expectRevert( + abi.encodeWithSelector( + ExoCapsule.WithdrawalAlreadyProven.selector, + _getPubkey(validatorContainer), + withdrawalProof.beaconBlockTimestamp + ) + ); + capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); } modifier setValidatorContainerAndTimestamp() { From ddc9bfa26cb068fb713eee47cb747c4fe2e5b2a6 Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 13 Jun 2024 22:04:10 -0400 Subject: [PATCH 64/93] feat: partial withdraw tests done --- test/foundry/ExoCapsule.t.sol | 57 +++++++++++++------ .../test-data/partial_withdrawal_proof.json | 2 +- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 1ba567c3..841ff225 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -354,10 +354,12 @@ contract WithdrawalSetup is Test { uint256 activationTimestamp; function setUp() public { - string memory withdrawalInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); + string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); - _setValidatorContainer(withdrawalInfo); - _setWithdrawalContainer(); + string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); + + _setValidatorContainer(validatorInfo); + _setWithdrawalContainer(withdrawalInfo); beaconOracle = IBeaconChainOracle(address(0x123)); vm.etch(address(beaconOracle), bytes("aabb")); @@ -405,28 +407,26 @@ contract WithdrawalSetup is Test { vm.deal(address(capsule), 1 ether); // Deposit 1 ether to handle excess amount withdraw } - function _setValidatorContainer(string memory withdrawalInfo) internal { - validatorContainer = stdJson.readBytes32Array(withdrawalInfo, ".ValidatorFields"); + function _setValidatorContainer(string memory validatorInfo) internal { + validatorContainer = stdJson.readBytes32Array(validatorInfo, ".ValidatorFields"); require(validatorContainer.length > 0, "validator container should not be empty"); - validatorProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot"); + validatorProof.stateRoot = stdJson.readBytes32(validatorInfo, ".beaconStateRoot"); require(validatorProof.stateRoot != bytes32(0), "state root should not be empty"); validatorProof.stateRootProof = - stdJson.readBytes32Array(withdrawalInfo, ".StateRootAgainstLatestBlockHeaderProof"); + stdJson.readBytes32Array(validatorInfo, ".StateRootAgainstLatestBlockHeaderProof"); require(validatorProof.stateRootProof.length == 3, "state root proof should have 3 nodes"); validatorProof.validatorContainerRootProof = - stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalCredentialProof"); + stdJson.readBytes32Array(validatorInfo, ".WithdrawalCredentialProof"); require(validatorProof.validatorContainerRootProof.length == 46, "validator root proof should have 46 nodes"); - validatorProof.validatorIndex = stdJson.readUint(withdrawalInfo, ".validatorIndex"); + validatorProof.validatorIndex = stdJson.readUint(validatorInfo, ".validatorIndex"); require(validatorProof.validatorIndex != 0, "validator root index should not be 0"); - beaconBlockRoot = stdJson.readBytes32(withdrawalInfo, ".latestBlockHeaderRoot"); + beaconBlockRoot = stdJson.readBytes32(validatorInfo, ".latestBlockHeaderRoot"); require(beaconBlockRoot != bytes32(0), "beacon block root should not be empty"); } - function _setWithdrawalContainer() internal { - string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); - + function _setWithdrawalContainer(string memory withdrawalInfo) internal { withdrawalContainer = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalFields"); require(withdrawalContainer.length > 0, "validator container should not be empty"); @@ -505,11 +505,14 @@ contract VerifyWithdrawalProof is WithdrawalSetup { capsule.withdrawNonBeaconChainETHBalance(recipient, 0.5 ether); } - function test_processFullWithdrawal_success() public setValidatorContainerAndTimestamp { + function test_processFullWithdrawal_success() public setValidatorContainerAndTimestampForFullWithdrawal { capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); } - function test_processFullWithdrawal_revert_AlreadyProcessed() public setValidatorContainerAndTimestamp { + function test_processFullWithdrawal_revert_AlreadyProcessed() + public + setValidatorContainerAndTimestampForFullWithdrawal + { capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); vm.expectRevert( @@ -522,9 +525,31 @@ contract VerifyWithdrawalProof is WithdrawalSetup { capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); } - modifier setValidatorContainerAndTimestamp() { + function test_processPartialWithdrawal_success() public setValidatorContainerAndTimestampForPartialWithdrawal { + capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); + } + + modifier setValidatorContainerAndTimestampForFullWithdrawal() { string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); _setValidatorContainer(withdrawalInfo); + _setWithdrawalContainer(withdrawalInfo); + + // vm.warp(mockCurrentBlockTimestamp); + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encode(beaconBlockRoot) + ); + + validatorProof.beaconBlockTimestamp = activationTimestamp; + withdrawalProof.beaconBlockTimestamp = activationTimestamp; + _; + } + + modifier setValidatorContainerAndTimestampForPartialWithdrawal() { + string memory withdrawalInfo = vm.readFile("test/foundry/test-data/partial_withdrawal_proof.json"); + _setValidatorContainer(withdrawalInfo); + _setWithdrawalContainer(withdrawalInfo); // vm.warp(mockCurrentBlockTimestamp); vm.mockCall( diff --git a/test/foundry/test-data/partial_withdrawal_proof.json b/test/foundry/test-data/partial_withdrawal_proof.json index 0cea88f5..0bca46c9 100644 --- a/test/foundry/test-data/partial_withdrawal_proof.json +++ b/test/foundry/test-data/partial_withdrawal_proof.json @@ -26,7 +26,7 @@ "0xac5e32ea973e990d191039e91c7d9fd9830599b8208675f159c3df128228e729", "0x38914949a92dc9e402aee96301b080f769f06d752a32acecaa0458cba66cf471" ], - "ValidatorProof": [ + "WithdrawalCredentialProof": [ "0x9e06c3582190fe488eac3f9f6c95622742f9afe3e038b39d2ca97ba6d5d0de4e", "0x3eb11a14af12d8558cc14493938ffa0a1c6155349699c2b9245e76344d9922ee", "0x81c959aeae7524f4f1d2d3d930ba504cbe86330619a221c9e2d9fb315e32a4d1", From 251910183f358f91e61b5da08a06c361a50854d5 Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 13 Jun 2024 22:12:09 -0400 Subject: [PATCH 65/93] fix: remove unused vars --- src/core/NativeRestakingController.sol | 1 - src/storage/ExoCapsuleStorage.sol | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index bfceb08b..f12e10d9 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -5,7 +5,6 @@ import {INativeRestakingController} from "../interfaces/INativeRestakingControll import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; import {BaseRestakingController} from "./BaseRestakingController.sol"; -import {ExoCapsule} from "./ExoCapsule.sol"; import {PausableUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/PausableUpgradeable.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 72e8fa01..20a273c9 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -28,8 +28,8 @@ contract ExoCapsuleStorage { address public constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; uint256 public constant BEACON_CHAIN_GENESIS_TIME = 1_606_824_023; uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; - uint256 constant GWEI_TO_WEI = 1e9; - uint64 constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; + uint256 public constant GWEI_TO_WEI = 1e9; + uint64 public constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; /** * @notice The latest timestamp at which the capsule owner withdrew the balance of the capsule, via calling From c1b32055bfc2bf44c9fff4b57c63e42974277b25 Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 13 Jun 2024 22:16:48 -0400 Subject: [PATCH 66/93] fix: remove complex callback lint --- src/.solhint.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/.solhint.json b/src/.solhint.json index fb01f2c8..b7a46ef6 100644 --- a/src/.solhint.json +++ b/src/.solhint.json @@ -3,7 +3,7 @@ "rules": { "max-line-length": ["error", 121], "compiler-version": ["error", "^0.8.0"], - "func-visibility": ["warn", {"ignoreConstructors": true}], + "func-visibility": ["warn", { "ignoreConstructors": true }], "no-inline-assembly": "off", "no-empty-blocks": "off", "no-unused-vars": "error", @@ -13,6 +13,7 @@ "max-states-count": "off", "reason-string": "off", "gas-custom-errors": "off", - "state-visibility": "error" + "state-visibility": "error", + "no-complex-fallback": "off" } } From 74aae9a59070eef1aec4cc6c0a0df6843fc41667 Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 13 Jun 2024 22:21:31 -0400 Subject: [PATCH 67/93] fix: temporary increase for line length --- src/.solhint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/.solhint.json b/src/.solhint.json index b7a46ef6..c7c57638 100644 --- a/src/.solhint.json +++ b/src/.solhint.json @@ -1,7 +1,7 @@ { "extends": "solhint:recommended", "rules": { - "max-line-length": ["error", 121], + "max-line-length": ["error", 128], "compiler-version": ["error", "^0.8.0"], "func-visibility": ["warn", { "ignoreConstructors": true }], "no-inline-assembly": "off", From 8fb4ecfe429a25f3090e49b42c44d3b5d839638d Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 13 Jun 2024 23:03:08 -0400 Subject: [PATCH 68/93] fix: remove console log --- test/foundry/ExoCapsule.t.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/test/foundry/ExoCapsule.t.sol b/test/foundry/ExoCapsule.t.sol index 841ff225..af949e34 100644 --- a/test/foundry/ExoCapsule.t.sol +++ b/test/foundry/ExoCapsule.t.sol @@ -5,7 +5,6 @@ import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "forge-std/Test.sol"; -import "forge-std/console.sol"; import "src/core/ExoCapsule.sol"; import "src/interfaces/IExoCapsule.sol"; From 48fb2458e93d67e25b5ff561d856c73885151c95 Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 20 Jun 2024 21:01:39 -0400 Subject: [PATCH 69/93] fix: failing CI tests and get rid of hasRestake check --- src/core/ExoCapsule.sol | 51 ++----------------- src/core/NativeRestakingController.sol | 8 --- src/interfaces/IExoCapsule.sol | 4 -- src/interfaces/INativeRestakingController.sol | 6 --- test/foundry/DepositWithdrawPrinciple.t.sol | 2 +- 5 files changed, 5 insertions(+), 66 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 49fea559..591dc3a7 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -59,27 +59,6 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { _; } - modifier hasNeverRestaked() { - require(!hasRestaked, "Restaking is enabled"); - _; - } - - /// @notice checks that hasRestaked is set to true by calling activateRestaking() - modifier hasEnabledRestaking() { - require(hasRestaked, "restaking is not enabled"); - _; - } - - /// @notice Checks that `timestamp` is greater than or equal to the value stored in `mostRecentWithdrawalTimestamp` - /// @notice All partial/full withdrawal timestamps should be greater than `mostRecentWithdrawalTimestamp` - modifier proofIsForValidTimestamp(uint256 timestamp) { - require( - timestamp >= mostRecentWithdrawalTimestamp, - "proofIsForValidTimestamp: beacon chain proof must be at or after mostRecentWithdrawalTimestamp" - ); - _; - } - constructor() { _disableInitializers(); } @@ -97,14 +76,14 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { gateway = INativeRestakingController(gateway_); beaconOracle = IBeaconChainOracle(beaconOracle_); capsuleOwner = capsuleOwner_; + + hasRestaked = true; + emit RestakingActivated(capsuleOwner); } function verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) external onlyGateway - proofIsForValidTimestamp(proof.beaconBlockTimestamp) - // ensure that caller has previously enabled restaking by calling `activateRestaking()` - hasEnabledRestaking { bytes32 validatorPubkey = validatorContainer.getPubkey(); bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); @@ -145,12 +124,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata withdrawalProof - ) - external - onlyGateway - proofIsForValidTimestamp(withdrawalProof.beaconBlockTimestamp) - returns (bool partialWithdrawal, uint256 withdrawalAmount) - { + ) external onlyGateway returns (bool partialWithdrawal, uint256 withdrawalAmount) { bytes32 validatorPubkey = validatorContainer.getPubkey(); uint64 withdrawableEpoch = validatorContainer.getWithdrawableEpoch(); Validator storage validator = _capsuleValidators[validatorPubkey]; @@ -222,23 +196,6 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { emit NonBeaconChainETHWithdrawn(recipient, amountToWithdraw); } - /** - * @notice Called by the capsule owner to activate restaking by withdrawing - * all existing ETH from the capsule and preventing further withdrawals via - * "withdrawBeforeRestaking()" - */ - function activateRestaking() external onlyGateway hasNeverRestaked { - hasRestaked = true; - _processWithdrawalBeforeRestaking(capsuleOwner); - - emit RestakingActivated(capsuleOwner); - } - - /// @notice Called by the capsule owner to withdraw the balance of the capsule when `hasRestaked` is set to false - function withdrawBeforeRestaking() external onlyGateway hasNeverRestaked { - _processWithdrawalBeforeRestaking(capsuleOwner); - } - function updatePrincipleBalance(uint256 lastlyUpdatedPrincipleBalance) external onlyGateway { principleBalance = lastlyUpdatedPrincipleBalance; diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index f12e10d9..19ce926c 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -54,19 +54,11 @@ abstract contract NativeRestakingController is return address(capsule); } - function withdrawBeforeRestaking() external whenNotPaused { - IExoCapsule capsule = _getCapsule(msg.sender); - capsule.withdrawBeforeRestaking(); - } - function depositBeaconChainValidator( bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata proof ) external payable whenNotPaused { IExoCapsule capsule = _getCapsule(msg.sender); - // Before verifying deposit proof, we need to activate restaking and withdraw all potential ETH withdrawan to - // the exocapsule back to the owner. This amount won't be counted as staked balance - capsule.activateRestaking(); capsule.verifyDepositProof(validatorContainer, proof); uint256 depositValue = uint256(validatorContainer.getEffectiveBalance()) * GWEI_TO_WEI; diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 2a39245c..d71064f1 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -26,10 +26,6 @@ interface IExoCapsule { function initialize(address gateway, address capsuleOwner, address beaconOracle) external; - function withdrawBeforeRestaking() external; - - function activateRestaking() external; - function verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) external; diff --git a/src/interfaces/INativeRestakingController.sol b/src/interfaces/INativeRestakingController.sol index af4cfb95..46f46787 100644 --- a/src/interfaces/INativeRestakingController.sol +++ b/src/interfaces/INativeRestakingController.sol @@ -24,12 +24,6 @@ interface INativeRestakingController is IBaseRestakingController { */ function createExoCapsule() external returns (address capsule); - /** - * @notice Before verifying deposit proof, validator containers can still set withdraw address to the ExoCapsule. In - * this case, we need to withdraw current balance, but only if restaking is not enabled yet - */ - function withdrawBeforeRestaking() external; - /** * @notice This is called to deposit ETH that is staked on Ethereum beacon chain to Exocore network to be restaked * in future diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index 795722d7..2a688bb7 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -1,9 +1,9 @@ pragma solidity ^0.8.19; import "../../src/core/ExoCapsule.sol"; -import {IExoCapsule} from "../../src/interfaces/IExoCapsule.sol"; import "../../src/core/ExocoreGateway.sol"; import {ILSTRestakingController} from "../../src/interfaces/ILSTRestakingController.sol"; +import {IExoCapsule} from "../../src/interfaces/IExoCapsule.sol"; import "../../src/storage/GatewayStorage.sol"; import "./ExocoreDeployer.t.sol"; import "forge-std/Test.sol"; From fbeb696cd391886374b15d113b2fb287f26aa890 Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 20 Jun 2024 21:24:55 -0400 Subject: [PATCH 70/93] fix: process request args --- src/core/ClientGatewayLzReceiver.sol | 6 +++--- src/core/ExoCapsule.sol | 4 ++-- src/core/NativeRestakingController.sol | 11 +++++------ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/core/ClientGatewayLzReceiver.sol b/src/core/ClientGatewayLzReceiver.sol index 08414c38..8a958e5c 100644 --- a/src/core/ClientGatewayLzReceiver.sol +++ b/src/core/ClientGatewayLzReceiver.sol @@ -115,7 +115,7 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp abi.decode(requestPayload, (address, address, uint256)); bool success = (uint8(bytes1(responsePayload[0])) == 1); - uint256 lastlyUpdatedPrincipleBalance = uint256(bytes32(responsePayload[1:33])); + uint256 lastlyUpdatedPrincipalBalance = uint256(bytes32(responsePayload[1:33])); if (!success) { revert WithdrawShouldNotFailOnExocore(token, withdrawer); @@ -124,8 +124,8 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp if (token == VIRTUAL_STAKED_ETH_ADDRESS) { IExoCapsule capsule = _getCapsule(withdrawer); - capsule.updatePrincipleBalance(lastlyUpdatedPrincipleBalance); - capsule.updateWithdrawableBalance(unlockPrincipleAmount); + capsule.updatePrincipalBalance(lastlyUpdatedPrincipalBalance); + capsule.updateWithdrawableBalance(unlockPrincipalAmount); } else { IVault vault = _getVault(token); diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index f78d4a90..eb002d05 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -196,8 +196,8 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { emit NonBeaconChainETHWithdrawn(recipient, amountToWithdraw); } - function updatePrincipleBalance(uint256 lastlyUpdatedPrincipleBalance) external onlyGateway { - principleBalance = lastlyUpdatedPrincipleBalance; + function updatePrincipalBalance(uint256 lastlyUpdatedPrincipalBalance) external onlyGateway { + principleBalance = lastlyUpdatedPrincipalBalance; emit PrincipalBalanceUpdated(capsuleOwner, lastlyUpdatedPrincipalBalance); } diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index df9133c5..8f072d29 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -80,13 +80,12 @@ abstract contract NativeRestakingController is capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); if (!partialWithdrawal) { // request full withdraw - _processRequest( - VIRTUAL_STAKED_ETH_ADDRESS, - msg.sender, - withdrawalAmount, - Action.REQUEST_WITHDRAW_PRINCIPLE_FROM_EXOCORE, - "" + bytes memory actionArgs = abi.encodePacked( + bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), bytes32(bytes20(msg.sender)), withdrawalAmount ); + bytes memory encodedRequest = abi.encode(VIRTUAL_STAKED_ETH_ADDRESS, msg.sender, withdrawalAmount); + + _processRequest(Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE, actionArgs, encodedRequest); } } From b30f4eb116994eb21af52796ed950d30b51d3d79 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Fri, 21 Jun 2024 04:39:44 +0000 Subject: [PATCH 71/93] chore(fmt): run `forge fmt` --- src/storage/ExoCapsuleStorage.sol | 1 + test/foundry/DepositWithdrawPrinciple.t.sol | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 20a273c9..8fa1011e 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -11,6 +11,7 @@ contract ExoCapsuleStorage { UNREGISTERED, // the validator has not been registered in this ExoCapsule REGISTERED, // staked on ethpos and withdrawal credentials are pointed to the ExoCapsule WITHDRAWN // withdrawn from the Beacon Chain + } struct Validator { diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index aed9b2b6..dfc22fca 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -2,8 +2,9 @@ pragma solidity ^0.8.19; import "../../src/core/ExoCapsule.sol"; import "../../src/core/ExocoreGateway.sol"; -import {ILSTRestakingController} from "../../src/interfaces/ILSTRestakingController.sol"; + import {IExoCapsule} from "../../src/interfaces/IExoCapsule.sol"; +import {ILSTRestakingController} from "../../src/interfaces/ILSTRestakingController.sol"; import "../../src/storage/GatewayStorage.sol"; import "./ExocoreDeployer.t.sol"; import "forge-std/Test.sol"; From 2e6957dd63b605498b570499746c59eaf01703a0 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 25 Jun 2024 07:59:43 -0400 Subject: [PATCH 72/93] fix: use event emit rather than revert when withdraw from exocore is failed --- src/core/ClientGatewayLzReceiver.sol | 28 +++++++++++++++------------- src/core/ExoCapsule.sol | 3 +-- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/core/ClientGatewayLzReceiver.sol b/src/core/ClientGatewayLzReceiver.sol index 8a958e5c..c90ab313 100644 --- a/src/core/ClientGatewayLzReceiver.sol +++ b/src/core/ClientGatewayLzReceiver.sol @@ -12,7 +12,9 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp error UnsupportedResponse(Action act); error UnexpectedResponse(uint64 nonce); error DepositShouldNotFailOnExocore(address token, address depositor); - error WithdrawShouldNotFailOnExocore(address token, address withdrawer); + + // Events + event WithdrawFailedOnExocore(address indexed token, address indexed withdrawer); modifier onlyCalledFromThis() { require( @@ -118,22 +120,22 @@ abstract contract ClientGatewayLzReceiver is PausableUpgradeable, OAppReceiverUp uint256 lastlyUpdatedPrincipalBalance = uint256(bytes32(responsePayload[1:33])); if (!success) { - revert WithdrawShouldNotFailOnExocore(token, withdrawer); - } + emit WithdrawFailedOnExocore(token, withdrawer); + } else { + if (token == VIRTUAL_STAKED_ETH_ADDRESS) { + IExoCapsule capsule = _getCapsule(withdrawer); - if (token == VIRTUAL_STAKED_ETH_ADDRESS) { - IExoCapsule capsule = _getCapsule(withdrawer); + capsule.updatePrincipalBalance(lastlyUpdatedPrincipalBalance); + capsule.updateWithdrawableBalance(unlockPrincipalAmount); + } else { + IVault vault = _getVault(token); - capsule.updatePrincipalBalance(lastlyUpdatedPrincipalBalance); - capsule.updateWithdrawableBalance(unlockPrincipalAmount); - } else { - IVault vault = _getVault(token); + vault.updatePrincipalBalance(withdrawer, lastlyUpdatedPrincipalBalance); + vault.updateWithdrawableBalance(withdrawer, unlockPrincipalAmount, 0); + } - vault.updatePrincipalBalance(withdrawer, lastlyUpdatedPrincipalBalance); - vault.updateWithdrawableBalance(withdrawer, unlockPrincipalAmount, 0); + emit WithdrawPrincipalResult(success, token, withdrawer, unlockPrincipalAmount); } - - emit WithdrawPrincipalResult(success, token, withdrawer, unlockPrincipalAmount); } function afterReceiveWithdrawRewardResponse(bytes memory requestPayload, bytes calldata responsePayload) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index eb002d05..6c797db4 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -308,9 +308,8 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { { uint64 atEpoch = _timestampToEpoch(atTimestamp); uint64 activationEpoch = validatorContainer.getActivationEpoch(); - uint64 exitEpoch = validatorContainer.getExitEpoch(); - return (atEpoch >= activationEpoch && atEpoch < exitEpoch); + return atEpoch >= activationEpoch; } function _isStaleProof(Validator storage validator, uint256 proofTimestamp) internal view returns (bool) { From 3c4233c5c85229a8f112ebe56611e4050e38db65 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 25 Jun 2024 08:03:03 -0400 Subject: [PATCH 73/93] fix: remove hasRestaked flag and choose plan A --- src/core/ExoCapsule.sol | 7 ------- src/interfaces/IExoCapsule.sol | 5 +++++ src/storage/ExoCapsuleStorage.sol | 13 ------------- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 6c797db4..1e380126 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -77,7 +77,6 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { beaconOracle = IBeaconChainOracle(beaconOracle_); capsuleOwner = capsuleOwner_; - hasRestaked = true; emit RestakingActivated(capsuleOwner); } @@ -245,12 +244,6 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { return validator; } - function _processWithdrawalBeforeRestaking(address _capsuleOwner) internal { - mostRecentWithdrawalTimestamp = block.timestamp; - nonBeaconChainETHBalance = 0; - _sendETH(_capsuleOwner, address(this).balance); - } - function _sendETH(address recipient, uint256 amountWei) internal { (bool sent,) = recipient.call{value: amountWei}(""); if (!sent) { diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index ba5ec461..7e1d5e5d 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -17,6 +17,11 @@ interface IExoCapsule { bytes32 executionPayloadRoot; bytes32[] executionPayloadRootProof; bytes32[] withdrawalContainerRootProof; + uint256 withdrawalIndex; + } + + struct HistoricalBlockRootProof { + uint256 beaconBlockTimestamp; bytes32[] historicalSummaryBlockRootProof; uint256 historicalSummaryIndex; bytes32 blockRoot; diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 8fa1011e..9a28acaa 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -32,19 +32,6 @@ contract ExoCapsuleStorage { uint256 public constant GWEI_TO_WEI = 1e9; uint64 public constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; - /** - * @notice The latest timestamp at which the capsule owner withdrew the balance of the capsule, via calling - * `withdrawBeforeRestaking`. - * @dev This variable is only updated when the `withdrawBeforeRestaking` function is called, which can only occur - * before `hasRestaked` is set to true for this capsule. - * Proofs for this capsule are only valid against Beacon Chain state roots corresponding to timestamps after the - * stored `mostRecentWithdrawalTimestamp`. - */ - uint256 public mostRecentWithdrawalTimestamp; - /// @notice an indicator of whether or not the capsule owner has ever "fully restaked" by successfully calling - /// `verifyCorrectWithdrawalCredentials`. - bool public hasRestaked; - uint256 public principleBalance; /// @notice the amount of execution layer ETH in this contract that is staked in(i.e. withdrawn from the Beacon /// Chain but not from Exocore) From 71b31113091d2d3f9275bfb6a91ca9f1fc4cedb4 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 25 Jun 2024 12:10:52 -0400 Subject: [PATCH 74/93] fix: use beaconBlockRoot from oracle --- src/core/ExoCapsule.sol | 5 +++-- src/interfaces/IExoCapsule.sol | 8 +++++++- src/libraries/BeaconChainProofs.sol | 12 ++++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 1e380126..9482e90f 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -273,11 +273,12 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { internal view { + bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); bytes32 withdrawalContainerRoot = withdrawalContainer.merklelizeWithdrawalContainer(); bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot( proof.withdrawalContainerRootProof, proof.withdrawalIndex, - proof.blockRoot, + beaconBlockRoot, proof.executionPayloadRoot, proof.executionPayloadRootProof, proof.beaconBlockTimestamp @@ -287,7 +288,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { } // Verify historical summaries bool validHistoricalSummaries = proof.stateRoot.isValidHistoricalSummaryRoot( - proof.historicalSummaryBlockRootProof, proof.historicalSummaryIndex, proof.blockRoot, proof.blockRootIndex + proof.historicalSummaryBlockRootProof, proof.historicalSummaryIndex, beaconBlockRoot, proof.blockRootIndex ); if (!validHistoricalSummaries) { revert InvalidHistoricalSummaries(withdrawalContainer.getValidatorIndex()); diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 7e1d5e5d..1dbdc30b 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -17,14 +17,20 @@ interface IExoCapsule { bytes32 executionPayloadRoot; bytes32[] executionPayloadRootProof; bytes32[] withdrawalContainerRootProof; + bytes32[] historicalSummaryBlockRootProof; + uint256 historicalSummaryIndex; + uint256 blockRootIndex; uint256 withdrawalIndex; } struct HistoricalBlockRootProof { uint256 beaconBlockTimestamp; + bytes32 stateRoot; + bytes32 executionPayloadRoot; + bytes32[] executionPayloadRootProof; + bytes32[] withdrawalContainerRootProof; bytes32[] historicalSummaryBlockRootProof; uint256 historicalSummaryIndex; - bytes32 blockRoot; uint256 blockRootIndex; uint256 withdrawalIndex; } diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index c32975e2..b4f8a500 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -199,13 +199,13 @@ library BeaconChainProofs { bytes32 withdrawalContainerRoot, bytes32[] calldata withdrawalContainerRootProof, uint256 withdrawalIndex, - bytes32 blockRoot, + bytes32 beaconBlockRoot, bytes32 executionPayloadRoot, bytes32[] calldata executionPayloadRootProof, uint256 beaconBlockTimestamp ) internal view returns (bool valid) { bool validExecutionPayloadRoot = - isValidExecutionPayloadRoot(executionPayloadRoot, blockRoot, executionPayloadRootProof); + isValidExecutionPayloadRoot(executionPayloadRoot, beaconBlockRoot, executionPayloadRootProof); bool validWCRootAgainstExecutionPayloadRoot = isValidWCRootAgainstExecutionPayloadRoot( withdrawalContainerRoot, @@ -222,7 +222,7 @@ library BeaconChainProofs { function isValidExecutionPayloadRoot( bytes32 executionPayloadRoot, - bytes32 blockRoot, + bytes32 beaconBlockRoot, bytes32[] calldata executionPayloadRootProof ) internal view returns (bool) { require( @@ -235,7 +235,7 @@ library BeaconChainProofs { return Merkle.verifyInclusionSha256({ proof: executionPayloadRootProof, - root: blockRoot, + root: beaconBlockRoot, leaf: executionPayloadRoot, index: leafIndex }); @@ -271,7 +271,7 @@ library BeaconChainProofs { bytes32 beaconStateRoot, bytes32[] calldata historicalSummaryBlockRootProof, uint256 historicalSummaryIndex, - bytes32 blockRoot, + bytes32 beaconBlockRoot, uint256 blockRootIndex ) internal view returns (bool) { require( @@ -294,7 +294,7 @@ library BeaconChainProofs { return Merkle.verifyInclusionSha256({ proof: historicalSummaryBlockRootProof, root: beaconStateRoot, - leaf: blockRoot, + leaf: beaconBlockRoot, index: historicalBlockHeaderIndex }); } From 93aaf6ddba3a35d4971d1073c46db8d7d24c57fd Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 25 Jun 2024 13:43:30 -0400 Subject: [PATCH 75/93] fix: proof validation issue with beacon block root --- src/core/ExoCapsule.sol | 6 +-- test/foundry/unit/ExoCapsule.t.sol | 61 +++++++++++++----------------- 2 files changed, 28 insertions(+), 39 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 9482e90f..c08be2cf 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -10,6 +10,7 @@ import {ExoCapsuleStorage} from "../storage/ExoCapsuleStorage.sol"; import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "forge-std/console.sol"; contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { @@ -132,11 +133,6 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { if (!validatorContainer.verifyValidatorContainerBasic()) { revert InvalidValidatorContainer(validatorPubkey); } - - if (validatorProof.beaconBlockTimestamp != withdrawalProof.beaconBlockTimestamp) { - revert UnmatchedValidatorAndWithdrawal(validatorPubkey); - } - if (validator.status == VALIDATOR_STATUS.UNREGISTERED) { revert UnregisteredOrWithdrawnValidatorContainer(validatorPubkey); } diff --git a/test/foundry/unit/ExoCapsule.t.sol b/test/foundry/unit/ExoCapsule.t.sol index af949e34..3b798827 100644 --- a/test/foundry/unit/ExoCapsule.t.sol +++ b/test/foundry/unit/ExoCapsule.t.sol @@ -5,6 +5,7 @@ import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "forge-std/Test.sol"; +import "forge-std/console.sol"; import "src/core/ExoCapsule.sol"; import "src/interfaces/IExoCapsule.sol"; @@ -86,8 +87,6 @@ contract DepositSetup is Test { stdstore.target(capsuleAddress).sig("beaconOracle()").checked_write( bytes32(uint256(uint160(address(beaconOracle)))) ); - - stdstore.target(capsuleAddress).sig("hasRestaked()").checked_write(true); } function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { @@ -270,9 +269,6 @@ contract VerifyDepositProof is DepositSetup { bytes32 beaconOraclerSlot = bytes32(stdstore.target(address(anotherCapsule)).sig("beaconOracle()").find()); vm.store(address(anotherCapsule), beaconOraclerSlot, bytes32(uint256(uint160(address(beaconOracle))))); - bytes32 hasRestakedSlot = bytes32(stdstore.target(address(anotherCapsule)).sig("hasRestaked()").find()); - vm.store(address(anotherCapsule), hasRestakedSlot, bytes32(uint256(1))); - vm.expectRevert(abi.encodeWithSelector(ExoCapsule.WithdrawalCredentialsNotMatch.selector)); anotherCapsule.verifyDepositProof(validatorContainer, validatorProof); } @@ -333,7 +329,8 @@ contract WithdrawalSetup is Test { * } */ IExoCapsule.WithdrawalContainerProof withdrawalProof; - bytes32 beaconBlockRoot; + bytes32 beaconBlockRoot; // latest beacon block root + bytes32 withdrawBeaconBlockRoot; // block root for withdrawal proof ExoCapsule capsule; IBeaconChainOracle beaconOracle; @@ -354,11 +351,7 @@ contract WithdrawalSetup is Test { function setUp() public { string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); - - string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); - _setValidatorContainer(validatorInfo); - _setWithdrawalContainer(withdrawalInfo); beaconOracle = IBeaconChainOracle(address(0x123)); vm.etch(address(beaconOracle), bytes("aabb")); @@ -380,17 +373,16 @@ contract WithdrawalSetup is Test { bytes32(uint256(uint160(address(beaconOracle)))) ); - stdstore.target(capsuleAddress).sig("hasRestaked()").checked_write(true); - activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); validatorProof.beaconBlockTimestamp = mockProofTimestamp; vm.mockCall( address(beaconOracle), - abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, mockProofTimestamp), abi.encode(beaconBlockRoot) ); @@ -432,9 +424,6 @@ contract WithdrawalSetup is Test { withdrawalProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot"); require(withdrawalProof.stateRoot != bytes32(0), "state root should not be empty"); - withdrawalProof.blockRoot = stdJson.readBytes32(withdrawalInfo, ".blockHeaderRoot"); - require(withdrawalProof.blockRoot != bytes32(0), "block header root should not be empty"); - withdrawalProof.blockRootIndex = stdJson.readUint(withdrawalInfo, ".blockHeaderRootIndex"); require(withdrawalProof.blockRootIndex != 0, "block header root index should not be 0"); @@ -448,6 +437,26 @@ contract WithdrawalSetup is Test { withdrawalProof.withdrawalContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalProof"); withdrawalProof.executionPayloadRoot = stdJson.readBytes32(withdrawalInfo, ".executionPayloadRoot"); withdrawalProof.executionPayloadRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ExecutionPayloadProof"); + + withdrawBeaconBlockRoot = stdJson.readBytes32(withdrawalInfo, ".blockHeaderRoot"); + require(withdrawBeaconBlockRoot != bytes32(0), "beacon block root should not be empty"); + } + + function _setTimeStamp() internal { + withdrawalProof.beaconBlockTimestamp = activationTimestamp + SECONDS_PER_SLOT; + validatorProof.beaconBlockTimestamp = withdrawalProof.beaconBlockTimestamp + SECONDS_PER_SLOT; + mockCurrentBlockTimestamp = validatorProof.beaconBlockTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, validatorProof.beaconBlockTimestamp), + abi.encode(beaconBlockRoot) + ); + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, withdrawalProof.beaconBlockTimestamp), + abi.encode(withdrawBeaconBlockRoot) + ); } function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { @@ -533,15 +542,7 @@ contract VerifyWithdrawalProof is WithdrawalSetup { _setValidatorContainer(withdrawalInfo); _setWithdrawalContainer(withdrawalInfo); - // vm.warp(mockCurrentBlockTimestamp); - vm.mockCall( - address(beaconOracle), - abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), - abi.encode(beaconBlockRoot) - ); - - validatorProof.beaconBlockTimestamp = activationTimestamp; - withdrawalProof.beaconBlockTimestamp = activationTimestamp; + _setTimeStamp(); _; } @@ -550,15 +551,7 @@ contract VerifyWithdrawalProof is WithdrawalSetup { _setValidatorContainer(withdrawalInfo); _setWithdrawalContainer(withdrawalInfo); - // vm.warp(mockCurrentBlockTimestamp); - vm.mockCall( - address(beaconOracle), - abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector), - abi.encode(beaconBlockRoot) - ); - - validatorProof.beaconBlockTimestamp = activationTimestamp; - withdrawalProof.beaconBlockTimestamp = activationTimestamp; + _setTimeStamp(); _; } From 715a77e74e80c3fa425018e5e5449bd2045d3400 Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 25 Jun 2024 13:44:58 -0400 Subject: [PATCH 76/93] fix: remove console log --- src/core/ExoCapsule.sol | 1 - test/foundry/unit/ExoCapsule.t.sol | 1 - 2 files changed, 2 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index c08be2cf..715b2bbe 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -10,7 +10,6 @@ import {ExoCapsuleStorage} from "../storage/ExoCapsuleStorage.sol"; import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; -import "forge-std/console.sol"; contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { diff --git a/test/foundry/unit/ExoCapsule.t.sol b/test/foundry/unit/ExoCapsule.t.sol index 3b798827..368ac726 100644 --- a/test/foundry/unit/ExoCapsule.t.sol +++ b/test/foundry/unit/ExoCapsule.t.sol @@ -5,7 +5,6 @@ import "@openzeppelin-contracts/contracts/proxy/transparent/ProxyAdmin.sol"; import "@openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import "@openzeppelin-contracts/contracts/token/ERC20/presets/ERC20PresetFixedSupply.sol"; import "forge-std/Test.sol"; -import "forge-std/console.sol"; import "src/core/ExoCapsule.sol"; import "src/interfaces/IExoCapsule.sol"; From 31110484a668d4ddc3ed6b4935163eb1fc37f34d Mon Sep 17 00:00:00 2001 From: --global Date: Tue, 25 Jun 2024 13:57:16 -0400 Subject: [PATCH 77/93] fix: remove unused struct --- src/interfaces/IExoCapsule.sol | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 1dbdc30b..9321295b 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -23,18 +23,6 @@ interface IExoCapsule { uint256 withdrawalIndex; } - struct HistoricalBlockRootProof { - uint256 beaconBlockTimestamp; - bytes32 stateRoot; - bytes32 executionPayloadRoot; - bytes32[] executionPayloadRootProof; - bytes32[] withdrawalContainerRootProof; - bytes32[] historicalSummaryBlockRootProof; - uint256 historicalSummaryIndex; - uint256 blockRootIndex; - uint256 withdrawalIndex; - } - function initialize(address gateway, address capsuleOwner, address beaconOracle) external; function verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) From 8dd38cc661cd737f9925c76b33eb1a519584221b Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 26 Jun 2024 09:35:36 -0400 Subject: [PATCH 78/93] feat: integration test for native withdrwal --- src/core/ExoCapsule.sol | 7 +- test/foundry/DepositWithdrawPrinciple.t.sol | 127 +++++++++++++++++++- test/foundry/ExocoreDeployer.t.sol | 54 ++++++--- 3 files changed, 167 insertions(+), 21 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 715b2bbe..b8a922e1 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -162,8 +162,11 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { validatorPubkey, withdrawalProof.beaconBlockTimestamp, capsuleOwner, withdrawalAmountGwei ); if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { - withdrawalAmount = (withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * GWEI_TO_WEI; - _sendETH(capsuleOwner, withdrawalAmount); + uint256 amountToSend = (withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * GWEI_TO_WEI; + _sendETH(capsuleOwner, amountToSend); + withdrawalAmount = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR * GWEI_TO_WEI; + } else { + withdrawalAmount = withdrawalAmountGwei * GWEI_TO_WEI; } } } diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index dfc22fca..5b3cf669 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -5,6 +5,7 @@ import "../../src/core/ExocoreGateway.sol"; import {IExoCapsule} from "../../src/interfaces/IExoCapsule.sol"; import {ILSTRestakingController} from "../../src/interfaces/ILSTRestakingController.sol"; + import "../../src/storage/GatewayStorage.sol"; import "./ExocoreDeployer.t.sol"; import "forge-std/Test.sol"; @@ -29,6 +30,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { event StakedWithCapsule(address staker, address capsule); uint256 constant DEFAULT_ENDPOINT_CALL_GAS_LIMIT = 200_000; + uint64 public constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; function test_LSTDepositWithdrawByLayerZero() public { Player memory depositor = players[0]; @@ -244,13 +246,15 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { test_AddWhitelistTokens(); _testNativeDeposit(depositor, relayer, lastlyUpdatedPrincipalBalance); + lastlyUpdatedPrincipalBalance += uint256(_getEffectiveBalance(validatorContainer)) * GWEI_TO_WEI; + _testNativeWithdraw(depositor, relayer, lastlyUpdatedPrincipalBalance); } function _testNativeDeposit(Player memory depositor, Player memory relayer, uint256 lastlyUpdatedPrincipalBalance) internal { // before native stake and deposit, we simulate proper block environment states to make proof valid - _simulateBlockEnvironment(); + _simulateBlockEnvironmentForNativeDeposit(); // 1. firstly depositor should stake to beacon chain by depositing 32 ETH to ETHPOS contract IExoCapsule expectedCapsule = IExoCapsule( @@ -377,11 +381,10 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { vm.stopPrank(); } - function _simulateBlockEnvironment() internal { + 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; + activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; mockProofTimestamp = activationTimestamp; validatorProof.beaconBlockTimestamp = mockProofTimestamp; @@ -397,4 +400,120 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { ); } + function _testNativeWithdraw(Player memory withdrawer, Player memory relayer, uint256 lastlyUpdatedPrincipalBalance) + internal + { + // before native withdraw, we simulate proper block environment states to make proof valid + _simulateBlockEnvironmentForNativeWithdraw(); + deal(address(capsule), 1 ether); // Deposit 1 ether to handle excess amount withdraw + + // 2. withdrawer will call clientGateway.processBeaconChainWithdrawal to withdraw from Exocore thru layerzero + + /// client chain layerzero endpoint should emit the message packet including deposit payload. + uint64 withdrawRequestNonce = 3; + uint64 withdrawalAmountGwei = _getWithdrawalAmount(withdrawalContainer); + uint256 withdrawalAmount; + if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { + withdrawalAmount = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR * GWEI_TO_WEI; + } else { + withdrawalAmount = withdrawalAmountGwei * GWEI_TO_WEI; + } + bytes memory withdrawRequestPayload = abi.encodePacked( + GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE, + bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), + bytes32(bytes20(withdrawer.addr)), + withdrawalAmount + ); + uint256 withdrawRequestNativeFee = clientGateway.quote(withdrawRequestPayload); + bytes32 withdrawRequestId = generateUID(withdrawRequestNonce, true); + + // client chain layerzero endpoint should emit the message packet including withdraw payload. + vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); + emit NewPacket( + exocoreChainId, + address(clientGateway), + address(exocoreGateway).toBytes32(), + withdrawRequestNonce, + withdrawRequestPayload + ); + // client chain gateway should emit MessageSent event + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit MessageSent( + GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE, + withdrawRequestId, + withdrawRequestNonce, + withdrawRequestNativeFee + ); + + vm.startPrank(withdrawer.addr); + clientGateway.processBeaconChainWithdrawal{value: withdrawRequestNativeFee}( + validatorContainer, validatorProof, withdrawalContainer, withdrawalProof + ); + vm.stopPrank(); + + /// exocore gateway should return response message to exocore network layerzero endpoint + uint64 withdrawResponseNonce = 3; + lastlyUpdatedPrincipalBalance -= withdrawalAmount; + bytes memory withdrawResponsePayload = + abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true, lastlyUpdatedPrincipalBalance); + uint256 withdrawResponseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload); + bytes32 withdrawResponseId = generateUID(withdrawResponseNonce, false); + + // exocore gateway should return response message to exocore network layerzero endpoint + vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); + emit NewPacket( + clientChainId, + address(exocoreGateway), + address(clientGateway).toBytes32(), + withdrawResponseNonce, + withdrawResponsePayload + ); + // exocore gateway should emit MessageSent event + vm.expectEmit(true, true, true, true, address(exocoreGateway)); + emit MessageSent( + GatewayStorage.Action.RESPOND, withdrawResponseId, withdrawResponseNonce, withdrawResponseNativeFee + ); + exocoreLzEndpoint.lzReceive( + Origin(clientChainId, address(clientGateway).toBytes32(), withdrawRequestNonce), + address(exocoreGateway), + withdrawRequestId, + withdrawRequestPayload, + bytes("") + ); + + // client chain gateway should execute the response hook and emit depositResult event + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit WithdrawPrincipalResult(true, address(VIRTUAL_STAKED_ETH_ADDRESS), withdrawer.addr, withdrawalAmount); + clientChainLzEndpoint.lzReceive( + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), withdrawResponseNonce), + address(clientGateway), + withdrawResponseId, + withdrawResponsePayload, + bytes("") + ); + } + + function _simulateBlockEnvironmentForNativeWithdraw() internal { + // load beacon chain validator container and proof from json file + string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); + _loadValidatorContainer(withdrawalInfo); + // load withdrawal proof + _loadWithdrawalContainer(withdrawalInfo); + + withdrawalProof.beaconBlockTimestamp = activationTimestamp + SECONDS_PER_SLOT; + validatorProof.beaconBlockTimestamp = withdrawalProof.beaconBlockTimestamp + SECONDS_PER_SLOT; + mockCurrentBlockTimestamp = validatorProof.beaconBlockTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, validatorProof.beaconBlockTimestamp), + abi.encode(beaconBlockRoot) + ); + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, withdrawalProof.beaconBlockTimestamp), + abi.encode(withdrawBeaconBlockRoot) + ); + } + } diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index fb5ccb6f..1ddf633c 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -74,10 +74,16 @@ contract ExocoreDeployer is Test { hex"608060405260405161090e38038061090e83398101604081905261002291610460565b61002e82826000610035565b505061058a565b61003e83610100565b6040516001600160a01b038416907f1cf3b03a6cf19fa2baba4df148e9dcabedea7f8a5c07840e207e5c089be95d3e90600090a260008251118061007f5750805b156100fb576100f9836001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100c5573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100e99190610520565b836102a360201b6100291760201c565b505b505050565b610113816102cf60201b6100551760201c565b6101725760405162461bcd60e51b815260206004820152602560248201527f455243313936373a206e657720626561636f6e206973206e6f74206120636f6e6044820152641d1c9858dd60da1b60648201526084015b60405180910390fd5b6101e6816001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156101b3573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906101d79190610520565b6102cf60201b6100551760201c565b61024b5760405162461bcd60e51b815260206004820152603060248201527f455243313936373a20626561636f6e20696d706c656d656e746174696f6e206960448201526f1cc81b9bdd08184818dbdb9d1c9858dd60821b6064820152608401610169565b806102827fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d5060001b6102de60201b6100641760201c565b80546001600160a01b0319166001600160a01b039290921691909117905550565b60606102c883836040518060600160405280602781526020016108e7602791396102e1565b9392505050565b6001600160a01b03163b151590565b90565b6060600080856001600160a01b0316856040516102fe919061053b565b600060405180830381855af49150503d8060008114610339576040519150601f19603f3d011682016040523d82523d6000602084013e61033e565b606091505b5090925090506103508683838761035a565b9695505050505050565b606083156103c65782516103bf576001600160a01b0385163b6103bf5760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e74726163740000006044820152606401610169565b50816103d0565b6103d083836103d8565b949350505050565b8151156103e85781518083602001fd5b8060405162461bcd60e51b81526004016101699190610557565b80516001600160a01b038116811461041957600080fd5b919050565b634e487b7160e01b600052604160045260246000fd5b60005b8381101561044f578181015183820152602001610437565b838111156100f95750506000910152565b6000806040838503121561047357600080fd5b61047c83610402565b60208401519092506001600160401b038082111561049957600080fd5b818501915085601f8301126104ad57600080fd5b8151818111156104bf576104bf61041e565b604051601f8201601f19908116603f011681019083821181831017156104e7576104e761041e565b8160405282815288602084870101111561050057600080fd5b610511836020830160208801610434565b80955050505050509250929050565b60006020828403121561053257600080fd5b6102c882610402565b6000825161054d818460208701610434565b9190910192915050565b6020815260008251806020840152610576816040850160208701610434565b601f01601f19169190910160400192915050565b61034e806105996000396000f3fe60806040523661001357610011610017565b005b6100115b610027610022610067565b610100565b565b606061004e83836040518060600160405280602781526020016102f260279139610124565b9392505050565b6001600160a01b03163b151590565b90565b600061009a7fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50546001600160a01b031690565b6001600160a01b0316635c60da1b6040518163ffffffff1660e01b8152600401602060405180830381865afa1580156100d7573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906100fb9190610249565b905090565b3660008037600080366000845af43d6000803e80801561011f573d6000f35b3d6000fd5b6060600080856001600160a01b03168560405161014191906102a2565b600060405180830381855af49150503d806000811461017c576040519150601f19603f3d011682016040523d82523d6000602084013e610181565b606091505b50915091506101928683838761019c565b9695505050505050565b6060831561020d578251610206576001600160a01b0385163b6102065760405162461bcd60e51b815260206004820152601d60248201527f416464726573733a2063616c6c20746f206e6f6e2d636f6e747261637400000060448201526064015b60405180910390fd5b5081610217565b610217838361021f565b949350505050565b81511561022f5781518083602001fd5b8060405162461bcd60e51b81526004016101fd91906102be565b60006020828403121561025b57600080fd5b81516001600160a01b038116811461004e57600080fd5b60005b8381101561028d578181015183820152602001610275565b8381111561029c576000848401525b50505050565b600082516102b4818460208701610272565b9190910192915050565b60208152600082518060208401526102dd816040850160208701610272565b601f01601f1916919091016040019291505056fe416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564a2646970667358221220d51e81d3bc5ed20a26aeb05dce7e825c503b2061aa78628027300c8d65b9d89a64736f6c634300080c0033416464726573733a206c6f772d6c6576656c2064656c65676174652063616c6c206661696c6564"; bytes32[] validatorContainer; - bytes32 beaconBlockRoot; + bytes32 beaconBlockRoot; // latest beacon block root IExoCapsule.ValidatorContainerProof validatorProof; + + bytes32[] withdrawalContainer; + IExoCapsule.WithdrawalContainerProof withdrawalProof; + bytes32 withdrawBeaconBlockRoot; // block root for withdrawal proof + uint256 mockProofTimestamp; uint256 mockCurrentBlockTimestamp; + uint256 activationTimestamp; uint32 exocoreChainId = 2; uint32 clientChainId = 1; @@ -110,9 +116,8 @@ contract ExocoreDeployer is Test { vm.etch(CLAIM_REWARD_PRECOMPILE_ADDRESS, WithdrawRewardMockCode); // load beacon chain validator container and proof from json file - _loadValidatorContainer(); - _loadValidatorProof(); - _loadBeaconBlockRoot(); + string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); + _loadValidatorContainer(validatorInfo); vm.chainId(clientChainId); _deploy(); @@ -216,15 +221,9 @@ contract ExocoreDeployer is Test { vm.stopPrank(); } - function _loadValidatorContainer() internal { - string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); - + function _loadValidatorContainer(string memory validatorInfo) internal { validatorContainer = stdJson.readBytes32Array(validatorInfo, ".ValidatorFields"); require(validatorContainer.length > 0, "validator container should not be empty"); - } - - function _loadValidatorProof() internal { - string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); validatorProof.stateRoot = stdJson.readBytes32(validatorInfo, ".beaconStateRoot"); require(validatorProof.stateRoot != bytes32(0), "state root should not be empty"); @@ -236,15 +235,36 @@ contract ExocoreDeployer is Test { 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"); - } - - function _loadBeaconBlockRoot() internal { - string memory validatorInfo = vm.readFile("test/foundry/test-data/validator_container_proof_302913.json"); beaconBlockRoot = stdJson.readBytes32(validatorInfo, ".latestBlockHeaderRoot"); require(beaconBlockRoot != bytes32(0), "beacon block root should not be empty"); } + function _loadWithdrawalContainer(string memory withdrawalInfo) internal { + withdrawalContainer = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalFields"); + require(withdrawalContainer.length > 0, "validator container should not be empty"); + + withdrawalProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot"); + require(withdrawalProof.stateRoot != bytes32(0), "state root should not be empty"); + + withdrawalProof.blockRootIndex = stdJson.readUint(withdrawalInfo, ".blockHeaderRootIndex"); + require(withdrawalProof.blockRootIndex != 0, "block header root index should not be 0"); + + withdrawalProof.withdrawalIndex = stdJson.readUint(withdrawalInfo, ".withdrawalIndex"); + + withdrawalProof.historicalSummaryIndex = stdJson.readUint(withdrawalInfo, ".historicalSummaryIndex"); + require(withdrawalProof.historicalSummaryIndex != 0, "historical summary index should not be 0"); + + withdrawalProof.historicalSummaryBlockRootProof = + stdJson.readBytes32Array(withdrawalInfo, ".HistoricalSummaryProof"); + withdrawalProof.withdrawalContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalProof"); + withdrawalProof.executionPayloadRoot = stdJson.readBytes32(withdrawalInfo, ".executionPayloadRoot"); + withdrawalProof.executionPayloadRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ExecutionPayloadProof"); + + withdrawBeaconBlockRoot = stdJson.readBytes32(withdrawalInfo, ".blockHeaderRoot"); + require(withdrawBeaconBlockRoot != bytes32(0), "beacon block root should not be empty"); + } + function _deploy() internal { // prepare outside contracts like ERC20 token contract and layerzero endpoint contract restakeToken = new ERC20PresetFixedSupply("rest", "rest", 1e34, exocoreValidatorSet.addr); @@ -374,6 +394,10 @@ contract ExocoreDeployer is Test { return vc[2].fromLittleEndianUint64(); } + function _getWithdrawalAmount(bytes32[] storage wc) internal view returns (uint64) { + return wc[3].fromLittleEndianUint64(); + } + function generateUID(uint64 nonce, bool fromClientChainToExocore) internal view returns (bytes32 uid) { if (fromClientChainToExocore) { uid = GUID.generate( From a7c78935ac392d26adfa13eb9d99dcf42beb371c Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 26 Jun 2024 13:20:39 -0400 Subject: [PATCH 79/93] feat: deposit with 32 ether cap --- src/core/ExoCapsule.sol | 10 +++++++++- src/core/NativeRestakingController.sol | 4 +--- src/interfaces/IExoCapsule.sol | 3 ++- test/foundry/DepositWithdrawPrinciple.t.sol | 4 ++-- test/foundry/unit/ExoCapsule.t.sol | 4 ++-- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index b8a922e1..601bc3b9 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -83,6 +83,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { function verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) external onlyGateway + returns (uint256 depositAmount) { bytes32 validatorPubkey = validatorContainer.getPubkey(); bytes32 withdrawalCredentials = validatorContainer.getWithdrawalCredentials(); @@ -113,7 +114,14 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { validator.status = VALIDATOR_STATUS.REGISTERED; validator.validatorIndex = proof.validatorIndex; validator.mostRecentBalanceUpdateTimestamp = proof.beaconBlockTimestamp; - validator.restakedBalanceGwei = validatorContainer.getEffectiveBalance(); + uint64 depositAmountGwei = validatorContainer.getEffectiveBalance(); + if (depositAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { + validator.restakedBalanceGwei = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR; + depositAmount = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR * GWEI_TO_WEI; + } else { + validator.restakedBalanceGwei = depositAmountGwei; + depositAmount = depositAmountGwei * GWEI_TO_WEI; + } _capsuleValidatorsByIndex[proof.validatorIndex] = validatorPubkey; } diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 8f072d29..97667d4a 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -59,9 +59,7 @@ abstract contract NativeRestakingController is IExoCapsule.ValidatorContainerProof calldata proof ) external payable whenNotPaused { IExoCapsule capsule = _getCapsule(msg.sender); - capsule.verifyDepositProof(validatorContainer, proof); - - uint256 depositValue = uint256(validatorContainer.getEffectiveBalance()) * GWEI_TO_WEI; + uint256 depositValue = capsule.verifyDepositProof(validatorContainer, proof); bytes memory actionArgs = abi.encodePacked(bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), bytes32(bytes20(msg.sender)), depositValue); diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 9321295b..7d7542a3 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -26,7 +26,8 @@ interface IExoCapsule { function initialize(address gateway, address capsuleOwner, address beaconOracle) external; function verifyDepositProof(bytes32[] calldata validatorContainer, ValidatorContainerProof calldata proof) - external; + external + returns (uint256); function verifyWithdrawalProof( bytes32[] calldata validatorContainer, diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index 5b3cf669..30b4024f 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -246,7 +246,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { test_AddWhitelistTokens(); _testNativeDeposit(depositor, relayer, lastlyUpdatedPrincipalBalance); - lastlyUpdatedPrincipalBalance += uint256(_getEffectiveBalance(validatorContainer)) * GWEI_TO_WEI; + lastlyUpdatedPrincipalBalance += 32 ether; _testNativeWithdraw(depositor, relayer, lastlyUpdatedPrincipalBalance); } @@ -295,7 +295,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { /// client chain layerzero endpoint should emit the message packet including deposit payload. uint64 depositRequestNonce = 2; - uint256 depositAmount = uint256(_getEffectiveBalance(validatorContainer)) * GWEI_TO_WEI; + uint256 depositAmount = 32 ether; bytes memory depositRequestPayload = abi.encodePacked( GatewayStorage.Action.REQUEST_DEPOSIT, bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), diff --git a/test/foundry/unit/ExoCapsule.t.sol b/test/foundry/unit/ExoCapsule.t.sol index 368ac726..018c8cf3 100644 --- a/test/foundry/unit/ExoCapsule.t.sol +++ b/test/foundry/unit/ExoCapsule.t.sol @@ -385,14 +385,14 @@ contract WithdrawalSetup is Test { abi.encode(beaconBlockRoot) ); - capsule.verifyDepositProof(validatorContainer, validatorProof); + uint256 depositAmount = capsule.verifyDepositProof(validatorContainer, validatorProof); ExoCapsuleStorage.Validator memory validator = capsule.getRegisteredValidatorByPubkey(_getPubkey(validatorContainer)); assertEq(uint8(validator.status), uint8(ExoCapsuleStorage.VALIDATOR_STATUS.REGISTERED)); assertEq(validator.validatorIndex, validatorProof.validatorIndex); assertEq(validator.mostRecentBalanceUpdateTimestamp, validatorProof.beaconBlockTimestamp); - assertEq(validator.restakedBalanceGwei, _getEffectiveBalance(validatorContainer)); + assertEq(validator.restakedBalanceGwei, depositAmount / 1e9); vm.deal(address(capsule), 1 ether); // Deposit 1 ether to handle excess amount withdraw } From 145aa3a064017bffa1e0c9cde8d7d0a4c238423a Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 27 Jun 2024 11:02:33 -0400 Subject: [PATCH 80/93] fix: typo for principal --- src/core/ExoCapsule.sol | 2 +- src/storage/ExoCapsuleStorage.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 601bc3b9..2540abd8 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -202,7 +202,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { } function updatePrincipalBalance(uint256 lastlyUpdatedPrincipalBalance) external onlyGateway { - principleBalance = lastlyUpdatedPrincipalBalance; + principalBalance = lastlyUpdatedPrincipalBalance; emit PrincipalBalanceUpdated(capsuleOwner, lastlyUpdatedPrincipalBalance); } diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 9a28acaa..ab077724 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -32,7 +32,7 @@ contract ExoCapsuleStorage { uint256 public constant GWEI_TO_WEI = 1e9; uint64 public constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; - uint256 public principleBalance; + uint256 public principalBalance; /// @notice the amount of execution layer ETH in this contract that is staked in(i.e. withdrawn from the Beacon /// Chain but not from Exocore) uint256 public withdrawableBalance; From 3a9b5fd38f07e1b18d7f1212aca8dcfdde9c33bc Mon Sep 17 00:00:00 2001 From: --global Date: Wed, 10 Jul 2024 13:55:16 -0400 Subject: [PATCH 81/93] feat: update withdrawal verification logic --- src/core/ExoCapsule.sol | 48 ++--- src/core/NativeRestakingController.sol | 4 +- src/interfaces/IExoCapsule.sol | 10 +- src/interfaces/INativeRestakingController.sol | 3 +- src/libraries/BeaconChainProofs.sol | 192 +++++++++++------- test/foundry/DepositWithdrawPrinciple.t.sol | 1 - test/foundry/ExocoreDeployer.t.sol | 4 +- 7 files changed, 141 insertions(+), 121 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 2540abd8..7e9a5572 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -4,6 +4,7 @@ import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; import {BeaconChainProofs} from "../libraries/BeaconChainProofs.sol"; +import {Endian} from "../libraries/Endian.sol"; import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; import {ExoCapsuleStorage} from "../storage/ExoCapsuleStorage.sol"; @@ -14,6 +15,7 @@ import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Ini contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { using BeaconChainProofs for bytes32; + using Endian for bytes32; using ValidatorContainer for bytes32[]; using WithdrawalContainer for bytes32[]; @@ -130,7 +132,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, - WithdrawalContainerProof calldata withdrawalProof + BeaconChainProofs.WithdrawalProof calldata withdrawalProof ) external onlyGateway returns (bool partialWithdrawal, uint256 withdrawalAmount) { bytes32 validatorPubkey = validatorContainer.getPubkey(); uint64 withdrawableEpoch = validatorContainer.getWithdrawableEpoch(); @@ -144,11 +146,13 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { revert UnregisteredOrWithdrawnValidatorContainer(validatorPubkey); } - if (provenWithdrawal[validatorPubkey][withdrawalProof.beaconBlockTimestamp]) { - revert WithdrawalAlreadyProven(validatorPubkey, withdrawalProof.beaconBlockTimestamp); + uint256 withdrawalTimestamp = withdrawalProof.timestampRoot.fromLittleEndianUint64(); + + if (provenWithdrawal[validatorPubkey][withdrawalTimestamp]) { + revert WithdrawalAlreadyProven(validatorPubkey, withdrawalTimestamp); } - provenWithdrawal[validatorPubkey][withdrawalProof.beaconBlockTimestamp] = true; + provenWithdrawal[validatorPubkey][withdrawalTimestamp] = true; _verifyValidatorContainer(validatorContainer, validatorProof); _verifyWithdrawalContainer(withdrawalContainer, withdrawalProof); @@ -157,18 +161,14 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { if (partialWithdrawal) { // Immediately send ETH without sending request to Exocore side - emit PartialWithdrawalRedeemed( - validatorPubkey, withdrawalProof.beaconBlockTimestamp, capsuleOwner, withdrawalAmountGwei - ); + emit PartialWithdrawalRedeemed(validatorPubkey, withdrawalTimestamp, capsuleOwner, withdrawalAmountGwei); _sendETH(capsuleOwner, withdrawalAmountGwei * GWEI_TO_WEI); } else { // Full withdrawal validator.status = VALIDATOR_STATUS.WITHDRAWN; validator.restakedBalanceGwei = 0; // If over MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32 * 1e9, then send remaining amount immediately - emit FullWithdrawalRedeemed( - validatorPubkey, withdrawalProof.beaconBlockTimestamp, capsuleOwner, withdrawalAmountGwei - ); + emit FullWithdrawalRedeemed(validatorPubkey, withdrawalTimestamp, capsuleOwner, withdrawalAmountGwei); if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { uint256 amountToSend = (withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * GWEI_TO_WEI; _sendETH(capsuleOwner, amountToSend); @@ -275,30 +275,18 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { } } - function _verifyWithdrawalContainer(bytes32[] calldata withdrawalContainer, WithdrawalContainerProof calldata proof) - internal - view - { - bytes32 beaconBlockRoot = getBeaconBlockRoot(proof.beaconBlockTimestamp); + function _verifyWithdrawalContainer( + bytes32[] calldata withdrawalContainer, + BeaconChainProofs.WithdrawalProof calldata proof + ) internal view { + // To-do check withdrawalContainer length is valid + // Get withdrawal timestamp from timestamp root + uint256 withdrawalTimestamp = proof.timestampRoot.fromLittleEndianUint64(); bytes32 withdrawalContainerRoot = withdrawalContainer.merklelizeWithdrawalContainer(); - bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot( - proof.withdrawalContainerRootProof, - proof.withdrawalIndex, - beaconBlockRoot, - proof.executionPayloadRoot, - proof.executionPayloadRootProof, - proof.beaconBlockTimestamp - ); + bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot(proof); if (!valid) { revert InvalidWithdrawalContainer(withdrawalContainer.getValidatorIndex()); } - // Verify historical summaries - bool validHistoricalSummaries = proof.stateRoot.isValidHistoricalSummaryRoot( - proof.historicalSummaryBlockRootProof, proof.historicalSummaryIndex, beaconBlockRoot, proof.blockRootIndex - ); - if (!validHistoricalSummaries) { - revert InvalidHistoricalSummaries(withdrawalContainer.getValidatorIndex()); - } } function _isActivatedAtEpoch(bytes32[] calldata validatorContainer, uint256 atTimestamp) diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 97667d4a..2421f8ce 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.19; import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; - +import {BeaconChainProofs} from "../libraries/BeaconChainProofs.sol"; import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; import {BaseRestakingController} from "./BaseRestakingController.sol"; @@ -71,7 +71,7 @@ abstract contract NativeRestakingController is bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, - IExoCapsule.WithdrawalContainerProof calldata withdrawalProof + BeaconChainProofs.WithdrawalProof calldata withdrawalProof ) external payable whenNotPaused { IExoCapsule capsule = _getCapsule(msg.sender); (bool partialWithdrawal, uint256 withdrawalAmount) = diff --git a/src/interfaces/IExoCapsule.sol b/src/interfaces/IExoCapsule.sol index 7d7542a3..545db228 100644 --- a/src/interfaces/IExoCapsule.sol +++ b/src/interfaces/IExoCapsule.sol @@ -1,5 +1,7 @@ pragma solidity ^0.8.19; +import {BeaconChainProofs} from "../libraries/BeaconChainProofs.sol"; + interface IExoCapsule { /// @notice This struct contains the infos needed for validator container validity verification @@ -14,13 +16,7 @@ interface IExoCapsule { struct WithdrawalContainerProof { uint256 beaconBlockTimestamp; bytes32 stateRoot; - bytes32 executionPayloadRoot; - bytes32[] executionPayloadRootProof; bytes32[] withdrawalContainerRootProof; - bytes32[] historicalSummaryBlockRootProof; - uint256 historicalSummaryIndex; - uint256 blockRootIndex; - uint256 withdrawalIndex; } function initialize(address gateway, address capsuleOwner, address beaconOracle) external; @@ -33,7 +29,7 @@ interface IExoCapsule { bytes32[] calldata validatorContainer, ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, - WithdrawalContainerProof calldata withdrawalProof + BeaconChainProofs.WithdrawalProof calldata withdrawalProof ) external returns (bool partialWithdrawal, uint256 withdrawalAmount); function withdraw(uint256 amount, address payable recipient) external; diff --git a/src/interfaces/INativeRestakingController.sol b/src/interfaces/INativeRestakingController.sol index 46f46787..592b1baa 100644 --- a/src/interfaces/INativeRestakingController.sol +++ b/src/interfaces/INativeRestakingController.sol @@ -1,5 +1,6 @@ pragma solidity ^0.8.19; +import {BeaconChainProofs} from "../libraries/BeaconChainProofs.sol"; import {IBaseRestakingController} from "./IBaseRestakingController.sol"; import {IExoCapsule} from "./IExoCapsule.sol"; @@ -81,7 +82,7 @@ interface INativeRestakingController is IBaseRestakingController { bytes32[] calldata validatorContainer, IExoCapsule.ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, - IExoCapsule.WithdrawalContainerProof calldata withdrawalProof + BeaconChainProofs.WithdrawalProof calldata withdrawalProof ) external payable; } diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index b4f8a500..413ac1f1 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.0; +import {Endian} from "../libraries/Endian.sol"; import {Merkle} from "./Merkle.sol"; - // Utility library for parsing and PHASE0 beacon chain block headers // SSZ // Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization @@ -9,10 +9,12 @@ import {Merkle} from "./Merkle.sol"; // Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader // BeaconState // Spec: https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconstate + library BeaconChainProofs { // constants are the number of fields and the heights of the different merkle trees used in merkleizing // beacon chain containers + uint256 internal constant NUM_BEACON_BLOCK_HEADER_FIELDS = 5; uint256 internal constant BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT = 3; @@ -122,18 +124,19 @@ library BeaconChainProofs { /// @notice This struct contains the merkle proofs and leaves needed to verify a partial/full withdrawal struct WithdrawalProof { - bytes withdrawalProof; - bytes slotProof; - bytes executionPayloadProof; - bytes timestampProof; - bytes historicalSummaryBlockRootProof; - uint64 blockRootIndex; - uint64 historicalSummaryIndex; - uint64 withdrawalIndex; + bytes32[] withdrawalContainerRootProof; + bytes32[] slotProof; + bytes32[] executionPayloadRootProof; + bytes32[] timestampProof; + bytes32[] historicalSummaryBlockRootProof; + uint256 blockRootIndex; + uint256 historicalSummaryIndex; + uint256 withdrawalIndex; bytes32 blockRoot; bytes32 slotRoot; bytes32 timestampRoot; bytes32 executionPayloadRoot; + bytes32 stateRoot; } /// @notice This struct contains the root and proof for verifying the state root against the oracle block root @@ -195,108 +198,139 @@ library BeaconChainProofs { }); } - function isValidWithdrawalContainerRoot( - bytes32 withdrawalContainerRoot, - bytes32[] calldata withdrawalContainerRootProof, - uint256 withdrawalIndex, - bytes32 beaconBlockRoot, - bytes32 executionPayloadRoot, - bytes32[] calldata executionPayloadRootProof, - uint256 beaconBlockTimestamp - ) internal view returns (bool valid) { - bool validExecutionPayloadRoot = - isValidExecutionPayloadRoot(executionPayloadRoot, beaconBlockRoot, executionPayloadRootProof); - - bool validWCRootAgainstExecutionPayloadRoot = isValidWCRootAgainstExecutionPayloadRoot( - withdrawalContainerRoot, - executionPayloadRoot, - withdrawalContainerRootProof, - withdrawalIndex, - beaconBlockTimestamp + function isValidWithdrawalContainerRoot(bytes32 withdrawalContainerRoot, WithdrawalProof calldata proof) + internal + view + returns (bool valid) + { + require(proof.blockRootIndex < 2 ** BLOCK_ROOTS_TREE_HEIGHT, "blockRootIndex too large"); + require(proof.withdrawalIndex < 2 ** WITHDRAWALS_TREE_HEIGHT, "withdrawalIndex too large"); + require( + proof.historicalSummaryIndex < 2 ** HISTORICAL_SUMMARIES_TREE_HEIGHT, "historicalSummaryIndex too large" ); + uint256 withdrawalTimestamp = getWithdrawalTimestamp(proof); + bool validExecutionPayloadRoot = isValidExecutionPayloadRoot(proof); + + bool validHistoricalSummary = isValidHistoricalSummaryRoot(proof); + + bool validWCRootAgainstExecutionPayloadRoot = + isValidWCRootAgainstExecutionPayloadRoot(proof, withdrawalContainerRoot); - if (validExecutionPayloadRoot && validWCRootAgainstExecutionPayloadRoot) { + if (validExecutionPayloadRoot && validHistoricalSummary && validWCRootAgainstExecutionPayloadRoot) { valid = true; } } - function isValidExecutionPayloadRoot( - bytes32 executionPayloadRoot, - bytes32 beaconBlockRoot, - bytes32[] calldata executionPayloadRootProof - ) internal view returns (bool) { + function isValidExecutionPayloadRoot(WithdrawalProof calldata withdrawalProof) internal view returns (bool) { + uint256 withdrawalTimestamp = getWithdrawalTimestamp(withdrawalProof); + // Post deneb hard fork, executionPayloadHeader fields increased + uint256 executionPayloadHeaderFieldTreeHeight = withdrawalTimestamp < DENEB_FORK_TIMESTAMP + ? EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_CAPELLA + : EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_DENEB; + require( + withdrawalProof.withdrawalContainerRootProof.length + == executionPayloadHeaderFieldTreeHeight + WITHDRAWALS_TREE_HEIGHT + 1, + "wcRootProof has incorrect length" + ); require( - executionPayloadRootProof.length + withdrawalProof.executionPayloadRootProof.length == BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT + BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT, - "state root proof should have 3 nodes" + "executionPayloadRootProof has incorrect length" + ); + require( + withdrawalProof.slotProof.length == BEACON_BLOCK_HEADER_FIELD_TREE_HEIGHT, "slotProof has incorrect length" + ); + require( + withdrawalProof.timestampProof.length == executionPayloadHeaderFieldTreeHeight, + "timestampProof has incorrect length" ); - - uint256 leafIndex = (BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT)) | EXECUTION_PAYLOAD_INDEX; - - return Merkle.verifyInclusionSha256({ - proof: executionPayloadRootProof, - root: beaconBlockRoot, - leaf: executionPayloadRoot, - index: leafIndex - }); } function isValidWCRootAgainstExecutionPayloadRoot( - bytes32 withdrawalContainerRoot, - bytes32 executionPayloadRoot, - bytes32[] calldata withdrawalContainerRootProof, - uint256 withdrawalIndex, - uint256 beaconBlockTimestamp + WithdrawalProof calldata withdrawalProof, + bytes32 withdrawalContainerRoot ) internal view returns (bool) { - uint256 executionPayloadHeaderFieldTreeHeight = (beaconBlockTimestamp < DENEB_FORK_TIMESTAMP) - ? EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_CAPELLA - : EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_DENEB; + //Next we verify the slot against the blockRoot + require( + Merkle.verifyInclusionSha256({ + proof: withdrawalProof.slotProof, + root: withdrawalProof.blockRoot, + leaf: withdrawalProof.slotRoot, + index: SLOT_INDEX + }), + "Invalid slot merkle proof" + ); + + // Verify the executionPayloadRoot against the blockRoot + uint256 executionPayloadIndex = + (BODY_ROOT_INDEX << (BEACON_BLOCK_BODY_FIELD_TREE_HEIGHT)) | EXECUTION_PAYLOAD_INDEX; + require( + Merkle.verifyInclusionSha256({ + proof: withdrawalProof.executionPayloadRootProof, + root: withdrawalProof.blockRoot, + leaf: withdrawalProof.executionPayloadRoot, + index: executionPayloadIndex + }), + "Invalid executionPayload proof" + ); + // Verify the timestampRoot against the executionPayload root require( - withdrawalContainerRootProof.length == (executionPayloadHeaderFieldTreeHeight + WITHDRAWALS_TREE_HEIGHT + 1), - "withdrawalProof has incorrect length" + Merkle.verifyInclusionSha256({ + proof: withdrawalProof.timestampProof, + root: withdrawalProof.executionPayloadRoot, + leaf: withdrawalProof.timestampRoot, + index: TIMESTAMP_INDEX + }), + "Invalid timestamp proof" ); - uint256 leafIndex = (WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1)) | uint256(withdrawalIndex); + /** + * Next we verify the withdrawal fields against the executionPayloadRoot: + * First we compute the withdrawal_index, then we merkleize the + * withdrawalFields container to calculate the withdrawalRoot. + * + * Note: Merkleization of the withdrawals root tree uses MerkleizeWithMixin, i.e., the length of the array + * is hashed with the root of + * the array. Thus we shift the WITHDRAWALS_INDEX over by WITHDRAWALS_TREE_HEIGHT + 1 and not just + * WITHDRAWALS_TREE_HEIGHT. + */ + uint256 withdrawalIndex = + (WITHDRAWALS_INDEX << (WITHDRAWALS_TREE_HEIGHT + 1)) | uint256(withdrawalProof.withdrawalIndex); return Merkle.verifyInclusionSha256({ - proof: withdrawalContainerRootProof, - root: executionPayloadRoot, + proof: withdrawalProof.withdrawalContainerRootProof, + root: withdrawalProof.executionPayloadRoot, leaf: withdrawalContainerRoot, - index: leafIndex + index: withdrawalIndex }); } - function isValidHistoricalSummaryRoot( - bytes32 beaconStateRoot, - bytes32[] calldata historicalSummaryBlockRootProof, - uint256 historicalSummaryIndex, - bytes32 beaconBlockRoot, - uint256 blockRootIndex - ) internal view returns (bool) { + function isValidHistoricalSummaryRoot(WithdrawalProof calldata withdrawalProof) internal view returns (bool) { require( - historicalSummaryBlockRootProof.length - == (BEACON_STATE_FIELD_TREE_HEIGHT + (HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + 1 + (BLOCK_ROOTS_TREE_HEIGHT)), + withdrawalProof.historicalSummaryBlockRootProof.length + == BEACON_STATE_FIELD_TREE_HEIGHT + (HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + 1 + (BLOCK_ROOTS_TREE_HEIGHT), "historicalSummaryBlockRootProof has incorrect length" ); - /** - * Note: Here, the "1" in "1 + (BLOCK_ROOTS_TREE_HEIGHT)" signifies that extra step of choosing the - * "block_root_summary" within the individual - * "historical_summary". Everywhere else it signifies merkelize_with_mixin, where the length of an array is - * hashed with the root of the array, - * but not here. - */ + uint256 historicalBlockHeaderIndex = ( HISTORICAL_SUMMARIES_INDEX << ((HISTORICAL_SUMMARIES_TREE_HEIGHT + 1) + 1 + (BLOCK_ROOTS_TREE_HEIGHT)) - ) | (historicalSummaryIndex << (1 + (BLOCK_ROOTS_TREE_HEIGHT))) - | (BLOCK_SUMMARY_ROOT_INDEX << (BLOCK_ROOTS_TREE_HEIGHT)) | blockRootIndex; + ) | (withdrawalProof.historicalSummaryIndex << (1 + (BLOCK_ROOTS_TREE_HEIGHT))) + | (BLOCK_SUMMARY_ROOT_INDEX << (BLOCK_ROOTS_TREE_HEIGHT)) | withdrawalProof.blockRootIndex; return Merkle.verifyInclusionSha256({ - proof: historicalSummaryBlockRootProof, - root: beaconStateRoot, - leaf: beaconBlockRoot, + proof: withdrawalProof.historicalSummaryBlockRootProof, + root: withdrawalProof.stateRoot, + leaf: withdrawalProof.blockRoot, index: historicalBlockHeaderIndex }); } + /** + * @dev Retrieve the withdrawal timestamp + */ + + function getWithdrawalTimestamp(WithdrawalProof calldata withdrawalProof) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(withdrawalProof.timestampRoot); + } } diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index 30b4024f..38722dc7 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -500,7 +500,6 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { // load withdrawal proof _loadWithdrawalContainer(withdrawalInfo); - withdrawalProof.beaconBlockTimestamp = activationTimestamp + SECONDS_PER_SLOT; validatorProof.beaconBlockTimestamp = withdrawalProof.beaconBlockTimestamp + SECONDS_PER_SLOT; mockCurrentBlockTimestamp = validatorProof.beaconBlockTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index 1ddf633c..2d09df99 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -30,6 +30,8 @@ import {NonShortCircuitEndpointV2Mock} from "../mocks/NonShortCircuitEndpointV2M import "src/core/BeaconProxyBytecode.sol"; import "src/core/ExoCapsule.sol"; + +import "src/libraries/BeaconChainProofs.sol"; import "src/libraries/Endian.sol"; import "test/mocks/ETHPOSDepositMock.sol"; @@ -78,7 +80,7 @@ contract ExocoreDeployer is Test { IExoCapsule.ValidatorContainerProof validatorProof; bytes32[] withdrawalContainer; - IExoCapsule.WithdrawalContainerProof withdrawalProof; + BeaconChainProofs.WithdrawalProof withdrawalProof; bytes32 withdrawBeaconBlockRoot; // block root for withdrawal proof uint256 mockProofTimestamp; From 080f796ddffd6396e845c2c00c0195e4a708d573 Mon Sep 17 00:00:00 2001 From: --global Date: Thu, 11 Jul 2024 23:04:21 -0400 Subject: [PATCH 82/93] feat: withdraw index instead of withdraw timestamp --- src/core/ExoCapsule.sol | 8 +++----- src/storage/ExoCapsuleStorage.sol | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 7e9a5572..4f33aa07 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -146,13 +146,11 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { revert UnregisteredOrWithdrawnValidatorContainer(validatorPubkey); } - uint256 withdrawalTimestamp = withdrawalProof.timestampRoot.fromLittleEndianUint64(); - - if (provenWithdrawal[validatorPubkey][withdrawalTimestamp]) { - revert WithdrawalAlreadyProven(validatorPubkey, withdrawalTimestamp); + if (provenWithdrawal[validatorPubkey][withdrawalProof.withdrawalIndex]) { + revert WithdrawalAlreadyProven(validatorPubkey, withdrawalProof.withdrawalIndex); } - provenWithdrawal[validatorPubkey][withdrawalTimestamp] = true; + provenWithdrawal[validatorPubkey][withdrawalProof.withdrawalIndex] = true; _verifyValidatorContainer(validatorContainer, validatorProof); _verifyWithdrawalContainer(withdrawalContainer, withdrawalProof); diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index ab077724..43be9fdc 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -44,8 +44,7 @@ contract ExoCapsuleStorage { mapping(bytes32 pubkey => Validator validator) internal _capsuleValidators; mapping(uint256 index => bytes32 pubkey) internal _capsuleValidatorsByIndex; - /// @notice This is a mapping of validatorPubkeyHash to timestamp to whether or not they have proven a withdrawal - /// for that timestamp + /// @notice This is a mapping of validatorPubkeyHash to withdrawal index to whether or not they have proven a withdrawal mapping(bytes32 => mapping(uint256 => bool)) public provenWithdrawal; uint256[40] private __gap; From 5b6b11e9b71675189920da1578385cf9173391b1 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 15 Jul 2024 08:53:49 -0400 Subject: [PATCH 83/93] feat: reentrancy guard for sending ETH --- src/core/ExoCapsule.sol | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 4f33aa07..5d8e49b9 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -10,9 +10,9 @@ import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; import {ExoCapsuleStorage} from "../storage/ExoCapsuleStorage.sol"; import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; -import {Initializable} from "@openzeppelin-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol"; -contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { +contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsule { using BeaconChainProofs for bytes32; using Endian for bytes32; @@ -181,6 +181,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { require( amount <= withdrawableBalance, "ExoCapsule: withdrawal amount is larger than staker's withdrawable balance" ); + require(recipient != address(0), "Zero Address"); withdrawableBalance -= amount; _sendETH(recipient, amount); @@ -194,6 +195,8 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { amountToWithdraw <= nonBeaconChainETHBalance, "ExoCapsule.withdrawNonBeaconChainETHBalance: amountToWithdraw is greater than nonBeaconChainETHBalance" ); + require(recipient != address(0), "Zero Address"); + nonBeaconChainETHBalance -= amountToWithdraw; _sendETH(recipient, amountToWithdraw); emit NonBeaconChainETHWithdrawn(recipient, amountToWithdraw); @@ -248,7 +251,7 @@ contract ExoCapsule is Initializable, ExoCapsuleStorage, IExoCapsule { return validator; } - function _sendETH(address recipient, uint256 amountWei) internal { + function _sendETH(address recipient, uint256 amountWei) internal nonReentrant { (bool sent,) = recipient.call{value: amountWei}(""); if (!sent) { revert WithdrawalFailure(capsuleOwner, recipient, amountWei); From 0d4c19a9e7a0efdf8915f765422ae1885127df66 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 15 Jul 2024 08:55:21 -0400 Subject: [PATCH 84/93] fix: typo --- src/interfaces/INativeRestakingController.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/INativeRestakingController.sol b/src/interfaces/INativeRestakingController.sol index 592b1baa..34d83653 100644 --- a/src/interfaces/INativeRestakingController.sol +++ b/src/interfaces/INativeRestakingController.sol @@ -29,7 +29,7 @@ interface INativeRestakingController is IBaseRestakingController { * @notice This is called to deposit ETH that is staked on Ethereum beacon chain to Exocore network to be restaked * in future * @dev Before deposit, staker should have created the ExoCapsule that it owns and point the validator's withdrawal - * crendentials + * credentials * to the ExoCapsule owned by staker. The effective balance of `validatorContainer` would be credited as deposited * value by Exocore network. * @ param From 1128eac0c5ad03512afb910b5c4700590f02f59e Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 15 Jul 2024 09:35:31 -0400 Subject: [PATCH 85/93] fix: forge formatter --- lib/LayerZero | 2 +- lib/LayerZero-v2 | 2 +- lib/eigenlayer-beacon-oracle | 2 +- lib/forge-std | 2 +- lib/openzeppelin-contracts | 2 +- lib/openzeppelin-contracts-upgradeable | 2 +- lib/solidity-bytes-utils | 2 +- lib/solidity-examples | 2 +- src/storage/ExoCapsuleStorage.sol | 3 ++- 9 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/LayerZero b/lib/LayerZero index 48c21c39..a1fb11a3 160000 --- a/lib/LayerZero +++ b/lib/LayerZero @@ -1 +1 @@ -Subproject commit 48c21c3921931798184367fc02d3a8132b041942 +Subproject commit a1fb11a3b9c0ac449291816e71eacede4e36613e diff --git a/lib/LayerZero-v2 b/lib/LayerZero-v2 index 142846c3..7aebbd7c 160000 --- a/lib/LayerZero-v2 +++ b/lib/LayerZero-v2 @@ -1 +1 @@ -Subproject commit 142846c3d6d51e3c2a0852c41b4c2b63fcda5a0a +Subproject commit 7aebbd7c79b2dc818f7bb054aed2405ca076b9d6 diff --git a/lib/eigenlayer-beacon-oracle b/lib/eigenlayer-beacon-oracle index a4aba332..74cb886b 160000 --- a/lib/eigenlayer-beacon-oracle +++ b/lib/eigenlayer-beacon-oracle @@ -1 +1 @@ -Subproject commit a4aba33207b07bc55f1a10338507fba97f93d41f +Subproject commit 74cb886bd038c4faeb34e4856744571e8cc852eb diff --git a/lib/forge-std b/lib/forge-std index 1d9650e9..07263d19 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1d9650e951204a0ddce9ff89c32f1997984cef4d +Subproject commit 07263d193d621c4b2b0ce8b4d54af58f6957d97d diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index dc44c9f1..4b33d326 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit dc44c9f1a4c3b10af99492eed84f83ed244203f6 +Subproject commit 4b33d326fa818082649830b2dc8dab84419852d6 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index 2d081f24..2bb98f7a 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 2d081f24cac1a867f6f73d512f2022e1fa987854 +Subproject commit 2bb98f7ae310af7bc2554de59f8ed164f11c383e diff --git a/lib/solidity-bytes-utils b/lib/solidity-bytes-utils index e0115c4d..df88556c 160000 --- a/lib/solidity-bytes-utils +++ b/lib/solidity-bytes-utils @@ -1 +1 @@ -Subproject commit e0115c4d231910df47ce3b60625ce562fe4af985 +Subproject commit df88556cbbc267b33a787a3a6eaa32fd7247b589 diff --git a/lib/solidity-examples b/lib/solidity-examples index e4390844..c04e7d21 160000 --- a/lib/solidity-examples +++ b/lib/solidity-examples @@ -1 +1 @@ -Subproject commit e43908440cefdcbc93cd8e0ea863326c4bd904eb +Subproject commit c04e7d211b1b610f84761df943e6a38b0a53d304 diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index 43be9fdc..283bb25b 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -44,7 +44,8 @@ contract ExoCapsuleStorage { mapping(bytes32 pubkey => Validator validator) internal _capsuleValidators; mapping(uint256 index => bytes32 pubkey) internal _capsuleValidatorsByIndex; - /// @notice This is a mapping of validatorPubkeyHash to withdrawal index to whether or not they have proven a withdrawal + /// @notice This is a mapping of validatorPubkeyHash to withdrawal index to whether or not they have proven a + /// withdrawal mapping(bytes32 => mapping(uint256 => bool)) public provenWithdrawal; uint256[40] private __gap; From 8e27cedc664bdf7c8f6bb1e7804840c02aa87d61 Mon Sep 17 00:00:00 2001 From: --global Date: Mon, 15 Jul 2024 11:37:21 -0400 Subject: [PATCH 86/93] fix: withdraw epoch calculation --- src/core/ExoCapsule.sol | 3 +-- src/libraries/BeaconChainProofs.sol | 7 +++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 59946d9e..056b4b1b 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -135,9 +135,8 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul BeaconChainProofs.WithdrawalProof calldata withdrawalProof ) external onlyGateway returns (bool partialWithdrawal, uint256 withdrawalAmount) { bytes32 validatorPubkey = validatorContainer.getPubkey(); - uint64 withdrawableEpoch = validatorContainer.getWithdrawableEpoch(); Validator storage validator = _capsuleValidators[validatorPubkey]; - partialWithdrawal = _timestampToEpoch(validatorProof.beaconBlockTimestamp) < withdrawableEpoch; + partialWithdrawal = withdrawalProof.slotRoot.getWithdrawalEpoch() < validatorContainer.getWithdrawableEpoch(); if (!validatorContainer.verifyValidatorContainerBasic()) { revert InvalidValidatorContainer(validatorPubkey); diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 1dce16e2..04863a05 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -264,4 +264,11 @@ library BeaconChainProofs { return Endian.fromLittleEndianUint64(withdrawalProof.timestampRoot); } + /** + * @dev Converts the withdrawal's slot to an epoch + */ + function getWithdrawalEpoch(bytes32 slotRoot) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(slotRoot) / SLOTS_PER_EPOCH; + } + } From 5b246825eec8aa9f8db88783998071be0b72372c Mon Sep 17 00:00:00 2001 From: call-by Date: Mon, 15 Jul 2024 11:49:23 -0400 Subject: [PATCH 87/93] fix: lib version --- lib/LayerZero | 2 +- lib/LayerZero-v2 | 2 +- lib/eigenlayer-beacon-oracle | 2 +- lib/forge-std | 2 +- lib/openzeppelin-contracts | 2 +- lib/openzeppelin-contracts-upgradeable | 2 +- lib/solidity-bytes-utils | 2 +- lib/solidity-examples | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/LayerZero b/lib/LayerZero index a1fb11a3..48c21c39 160000 --- a/lib/LayerZero +++ b/lib/LayerZero @@ -1 +1 @@ -Subproject commit a1fb11a3b9c0ac449291816e71eacede4e36613e +Subproject commit 48c21c3921931798184367fc02d3a8132b041942 diff --git a/lib/LayerZero-v2 b/lib/LayerZero-v2 index 7aebbd7c..142846c3 160000 --- a/lib/LayerZero-v2 +++ b/lib/LayerZero-v2 @@ -1 +1 @@ -Subproject commit 7aebbd7c79b2dc818f7bb054aed2405ca076b9d6 +Subproject commit 142846c3d6d51e3c2a0852c41b4c2b63fcda5a0a diff --git a/lib/eigenlayer-beacon-oracle b/lib/eigenlayer-beacon-oracle index 74cb886b..a4aba332 160000 --- a/lib/eigenlayer-beacon-oracle +++ b/lib/eigenlayer-beacon-oracle @@ -1 +1 @@ -Subproject commit 74cb886bd038c4faeb34e4856744571e8cc852eb +Subproject commit a4aba33207b07bc55f1a10338507fba97f93d41f diff --git a/lib/forge-std b/lib/forge-std index 07263d19..1d9650e9 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 07263d193d621c4b2b0ce8b4d54af58f6957d97d +Subproject commit 1d9650e951204a0ddce9ff89c32f1997984cef4d diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 4b33d326..dc44c9f1 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 4b33d326fa818082649830b2dc8dab84419852d6 +Subproject commit dc44c9f1a4c3b10af99492eed84f83ed244203f6 diff --git a/lib/openzeppelin-contracts-upgradeable b/lib/openzeppelin-contracts-upgradeable index 2bb98f7a..2d081f24 160000 --- a/lib/openzeppelin-contracts-upgradeable +++ b/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 2bb98f7ae310af7bc2554de59f8ed164f11c383e +Subproject commit 2d081f24cac1a867f6f73d512f2022e1fa987854 diff --git a/lib/solidity-bytes-utils b/lib/solidity-bytes-utils index df88556c..e0115c4d 160000 --- a/lib/solidity-bytes-utils +++ b/lib/solidity-bytes-utils @@ -1 +1 @@ -Subproject commit df88556cbbc267b33a787a3a6eaa32fd7247b589 +Subproject commit e0115c4d231910df47ce3b60625ce562fe4af985 diff --git a/lib/solidity-examples b/lib/solidity-examples index c04e7d21..e4390844 160000 --- a/lib/solidity-examples +++ b/lib/solidity-examples @@ -1 +1 @@ -Subproject commit c04e7d211b1b610f84761df943e6a38b0a53d304 +Subproject commit e43908440cefdcbc93cd8e0ea863326c4bd904eb From 260f6deb3519f7c5e16f0c31e76262aea4f173ad Mon Sep 17 00:00:00 2001 From: call-by Date: Mon, 15 Jul 2024 11:54:19 -0400 Subject: [PATCH 88/93] fix: update reentrancy guard import --- src/core/ExoCapsule.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 056b4b1b..7eb1d1f0 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -10,7 +10,7 @@ import {WithdrawalContainer} from "../libraries/WithdrawalContainer.sol"; import {ExoCapsuleStorage} from "../storage/ExoCapsuleStorage.sol"; import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; -import {ReentrancyGuardUpgradeable} from "@openzeppelin-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol"; +import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsule { From 8b3634c72449371bcaa4907311102b9ab40bfb51 Mon Sep 17 00:00:00 2001 From: call-by Date: Mon, 15 Jul 2024 12:06:05 -0400 Subject: [PATCH 89/93] fix: add more constants for beaconchain proofs --- src/core/ExoCapsule.sol | 13 ++++++------- src/libraries/BeaconChainProofs.sol | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 7eb1d1f0..83ec0a8c 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -24,11 +24,11 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul event WithdrawalSuccess(address, address, uint256); /// @notice Emitted when a partial withdrawal claim is successfully redeemed event PartialWithdrawalRedeemed( - bytes32 pubkey, uint256 withdrawalTimestamp, address indexed recipient, uint64 partialWithdrawalAmountGwei + bytes32 pubkey, uint256 withdrawalEpoch, address indexed recipient, uint64 partialWithdrawalAmountGwei ); /// @notice Emitted when an ETH validator is prove to have fully withdrawn from the beacon chain event FullWithdrawalRedeemed( - bytes32 pubkey, uint256 withdrawalTimestamp, address indexed recipient, uint64 withdrawalAmountGwei + bytes32 pubkey, uint64 withdrawalEpoch, address indexed recipient, uint64 withdrawalAmountGwei ); /// @notice Emitted when capsuleOwner enables restaking event RestakingActivated(address indexed capsuleOwner); @@ -136,7 +136,8 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul ) external onlyGateway returns (bool partialWithdrawal, uint256 withdrawalAmount) { bytes32 validatorPubkey = validatorContainer.getPubkey(); Validator storage validator = _capsuleValidators[validatorPubkey]; - partialWithdrawal = withdrawalProof.slotRoot.getWithdrawalEpoch() < validatorContainer.getWithdrawableEpoch(); + uint64 withdrawalEpoch = withdrawalProof.slotRoot.getWithdrawalEpoch(); + partialWithdrawal = withdrawalEpoch < validatorContainer.getWithdrawableEpoch(); if (!validatorContainer.verifyValidatorContainerBasic()) { revert InvalidValidatorContainer(validatorPubkey); @@ -158,14 +159,14 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul if (partialWithdrawal) { // Immediately send ETH without sending request to Exocore side - emit PartialWithdrawalRedeemed(validatorPubkey, withdrawalTimestamp, capsuleOwner, withdrawalAmountGwei); + emit PartialWithdrawalRedeemed(validatorPubkey, withdrawalEpoch, capsuleOwner, withdrawalAmountGwei); _sendETH(capsuleOwner, withdrawalAmountGwei * GWEI_TO_WEI); } else { // Full withdrawal validator.status = VALIDATOR_STATUS.WITHDRAWN; validator.restakedBalanceGwei = 0; // If over MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32 * 1e9, then send remaining amount immediately - emit FullWithdrawalRedeemed(validatorPubkey, withdrawalTimestamp, capsuleOwner, withdrawalAmountGwei); + emit FullWithdrawalRedeemed(validatorPubkey, withdrawalEpoch, capsuleOwner, withdrawalAmountGwei); if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { uint256 amountToSend = (withdrawalAmountGwei - MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) * GWEI_TO_WEI; _sendETH(capsuleOwner, amountToSend); @@ -278,8 +279,6 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul BeaconChainProofs.WithdrawalProof calldata proof ) internal view { // To-do check withdrawalContainer length is valid - // Get withdrawal timestamp from timestamp root - uint256 withdrawalTimestamp = proof.timestampRoot.fromLittleEndianUint64(); bytes32 withdrawalContainerRoot = withdrawalContainer.merklelizeWithdrawalContainer(); bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot(proof); if (!valid) { diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 04863a05..745dd8a0 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -20,6 +20,19 @@ library BeaconChainProofs { uint256 internal constant BEACON_STATE_FIELD_TREE_HEIGHT = 5; + uint256 internal constant DENEB_FORK_TIMESTAMP = 1_710_338_135; + uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_CAPELLA = 4; + uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_DENEB = 5; // After deneb hard fork, it's + + // increased from 4 to 5 + // SLOTS_PER_HISTORICAL_ROOT = 2**13, so tree height is 13 + uint256 internal constant BLOCK_ROOTS_TREE_HEIGHT = 13; + + //Index of block_summary_root in historical_summary container + uint256 internal constant BLOCK_SUMMARY_ROOT_INDEX = 0; + //HISTORICAL_ROOTS_LIMIT = 2**24, so tree height is 24 + uint256 internal constant HISTORICAL_SUMMARIES_TREE_HEIGHT = 24; + uint256 internal constant VALIDATOR_TREE_HEIGHT = 40; // MAX_WITHDRAWALS_PER_PAYLOAD = 2**4, making tree height = 4 @@ -29,6 +42,7 @@ library BeaconChainProofs { // https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconblockbody uint256 internal constant EXECUTION_PAYLOAD_INDEX = 9; + uint256 internal constant SLOT_INDEX = 0; // in beacon block header // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader uint256 internal constant STATE_ROOT_INDEX = 3; @@ -36,7 +50,10 @@ library BeaconChainProofs { // in beacon state // https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#beaconstate uint256 internal constant VALIDATOR_TREE_ROOT_INDEX = 11; + uint256 internal constant HISTORICAL_SUMMARIES_INDEX = 27; + // in execution payload header + uint256 internal constant TIMESTAMP_INDEX = 9; //in execution payload uint256 internal constant WITHDRAWALS_INDEX = 14; From cc415934878ce6157e4ffe23f0466d81f305dee5 Mon Sep 17 00:00:00 2001 From: call-by Date: Mon, 15 Jul 2024 22:05:35 -0400 Subject: [PATCH 90/93] feat: beacon chain proof verification updated --- src/core/ExoCapsule.sol | 2 +- src/libraries/BeaconChainProofs.sol | 8 +- test/foundry/DepositWithdrawPrinciple.t.sol | 236 ++++++++++---------- test/foundry/ExocoreDeployer.t.sol | 27 ++- test/foundry/unit/ExoCapsule.t.sol | 53 ++--- 5 files changed, 158 insertions(+), 168 deletions(-) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 83ec0a8c..636546d9 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -193,7 +193,7 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul amountToWithdraw <= nonBeaconChainETHBalance, "ExoCapsule.withdrawNonBeaconChainETHBalance: amountToWithdraw is greater than nonBeaconChainETHBalance" ); - require(recipient != address(0), "Zero Address"); + require(recipient != address(0), "ExoCapsule: recipient address cannot be zero or empty"); nonBeaconChainETHBalance -= amountToWithdraw; _sendETH(recipient, amountToWithdraw); diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 745dd8a0..ebf19978 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -2,6 +2,7 @@ pragma solidity ^0.8.0; import {Endian} from "../libraries/Endian.sol"; import {Merkle} from "./Merkle.sol"; + // Utility library for parsing and PHASE0 beacon chain block headers // SSZ // Spec: https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#merkleization @@ -156,20 +157,16 @@ library BeaconChainProofs { require( proof.historicalSummaryIndex < 2 ** HISTORICAL_SUMMARIES_TREE_HEIGHT, "historicalSummaryIndex too large" ); - uint256 withdrawalTimestamp = getWithdrawalTimestamp(proof); bool validExecutionPayloadRoot = isValidExecutionPayloadRoot(proof); - bool validHistoricalSummary = isValidHistoricalSummaryRoot(proof); - bool validWCRootAgainstExecutionPayloadRoot = isValidWCRootAgainstExecutionPayloadRoot(proof, withdrawalContainerRoot); - if (validExecutionPayloadRoot && validHistoricalSummary && validWCRootAgainstExecutionPayloadRoot) { valid = true; } } - function isValidExecutionPayloadRoot(WithdrawalProof calldata withdrawalProof) internal view returns (bool) { + function isValidExecutionPayloadRoot(WithdrawalProof calldata withdrawalProof) internal pure returns (bool) { uint256 withdrawalTimestamp = getWithdrawalTimestamp(withdrawalProof); // Post deneb hard fork, executionPayloadHeader fields increased uint256 executionPayloadHeaderFieldTreeHeight = withdrawalTimestamp < DENEB_FORK_TIMESTAMP @@ -192,6 +189,7 @@ library BeaconChainProofs { withdrawalProof.timestampProof.length == executionPayloadHeaderFieldTreeHeight, "timestampProof has incorrect length" ); + return true; } function isValidWCRootAgainstExecutionPayloadRoot( diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index b8310e6f..274c8cac 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -247,7 +247,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { _testNativeDeposit(depositor, relayer, lastlyUpdatedPrincipalBalance); lastlyUpdatedPrincipalBalance += 32 ether; - _testNativeWithdraw(depositor, relayer, lastlyUpdatedPrincipalBalance); + // _testNativeWithdraw(depositor, relayer, lastlyUpdatedPrincipalBalance); } function _testNativeDeposit(Player memory depositor, Player memory relayer, uint256 lastlyUpdatedPrincipalBalance) @@ -313,7 +313,6 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { depositRequestNonce, depositRequestPayload ); - /// client chain gateway should emit MessageSent event vm.expectEmit(true, true, true, true, address(clientGateway)); emit MessageSent( @@ -350,7 +349,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { emit MessageSent( GatewayStorage.Action.RESPOND, depositResponseId, depositResponseNonce, depositResponseNativeFee ); - + console.log("--> received"); /// relayer catches the request message packet by listening to client chain event and feed it to Exocore network vm.startPrank(relayer.addr); exocoreLzEndpoint.lzReceive( @@ -400,119 +399,122 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { ); } - function _testNativeWithdraw(Player memory withdrawer, Player memory relayer, uint256 lastlyUpdatedPrincipalBalance) - internal - { - // before native withdraw, we simulate proper block environment states to make proof valid - _simulateBlockEnvironmentForNativeWithdraw(); - deal(address(capsule), 1 ether); // Deposit 1 ether to handle excess amount withdraw - - // 2. withdrawer will call clientGateway.processBeaconChainWithdrawal to withdraw from Exocore thru layerzero - - /// client chain layerzero endpoint should emit the message packet including deposit payload. - uint64 withdrawRequestNonce = 3; - uint64 withdrawalAmountGwei = _getWithdrawalAmount(withdrawalContainer); - uint256 withdrawalAmount; - if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { - withdrawalAmount = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR * GWEI_TO_WEI; - } else { - withdrawalAmount = withdrawalAmountGwei * GWEI_TO_WEI; - } - bytes memory withdrawRequestPayload = abi.encodePacked( - GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE, - bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), - bytes32(bytes20(withdrawer.addr)), - withdrawalAmount - ); - uint256 withdrawRequestNativeFee = clientGateway.quote(withdrawRequestPayload); - bytes32 withdrawRequestId = generateUID(withdrawRequestNonce, true); - - // client chain layerzero endpoint should emit the message packet including withdraw payload. - vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); - emit NewPacket( - exocoreChainId, - address(clientGateway), - address(exocoreGateway).toBytes32(), - withdrawRequestNonce, - withdrawRequestPayload - ); - // client chain gateway should emit MessageSent event - vm.expectEmit(true, true, true, true, address(clientGateway)); - emit MessageSent( - GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE, - withdrawRequestId, - withdrawRequestNonce, - withdrawRequestNativeFee - ); - - vm.startPrank(withdrawer.addr); - clientGateway.processBeaconChainWithdrawal{value: withdrawRequestNativeFee}( - validatorContainer, validatorProof, withdrawalContainer, withdrawalProof - ); - vm.stopPrank(); - - /// exocore gateway should return response message to exocore network layerzero endpoint - uint64 withdrawResponseNonce = 3; - lastlyUpdatedPrincipalBalance -= withdrawalAmount; - bytes memory withdrawResponsePayload = - abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true, lastlyUpdatedPrincipalBalance); - uint256 withdrawResponseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload); - bytes32 withdrawResponseId = generateUID(withdrawResponseNonce, false); - - // exocore gateway should return response message to exocore network layerzero endpoint - vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); - emit NewPacket( - clientChainId, - address(exocoreGateway), - address(clientGateway).toBytes32(), - withdrawResponseNonce, - withdrawResponsePayload - ); - // exocore gateway should emit MessageSent event - vm.expectEmit(true, true, true, true, address(exocoreGateway)); - emit MessageSent( - GatewayStorage.Action.RESPOND, withdrawResponseId, withdrawResponseNonce, withdrawResponseNativeFee - ); - exocoreLzEndpoint.lzReceive( - Origin(clientChainId, address(clientGateway).toBytes32(), withdrawRequestNonce), - address(exocoreGateway), - withdrawRequestId, - withdrawRequestPayload, - bytes("") - ); - - // client chain gateway should execute the response hook and emit depositResult event - vm.expectEmit(true, true, true, true, address(clientGateway)); - emit WithdrawPrincipalResult(true, address(VIRTUAL_STAKED_ETH_ADDRESS), withdrawer.addr, withdrawalAmount); - clientChainLzEndpoint.lzReceive( - Origin(exocoreChainId, address(exocoreGateway).toBytes32(), withdrawResponseNonce), - address(clientGateway), - withdrawResponseId, - withdrawResponsePayload, - bytes("") - ); - } - - function _simulateBlockEnvironmentForNativeWithdraw() internal { - // load beacon chain validator container and proof from json file - string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); - _loadValidatorContainer(withdrawalInfo); - // load withdrawal proof - _loadWithdrawalContainer(withdrawalInfo); - - validatorProof.beaconBlockTimestamp = withdrawalProof.beaconBlockTimestamp + SECONDS_PER_SLOT; - mockCurrentBlockTimestamp = validatorProof.beaconBlockTimestamp + SECONDS_PER_SLOT; - vm.warp(mockCurrentBlockTimestamp); - vm.mockCall( - address(beaconOracle), - abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, validatorProof.beaconBlockTimestamp), - abi.encode(beaconBlockRoot) - ); - vm.mockCall( - address(beaconOracle), - abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, withdrawalProof.beaconBlockTimestamp), - abi.encode(withdrawBeaconBlockRoot) - ); - } + // function _testNativeWithdraw(Player memory withdrawer, Player memory relayer, uint256 + // lastlyUpdatedPrincipalBalance) + // internal + // { + // // before native withdraw, we simulate proper block environment states to make proof valid + // _simulateBlockEnvironmentForNativeWithdraw(); + // deal(address(capsule), 1 ether); // Deposit 1 ether to handle excess amount withdraw + + // // 2. withdrawer will call clientGateway.processBeaconChainWithdrawal to withdraw from Exocore thru layerzero + + // /// client chain layerzero endpoint should emit the message packet including deposit payload. + // uint64 withdrawRequestNonce = 3; + // uint64 withdrawalAmountGwei = _getWithdrawalAmount(withdrawalContainer); + // uint256 withdrawalAmount; + // if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { + // withdrawalAmount = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR * GWEI_TO_WEI; + // } else { + // withdrawalAmount = withdrawalAmountGwei * GWEI_TO_WEI; + // } + // bytes memory withdrawRequestPayload = abi.encodePacked( + // GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE, + // bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), + // bytes32(bytes20(withdrawer.addr)), + // withdrawalAmount + // ); + // uint256 withdrawRequestNativeFee = clientGateway.quote(withdrawRequestPayload); + // bytes32 withdrawRequestId = generateUID(withdrawRequestNonce, true); + + // // client chain layerzero endpoint should emit the message packet including withdraw payload. + // vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); + // emit NewPacket( + // exocoreChainId, + // address(clientGateway), + // address(exocoreGateway).toBytes32(), + // withdrawRequestNonce, + // withdrawRequestPayload + // ); + // // client chain gateway should emit MessageSent event + // vm.expectEmit(true, true, true, true, address(clientGateway)); + // emit MessageSent( + // GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE, + // withdrawRequestId, + // withdrawRequestNonce, + // withdrawRequestNativeFee + // ); + + // vm.startPrank(withdrawer.addr); + // clientGateway.processBeaconChainWithdrawal{value: withdrawRequestNativeFee}( + // validatorContainer, validatorProof, withdrawalContainer, withdrawalProof + // ); + // vm.stopPrank(); + + // /// exocore gateway should return response message to exocore network layerzero endpoint + // uint64 withdrawResponseNonce = 3; + // lastlyUpdatedPrincipalBalance -= withdrawalAmount; + // bytes memory withdrawResponsePayload = + // abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true, + // lastlyUpdatedPrincipalBalance); + // uint256 withdrawResponseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload); + // bytes32 withdrawResponseId = generateUID(withdrawResponseNonce, false); + + // // exocore gateway should return response message to exocore network layerzero endpoint + // vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); + // emit NewPacket( + // clientChainId, + // address(exocoreGateway), + // address(clientGateway).toBytes32(), + // withdrawResponseNonce, + // withdrawResponsePayload + // ); + // // exocore gateway should emit MessageSent event + // vm.expectEmit(true, true, true, true, address(exocoreGateway)); + // emit MessageSent( + // GatewayStorage.Action.RESPOND, withdrawResponseId, withdrawResponseNonce, withdrawResponseNativeFee + // ); + // exocoreLzEndpoint.lzReceive( + // Origin(clientChainId, address(clientGateway).toBytes32(), withdrawRequestNonce), + // address(exocoreGateway), + // withdrawRequestId, + // withdrawRequestPayload, + // bytes("") + // ); + + // // client chain gateway should execute the response hook and emit depositResult event + // vm.expectEmit(true, true, true, true, address(clientGateway)); + // emit WithdrawPrincipalResult(true, address(VIRTUAL_STAKED_ETH_ADDRESS), withdrawer.addr, withdrawalAmount); + // clientChainLzEndpoint.lzReceive( + // Origin(exocoreChainId, address(exocoreGateway).toBytes32(), withdrawResponseNonce), + // address(clientGateway), + // withdrawResponseId, + // withdrawResponsePayload, + // bytes("") + // ); + // } + + // function _simulateBlockEnvironmentForNativeWithdraw() internal { + // // load beacon chain validator container and proof from json file + // string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); + // _loadValidatorContainer(withdrawalInfo); + // // load withdrawal proof + // _loadWithdrawalContainer(withdrawalInfo); + + // activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * + // SECONDS_PER_EPOCH; + // mockProofTimestamp = activationTimestamp; + // validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + // /// we set current block timestamp to be exactly one slot after the proof generation timestamp + // mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + // vm.warp(mockCurrentBlockTimestamp); + + // vm.mockCall( + // address(beaconOracle), + // abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, validatorProof.beaconBlockTimestamp), + // abi.encode(beaconBlockRoot) + // ); + // } } diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index f7c4263e..09fa9b9c 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -238,25 +238,30 @@ contract ExocoreDeployer is Test { withdrawalContainer = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalFields"); require(withdrawalContainer.length > 0, "validator container should not be empty"); - withdrawalProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot"); - require(withdrawalProof.stateRoot != bytes32(0), "state root should not be empty"); + // bytes32 array proof data + withdrawalProof.withdrawalContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalProof"); + withdrawalProof.slotProof = stdJson.readBytes32Array(withdrawalInfo, ".SlotProof"); + withdrawalProof.executionPayloadRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ExecutionPayloadProof"); + withdrawalProof.timestampProof = stdJson.readBytes32Array(withdrawalInfo, ".TimestampProof"); + withdrawalProof.historicalSummaryBlockRootProof = + stdJson.readBytes32Array(withdrawalInfo, ".HistoricalSummaryProof"); + // Index data withdrawalProof.blockRootIndex = stdJson.readUint(withdrawalInfo, ".blockHeaderRootIndex"); require(withdrawalProof.blockRootIndex != 0, "block header root index should not be 0"); - withdrawalProof.withdrawalIndex = stdJson.readUint(withdrawalInfo, ".withdrawalIndex"); - withdrawalProof.historicalSummaryIndex = stdJson.readUint(withdrawalInfo, ".historicalSummaryIndex"); require(withdrawalProof.historicalSummaryIndex != 0, "historical summary index should not be 0"); - withdrawalProof.historicalSummaryBlockRootProof = - stdJson.readBytes32Array(withdrawalInfo, ".HistoricalSummaryProof"); - withdrawalProof.withdrawalContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalProof"); - withdrawalProof.executionPayloadRoot = stdJson.readBytes32(withdrawalInfo, ".executionPayloadRoot"); - withdrawalProof.executionPayloadRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ExecutionPayloadProof"); + withdrawalProof.withdrawalIndex = stdJson.readUint(withdrawalInfo, ".withdrawalIndex"); - withdrawBeaconBlockRoot = stdJson.readBytes32(withdrawalInfo, ".blockHeaderRoot"); - require(withdrawBeaconBlockRoot != bytes32(0), "beacon block root should not be empty"); + // Root data + withdrawalProof.blockRoot = stdJson.readBytes32(withdrawalInfo, ".blockHeaderRoot"); + withdrawalProof.slotRoot = stdJson.readBytes32(withdrawalInfo, ".slotRoot"); + withdrawalProof.timestampRoot = stdJson.readBytes32(withdrawalInfo, ".timestampRoot"); + withdrawalProof.executionPayloadRoot = stdJson.readBytes32(withdrawalInfo, ".executionPayloadRoot"); + withdrawalProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot"); + require(withdrawalProof.stateRoot != bytes32(0), "state root should not be empty"); } function _deploy() internal { diff --git a/test/foundry/unit/ExoCapsule.t.sol b/test/foundry/unit/ExoCapsule.t.sol index 14c4b363..71884007 100644 --- a/test/foundry/unit/ExoCapsule.t.sol +++ b/test/foundry/unit/ExoCapsule.t.sol @@ -314,22 +314,8 @@ contract WithdrawalSetup is Test { IExoCapsule.ValidatorContainerProof validatorProof; bytes32[] withdrawalContainer; - /** - * struct WithdrawalContainerProof { - * uint256 beaconBlockTimestamp; - * bytes32 executionPayloadRoot; - * bytes32[] executionPayloadRootProof; - * bytes32[] withdrawalContainerRootProof; - * bytes32[] historicalSummaryBlockRootProof; - * uint256 historicalSummaryIndex; - * bytes32 blockRoot; - * uint256 blockRootIndex; - * uint256 withdrawalIndex; - * } - */ - IExoCapsule.WithdrawalContainerProof withdrawalProof; + BeaconChainProofs.WithdrawalProof withdrawalProof; bytes32 beaconBlockRoot; // latest beacon block root - bytes32 withdrawBeaconBlockRoot; // block root for withdrawal proof ExoCapsule capsule; IBeaconChainOracle beaconOracle; @@ -420,30 +406,34 @@ contract WithdrawalSetup is Test { withdrawalContainer = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalFields"); require(withdrawalContainer.length > 0, "validator container should not be empty"); - withdrawalProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot"); - require(withdrawalProof.stateRoot != bytes32(0), "state root should not be empty"); + // bytes32 array proof data + withdrawalProof.withdrawalContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalProof"); + withdrawalProof.slotProof = stdJson.readBytes32Array(withdrawalInfo, ".SlotProof"); + withdrawalProof.executionPayloadRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ExecutionPayloadProof"); + withdrawalProof.timestampProof = stdJson.readBytes32Array(withdrawalInfo, ".TimestampProof"); + withdrawalProof.historicalSummaryBlockRootProof = + stdJson.readBytes32Array(withdrawalInfo, ".HistoricalSummaryProof"); + // Index data withdrawalProof.blockRootIndex = stdJson.readUint(withdrawalInfo, ".blockHeaderRootIndex"); require(withdrawalProof.blockRootIndex != 0, "block header root index should not be 0"); - withdrawalProof.withdrawalIndex = stdJson.readUint(withdrawalInfo, ".withdrawalIndex"); - withdrawalProof.historicalSummaryIndex = stdJson.readUint(withdrawalInfo, ".historicalSummaryIndex"); require(withdrawalProof.historicalSummaryIndex != 0, "historical summary index should not be 0"); - withdrawalProof.historicalSummaryBlockRootProof = - stdJson.readBytes32Array(withdrawalInfo, ".HistoricalSummaryProof"); - withdrawalProof.withdrawalContainerRootProof = stdJson.readBytes32Array(withdrawalInfo, ".WithdrawalProof"); - withdrawalProof.executionPayloadRoot = stdJson.readBytes32(withdrawalInfo, ".executionPayloadRoot"); - withdrawalProof.executionPayloadRootProof = stdJson.readBytes32Array(withdrawalInfo, ".ExecutionPayloadProof"); + withdrawalProof.withdrawalIndex = stdJson.readUint(withdrawalInfo, ".withdrawalIndex"); - withdrawBeaconBlockRoot = stdJson.readBytes32(withdrawalInfo, ".blockHeaderRoot"); - require(withdrawBeaconBlockRoot != bytes32(0), "beacon block root should not be empty"); + // Root data + withdrawalProof.blockRoot = stdJson.readBytes32(withdrawalInfo, ".blockHeaderRoot"); + withdrawalProof.slotRoot = stdJson.readBytes32(withdrawalInfo, ".slotRoot"); + withdrawalProof.timestampRoot = stdJson.readBytes32(withdrawalInfo, ".timestampRoot"); + withdrawalProof.executionPayloadRoot = stdJson.readBytes32(withdrawalInfo, ".executionPayloadRoot"); + withdrawalProof.stateRoot = stdJson.readBytes32(withdrawalInfo, ".beaconStateRoot"); + require(withdrawalProof.stateRoot != bytes32(0), "state root should not be empty"); } function _setTimeStamp() internal { - withdrawalProof.beaconBlockTimestamp = activationTimestamp + SECONDS_PER_SLOT; - validatorProof.beaconBlockTimestamp = withdrawalProof.beaconBlockTimestamp + SECONDS_PER_SLOT; + validatorProof.beaconBlockTimestamp = activationTimestamp + SECONDS_PER_SLOT; mockCurrentBlockTimestamp = validatorProof.beaconBlockTimestamp + SECONDS_PER_SLOT; vm.warp(mockCurrentBlockTimestamp); vm.mockCall( @@ -451,11 +441,6 @@ contract WithdrawalSetup is Test { abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, validatorProof.beaconBlockTimestamp), abi.encode(beaconBlockRoot) ); - vm.mockCall( - address(beaconOracle), - abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, withdrawalProof.beaconBlockTimestamp), - abi.encode(withdrawBeaconBlockRoot) - ); } function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { @@ -526,7 +511,7 @@ contract VerifyWithdrawalProof is WithdrawalSetup { abi.encodeWithSelector( ExoCapsule.WithdrawalAlreadyProven.selector, _getPubkey(validatorContainer), - withdrawalProof.beaconBlockTimestamp + withdrawalProof.withdrawalIndex ) ); capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); From 806c749b378c2bcc128d260a53ddbd9f7eb1cd8b Mon Sep 17 00:00:00 2001 From: call-by Date: Tue, 16 Jul 2024 08:30:06 -0400 Subject: [PATCH 91/93] fix: integration test with 32 ether deposit cap --- src/core/NativeRestakingController.sol | 2 +- test/foundry/DepositWithdrawPrinciple.t.sol | 240 ++++++++++---------- 2 files changed, 121 insertions(+), 121 deletions(-) diff --git a/src/core/NativeRestakingController.sol b/src/core/NativeRestakingController.sol index 1475c3b4..1fea7986 100644 --- a/src/core/NativeRestakingController.sol +++ b/src/core/NativeRestakingController.sol @@ -88,7 +88,7 @@ abstract contract NativeRestakingController is IExoCapsule.ValidatorContainerProof calldata validatorProof, bytes32[] calldata withdrawalContainer, BeaconChainProofs.WithdrawalProof calldata withdrawalProof - ) external payable whenNotPaused { + ) external payable whenNotPaused nonReentrant nativeRestakingEnabled { IExoCapsule capsule = _getCapsule(msg.sender); (bool partialWithdrawal, uint256 withdrawalAmount) = capsule.verifyWithdrawalProof(validatorContainer, validatorProof, withdrawalContainer, withdrawalProof); diff --git a/test/foundry/DepositWithdrawPrinciple.t.sol b/test/foundry/DepositWithdrawPrinciple.t.sol index 274c8cac..44271540 100644 --- a/test/foundry/DepositWithdrawPrinciple.t.sol +++ b/test/foundry/DepositWithdrawPrinciple.t.sol @@ -13,7 +13,6 @@ import "forge-std/Test.sol"; import "@layerzero-v2/protocol/contracts/libs/AddressCast.sol"; import "@layerzerolabs/lz-evm-protocol-v2/contracts/libs/GUID.sol"; import "@openzeppelin/contracts/utils/Create2.sol"; -import "forge-std/console.sol"; contract DepositWithdrawPrincipalTest is ExocoreDeployer { @@ -247,7 +246,7 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { _testNativeDeposit(depositor, relayer, lastlyUpdatedPrincipalBalance); lastlyUpdatedPrincipalBalance += 32 ether; - // _testNativeWithdraw(depositor, relayer, lastlyUpdatedPrincipalBalance); + _testNativeWithdraw(depositor, relayer, lastlyUpdatedPrincipalBalance); } function _testNativeDeposit(Player memory depositor, Player memory relayer, uint256 lastlyUpdatedPrincipalBalance) @@ -296,6 +295,11 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { /// client chain layerzero endpoint should emit the message packet including deposit payload. uint64 depositRequestNonce = 1; uint256 depositAmount = uint256(_getEffectiveBalance(validatorContainer)) * GWEI_TO_WEI; + // Cap to 32 ether + if (depositAmount >= 32 ether) { + depositAmount = 32 ether; + } + bytes memory depositRequestPayload = abi.encodePacked( GatewayStorage.Action.REQUEST_DEPOSIT, bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), @@ -349,7 +353,6 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { emit MessageSent( GatewayStorage.Action.RESPOND, depositResponseId, depositResponseNonce, depositResponseNativeFee ); - console.log("--> received"); /// relayer catches the request message packet by listening to client chain event and feed it to Exocore network vm.startPrank(relayer.addr); exocoreLzEndpoint.lzReceive( @@ -399,122 +402,119 @@ contract DepositWithdrawPrincipalTest is ExocoreDeployer { ); } - // function _testNativeWithdraw(Player memory withdrawer, Player memory relayer, uint256 - // lastlyUpdatedPrincipalBalance) - // internal - // { - // // before native withdraw, we simulate proper block environment states to make proof valid - // _simulateBlockEnvironmentForNativeWithdraw(); - // deal(address(capsule), 1 ether); // Deposit 1 ether to handle excess amount withdraw - - // // 2. withdrawer will call clientGateway.processBeaconChainWithdrawal to withdraw from Exocore thru layerzero - - // /// client chain layerzero endpoint should emit the message packet including deposit payload. - // uint64 withdrawRequestNonce = 3; - // uint64 withdrawalAmountGwei = _getWithdrawalAmount(withdrawalContainer); - // uint256 withdrawalAmount; - // if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { - // withdrawalAmount = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR * GWEI_TO_WEI; - // } else { - // withdrawalAmount = withdrawalAmountGwei * GWEI_TO_WEI; - // } - // bytes memory withdrawRequestPayload = abi.encodePacked( - // GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE, - // bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), - // bytes32(bytes20(withdrawer.addr)), - // withdrawalAmount - // ); - // uint256 withdrawRequestNativeFee = clientGateway.quote(withdrawRequestPayload); - // bytes32 withdrawRequestId = generateUID(withdrawRequestNonce, true); - - // // client chain layerzero endpoint should emit the message packet including withdraw payload. - // vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); - // emit NewPacket( - // exocoreChainId, - // address(clientGateway), - // address(exocoreGateway).toBytes32(), - // withdrawRequestNonce, - // withdrawRequestPayload - // ); - // // client chain gateway should emit MessageSent event - // vm.expectEmit(true, true, true, true, address(clientGateway)); - // emit MessageSent( - // GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE, - // withdrawRequestId, - // withdrawRequestNonce, - // withdrawRequestNativeFee - // ); - - // vm.startPrank(withdrawer.addr); - // clientGateway.processBeaconChainWithdrawal{value: withdrawRequestNativeFee}( - // validatorContainer, validatorProof, withdrawalContainer, withdrawalProof - // ); - // vm.stopPrank(); - - // /// exocore gateway should return response message to exocore network layerzero endpoint - // uint64 withdrawResponseNonce = 3; - // lastlyUpdatedPrincipalBalance -= withdrawalAmount; - // bytes memory withdrawResponsePayload = - // abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true, - // lastlyUpdatedPrincipalBalance); - // uint256 withdrawResponseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload); - // bytes32 withdrawResponseId = generateUID(withdrawResponseNonce, false); - - // // exocore gateway should return response message to exocore network layerzero endpoint - // vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); - // emit NewPacket( - // clientChainId, - // address(exocoreGateway), - // address(clientGateway).toBytes32(), - // withdrawResponseNonce, - // withdrawResponsePayload - // ); - // // exocore gateway should emit MessageSent event - // vm.expectEmit(true, true, true, true, address(exocoreGateway)); - // emit MessageSent( - // GatewayStorage.Action.RESPOND, withdrawResponseId, withdrawResponseNonce, withdrawResponseNativeFee - // ); - // exocoreLzEndpoint.lzReceive( - // Origin(clientChainId, address(clientGateway).toBytes32(), withdrawRequestNonce), - // address(exocoreGateway), - // withdrawRequestId, - // withdrawRequestPayload, - // bytes("") - // ); - - // // client chain gateway should execute the response hook and emit depositResult event - // vm.expectEmit(true, true, true, true, address(clientGateway)); - // emit WithdrawPrincipalResult(true, address(VIRTUAL_STAKED_ETH_ADDRESS), withdrawer.addr, withdrawalAmount); - // clientChainLzEndpoint.lzReceive( - // Origin(exocoreChainId, address(exocoreGateway).toBytes32(), withdrawResponseNonce), - // address(clientGateway), - // withdrawResponseId, - // withdrawResponsePayload, - // bytes("") - // ); - // } - - // function _simulateBlockEnvironmentForNativeWithdraw() internal { - // // load beacon chain validator container and proof from json file - // string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); - // _loadValidatorContainer(withdrawalInfo); - // // load withdrawal proof - // _loadWithdrawalContainer(withdrawalInfo); - - // activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * - // SECONDS_PER_EPOCH; - // mockProofTimestamp = activationTimestamp; - // validatorProof.beaconBlockTimestamp = mockProofTimestamp; - - // /// we set current block timestamp to be exactly one slot after the proof generation timestamp - // mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; - // vm.warp(mockCurrentBlockTimestamp); - - // vm.mockCall( - // address(beaconOracle), - // abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, validatorProof.beaconBlockTimestamp), - // abi.encode(beaconBlockRoot) - // ); - // } + function _testNativeWithdraw(Player memory withdrawer, Player memory relayer, uint256 lastlyUpdatedPrincipalBalance) + internal + { + // before native withdraw, we simulate proper block environment states to make proof valid + _simulateBlockEnvironmentForNativeWithdraw(); + deal(address(capsule), 1 ether); // Deposit 1 ether to handle excess amount withdraw + + // 2. withdrawer will call clientGateway.processBeaconChainWithdrawal to withdraw from Exocore thru layerzero + + /// client chain layerzero endpoint should emit the message packet including deposit payload. + uint64 withdrawRequestNonce = 2; + uint64 withdrawalAmountGwei = _getWithdrawalAmount(withdrawalContainer); + uint256 withdrawalAmount; + if (withdrawalAmountGwei > MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR) { + withdrawalAmount = MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR * GWEI_TO_WEI; + } else { + withdrawalAmount = withdrawalAmountGwei * GWEI_TO_WEI; + } + bytes memory withdrawRequestPayload = abi.encodePacked( + GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE, + bytes32(bytes20(VIRTUAL_STAKED_ETH_ADDRESS)), + bytes32(bytes20(withdrawer.addr)), + withdrawalAmount + ); + uint256 withdrawRequestNativeFee = clientGateway.quote(withdrawRequestPayload); + bytes32 withdrawRequestId = generateUID(withdrawRequestNonce, true); + + // client chain layerzero endpoint should emit the message packet including withdraw payload. + vm.expectEmit(true, true, true, true, address(clientChainLzEndpoint)); + emit NewPacket( + exocoreChainId, + address(clientGateway), + address(exocoreGateway).toBytes32(), + withdrawRequestNonce, + withdrawRequestPayload + ); + // client chain gateway should emit MessageSent event + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit MessageSent( + GatewayStorage.Action.REQUEST_WITHDRAW_PRINCIPAL_FROM_EXOCORE, + withdrawRequestId, + withdrawRequestNonce, + withdrawRequestNativeFee + ); + + vm.startPrank(withdrawer.addr); + clientGateway.processBeaconChainWithdrawal{value: withdrawRequestNativeFee}( + validatorContainer, validatorProof, withdrawalContainer, withdrawalProof + ); + vm.stopPrank(); + + /// exocore gateway should return response message to exocore network layerzero endpoint + uint64 withdrawResponseNonce = 3; + lastlyUpdatedPrincipalBalance -= withdrawalAmount; + bytes memory withdrawResponsePayload = + abi.encodePacked(GatewayStorage.Action.RESPOND, withdrawRequestNonce, true, lastlyUpdatedPrincipalBalance); + uint256 withdrawResponseNativeFee = exocoreGateway.quote(clientChainId, withdrawResponsePayload); + bytes32 withdrawResponseId = generateUID(withdrawResponseNonce, false); + + // exocore gateway should return response message to exocore network layerzero endpoint + vm.expectEmit(true, true, true, true, address(exocoreLzEndpoint)); + emit NewPacket( + clientChainId, + address(exocoreGateway), + address(clientGateway).toBytes32(), + withdrawResponseNonce, + withdrawResponsePayload + ); + // exocore gateway should emit MessageSent event + vm.expectEmit(true, true, true, true, address(exocoreGateway)); + emit MessageSent( + GatewayStorage.Action.RESPOND, withdrawResponseId, withdrawResponseNonce, withdrawResponseNativeFee + ); + exocoreLzEndpoint.lzReceive( + Origin(clientChainId, address(clientGateway).toBytes32(), withdrawRequestNonce), + address(exocoreGateway), + withdrawRequestId, + withdrawRequestPayload, + bytes("") + ); + + // client chain gateway should execute the response hook and emit depositResult event + vm.expectEmit(true, true, true, true, address(clientGateway)); + emit WithdrawPrincipalResult(true, address(VIRTUAL_STAKED_ETH_ADDRESS), withdrawer.addr, withdrawalAmount); + clientChainLzEndpoint.lzReceive( + Origin(exocoreChainId, address(exocoreGateway).toBytes32(), withdrawResponseNonce), + address(clientGateway), + withdrawResponseId, + withdrawResponsePayload, + bytes("") + ); + } + + function _simulateBlockEnvironmentForNativeWithdraw() internal { + // load beacon chain validator container and proof from json file + string memory withdrawalInfo = vm.readFile("test/foundry/test-data/full_withdrawal_proof.json"); + _loadValidatorContainer(withdrawalInfo); + // load withdrawal proof + _loadWithdrawalContainer(withdrawalInfo); + + activationTimestamp = BEACON_CHAIN_GENESIS_TIME + _getActivationEpoch(validatorContainer) * SECONDS_PER_EPOCH; + mockProofTimestamp = activationTimestamp; + validatorProof.beaconBlockTimestamp = mockProofTimestamp; + + /// we set current block timestamp to be exactly one slot after the proof generation timestamp + mockCurrentBlockTimestamp = mockProofTimestamp + SECONDS_PER_SLOT; + vm.warp(mockCurrentBlockTimestamp); + + vm.mockCall( + address(beaconOracle), + abi.encodeWithSelector(beaconOracle.timestampToBlockRoot.selector, validatorProof.beaconBlockTimestamp), + abi.encode(beaconBlockRoot) + ); + } } From 942632567acf1f10639f46b14785dd320542e115 Mon Sep 17 00:00:00 2001 From: call-by Date: Tue, 16 Jul 2024 08:33:39 -0400 Subject: [PATCH 92/93] fix: remove unused variable for slither check --- src/storage/ClientChainGatewayStorage.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/storage/ClientChainGatewayStorage.sol b/src/storage/ClientChainGatewayStorage.sol index dea39f85..a4b15338 100644 --- a/src/storage/ClientChainGatewayStorage.sol +++ b/src/storage/ClientChainGatewayStorage.sol @@ -24,7 +24,6 @@ contract ClientChainGatewayStorage is BootstrapStorage { // constant state variables uint256 internal constant TOKEN_ADDRESS_BYTES_LENGTH = 32; - uint256 internal constant GWEI_TO_WEI = 1e9; address internal constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; IETHPOSDeposit internal constant ETH_POS = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); // constants used for layerzero messaging From be519f2362587cd389b5d70f553cc59492080b19 Mon Sep 17 00:00:00 2001 From: call-by Date: Tue, 16 Jul 2024 08:43:26 -0400 Subject: [PATCH 93/93] fix: ignore slither for withdraw eth to recipient address --- src/core/ExoCapsule.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 636546d9..0a475153 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -249,6 +249,7 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul return validator; } + // slither-disable-next-line arbitrary-send-eth function _sendETH(address recipient, uint256 amountWei) internal nonReentrant { (bool sent,) = recipient.call{value: amountWei}(""); if (!sent) {