From a27655f5cb3824dd44c3f6e9cb3a2a265b56f1fa Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Sat, 9 Nov 2024 17:02:18 +0000 Subject: [PATCH 01/22] refactor: use correct constants A lot of the parameters such as the beacon genesis time, the deneb time and the deposit address are dependent on the network chosen for deployment. This change allows a library to provide these constants, based on the `block.chainid`. Additionally, it creates the possibility of using a separately deployed contract for integration networks (refer to the next commit), for which, the parameters can be dynamically provided via `INetworkConfig`. As a side effect, the default chain_id used in tests has been changed to let `NetworkConstants` library provide these constants during tests. --- foundry.toml | 2 + script/12_RedeployClientChainGateway.s.sol | 3 +- script/13_DepositValidator.s.sol | 4 +- script/14_CorrectBootstrapErrors.s.sol | 3 +- script/16_UpgradeExoCapsule.s.sol | 2 +- script/17_WithdrawalValidator.s.sol | 4 +- script/2_DeployBoth.s.sol | 8 +- script/7_DeployBootstrap.s.sol | 8 +- script/BaseScript.sol | 20 ---- src/core/ExoCapsule.sol | 16 +-- src/interfaces/INetworkConfig.sol | 47 +++++++++ src/libraries/BeaconChainProofs.sol | 62 +++++------ src/libraries/NetworkConstants.sol | 116 +++++++++++++++++++++ src/storage/BootstrapStorage.sol | 25 +++-- src/storage/ExoCapsuleStorage.sol | 69 ++++++++++-- test/foundry/BootstrapDepositNST.t.sol | 5 +- test/foundry/ExocoreDeployer.t.sol | 32 ++---- test/foundry/Governance.t.sol | 31 +----- test/foundry/unit/Bootstrap.t.sol | 29 ++++-- test/foundry/unit/ClientChainGateway.t.sol | 31 +----- test/foundry/unit/ExoCapsule.t.sol | 6 +- 21 files changed, 340 insertions(+), 183 deletions(-) create mode 100644 src/interfaces/INetworkConfig.sol create mode 100644 src/libraries/NetworkConstants.sol diff --git a/foundry.toml b/foundry.toml index 4a2dda20..2759d258 100644 --- a/foundry.toml +++ b/foundry.toml @@ -13,6 +13,8 @@ ignored_warnings_from = ["script", "test"] # fail compilation if the warnings are not fixed. # this is super useful for the code size warning. deny_warnings = true +# for tests, use the mainnet chain_id for NetworkConstants to work. +chain_id = 1 [rpc_endpoints] ethereum_local_rpc = "${ETHEREUM_LOCAL_RPC}" diff --git a/script/12_RedeployClientChainGateway.s.sol b/script/12_RedeployClientChainGateway.s.sol index 342101a9..423dd4bb 100644 --- a/script/12_RedeployClientChainGateway.s.sol +++ b/script/12_RedeployClientChainGateway.s.sol @@ -64,7 +64,8 @@ contract RedeployClientChainGateway is BaseScript { beaconOracleAddress: address(beaconOracle), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); // Update ClientChainGateway constructor call diff --git a/script/13_DepositValidator.s.sol b/script/13_DepositValidator.s.sol index 06fbfb75..7048ba8e 100644 --- a/script/13_DepositValidator.s.sol +++ b/script/13_DepositValidator.s.sol @@ -19,6 +19,8 @@ import "src/libraries/Endian.sol"; import {BaseScript} from "./BaseScript.sol"; import "forge-std/StdJson.sol"; +import {NetworkConstants} from "src/libraries/NetworkConstants.sol"; + contract DepositScript is BaseScript { using AddressCast for address; @@ -27,7 +29,7 @@ contract DepositScript is BaseScript { bytes32[] validatorContainer; BeaconChainProofs.ValidatorContainerProof validatorProof; - uint256 internal constant GENESIS_BLOCK_TIMESTAMP = 1_695_902_400; + uint256 internal immutable GENESIS_BLOCK_TIMESTAMP = NetworkConstants.getBeaconGenesisTimestamp(); uint256 internal constant SECONDS_PER_SLOT = 12; uint256 constant GWEI_TO_WEI = 1e9; diff --git a/script/14_CorrectBootstrapErrors.s.sol b/script/14_CorrectBootstrapErrors.s.sol index 5ba01423..27666895 100644 --- a/script/14_CorrectBootstrapErrors.s.sol +++ b/script/14_CorrectBootstrapErrors.s.sol @@ -96,7 +96,8 @@ contract CorrectBootstrapErrors is BaseScript { beaconOracleAddress: address(beaconOracle), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); diff --git a/script/16_UpgradeExoCapsule.s.sol b/script/16_UpgradeExoCapsule.s.sol index 349ef424..6ebf619e 100644 --- a/script/16_UpgradeExoCapsule.s.sol +++ b/script/16_UpgradeExoCapsule.s.sol @@ -24,7 +24,7 @@ contract UpgradeExoCapsuleScript is BaseScript { vm.selectFork(clientChain); vm.startBroadcast(deployer.privateKey); console.log("owner", capsuleBeaconContract.owner()); - ExoCapsule capsule = new ExoCapsule(); + ExoCapsule capsule = new ExoCapsule(address(0)); capsuleBeaconContract.upgradeTo(address(capsule)); vm.stopBroadcast(); diff --git a/script/17_WithdrawalValidator.s.sol b/script/17_WithdrawalValidator.s.sol index 607ae531..68cd7d7e 100644 --- a/script/17_WithdrawalValidator.s.sol +++ b/script/17_WithdrawalValidator.s.sol @@ -23,6 +23,8 @@ import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/Upgradeabl import "forge-std/StdJson.sol"; import "src/libraries/BeaconChainProofs.sol"; +import {NetworkConstants} from "src/libraries/NetworkConstants.sol"; + contract WithdrawalValidatorScript is BaseScript { using AddressCast for address; @@ -33,7 +35,7 @@ contract WithdrawalValidatorScript is BaseScript { bytes32[] withdrawalContainer; BeaconChainProofs.WithdrawalProof withdrawalProof; - uint256 internal constant GENESIS_BLOCK_TIMESTAMP = 1_695_902_400; + uint256 internal immutable GENESIS_BLOCK_TIMESTAMP = NetworkConstants.getBeaconGenesisTimestamp(); uint256 internal constant SECONDS_PER_SLOT = 12; uint256 constant GWEI_TO_WEI = 1e9; diff --git a/script/2_DeployBoth.s.sol b/script/2_DeployBoth.s.sol index d9760c3b..d17b671c 100644 --- a/script/2_DeployBoth.s.sol +++ b/script/2_DeployBoth.s.sol @@ -6,6 +6,7 @@ import "../src/core/ExocoreGateway.sol"; import {RewardVault} from "../src/core/RewardVault.sol"; import {Vault} from "../src/core/Vault.sol"; +import {NetworkConstants} from "../src/libraries/NetworkConstants.sol"; import "../src/utils/BeaconProxyBytecode.sol"; import "../src/utils/CustomProxyAdmin.sol"; import {ExocoreGatewayMock} from "../test/mocks/ExocoreGatewayMock.sol"; @@ -58,11 +59,11 @@ contract DeployScript is BaseScript { vm.startBroadcast(deployer.privateKey); // deploy beacon chain oracle - beaconOracle = _deployBeaconOracle(); + beaconOracle = new EigenLayerBeaconOracle(NetworkConstants.getBeaconGenesisTimestamp()); /// deploy implementations and beacons vaultImplementation = new Vault(); - capsuleImplementation = new ExoCapsule(); + capsuleImplementation = new ExoCapsule(address(0)); rewardVaultImplementation = new RewardVault(); vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); @@ -78,7 +79,8 @@ contract DeployScript is BaseScript { beaconOracleAddress: address(beaconOracle), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); /// deploy client chain gateway diff --git a/script/7_DeployBootstrap.s.sol b/script/7_DeployBootstrap.s.sol index bbd35099..22d5b2e2 100644 --- a/script/7_DeployBootstrap.s.sol +++ b/script/7_DeployBootstrap.s.sol @@ -61,13 +61,12 @@ contract DeployBootstrapOnly is BaseScript { // proxy deployment clientChainProxyAdmin = new CustomProxyAdmin(); - // deploy beacon chain oracle - beaconOracle = _deployBeaconOracle(); + // do not deploy beacon chain oracle, instead use the pre-requisite /// deploy vault implementation contract, capsule implementation contract, reward vault implementation contract /// that has logics called by proxy vaultImplementation = new Vault(); - capsuleImplementation = new ExoCapsule(); + capsuleImplementation = new ExoCapsule(address(0)); /// deploy the vault beacon, capsule beacon, reward vault beacon that store the implementation contract address vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); @@ -79,7 +78,8 @@ contract DeployBootstrapOnly is BaseScript { beaconOracleAddress: address(beaconOracle), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); // bootstrap logic diff --git a/script/BaseScript.sol b/script/BaseScript.sol index 2d251a56..24ac9aa1 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/IExoCapsule.sol"; import "../src/interfaces/IExocoreGateway.sol"; - import "../src/interfaces/IRewardVault.sol"; import "../src/interfaces/IVault.sol"; import "../src/utils/BeaconProxyBytecode.sol"; @@ -111,25 +110,6 @@ contract BaseScript is Script, StdCheats { exocoreRPCURL = vm.envString("EXOCORE_TESETNET_RPC"); } - function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { - uint256 GENESIS_BLOCK_TIMESTAMP; - - if (block.chainid == 1) { - GENESIS_BLOCK_TIMESTAMP = 1_606_824_023; - } else if (block.chainid == 5) { - GENESIS_BLOCK_TIMESTAMP = 1_616_508_000; - } else if (block.chainid == 11_155_111) { - GENESIS_BLOCK_TIMESTAMP = 1_655_733_600; - } else if (block.chainid == 17_000) { - GENESIS_BLOCK_TIMESTAMP = 1_695_902_400; - } else { - revert("Unsupported chainId."); - } - - EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); - return oracle; - } - function _bindPrecompileMocks() internal { uint256 previousFork = type(uint256).max; try vm.activeFork() returns (uint256 forkId) { diff --git a/src/core/ExoCapsule.sol b/src/core/ExoCapsule.sol index 5e364d1f..538cabd1 100644 --- a/src/core/ExoCapsule.sol +++ b/src/core/ExoCapsule.sol @@ -132,7 +132,8 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul } /// @notice Constructor to create the ExoCapsule contract. - constructor() { + /// @param networkConfig_ network configuration contract address. + constructor(address networkConfig_) ExoCapsuleStorage(networkConfig_) { _disableInitializers(); } @@ -205,7 +206,7 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul ) external onlyGateway returns (bool partialWithdrawal, uint256 withdrawalAmount) { bytes32 validatorPubkey = validatorContainer.getPubkey(); Validator storage validator = _capsuleValidators[validatorPubkey]; - uint64 withdrawalEpoch = withdrawalProof.slotRoot.getWithdrawalEpoch(); + uint64 withdrawalEpoch = withdrawalProof.slotRoot.getWithdrawalEpoch(getSlotsPerEpoch()); partialWithdrawal = withdrawalEpoch < validatorContainer.getWithdrawableEpoch(); uint256 withdrawalId = uint256(withdrawalContainer.getWithdrawalIndex()); @@ -379,7 +380,7 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul ) internal view { // To-do check withdrawalContainer length is valid bytes32 withdrawalContainerRoot = withdrawalContainer.merkleizeWithdrawalContainer(); - bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot(proof); + bool valid = withdrawalContainerRoot.isValidWithdrawalContainerRoot(proof, getDenebHardForkTimestamp()); if (!valid) { revert InvalidWithdrawalContainer(withdrawalContainer.getValidatorIndex()); } @@ -404,11 +405,10 @@ contract ExoCapsule is ReentrancyGuardUpgradeable, ExoCapsuleStorage, IExoCapsul /// reference: https://github.com/ethereum/consensus-specs/blob/dev/specs/bellatrix/beacon-chain.md /// @param timestamp The timestamp to convert. /// @return The epoch number. - function _timestampToEpoch(uint256 timestamp) internal pure returns (uint64) { - 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); + function _timestampToEpoch(uint256 timestamp) internal view returns (uint64) { + uint256 beaconChainGenesisTime = getBeaconGenesisTimestamp(); + require(timestamp >= beaconChainGenesisTime, "timestamp should be greater than beacon chain genesis timestamp"); + return uint64((timestamp - beaconChainGenesisTime) / getSecondsPerEpoch()); } } diff --git a/src/interfaces/INetworkConfig.sol b/src/interfaces/INetworkConfig.sol new file mode 100644 index 00000000..2d599d64 --- /dev/null +++ b/src/interfaces/INetworkConfig.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/// @title INetworkConfig +/// @notice Interface for a network config contract to report params like slots per epoch and seconds per slot. +/// @dev This interface defines the necessary functions for interacting with the NetworkConfig contract. +/// @author ExocoreNetwork +interface INetworkConfig { + + /// @notice Returns the deposit contract address. + /// @return The deposit contract address. + function getDepositContractAddress() external view returns (address); + + /// @notice Returns the Deneb hard fork timestamp. + /// @return The Deneb hard fork timestamp. + function getDenebHardForkTimestamp() external view returns (uint256); + + /// @notice Returns the number of slots per epoch. + /// @return The number of slots per epoch. + function getSlotsPerEpoch() external view returns (uint64); + + /// @notice Returns the number of seconds per slot. + /// @return The number of seconds per slot. + function getSecondsPerSlot() external view returns (uint64); + + /// @notice Returns the number of seconds per epoch. + /// @return The number of seconds per epoch. + function getSecondsPerEpoch() external view returns (uint64); + + /// @notice Returns the beacon chain genesis timestamp. + /// @return The beacon chain genesis timestamp. + function getBeaconGenesisTimestamp() external view returns (uint256); + +} + +/// @notice Struct representing the configuration of a network. +/// @param depositContractAddress The address of the deposit contract for the network. +/// @param denebHardForkTimestamp The timestamp of the Deneb hard fork for the network. +/// @param slotsPerEpoch The number of slots in an epoch for the network. +/// @param secondsPerSlot The number of seconds in a slot for the network. +struct NetworkParams { + address depositContractAddress; + uint256 denebHardForkTimestamp; + uint64 slotsPerEpoch; + uint64 secondsPerSlot; + uint256 beaconGenesisTimestamp; +} diff --git a/src/libraries/BeaconChainProofs.sol b/src/libraries/BeaconChainProofs.sol index 7506fcf4..180e1b2a 100644 --- a/src/libraries/BeaconChainProofs.sol +++ b/src/libraries/BeaconChainProofs.sol @@ -22,56 +22,40 @@ 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 + // After deneb hard fork, it's increased from 4 to 5 + uint256 internal constant EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_DENEB = 5; - // 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 + // 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 + // HISTORICAL_ROOTS_LIMIT = 2**24, so tree height is 24 uint256 internal constant HISTORICAL_SUMMARIES_TREE_HEIGHT = 24; - + // VALIDATOR_REGISTRY_LIMIT = 2 ** 40, so tree height is 40 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 + // In beacon block body, these data points are indexed by the following numbers. The API does not change + // without incrmenting the version number, so these constants are safe to use. // 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 + // in beacon block header, ... same as above. // https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md#beaconblockheader uint256 internal constant STATE_ROOT_INDEX = 3; uint256 internal constant BODY_ROOT_INDEX = 4; - // in beacon state + // in beacon state, ... same as above // 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 + // in execution payload header, ... same as above uint256 internal constant TIMESTAMP_INDEX = 9; - //in execution payload + // in execution payload, .... same as above uint256 internal constant WITHDRAWALS_INDEX = 14; - //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 - /// @dev This constant would be used by other contracts that import this library - // slither-disable-next-line unused-state - uint64 internal constant SECONDS_PER_EPOCH = SLOTS_PER_EPOCH * SECONDS_PER_SLOT; - /// @notice This struct contains the information needed for validator container validity verification struct ValidatorContainerProof { uint256 beaconBlockTimestamp; @@ -157,17 +141,17 @@ library BeaconChainProofs { }); } - function isValidWithdrawalContainerRoot(bytes32 withdrawalContainerRoot, WithdrawalProof calldata proof) - internal - view - returns (bool valid) - { + function isValidWithdrawalContainerRoot( + bytes32 withdrawalContainerRoot, + WithdrawalProof calldata proof, + uint256 denebForkTimestamp + ) 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" ); - bool validExecutionPayloadRoot = isValidExecutionPayloadRoot(proof); + bool validExecutionPayloadRoot = isValidExecutionPayloadRoot(proof, denebForkTimestamp); bool validHistoricalSummary = isValidHistoricalSummaryRoot(proof); bool validWCRootAgainstExecutionPayloadRoot = isValidWCRootAgainstBlockRoot(proof, withdrawalContainerRoot); if (validExecutionPayloadRoot && validHistoricalSummary && validWCRootAgainstExecutionPayloadRoot) { @@ -175,10 +159,14 @@ library BeaconChainProofs { } } - function isValidExecutionPayloadRoot(WithdrawalProof calldata withdrawalProof) internal pure returns (bool) { + function isValidExecutionPayloadRoot(WithdrawalProof calldata withdrawalProof, uint256 denebForkTimestamp) + internal + pure + returns (bool) + { uint256 withdrawalTimestamp = getWithdrawalTimestamp(withdrawalProof); // Post deneb hard fork, executionPayloadHeader fields increased - uint256 executionPayloadHeaderFieldTreeHeight = withdrawalTimestamp < DENEB_FORK_TIMESTAMP + uint256 executionPayloadHeaderFieldTreeHeight = withdrawalTimestamp < denebForkTimestamp ? EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_CAPELLA : EXECUTION_PAYLOAD_HEADER_FIELD_TREE_HEIGHT_DENEB; require( @@ -292,8 +280,8 @@ library BeaconChainProofs { /** * @dev Converts the withdrawal's slot to an epoch */ - function getWithdrawalEpoch(bytes32 slotRoot) internal pure returns (uint64) { - return Endian.fromLittleEndianUint64(slotRoot) / SLOTS_PER_EPOCH; + function getWithdrawalEpoch(bytes32 slotRoot, uint64 slotsPerEpoch) internal pure returns (uint64) { + return Endian.fromLittleEndianUint64(slotRoot) / slotsPerEpoch; } } diff --git a/src/libraries/NetworkConstants.sol b/src/libraries/NetworkConstants.sol new file mode 100644 index 00000000..56044cee --- /dev/null +++ b/src/libraries/NetworkConstants.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {NetworkParams} from "../interfaces/INetworkConfig.sol"; + +/// @title NetworkConstants +/// @notice This library provides constants for known Ethereum PoS networks. +/// @author ExocoreNetwork +/// @dev It does not have `is INetworkConfig` since libraries cannot do that. +/// @dev It is a library because we do not expect the parameters to change at all. +library NetworkConstants { + + /// @notice The default number of slots in an epoch. + uint64 public constant SLOTS_PER_EPOCH_DEFAULT = 32; + + /// @notice The default number of seconds in a slot. + uint64 public constant SECONDS_PER_SLOT_DEFAULT = 12; + + /// @notice Returns the network params for the running chain ID. + /// @notice Reverts if the chain ID is not supported. + function getNetworkParams() internal view returns (NetworkParams memory) { + uint256 chainId = block.chainid; + if (chainId == 1) { + // mainnet + return NetworkParams( + // https://github.com/eth-clients/mainnet/blob/f6b7882618a5ad2c1d2731ae35e5d16a660d5bb7/metadata/config.yaml#L101 + 0x00000000219ab540356cBB839Cbe05303d7705Fa, + // https://eips.ethereum.org/EIPS/eip-7569 + 1_710_338_135, + // the `config.yaml` above uses the below preset as a base + // https://github.com/ethereum/consensus-specs/blob/a09d0c321550c5411557674a981e2b444a1178c0/presets/mainnet/phase0.yaml#L36 + SLOTS_PER_EPOCH_DEFAULT, + // https://github.com/eth-clients/mainnet/blob/f6b7882618a5ad2c1d2731ae35e5d16a660d5bb7/metadata/config.yaml#L58 + SECONDS_PER_SLOT_DEFAULT, + // https://github.com/eth-clients/mainnet?tab=readme-ov-file + 1_606_824_023 + ); + } else if (chainId == 11_155_111) { + // sepolia + return NetworkParams( + // https://github.com/eth-clients/sepolia/blob/f2c219a93c4491cee3d90c18f2f8e82aed850eab/metadata/config.yaml#L77 + 0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D, + // https://eips.ethereum.org/EIPS/eip-7569 + 1_706_655_072, + // the `config.yaml` above uses the below preset as a base + // https://github.com/ethereum/consensus-specs/blob/a09d0c321550c5411557674a981e2b444a1178c0/presets/mainnet/phase0.yaml#L36 + SLOTS_PER_EPOCH_DEFAULT, + // https://github.com/eth-clients/sepolia/blob/f2c219a93c4491cee3d90c18f2f8e82aed850eab/metadata/config.yaml#L42 + SECONDS_PER_SLOT_DEFAULT, + // https://github.com/eth-clients/sepolia?tab=readme-ov-file#meta-data-bepolia + 1_655_733_600 + ); + } else if (chainId == 17_000) { + // holesky + return NetworkParams( + // https://github.com/eth-clients/holesky/blob/901c0f33339f8e79250a1053dc9d995270b666e9/metadata/config.yaml#L78 + 0x4242424242424242424242424242424242424242, + // https://eips.ethereum.org/EIPS/eip-7569 + 1_707_305_664, + // the `config.yaml` above uses the below preset as a base + // https://github.com/ethereum/consensus-specs/blob/a09d0c321550c5411557674a981e2b444a1178c0/presets/mainnet/phase0.yaml#L36 + SLOTS_PER_EPOCH_DEFAULT, + // https://github.com/eth-clients/holesky/blob/901c0f33339f8e79250a1053dc9d995270b666e9/metadata/config.yaml#L43 + SECONDS_PER_SLOT_DEFAULT, + // Holesky launched with Shanghai fork (which has the Beacon), hence there is no separate genesis time + // for the beacon. + // In other words, the genesis time of the execution layer is the same as that of the Beacon. + // https://github.com/eth-clients/holesky?tab=readme-ov-file#metadata + 1_695_902_400 + ); + } else { + // note that goerli is deprecated + revert("Unsupported network"); + } + } + + /// @notice Returns the deposit contract address. + /// @return The deposit contract address. + function getDepositContractAddress() external view returns (address) { + return getNetworkParams().depositContractAddress; + } + + /// @notice Returns the Deneb hard fork timestamp. + /// @return The Deneb hard fork timestamp. + function getDenebHardForkTimestamp() external view returns (uint256) { + return getNetworkParams().denebHardForkTimestamp; + } + + /// @notice Returns the number of slots per epoch. + /// @return The number of slots per epoch. + function getSlotsPerEpoch() external view returns (uint64) { + // technically it is known to us that this is always 32 but we avoid returning the constant intentionally. + return getNetworkParams().slotsPerEpoch; + } + + /// @notice Returns the number of seconds per slot. + /// @return The number of seconds per slot. + function getSecondsPerSlot() external view returns (uint64) { + // technically it is known to us that this is always 12 but we avoid returning the constant intentionally. + return getNetworkParams().secondsPerSlot; + } + + /// @notice Returns the number of seconds per epoch. + /// @return The number of seconds per epoch. + function getSecondsPerEpoch() external view returns (uint64) { + // reading from storage is more expensive than performing the calculation + return getNetworkParams().slotsPerEpoch * getNetworkParams().secondsPerSlot; + } + + /// @notice Returns the beacon chain genesis timestamp. + /// @return The beacon chain genesis timestamp. + function getBeaconGenesisTimestamp() external view returns (uint256) { + return getNetworkParams().beaconGenesisTimestamp; + } + +} diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index cdf84b09..6b1cb635 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +import {NetworkConstants} from "../libraries/NetworkConstants.sol"; + import {Vault} from "../core/Vault.sol"; import {IETHPOSDeposit} from "../interfaces/IETHPOSDeposit.sol"; import {IExoCapsule} from "../interfaces/IExoCapsule.sol"; +import {INetworkConfig} from "../interfaces/INetworkConfig.sol"; import {IValidatorRegistry} from "../interfaces/IValidatorRegistry.sol"; import {IVault} from "../interfaces/IVault.sol"; @@ -140,7 +143,7 @@ contract BootstrapStorage is GatewayStorage { address internal constant VIRTUAL_NST_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /// @dev The address of the ETHPOS deposit contract. - IETHPOSDeposit internal constant ETH_POS = IETHPOSDeposit(0x00000000219ab540356cBB839Cbe05303d7705Fa); + IETHPOSDeposit internal immutable ETH_POS; /// @notice Used to identify the specific Exocore chain this contract interacts with for cross-chain /// functionalities. @@ -306,11 +309,12 @@ contract BootstrapStorage is GatewayStorage { /** * @dev Struct to store the parameters to initialize the immutable variables for the contract. - * @param exocoreChainId_ The chain ID of the Exocore chain. - * @param beaconOracleAddress_ The address of the beacon chain oracle. - * @param vaultBeacon_ The address of the vault beacon. - * @param exoCapsuleBeacon_ The address of the ExoCapsule beacon. - * @param beaconProxyBytecode_ The address of the beacon proxy bytecode contract. + * @param exocoreChainId The chain ID of the Exocore chain. + * @param beaconOracleAddress The address of the beacon chain oracle. + * @param vaultBeacon The address of the vault beacon. + * @param exoCapsuleBeacon The address of the ExoCapsule beacon. + * @param beaconProxyBytecode The address of the beacon proxy bytecode contract. + * @param networkConfig The address of the network config contract, if any. */ struct ImmutableConfig { uint32 exocoreChainId; @@ -318,6 +322,7 @@ contract BootstrapStorage is GatewayStorage { address vaultBeacon; address exoCapsuleBeacon; address beaconProxyBytecode; + address networkConfig; } /// @dev Ensures that native restaking is enabled for this contract. @@ -356,6 +361,7 @@ contract BootstrapStorage is GatewayStorage { config.exocoreChainId == 0 || config.beaconOracleAddress == address(0) || config.vaultBeacon == address(0) || config.exoCapsuleBeacon == address(0) || config.beaconProxyBytecode == address(0) ) { + // networkConfig is allowed to be 0 revert Errors.InvalidImmutableConfig(); } @@ -364,6 +370,13 @@ contract BootstrapStorage is GatewayStorage { VAULT_BEACON = IBeacon(config.vaultBeacon); EXO_CAPSULE_BEACON = IBeacon(config.exoCapsuleBeacon); BEACON_PROXY_BYTECODE = BeaconProxyBytecode(config.beaconProxyBytecode); + address depositContract; + if (config.networkConfig == address(0)) { + depositContract = NetworkConstants.getDepositContractAddress(); + } else { + depositContract = INetworkConfig(config.networkConfig).getDepositContractAddress(); + } + ETH_POS = IETHPOSDeposit(depositContract); } /// @notice Returns the vault associated with the given token. diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index fa4cdaa3..c8d6f71b 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -1,13 +1,18 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.19; +import {NetworkConstants} from "../libraries/NetworkConstants.sol"; + import {INativeRestakingController} from "../interfaces/INativeRestakingController.sol"; +import {INetworkConfig} from "../interfaces/INetworkConfig.sol"; import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; /// @title ExoCapsuleStorage /// @author ExocoreNetwork /// @notice The storage contract for the ExoCapsule contract. +/// @dev It does not inherit from INetworkConfig because the functions are `internal` and not `external` or `public`. +/// Additionally, not all functions are used in the ExoCapsule contract. contract ExoCapsuleStorage { /// @notice Enum representing the status of a validator. @@ -32,13 +37,17 @@ contract ExoCapsuleStorage { } // constant state variables - /// @notice The address of the Beacon Chain's roots contract. - address public constant BEACON_ROOTS_ADDRESS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; - - /// @notice The genesis time of the Beacon Chain. - uint256 public constant BEACON_CHAIN_GENESIS_TIME = 1_606_824_023; - - /// @notice The maximum time after the withdrawal proof timestamp that a withdrawal can be proven. + /// @notice The maximum time after the deposit proof timestamp that a deposit can be proven. + /// @dev It is measured from the proof generation timestamp and not the deposit timestamp. If the proof becomes too + /// old, it can be regenerated and then submitted, as long as the beacon block root for the proof timestamp is + /// available (within the oracle or through the system contract). + /// @dev Without the beacon oracle, the maximum permissible window would be 8,191 blocks * 12 seconds / block + /// = 27.3 hours, according to EIP-4788. However, with the beacon oracle, the root is available for any timestamp + /// and hence, there is no technical limit. + /// @dev A smaller value is chosen to be more conservative, that is, the limit is more a practical one than a + /// technical one. + /// @dev On our integration test network, the seconds per slot is 4, so the maximum window becomes 9.1 hours, which + /// is higher than this one. So, there is no need to make this parameter configurable based on the network. uint256 internal constant VERIFY_BALANCE_UPDATE_WINDOW_SECONDS = 4.5 hours; /// @notice Conversion factor from gwei to wei. @@ -47,6 +56,10 @@ contract ExoCapsuleStorage { /// @notice The maximum amount of balance that a validator can restake, in gwei. uint64 public constant MAX_RESTAKED_BALANCE_GWEI_PER_VALIDATOR = 32e9; + /// @notice The address of the NetworkConfig contract. + /// @dev If it is set to the 0 address, the NetworkConstants library is used instead. + address public immutable networkConfig; + /// @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; @@ -77,4 +90,46 @@ contract ExoCapsuleStorage { /// @dev Storage gap to allow for future upgrades. uint256[40] private __gap; + /// @notice Sets the network configuration contract address for the ExoCapsule contract. + /// @param networkConfig_ The address of the NetworkConfig contract. + constructor(address networkConfig_) { + networkConfig = networkConfig_; + } + + /// @dev Gets the deneb hard fork timestamp, either from the NetworkConfig contract or the NetworkConstants library. + function getDenebHardForkTimestamp() internal view returns (uint256) { + if (networkConfig == address(0)) { + return NetworkConstants.getDenebHardForkTimestamp(); + } else { + return INetworkConfig(networkConfig).getDenebHardForkTimestamp(); + } + } + + /// @dev Gets the slots per epoch, either from the NetworkConfig contract or the NetworkConstants library. + function getSlotsPerEpoch() internal view returns (uint64) { + if (networkConfig == address(0)) { + return NetworkConstants.getSlotsPerEpoch(); + } else { + return INetworkConfig(networkConfig).getSlotsPerEpoch(); + } + } + + /// @dev Gets the seconds per slot, either from the NetworkConfig contract or the NetworkConstants library. + function getSecondsPerEpoch() internal view returns (uint64) { + if (networkConfig == address(0)) { + return NetworkConstants.getSecondsPerEpoch(); + } else { + return INetworkConfig(networkConfig).getSecondsPerEpoch(); + } + } + + /// @dev Gets the beacon genesis timestamp, either from the NetworkConfig contract or the NetworkConstants library. + function getBeaconGenesisTimestamp() internal view returns (uint256) { + if (networkConfig == address(0)) { + return NetworkConstants.getBeaconGenesisTimestamp(); + } else { + return INetworkConfig(networkConfig).getBeaconGenesisTimestamp(); + } + } + } diff --git a/test/foundry/BootstrapDepositNST.t.sol b/test/foundry/BootstrapDepositNST.t.sol index cc9c377c..67e36d36 100644 --- a/test/foundry/BootstrapDepositNST.t.sol +++ b/test/foundry/BootstrapDepositNST.t.sol @@ -81,7 +81,7 @@ contract BootstrapDepositNSTTest is Test { // deploy vault implementationcontract that has logics called by proxy vaultImplementation = new Vault(); - capsuleImplementation = new ExoCapsule(); + capsuleImplementation = new ExoCapsule(address(0)); // deploy the vault beacon that store the implementation contract address vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); @@ -100,7 +100,8 @@ contract BootstrapDepositNSTTest is Test { beaconOracleAddress: address(beaconOracle), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); diff --git a/test/foundry/ExocoreDeployer.t.sol b/test/foundry/ExocoreDeployer.t.sol index 7ba97ad3..91b0fc25 100644 --- a/test/foundry/ExocoreDeployer.t.sol +++ b/test/foundry/ExocoreDeployer.t.sol @@ -46,6 +46,8 @@ import "test/mocks/ETHPOSDepositMock.sol"; import {BootstrapStorage} from "../../src/storage/BootstrapStorage.sol"; +import {NetworkConstants} from "src/libraries/NetworkConstants.sol"; + contract ExocoreDeployer is Test { using AddressCast for address; @@ -329,12 +331,12 @@ contract ExocoreDeployer is Test { restakeToken = new ERC20PresetFixedSupply("rest", "rest", 1e34, exocoreValidatorSet.addr); clientChainLzEndpoint = new NonShortCircuitEndpointV2Mock(clientChainId, exocoreValidatorSet.addr); exocoreLzEndpoint = new NonShortCircuitEndpointV2Mock(exocoreChainId, exocoreValidatorSet.addr); - beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); + beaconOracle = IBeaconChainOracle(new EigenLayerBeaconOracle(NetworkConstants.getBeaconGenesisTimestamp())); // deploy vault implementation contract and capsule implementation contract // that has logics called by proxy vaultImplementation = new Vault(); - capsuleImplementation = new ExoCapsule(); + capsuleImplementation = new ExoCapsule(address(0)); rewardVaultImplementation = new RewardVault(); // deploy the vault beacon and capsule beacon that store the implementation contract address @@ -359,7 +361,8 @@ contract ExocoreDeployer is Test { beaconOracleAddress: address(beaconOracle), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); // Update ClientChainGateway constructor call @@ -424,29 +427,6 @@ contract ExocoreDeployer is Test { vm.stopPrank(); } - function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { - uint256 GENESIS_BLOCK_TIMESTAMP; - - // mainnet - if (block.chainid == 1) { - GENESIS_BLOCK_TIMESTAMP = 1_606_824_023; - // goerli - } else if (block.chainid == 5) { - GENESIS_BLOCK_TIMESTAMP = 1_616_508_000; - // sepolia - } else if (block.chainid == 11_155_111) { - GENESIS_BLOCK_TIMESTAMP = 1_655_733_600; - // holesky - } else if (block.chainid == 17_000) { - GENESIS_BLOCK_TIMESTAMP = 1_695_902_400; - } else { - revert("Unsupported chainId."); - } - - EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); - return oracle; - } - function _getCapsuleFromWithdrawalCredentials(bytes32 withdrawalCredentials) internal pure returns (address) { return address(bytes20(uint160(uint256(withdrawalCredentials)))); } diff --git a/test/foundry/Governance.t.sol b/test/foundry/Governance.t.sol index 4dbe7dbb..b6280f43 100644 --- a/test/foundry/Governance.t.sol +++ b/test/foundry/Governance.t.sol @@ -35,6 +35,7 @@ import "src/interfaces/IVault.sol"; import "src/utils/BeaconProxyBytecode.sol"; +import {NetworkConstants} from "src/libraries/NetworkConstants.sol"; import {BootstrapStorage} from "src/storage/BootstrapStorage.sol"; contract GovernanceTest is Test { @@ -135,11 +136,11 @@ contract GovernanceTest is Test { } function _deployClientChainGateway(address owner) internal { - beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); + beaconOracle = IBeaconChainOracle(new EigenLayerBeaconOracle(NetworkConstants.getBeaconGenesisTimestamp())); vaultImplementation = new Vault(); rewardVaultImplementation = new RewardVault(); - capsuleImplementation = new ExoCapsule(); + capsuleImplementation = new ExoCapsule(address(0)); vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); rewardVaultBeacon = new UpgradeableBeacon(address(rewardVaultImplementation)); @@ -158,7 +159,8 @@ contract GovernanceTest is Test { beaconOracleAddress: address(beaconOracle), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); // Update ClientChainGateway constructor call @@ -171,29 +173,6 @@ contract GovernanceTest is Test { clientGateway.initialize(payable(owner)); } - function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { - uint256 GENESIS_BLOCK_TIMESTAMP; - - // mainnet - if (block.chainid == 1) { - GENESIS_BLOCK_TIMESTAMP = 1_606_824_023; - // goerli - } else if (block.chainid == 5) { - GENESIS_BLOCK_TIMESTAMP = 1_616_508_000; - // sepolia - } else if (block.chainid == 11_155_111) { - GENESIS_BLOCK_TIMESTAMP = 1_655_733_600; - // holesky - } else if (block.chainid == 17_000) { - GENESIS_BLOCK_TIMESTAMP = 1_695_902_400; - } else { - revert("Unsupported chainId."); - } - - EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); - return oracle; - } - function testFuzz_MultisigCanPauseImmediately(uint8 signersMask) public { vm.assume(signersMask > 0 && signersMask < 8); // Ensure at least one signer and constrain to 3 bits diff --git a/test/foundry/unit/Bootstrap.t.sol b/test/foundry/unit/Bootstrap.t.sol index 3b0fa918..19f1cdcc 100644 --- a/test/foundry/unit/Bootstrap.t.sol +++ b/test/foundry/unit/Bootstrap.t.sol @@ -102,7 +102,7 @@ contract BootstrapTest is Test { // deploy vault implementationcontract that has logics called by proxy vaultImplementation = new Vault(); rewardVaultImplementation = new RewardVault(); - capsuleImplementation = new ExoCapsule(); + capsuleImplementation = new ExoCapsule(address(0)); // deploy the vault beacon that store the implementation contract address vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); @@ -122,7 +122,8 @@ contract BootstrapTest is Test { beaconOracleAddress: address(0x1), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); @@ -1131,7 +1132,8 @@ contract BootstrapTest is Test { beaconOracleAddress: address(0x1), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); vm.expectRevert(Errors.ZeroAddress.selector); @@ -1167,7 +1169,8 @@ contract BootstrapTest is Test { beaconOracleAddress: address(0x1), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); vm.warp(20); @@ -1204,7 +1207,8 @@ contract BootstrapTest is Test { beaconOracleAddress: address(0x1), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); vm.expectRevert(Errors.ZeroValue.selector); @@ -1240,7 +1244,8 @@ contract BootstrapTest is Test { beaconOracleAddress: address(0x1), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); vm.expectRevert(Errors.BootstrapSpawnTimeLessThanDuration.selector); @@ -1277,7 +1282,8 @@ contract BootstrapTest is Test { beaconOracleAddress: address(0x1), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); vm.expectRevert(Errors.BootstrapLockTimeAlreadyPast.selector); @@ -1314,7 +1320,8 @@ contract BootstrapTest is Test { beaconOracleAddress: address(0x1), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); vm.expectRevert(Errors.ZeroAddress.selector); @@ -1350,7 +1357,8 @@ contract BootstrapTest is Test { beaconOracleAddress: address(0x1), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); vm.expectRevert(Errors.ZeroAddress.selector); @@ -1386,7 +1394,8 @@ contract BootstrapTest is Test { beaconOracleAddress: address(0x1), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); vm.expectRevert(Errors.BootstrapClientChainDataMalformed.selector); diff --git a/test/foundry/unit/ClientChainGateway.t.sol b/test/foundry/unit/ClientChainGateway.t.sol index c469ab7f..73624752 100644 --- a/test/foundry/unit/ClientChainGateway.t.sol +++ b/test/foundry/unit/ClientChainGateway.t.sol @@ -37,6 +37,7 @@ import {IRewardVault} from "src/interfaces/IRewardVault.sol"; import "src/interfaces/IVault.sol"; import {Errors} from "src/libraries/Errors.sol"; +import {NetworkConstants} from "src/libraries/NetworkConstants.sol"; import "src/utils/BeaconProxyBytecode.sol"; contract SetUp is Test { @@ -104,11 +105,11 @@ contract SetUp is Test { function _deploy() internal { vm.startPrank(deployer.addr); - beaconOracle = IBeaconChainOracle(_deployBeaconOracle()); + beaconOracle = IBeaconChainOracle(new EigenLayerBeaconOracle(NetworkConstants.getBeaconGenesisTimestamp())); vaultImplementation = new Vault(); rewardVaultImplementation = new RewardVault(); - capsuleImplementation = new ExoCapsule(); + capsuleImplementation = new ExoCapsule(address(0)); vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); rewardVaultBeacon = new UpgradeableBeacon(address(rewardVaultImplementation)); @@ -127,7 +128,8 @@ contract SetUp is Test { beaconOracleAddress: address(beaconOracle), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(0) }); clientGatewayLogic = new ClientChainGateway(address(clientChainLzEndpoint), config, address(rewardVaultBeacon)); @@ -141,29 +143,6 @@ contract SetUp is Test { vm.stopPrank(); } - function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { - uint256 GENESIS_BLOCK_TIMESTAMP; - - // mainnet - if (block.chainid == 1) { - GENESIS_BLOCK_TIMESTAMP = 1_606_824_023; - // goerli - } else if (block.chainid == 5) { - GENESIS_BLOCK_TIMESTAMP = 1_616_508_000; - // sepolia - } else if (block.chainid == 11_155_111) { - GENESIS_BLOCK_TIMESTAMP = 1_655_733_600; - // holesky - } else if (block.chainid == 17_000) { - GENESIS_BLOCK_TIMESTAMP = 1_695_902_400; - } else { - revert("Unsupported chainId."); - } - - EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); - return oracle; - } - function generateUID(uint64 nonce, bool fromClientChainToExocore) internal view returns (bytes32 uid) { if (fromClientChainToExocore) { uid = GUID.generate( diff --git a/test/foundry/unit/ExoCapsule.t.sol b/test/foundry/unit/ExoCapsule.t.sol index 05247db6..017a96d1 100644 --- a/test/foundry/unit/ExoCapsule.t.sol +++ b/test/foundry/unit/ExoCapsule.t.sol @@ -72,7 +72,7 @@ contract DepositSetup is Test { capsuleOwner = payable(address(0x125)); - ExoCapsule phantomCapsule = new ExoCapsule(); + ExoCapsule phantomCapsule = new ExoCapsule(address(0)); address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); vm.etch(capsuleAddress, address(phantomCapsule).code); @@ -278,7 +278,7 @@ contract VerifyDepositProof is DepositSetup { ); // validator container withdrawal credentials are pointed to another capsule - ExoCapsule anotherCapsule = new ExoCapsule(); + ExoCapsule anotherCapsule = new ExoCapsule(address(0)); bytes32 gatewaySlot = bytes32(stdstore.target(address(anotherCapsule)).sig("gateway()").find()); vm.store(address(anotherCapsule), gatewaySlot, bytes32(uint256(uint160(address(this))))); @@ -364,7 +364,7 @@ contract WithdrawalSetup is Test { capsuleOwner = address(0x125); - ExoCapsule phantomCapsule = new ExoCapsule(); + ExoCapsule phantomCapsule = new ExoCapsule(address(0)); address capsuleAddress = _getCapsuleFromWithdrawalCredentials(_getWithdrawalCredentials(validatorContainer)); vm.etch(capsuleAddress, address(phantomCapsule).code); From 671d91f440ee7a4173a0c77513c37ac0d966ed30 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Sat, 9 Nov 2024 17:06:44 +0000 Subject: [PATCH 02/22] feat: introduce dynamic `NetworkConfig` --- script/integration/NetworkConfig.sol | 77 +++++++++++++++++++ test/foundry/unit/NetworkConfig.t.sol | 103 ++++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 script/integration/NetworkConfig.sol create mode 100644 test/foundry/unit/NetworkConfig.t.sol diff --git a/script/integration/NetworkConfig.sol b/script/integration/NetworkConfig.sol new file mode 100644 index 00000000..f2ae13b6 --- /dev/null +++ b/script/integration/NetworkConfig.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import {INetworkConfig, NetworkParams} from "src/interfaces/INetworkConfig.sol"; + +/// @title NetworkConfig +/// @author ExocoreNetwork +/// @notice This contract provides an interface to expose the network configuration. +/// @dev This contract is used for integration testing and is a substitute for the NetworkConstants library. Hence, it +/// is located in the `integration` folder, and it is not used in the production environment. It needs to have the +/// params defined in the constructor and they aren't changed later. +contract NetworkConfig is INetworkConfig { + + /// @notice The network configuration. + NetworkParams public params; + + /// @notice Constructs the NetworkConfig contract. + /// @param deposit The deposit contract address to set for the integration network. + /// @param denebTimestamp The deneb timestamp to set for the integration network. + /// @param slotsPerEpoch The number of slots per epoch to set for the integration network. + /// @param secondsPerSlot The number of seconds per slot to set for the integration network. + /// @param beaconGenesisTimestamp The timestamp of the beacon chain genesis. + /// @dev Given that this contract is only used during integration testing, the parameters are set in the + /// constructor and cannot be changed later. + constructor( + address deposit, + uint256 denebTimestamp, + uint64 slotsPerEpoch, + uint64 secondsPerSlot, + uint256 beaconGenesisTimestamp + ) { + // the value of 31337 is known to be a reserved chain id for testing. + // it is different from Anvil's 1337 to avoid confusion, since it does not support PoS. + // the downside of this number is that another chain id must be configured in `foundry.toml` to be used + // by default, during tests. setting this configuration also prevents NetworkConstants from complaining + // about Unsupported Network during tests, so it is worth it. + require(block.chainid == 31_337, "unsupported network"); + require(deposit != address(0), "Deposit contract address must be set for integration network"); + require(denebTimestamp > 0, "Deneb timestamp must be set for integration network"); + require(slotsPerEpoch > 0, "Slots per epoch must be set for integration network"); + require(secondsPerSlot > 0, "Seconds per slot must be set for integration network"); + require(beaconGenesisTimestamp > 0, "Beacon genesis timestamp must be set for integration network"); + params = NetworkParams(deposit, denebTimestamp, slotsPerEpoch, secondsPerSlot, beaconGenesisTimestamp); + } + + /// @inheritdoc INetworkConfig + function getDepositContractAddress() external view returns (address) { + return params.depositContractAddress; + } + + /// @inheritdoc INetworkConfig + function getDenebHardForkTimestamp() external view returns (uint256) { + return params.denebHardForkTimestamp; + } + + /// @inheritdoc INetworkConfig + function getSlotsPerEpoch() external view returns (uint64) { + return params.slotsPerEpoch; + } + + /// @inheritdoc INetworkConfig + function getSecondsPerSlot() external view returns (uint64) { + return params.secondsPerSlot; + } + + /// @inheritdoc INetworkConfig + function getSecondsPerEpoch() external view returns (uint64) { + // reading from storage is more expensive than performing the calculation + return params.slotsPerEpoch * params.secondsPerSlot; + } + + /// @inheritdoc INetworkConfig + function getBeaconGenesisTimestamp() external view returns (uint256) { + return params.beaconGenesisTimestamp; + } + +} diff --git a/test/foundry/unit/NetworkConfig.t.sol b/test/foundry/unit/NetworkConfig.t.sol new file mode 100644 index 00000000..fc2de668 --- /dev/null +++ b/test/foundry/unit/NetworkConfig.t.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Test.sol"; +import {NetworkConfig} from "script/integration/NetworkConfig.sol"; + +contract NetworkConfigTest is Test { + + NetworkConfig public networkConfig; + + uint256 public constant ALLOWED_CHAIN_ID = 31_337; + address public constant DEPOSIT_CONTRACT_ADDRESS = 0x1234567890AbcdEF1234567890aBcdef12345678; + uint256 public constant DENEB_TIMESTAMP = 1_710_338_135; + uint64 public constant SLOTS_PER_EPOCH = 32; + uint64 public constant SECONDS_PER_SLOT = 12; + uint256 public constant BEACON_GENESIS_TIMESTAMP = 1_606_824_023; + + /// @notice Sets up the chain ID for testing and initializes a new contract instance. + function setUp() public { + // Simulate the chain ID for integration testing (31337) + vm.chainId(31_337); + + // Deploy the contract with valid test parameters + networkConfig = new NetworkConfig( + DEPOSIT_CONTRACT_ADDRESS, DENEB_TIMESTAMP, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, BEACON_GENESIS_TIMESTAMP + ); + } + + /// @notice Tests if the contract was correctly initialized and returns the correct deposit contract address. + function testGetDepositContractAddress() public { + assertEq( + networkConfig.getDepositContractAddress(), DEPOSIT_CONTRACT_ADDRESS, "Deposit contract address mismatch" + ); + } + + /// @notice Tests if the contract correctly returns the Deneb hard fork timestamp. + function testGetDenebHardForkTimestamp() public { + assertEq(networkConfig.getDenebHardForkTimestamp(), DENEB_TIMESTAMP, "Deneb timestamp mismatch"); + } + + /// @notice Tests if the contract correctly returns the number of slots per epoch. + function testGetSlotsPerEpoch() public { + assertEq(networkConfig.getSlotsPerEpoch(), SLOTS_PER_EPOCH, "Slots per epoch mismatch"); + } + + /// @notice Tests if the contract correctly returns the number of seconds per slot. + function testGetSecondsPerSlot() public { + assertEq(networkConfig.getSecondsPerSlot(), SECONDS_PER_SLOT, "Seconds per slot mismatch"); + } + + /// @notice Tests if the contract correctly calculates the number of seconds per epoch. + function testGetSecondsPerEpoch() public { + assertEq(networkConfig.getSecondsPerEpoch(), SECONDS_PER_SLOT * SLOTS_PER_EPOCH, "Seconds per epoch mismatch"); + } + + /// @notice Tests if the contract correctly returns the beacon chain genesis timestamp. + function testGetBeaconGenesisTimestamp() public { + assertEq( + networkConfig.getBeaconGenesisTimestamp(), BEACON_GENESIS_TIMESTAMP, "Beacon genesis timestamp mismatch" + ); + } + + /// @notice Tests if the contract reverts when initialized with an unsupported chain ID. + function testRevertUnsupportedChainId() public { + // Change the chain ID to something other than 31337 + vm.chainId(1); + vm.expectRevert("unsupported network"); + new NetworkConfig( + DEPOSIT_CONTRACT_ADDRESS, DENEB_TIMESTAMP, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, BEACON_GENESIS_TIMESTAMP + ); + } + + /// @notice Tests if the contract reverts when initialized with an invalid deposit contract address. + function testRevertInvalidDepositAddress() public { + vm.expectRevert("Deposit contract address must be set for integration network"); + new NetworkConfig(address(0), DENEB_TIMESTAMP, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, BEACON_GENESIS_TIMESTAMP); + } + + /// @notice Tests if the contract reverts when initialized with an invalid Deneb timestamp. + function testRevertInvalidDenebTimestamp() public { + vm.expectRevert("Deneb timestamp must be set for integration network"); + new NetworkConfig(DEPOSIT_CONTRACT_ADDRESS, 0, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, BEACON_GENESIS_TIMESTAMP); + } + + /// @notice Tests if the contract reverts when initialized with invalid slots per epoch. + function testRevertInvalidSlotsPerEpoch() public { + vm.expectRevert("Slots per epoch must be set for integration network"); + new NetworkConfig(DEPOSIT_CONTRACT_ADDRESS, DENEB_TIMESTAMP, 0, SECONDS_PER_SLOT, BEACON_GENESIS_TIMESTAMP); + } + + /// @notice Tests if the contract reverts when initialized with invalid seconds per slot. + function testRevertInvalidSecondsPerSlot() public { + vm.expectRevert("Seconds per slot must be set for integration network"); + new NetworkConfig(DEPOSIT_CONTRACT_ADDRESS, DENEB_TIMESTAMP, SLOTS_PER_EPOCH, 0, BEACON_GENESIS_TIMESTAMP); + } + + /// @notice Tests if the contract reverts when initialized with an invalid beacon genesis timestamp. + function testRevertInvalidBeaconGenesisTimestamp() public { + vm.expectRevert("Beacon genesis timestamp must be set for integration network"); + new NetworkConfig(DEPOSIT_CONTRACT_ADDRESS, DENEB_TIMESTAMP, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, 0); + } + +} From 0055da2af5e18fa3572230780297071e1f97ed6e Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Sat, 9 Nov 2024 22:23:03 +0000 Subject: [PATCH 03/22] attempt to add deposit and prove scripts --- .env.example | 11 +- .gitignore | 2 + script/integration/1_DeployBootstrap.s.sol | 151 +++++++++++++------- script/integration/2_VerifyDepositNST.s.sol | 64 +++++++++ script/integration/BeaconOracle.sol | 106 ++++++++++++++ script/integration/NetworkConfig.sol | 5 +- script/integration/deposit.sh | 27 ++++ script/integration/prove.sh | 44 ++++++ 8 files changed, 360 insertions(+), 50 deletions(-) create mode 100644 script/integration/2_VerifyDepositNST.s.sol create mode 100644 script/integration/BeaconOracle.sol create mode 100755 script/integration/deposit.sh create mode 100755 script/integration/prove.sh diff --git a/.env.example b/.env.example index ae511fa6..ec3dbc6a 100644 --- a/.env.example +++ b/.env.example @@ -23,4 +23,13 @@ EXCHANGE_RATES= BASE_GENESIS_FILE_PATH= RESULT_GENESIS_FILE_PATH= -ETHERSCAN_API_KEY= \ No newline at end of file +# For contract verification +ETHERSCAN_API_KEY= + +# These are used for integration testing ETH PoS +INTEGRATION_DEPOSIT_ADDRESS=0x6969696969696969696969696969696969696969 +INTEGRATION_SECONDS_PER_EPOCH=4 +INTEGRATION_SLOTS_PER_EPOCH=3 +INTEGRATION_BEACON_GENESIS_TIMESTAMP= +INTEGRATION_DENEB_TIMESTAMP= +NST_DEPOSITOR= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 877c3346..a9054f07 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,5 @@ node_modules ## secret .secrets + +script/integration/*.json diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index 0971f1b7..a38f4939 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -2,45 +2,56 @@ pragma solidity ^0.8.0; import "forge-std/Script.sol"; + +import "forge-std/StdJson.sol"; import "forge-std/console.sol"; -import "@beacon-oracle/contracts/src/EigenLayerBeaconOracle.sol"; import {IBeacon} from "@openzeppelin/contracts/proxy/beacon/IBeacon.sol"; import {UpgradeableBeacon} from "@openzeppelin/contracts/proxy/beacon/UpgradeableBeacon.sol"; -import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {EndpointV2Mock} from "../../test/mocks/EndpointV2Mock.sol"; import {Bootstrap} from "../../src/core/Bootstrap.sol"; + import {BootstrapStorage} from "../../src/storage/BootstrapStorage.sol"; +import {BeaconOracle} from "./BeaconOracle.sol"; +import {ALLOWED_CHAIN_ID, NetworkConfig} from "./NetworkConfig.sol"; + import {ExoCapsule} from "../../src/core/ExoCapsule.sol"; import {Vault} from "../../src/core/Vault.sol"; import {IExoCapsule} from "../../src/interfaces/IExoCapsule.sol"; import {IValidatorRegistry} from "../../src/interfaces/IValidatorRegistry.sol"; import {IVault} from "../../src/interfaces/IVault.sol"; -import "../../src/utils/BeaconProxyBytecode.sol"; +import {BeaconProxyBytecode} from "../../src/utils/BeaconProxyBytecode.sol"; import {CustomProxyAdmin} from "../../src/utils/CustomProxyAdmin.sol"; import {MyToken} from "../../test/foundry/unit/MyToken.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 +// because it is a script that is used to deploy the contracts on Anvil / Prysm PoS // and setup the initial state of the Exocore chain. // The keys provided in the dot-env file are required to be already // initialized by Anvil by `anvil --accounts 20`. // When you run with this config, the keys already in the file will work -// because Anvil uses a common mnemonic across systems. +// because Anvil uses a common mnemonic across systems, which is also shared by Prysm. contract DeployContracts is Script { + using stdJson for string; + + // no cross-chain communication is part of this test so these are not relevant uint16 exocoreChainId = 1; uint16 clientChainId = 2; + // neither is the ownership of the contract being tested here address exocoreValidatorSet = vm.addr(uint256(0x8)); + // assumes 3 validators, to add more - change registerValidators and delegate. uint256[] validators; uint256[] stakers; uint256 contractDeployer; + uint256 nstDepositor; Bootstrap bootstrap; // to add more tokens, // 0. add deployer private keys @@ -56,15 +67,27 @@ contract DeployContracts is Script { IVault[] vaults; CustomProxyAdmin proxyAdmin; - EigenLayerBeaconOracle beaconOracle; + BeaconOracle beaconOracle; IVault vaultImplementation; IExoCapsule capsuleImplementation; IBeacon vaultBeacon; IBeacon capsuleBeacon; BeaconProxyBytecode beaconProxyBytecode; + NetworkConfig networkConfig; + + address internal constant VIRTUAL_STAKED_ETH_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + + address depositAddress; + uint256 denebTimestamp; + uint64 secondsPerSlot; + uint64 slotsPerEpoch; + uint256 beaconGenesisTimestamp; function setUp() private { + // placate the pre-simulation runner + vm.chainId(ALLOWED_CHAIN_ID); // these are default values for Anvil's usual mnemonic. + // the addresses are also funded in the prysm ethpos devnet! uint256[] memory ANVIL_VALIDATORS = new uint256[](3); ANVIL_VALIDATORS[0] = uint256(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80); ANVIL_VALIDATORS[1] = uint256(0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d); @@ -85,13 +108,36 @@ contract DeployContracts is Script { uint256 CONTRACT_DEPLOYER = uint256(0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897); + uint256 NST_DEPOSITOR = uint256(0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd); + validators = vm.envOr("ANVIL_VALIDATORS", ",", ANVIL_VALIDATORS); stakers = vm.envOr("ANVIL_STAKERS", ",", ANVIL_STAKERS); tokenDeployers = vm.envOr("ANVIL_TOKEN_DEPLOYERS", ",", ANVIL_TOKEN_DEPLOYERS); contractDeployer = vm.envOr("CONTRACT_DEPLOYER", CONTRACT_DEPLOYER); + nstDepositor = vm.envOr("NST_DEPOSITOR", NST_DEPOSITOR); + + // read the network configuration parameters and validate them + depositAddress = vm.envOr("INTEGRATION_DEPOSIT_ADDRESS", address(0x6969696969696969696969696969696969696969)); + denebTimestamp = vm.envUint("INTEGRATION_DENEB_TIMESTAMP"); + require(denebTimestamp > 0, "Deneb timestamp must be set"); + beaconGenesisTimestamp = vm.envUint("INTEGRATION_BEACON_GENESIS_TIMESTAMP"); + require(beaconGenesisTimestamp > 0, "Beacon timestamp must be set"); + // can not read uint64 from env + uint256 secondsPerSlot_ = vm.envOr("INTEGRATION_SECONDS_PER_SLOT", uint256(4)); + require(secondsPerSlot_ > 0, "Seconds per slot must be set"); + require(secondsPerSlot_ <= type(uint64).max, "Seconds per slot must be less than or equal to uint64 max"); + secondsPerSlot = uint64(secondsPerSlot_); + uint256 slotsPerEpoch_ = vm.envOr("INTEGRATION_SLOTS_PER_EPOCH", uint256(3)); + require(slotsPerEpoch_ > 0, "Slots per epoch must be set"); + require(slotsPerEpoch_ <= type(uint64).max, "Slots per epoch must be less than or equal to uint64 max"); + slotsPerEpoch = uint64(slotsPerEpoch_); } function deployTokens() private { + // first, add this guy to the whitelist so we can start from i = 1 + whitelistTokens.push(VIRTUAL_STAKED_ETH_ADDRESS); + tvlLimits.push(0); // not enforced for virtual staked eth + string[2] memory names = ["MyToken1", "MyToken2"]; string[2] memory symbols = ["MT1", "MT2"]; uint256[2] memory initialBalances = [2000 * 10 ** decimals[0], 5000 * 10 ** decimals[1]]; @@ -113,13 +159,13 @@ contract DeployContracts is Script { function deployContract() private { vm.startBroadcast(contractDeployer); - - // deploy beacon chain oracle - beaconOracle = _deployBeaconOracle(); + networkConfig = + new NetworkConfig(depositAddress, denebTimestamp, slotsPerEpoch, secondsPerSlot, beaconGenesisTimestamp); + beaconOracle = new BeaconOracle(address(networkConfig)); /// deploy vault implementation contract, capsule implementation contract vaultImplementation = new Vault(); - capsuleImplementation = new ExoCapsule(); + capsuleImplementation = new ExoCapsule(address(networkConfig)); /// deploy the vault beacon and capsule beacon vaultBeacon = new UpgradeableBeacon(address(vaultImplementation)); @@ -137,7 +183,8 @@ contract DeployContracts is Script { beaconOracleAddress: address(beaconOracle), vaultBeacon: address(vaultBeacon), exoCapsuleBeacon: address(capsuleBeacon), - beaconProxyBytecode: address(beaconProxyBytecode) + beaconProxyBytecode: address(beaconProxyBytecode), + networkConfig: address(networkConfig) }); Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); @@ -152,12 +199,15 @@ contract DeployContracts is Script { bootstrap.initialize, ( vm.addr(contractDeployer), - block.timestamp + 3 minutes, + // keep a large buffer because we are going to be depositing a lot of tokens + // and we do one tx per block + block.timestamp + 24 hours, 1 seconds, whitelistTokens, tvlLimits, address(proxyAdmin), - address(0x1), // these values don't matter for the localnet generate.js test + // the implementation upgrade and data don't matter for this test + address(0x1), bytes("123456") ) ) @@ -169,35 +219,51 @@ contract DeployContracts is Script { console.log("Bootstrap address: ", address(bootstrap)); // set the vaults - for (uint256 i = 0; i < whitelistTokens.length; i++) { + for (uint256 i = 1; i < whitelistTokens.length; i++) { IVault vault = bootstrap.tokenToVault(whitelistTokens[i]); vaults.push(vault); } } - function approveAndDeposit() private { + function approveAndDepositLST() private { // amounts deposited by each validators, for the tokens 1 and 2. uint256[2] memory validatorAmounts = [1500 * 10 ** decimals[0], 2000 * 10 ** decimals[1]]; // stakerAmounts - keep divisible by 3 for delegate uint256[2] memory stakerAmounts = [300 * 10 ** decimals[0], 600 * 10 ** decimals[1]]; - for (uint256 i = 0; i < whitelistTokens.length; i++) { + for (uint256 i = 1; i < whitelistTokens.length; i++) { for (uint256 j = 0; j < validators.length; j++) { vm.startBroadcast(validators[j]); - MyToken(whitelistTokens[i]).approve(address(vaults[i]), type(uint256).max); - bootstrap.deposit(whitelistTokens[i], validatorAmounts[i]); + MyToken(whitelistTokens[i]).approve(address(vaults[i - 1]), type(uint256).max); + bootstrap.deposit(whitelistTokens[i], validatorAmounts[i - 1]); vm.stopBroadcast(); } } - for (uint256 i = 0; i < whitelistTokens.length; i++) { + for (uint256 i = 1; i < whitelistTokens.length; i++) { for (uint256 j = 0; j < stakers.length; j++) { vm.startBroadcast(stakers[j]); - MyToken(whitelistTokens[i]).approve(address(vaults[i]), type(uint256).max); - bootstrap.deposit(whitelistTokens[i], stakerAmounts[i]); + MyToken(whitelistTokens[i]).approve(address(vaults[i - 1]), type(uint256).max); + bootstrap.deposit(whitelistTokens[i], stakerAmounts[i - 1]); vm.stopBroadcast(); } } } + function stakeNST() private { + vm.startBroadcast(nstDepositor); + address myAddress = address(bootstrap.ownerToCapsule(vm.addr(nstDepositor))); + if (myAddress == address(0)) { + myAddress = bootstrap.createExoCapsule(); + } + console.log("ExoCapsule address", myAddress); + bootstrap.stake{value: 32 ether}( + // mnemonic: margin tank lunch prison top episode peanut approve dish seat nominee illness + hex"98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9", // pubkey + hex"922a316bdc3516bfa66e88259d5e93e339ef81bc85b70e6c715542222025a28fa1e3644c853beb8c3ba76a2c5c03b726081bf605bde3a16e1f33f902cc1b6c01093c19609de87da9383fa4b1f347bd2d4222e1ae5428727a7896c8e553cc8071", // signature + bytes32(0x456934ced8f08ff106857418a6d885ba69d31e1b7fab9a931be06da25490cd1d) // deposit data root + ); + vm.stopBroadcast(); + } + function registerValidators() private { // the mnemonics corresponding to the consensus public keys are given here. to recover, // echo "${MNEMONIC}" | exocored init localnet --chain-id exocorelocal_233-1 --recover @@ -247,11 +313,11 @@ contract DeployContracts is Script { [120 * 10 ** decimals[1], 80 * 10 ** decimals[1], 400 * 10 ** decimals[1]] ] ]; - for (uint256 i = 0; i < whitelistTokens.length; i++) { + for (uint256 i = 1; i < whitelistTokens.length; i++) { for (uint256 j = 0; j < validators.length; j++) { uint256 delegator = validators[j]; for (uint256 k = 0; k < validators.length; k++) { - uint256 amount = validatorDelegations[i][j][k]; + uint256 amount = validatorDelegations[i - 1][j][k]; address validator = vm.addr(validators[k]); string memory validatorExo = bootstrap.ethToExocoreAddress(validator); vm.startBroadcast(delegator); @@ -267,12 +333,12 @@ contract DeployContracts is Script { // respectively // find a random number for those amounts for each validators // op1 = random1, op2 = random2, op3 = 1/3 - random1 - random2 - for (uint256 i = 0; i < whitelistTokens.length; i++) { + for (uint256 i = 1; i < whitelistTokens.length; i++) { for (uint256 j = 0; j < stakers.length; j++) { uint256 delegator = stakers[j]; address delegatorAddress = vm.addr(delegator); uint256 deposit = bootstrap.totalDepositAmounts(delegatorAddress, whitelistTokens[i]); - uint256 stakerDelegationToDo = (deposit * (i + 1)) / 3; + uint256 stakerDelegationToDo = (deposit * i) / 3; for (uint256 k = 0; k < validators.length; k++) { uint256 amount; if (k == validators.length - 1) { @@ -299,16 +365,24 @@ contract DeployContracts is Script { console.log("Tokens deployed"); deployContract(); console.log("Contract deployed"); - approveAndDeposit(); - console.log("Approved and deposited"); + approveAndDepositLST(); + console.log("Approved and deposited LSTs"); + stakeNST(); + console.log("Staked NST (will have to submit proof later to count the deposit)"); registerValidators(); console.log("Validators registered"); delegate(); - console.log("[Delegated]; done!"); + console.log("Delegated; done!"); for (uint256 i = 0; i < whitelistTokens.length; i++) { - console.log("Token ", i, " address: ", whitelistTokens[i]); + console.log("Token", i, " address", whitelistTokens[i]); } + + // finally save the bootstrap address + string memory key = "deployments"; + key.serialize("beaconOracleAddress", address(beaconOracle)); + string memory start = key.serialize("bootstrapAddress", address(bootstrap)); + vm.writeFile("script/integration/deployments.json", start); } // Helper function to generate a random number within a range @@ -317,23 +391,4 @@ contract DeployContracts is Script { return (uint256(keccak256(abi.encodePacked(block.timestamp, block.prevrandao))) % (_range - 1)) + 1; } - function _deployBeaconOracle() internal returns (EigenLayerBeaconOracle) { - uint256 GENESIS_BLOCK_TIMESTAMP; - - if (block.chainid == 1) { - GENESIS_BLOCK_TIMESTAMP = 1_606_824_023; - } else if (block.chainid == 5) { - GENESIS_BLOCK_TIMESTAMP = 1_616_508_000; - } else if (block.chainid == 11_155_111) { - GENESIS_BLOCK_TIMESTAMP = 1_655_733_600; - } else if (block.chainid == 17_000) { - GENESIS_BLOCK_TIMESTAMP = 1_695_902_400; - } else { - revert("Unsupported chainId."); - } - - EigenLayerBeaconOracle oracle = new EigenLayerBeaconOracle(GENESIS_BLOCK_TIMESTAMP); - return oracle; - } - } diff --git a/script/integration/2_VerifyDepositNST.s.sol b/script/integration/2_VerifyDepositNST.s.sol new file mode 100644 index 00000000..9a8a5453 --- /dev/null +++ b/script/integration/2_VerifyDepositNST.s.sol @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; + +import "forge-std/StdJson.sol"; +import "forge-std/console.sol"; + +import {Bootstrap} from "../../src/core/Bootstrap.sol"; + +import {BeaconOracle} from "./BeaconOracle.sol"; +import {ALLOWED_CHAIN_ID} from "./NetworkConfig.sol"; + +import {BeaconChainProofs} from "src/libraries/BeaconChainProofs.sol"; +import {Endian} from "src/libraries/Endian.sol"; + +contract VerifyDepositNST is Script { + + using Endian for bytes32; + using stdJson for string; + + bytes32[] validatorContainer; + BeaconChainProofs.ValidatorContainerProof validatorProof; + bytes32 beaconBlockRoot; + + address bootstrapAddress; + address beaconOracleAddress; + uint256 nstDepositor; + + function setUp() public virtual { + // vm.chainId(ALLOWED_CHAIN_ID); + // obtain the address + string memory deployments = vm.readFile("script/integration/deployments.json"); + bootstrapAddress = deployments.readAddress(".bootstrapAddress"); + require(bootstrapAddress != address(0), "Bootstrap address not found"); + beaconOracleAddress = deployments.readAddress(".beaconOracleAddress"); + require(beaconOracleAddress != address(0), "BeaconOracle address not found"); + nstDepositor = + vm.envOr("NST_DEPOSITOR", uint256(0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd)); + } + + function run() external { + vm.startBroadcast(nstDepositor); + string memory data = vm.readFile("script/integration/proof.json"); + // load the validator container + validatorContainer = data.readBytes32Array(".validatorContainer"); + // load the validator proof + validatorProof = BeaconChainProofs.ValidatorContainerProof({ + stateRoot: data.readBytes32(".stateRoot"), + stateRootProof: data.readBytes32Array(".stateRootProof"), + validatorContainerRootProof: data.readBytes32Array(".validatorContainerProof"), + validatorIndex: data.readUint(".validatorIndex"), + beaconBlockTimestamp: data.readUint(".timestamp") + }); + // since the oracle is not necessarily active during integration testing, trigger it manually + BeaconOracle oracle = BeaconOracle(beaconOracleAddress); + oracle.addTimestamp(validatorProof.beaconBlockTimestamp); + // now, the transaction + Bootstrap bootstrap = Bootstrap(bootstrapAddress); + bootstrap.verifyAndDepositNativeStake(validatorContainer, validatorProof); + vm.stopBroadcast(); + } + +} diff --git a/script/integration/BeaconOracle.sol b/script/integration/BeaconOracle.sol new file mode 100644 index 00000000..c74cb96d --- /dev/null +++ b/script/integration/BeaconOracle.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import {INetworkConfig} from "src/interfaces/INetworkConfig.sol"; +import {NetworkConstants} from "src/libraries/NetworkConstants.sol"; + +import {IBeaconChainOracle} from "@beacon-oracle/contracts/src/IBeaconChainOracle.sol"; + +/// @title BeaconOracle +/// @author Succinct Labs and ExocoreNetwork +contract BeaconOracle is IBeaconChainOracle { + + /// @notice The address of the beacon roots precompile. + /// @dev https://eips.ethereum.org/EIPS/eip-4788 + address internal constant BEACON_ROOTS = 0x000F3df6D732807Ef1319fB7B8bB8522d0Beac02; + + /// @notice The length of the beacon roots ring buffer. + /// @dev https://eips.ethereum.org/EIPS/eip-4788 + uint256 internal constant BEACON_ROOTS_HISTORY_BUFFER_LENGTH = 8191; + + /// @notice The timestamp to block root mapping. + mapping(uint256 => bytes32) public timestampToBlockRoot; + + /// @notice The genesis block timestamp. + uint256 public immutable GENESIS_BLOCK_TIMESTAMP; + + /// @notice The seconds per slot. + uint256 public immutable SECONDS_PER_SLOT; + + /// @notice The event emitted when a new block is added to the oracle. + event BeaconOracleUpdate(uint256 slot, uint256 timestamp, bytes32 blockRoot); + + /// @notice Block timestamp does not correspond to a valid slot. + error InvalidBlockTimestamp(); + + /// @notice Timestamp out of range for the the beacon roots precompile. + error TimestampOutOfRange(); + + /// @notice No block root is found using the beacon roots precompile. + error NoBlockRootFound(); + + constructor(address networkConfigAddress_) { + if (networkConfigAddress_ == address(0)) { + GENESIS_BLOCK_TIMESTAMP = NetworkConstants.getBeaconGenesisTimestamp(); + SECONDS_PER_SLOT = NetworkConstants.getSecondsPerSlot(); + } else { + INetworkConfig networkConfig = INetworkConfig(networkConfigAddress_); + GENESIS_BLOCK_TIMESTAMP = networkConfig.getBeaconGenesisTimestamp(); + SECONDS_PER_SLOT = networkConfig.getSecondsPerSlot(); + } + } + + function addTimestamp(uint256 _targetTimestamp) external { + // If the targetTimestamp is not guaranteed to be within the beacon block root ring buffer, revert. + if ((block.timestamp - _targetTimestamp) >= (BEACON_ROOTS_HISTORY_BUFFER_LENGTH * SECONDS_PER_SLOT)) { + revert TimestampOutOfRange(); + } + + // If _targetTimestamp corresponds to slot n, then the block root for slot n - 1 is returned. + (bool success,) = BEACON_ROOTS.staticcall(abi.encode(_targetTimestamp)); + + if (!success) { + revert InvalidBlockTimestamp(); + } + + uint256 slot = (_targetTimestamp - GENESIS_BLOCK_TIMESTAMP) / SECONDS_PER_SLOT; + + // Find the block root for the target timestamp. + bytes32 blockRoot = findBlockRoot(uint64(slot)); + + // Add the block root to the mapping. + timestampToBlockRoot[_targetTimestamp] = blockRoot; + + // Emit the event. + emit BeaconOracleUpdate(slot, _targetTimestamp, blockRoot); + } + + /// @notice Attempts to find the block root for the given slot. + /// @param _slot The slot to get the block root for. + /// @return blockRoot The beacon block root of the given slot. + /// @dev BEACON_ROOTS returns a block root for a given parent block's timestamp. To get the block root for slot + /// N, you use the timestamp of slot N+1. If N+1 is not avaliable, you use the timestamp of slot N+2, and + // so on. + function findBlockRoot(uint64 _slot) public view returns (bytes32 blockRoot) { + uint256 currBlockTimestamp = GENESIS_BLOCK_TIMESTAMP + ((_slot + 1) * SECONDS_PER_SLOT); + + uint256 earliestBlockTimestamp = block.timestamp - (BEACON_ROOTS_HISTORY_BUFFER_LENGTH * SECONDS_PER_SLOT); + if (currBlockTimestamp <= earliestBlockTimestamp) { + revert TimestampOutOfRange(); + } + + while (currBlockTimestamp <= block.timestamp) { + (bool success, bytes memory result) = BEACON_ROOTS.staticcall(abi.encode(currBlockTimestamp)); + if (success && result.length > 0) { + return abi.decode(result, (bytes32)); + } + + unchecked { + currBlockTimestamp += SECONDS_PER_SLOT; + } + } + + revert NoBlockRootFound(); + } + +} diff --git a/script/integration/NetworkConfig.sol b/script/integration/NetworkConfig.sol index f2ae13b6..e6d052fa 100644 --- a/script/integration/NetworkConfig.sol +++ b/script/integration/NetworkConfig.sol @@ -3,6 +3,9 @@ pragma solidity ^0.8.0; import {INetworkConfig, NetworkParams} from "src/interfaces/INetworkConfig.sol"; +/// @dev The chain ID that is allowed for integration tests. +uint256 constant ALLOWED_CHAIN_ID = 31_337; + /// @title NetworkConfig /// @author ExocoreNetwork /// @notice This contract provides an interface to expose the network configuration. @@ -34,7 +37,7 @@ contract NetworkConfig is INetworkConfig { // the downside of this number is that another chain id must be configured in `foundry.toml` to be used // by default, during tests. setting this configuration also prevents NetworkConstants from complaining // about Unsupported Network during tests, so it is worth it. - require(block.chainid == 31_337, "unsupported network"); + require(block.chainid == ALLOWED_CHAIN_ID, "only the 31337 chain ID is supported for integration tests"); require(deposit != address(0), "Deposit contract address must be set for integration network"); require(denebTimestamp > 0, "Deneb timestamp must be set for integration network"); require(slotsPerEpoch > 0, "Slots per epoch must be set for integration network"); diff --git a/script/integration/deposit.sh b/script/integration/deposit.sh new file mode 100755 index 00000000..9b9d60d8 --- /dev/null +++ b/script/integration/deposit.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# Get the directory of the script +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") + +# Fetch the validator details and save them to container.json +curl -s -X GET "http://localhost:3500/eth/v1/beacon/genesis" -H "accept: application/json" | jq > "$SCRIPT_DIR/genesis.json" + +# Ensure the request was successful +if [ $? -ne 0 ]; then + echo "Error: Failed to fetch genesis data of the beacon chain." + exit 1 +fi + +timestamp=$(jq -r .data.genesis_time "$SCRIPT_DIR/genesis.json") +# Since the devnet uses `--fork=deneb` and `DENEB_FORK_EPOCH: 0`, the deneb time is equal to the beacon genesis time + +private_key=${NST_DEPOSITOR:-"0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd"} +sender=$(cast wallet a $private_key) + +INTEGRATION_BEACON_GENESIS_TIMESTAMP=$timestamp \ +INTEGRATION_DENEB_TIMESTAMP=$timestamp \ +SENDER=$sender \ + forge script --skip-simulation script/integration/1_DeployBootstrap.s.sol \ + --rpc-url $CLIENT_CHAIN_RPC \ + --broadcast -vvvv --slow \ + --sender $SENDER \ No newline at end of file diff --git a/script/integration/prove.sh b/script/integration/prove.sh new file mode 100755 index 00000000..294a9122 --- /dev/null +++ b/script/integration/prove.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# Get the directory of the script +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") + +# Fetch the validator details and save them to container.json +curl -s -X GET "http://localhost:3500/eth/v1/beacon/states/head/validators/0x98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9" -H "accept: application/json" | jq > "$SCRIPT_DIR/container.json" + +# Ensure the request was successful +if [ $? -ne 0 ]; then + echo "Error: Failed to fetch validator details." + exit 1 +fi + +# Fetch slots per epoch from the spec +slots_per_epoch=$(curl -s http://localhost:3500/eth/v1/config/spec | jq -r .data.SLOTS_PER_EPOCH) + +# Ensure slots_per_epoch was fetched successfully +if [ -z "$slots_per_epoch" ]; then + echo "Error: Failed to fetch SLOTS_PER_EPOCH." + exit 1 +fi + +# Extract the validator index and activation epoch from container.json +validator_index=$(jq -r .data.index "$SCRIPT_DIR/container.json") +epoch=$(jq -r .data.validator.activation_eligibility_epoch "$SCRIPT_DIR/container.json") + +# Ensure epoch value is valid +if [ -z "$epoch" ] || [ "$epoch" == "null" ]; then + echo "Error: Activation epoch not found for the validator." + exit 1 +fi + +# Calculate the slot number +slot=$((slots_per_epoch * epoch)) + +# Now derive the proof using the proof generation binary, which must already be running configured to the localnet +curl -X POST -H "Content-Type: application/json" \ + -d "{\"slot\": $slot, \"validator_index\": $validator_index}" \ + http://localhost:8989/v1/validator-proof | jq > "$SCRIPT_DIR/proof.json" + +forge script script/integration/2_VerifyDepositNST.s.sol --skip-simulation \ + --rpc-url $CLIENT_CHAIN_RPC --broadcast \ + --evm-version cancun # required, otherwise you get EvmError: NotActtivated From 70c4431b77e0c83792cdb093159414eb3fb0520f Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Thu, 14 Nov 2024 20:20:20 +0000 Subject: [PATCH 04/22] fix: update proof generation script + generate.js --- .env.example | 3 +- package-lock.json | 291 +++++++++++++++++++- package.json | 2 + script/generate.js | 186 +++++++++++-- script/integration/1_DeployBootstrap.s.sol | 4 +- script/integration/2_VerifyDepositNST.s.sol | 10 +- script/integration/deposit.sh | 35 ++- script/integration/prove.sh | 35 ++- src/core/Bootstrap.sol | 22 ++ src/libraries/NetworkConstants.sol | 1 + src/storage/BootstrapStorage.sol | 4 + src/storage/ExoCapsuleStorage.sol | 20 +- test/foundry/unit/NetworkConfig.t.sol | 2 +- 13 files changed, 547 insertions(+), 68 deletions(-) diff --git a/.env.example b/.env.example index ec3dbc6a..56ed6081 100644 --- a/.env.example +++ b/.env.example @@ -22,13 +22,14 @@ BOOTSTRAP_ADDRESS= EXCHANGE_RATES= BASE_GENESIS_FILE_PATH= RESULT_GENESIS_FILE_PATH= +BEACON_CHAIN_ENDPOINT= # For contract verification ETHERSCAN_API_KEY= # These are used for integration testing ETH PoS INTEGRATION_DEPOSIT_ADDRESS=0x6969696969696969696969696969696969696969 -INTEGRATION_SECONDS_PER_EPOCH=4 +INTEGRATION_SECONDS_PER_SLOT=4 INTEGRATION_SLOTS_PER_EPOCH=3 INTEGRATION_BEACON_GENESIS_TIMESTAMP= INTEGRATION_DENEB_TIMESTAMP= diff --git a/package-lock.json b/package-lock.json index e66d06cc..1c7ee89e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@lodestar/api": "^1.23.0", "abbrev": "^1.0.9", "abstract-level": "^1.0.3", "acorn": "^8.11.2", @@ -431,6 +432,64 @@ "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz", "integrity": "sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg==" }, + "node_modules/@chainsafe/hashtree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@chainsafe/hashtree/-/hashtree-1.0.1.tgz", + "integrity": "sha512-bleu9FjqBeR/l6W1u2Lz+HsS0b0LLJX2eUt3hOPBN7VqOhidx8wzkVh2S7YurS+iTQtfdK4K5QU9tcTGNrGwDg==", + "engines": { + "node": ">= 18" + }, + "optionalDependencies": { + "@chainsafe/hashtree-darwin-arm64": "1.0.1", + "@chainsafe/hashtree-linux-arm64-gnu": "1.0.1", + "@chainsafe/hashtree-linux-x64-gnu": "1.0.1" + } + }, + "node_modules/@chainsafe/hashtree-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@chainsafe/hashtree-darwin-arm64/-/hashtree-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-+KmEgQMpO7FDL3klAcpXbQ4DPZvfCe0qSaBBrtT4vLF8V1JGm3sp+j7oibtxtOsLKz7nJMiK1pZExi7vjXu8og==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@chainsafe/hashtree-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@chainsafe/hashtree-linux-arm64-gnu/-/hashtree-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-p1hnhGq2aFY+Zhdn1Q6L/6yLYNKjqXfn/Pc8jiM0e3+Lf/hB+yCdqYVu1pto26BrZjugCFZfupHaL4DjUTDttw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@chainsafe/hashtree-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@chainsafe/hashtree-linux-x64-gnu/-/hashtree-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-uCIGuUWuWV0LiB4KLMy6JFa7Jp6NmPl3hKF5BYWu8TzUBe7vSXMZfqTzGxXPggFYN2/0KymfRdG9iDCOJfGRqg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@chainsafe/persistent-merkle-tree": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz", @@ -1243,6 +1302,194 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@lodestar/api": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@lodestar/api/-/api-1.23.0.tgz", + "integrity": "sha512-ackvkEvA2EPyvtZaniMZvM/IBrpwy+BcA7EQGTxMgzILngkkoL9WZukGb0Mq2rVe6ccU+Q0YXfG/dlByW/tW4Q==", + "dependencies": { + "@chainsafe/persistent-merkle-tree": "^0.8.0", + "@chainsafe/ssz": "^0.18.0", + "@lodestar/config": "^1.23.0", + "@lodestar/params": "^1.23.0", + "@lodestar/types": "^1.23.0", + "@lodestar/utils": "^1.23.0", + "eventsource": "^2.0.2", + "qs": "^6.11.1" + } + }, + "node_modules/@lodestar/api/node_modules/@chainsafe/as-sha256": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.5.0.tgz", + "integrity": "sha512-dTIY6oUZNdC5yDTVP5Qc9hAlKAsn0QTQ2DnQvvsbTnKSTbYs3p5RPN0aIUqN0liXei/9h24c7V0dkV44cnWIQA==" + }, + "node_modules/@lodestar/api/node_modules/@chainsafe/persistent-merkle-tree": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.8.0.tgz", + "integrity": "sha512-hh6C1JO6SKlr0QGNTNtTLqgGVMA/Bc20wD6CeMHp+wqbFKCULRJuBUxhF4WDx/7mX8QlqF3nFriF/Eo8oYJ4/A==", + "dependencies": { + "@chainsafe/as-sha256": "0.5.0", + "@chainsafe/hashtree": "1.0.1", + "@noble/hashes": "^1.3.0" + } + }, + "node_modules/@lodestar/api/node_modules/@chainsafe/ssz": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.18.0.tgz", + "integrity": "sha512-1ikTjk3JK6+fsGWiT5IvQU0AP6gF3fDzGmPfkKthbcbgTUR8fjB83Ywp9ko/ZoiDGfrSFkATgT4hvRzclu0IAA==", + "dependencies": { + "@chainsafe/as-sha256": "0.5.0", + "@chainsafe/persistent-merkle-tree": "0.8.0" + } + }, + "node_modules/@lodestar/config": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@lodestar/config/-/config-1.23.0.tgz", + "integrity": "sha512-K4MlD/LjW1IvQQL1I3QTYx1HOUITgKRmyazv9Xm7NlIk073esQW7iK0wVO8nJfW5gglTK0amQnC9SFgcGOqqYg==", + "dependencies": { + "@chainsafe/ssz": "^0.18.0", + "@lodestar/params": "^1.23.0", + "@lodestar/types": "^1.23.0", + "@lodestar/utils": "^1.23.0" + } + }, + "node_modules/@lodestar/config/node_modules/@chainsafe/as-sha256": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.5.0.tgz", + "integrity": "sha512-dTIY6oUZNdC5yDTVP5Qc9hAlKAsn0QTQ2DnQvvsbTnKSTbYs3p5RPN0aIUqN0liXei/9h24c7V0dkV44cnWIQA==" + }, + "node_modules/@lodestar/config/node_modules/@chainsafe/persistent-merkle-tree": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.8.0.tgz", + "integrity": "sha512-hh6C1JO6SKlr0QGNTNtTLqgGVMA/Bc20wD6CeMHp+wqbFKCULRJuBUxhF4WDx/7mX8QlqF3nFriF/Eo8oYJ4/A==", + "dependencies": { + "@chainsafe/as-sha256": "0.5.0", + "@chainsafe/hashtree": "1.0.1", + "@noble/hashes": "^1.3.0" + } + }, + "node_modules/@lodestar/config/node_modules/@chainsafe/ssz": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.18.0.tgz", + "integrity": "sha512-1ikTjk3JK6+fsGWiT5IvQU0AP6gF3fDzGmPfkKthbcbgTUR8fjB83Ywp9ko/ZoiDGfrSFkATgT4hvRzclu0IAA==", + "dependencies": { + "@chainsafe/as-sha256": "0.5.0", + "@chainsafe/persistent-merkle-tree": "0.8.0" + } + }, + "node_modules/@lodestar/params": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@lodestar/params/-/params-1.23.0.tgz", + "integrity": "sha512-NphFvYezC6RQg8xKUFQmEMm2YfntuirNSKo+EId1/LntXtzcZM1QTRNyuW9GJqA7mnMi+ZKs7NvE0kqU9Yocdg==" + }, + "node_modules/@lodestar/types": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@lodestar/types/-/types-1.23.0.tgz", + "integrity": "sha512-7bzS4ZaW5n+rKdErycxnP+oxzM+JaEolTaIjoUMWbuS6jADZsgh74kbJVgS2yNO6HV6a9o0igp11jUg1UcnSLw==", + "dependencies": { + "@chainsafe/ssz": "^0.18.0", + "@lodestar/params": "^1.23.0", + "ethereum-cryptography": "^2.0.0" + } + }, + "node_modules/@lodestar/types/node_modules/@chainsafe/as-sha256": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.5.0.tgz", + "integrity": "sha512-dTIY6oUZNdC5yDTVP5Qc9hAlKAsn0QTQ2DnQvvsbTnKSTbYs3p5RPN0aIUqN0liXei/9h24c7V0dkV44cnWIQA==" + }, + "node_modules/@lodestar/types/node_modules/@chainsafe/persistent-merkle-tree": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.8.0.tgz", + "integrity": "sha512-hh6C1JO6SKlr0QGNTNtTLqgGVMA/Bc20wD6CeMHp+wqbFKCULRJuBUxhF4WDx/7mX8QlqF3nFriF/Eo8oYJ4/A==", + "dependencies": { + "@chainsafe/as-sha256": "0.5.0", + "@chainsafe/hashtree": "1.0.1", + "@noble/hashes": "^1.3.0" + } + }, + "node_modules/@lodestar/types/node_modules/@chainsafe/ssz": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.18.0.tgz", + "integrity": "sha512-1ikTjk3JK6+fsGWiT5IvQU0AP6gF3fDzGmPfkKthbcbgTUR8fjB83Ywp9ko/ZoiDGfrSFkATgT4hvRzclu0IAA==", + "dependencies": { + "@chainsafe/as-sha256": "0.5.0", + "@chainsafe/persistent-merkle-tree": "0.8.0" + } + }, + "node_modules/@lodestar/types/node_modules/@noble/curves": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", + "dependencies": { + "@noble/hashes": "1.4.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lodestar/types/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lodestar/types/node_modules/@scure/bip32": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", + "dependencies": { + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lodestar/types/node_modules/@scure/bip39": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "dependencies": { + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@lodestar/types/node_modules/ethereum-cryptography": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-2.2.1.tgz", + "integrity": "sha512-r/W8lkHSiTLxUxW8Rf3u4HGB0xQweG2RyETjywylKZSzLWoWAijRz8WCuOtJ6wah+avllXBqZuk29HCCvhEIRg==", + "dependencies": { + "@noble/curves": "1.4.2", + "@noble/hashes": "1.4.0", + "@scure/bip32": "1.4.0", + "@scure/bip39": "1.3.0" + } + }, + "node_modules/@lodestar/utils": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@lodestar/utils/-/utils-1.23.0.tgz", + "integrity": "sha512-J0+0Mo2ufWrdg8nj9ciujOGrIJK+6sczYpSpNgyxupq+2i/XGNpjxXLqXznpW5KPOeWEPkRgR99lp/UYbTnFWA==", + "dependencies": { + "@chainsafe/as-sha256": "^0.5.0", + "any-signal": "3.0.1", + "bigint-buffer": "^1.1.5", + "case": "^1.6.3", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@lodestar/utils/node_modules/@chainsafe/as-sha256": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.5.0.tgz", + "integrity": "sha512-dTIY6oUZNdC5yDTVP5Qc9hAlKAsn0QTQ2DnQvvsbTnKSTbYs3p5RPN0aIUqN0liXei/9h24c7V0dkV44cnWIQA==" + }, "node_modules/@metamask/eth-sig-util": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz", @@ -1817,9 +2064,9 @@ } }, "node_modules/@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", "funding": { "url": "https://paulmillr.com/funding/" } @@ -2386,6 +2633,11 @@ "resolved": "https://registry.npmjs.org/antlr4ts/-/antlr4ts-0.5.0-alpha.4.tgz", "integrity": "sha512-WPQDt1B74OfPv/IMS2ekXAKkTZIHl88uMetg6q3OTqgFxZ/dxDXI0EWLyZid/1Pe6hTftyg5N7gel5wNAGxXyQ==" }, + "node_modules/any-signal": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/any-signal/-/any-signal-3.0.1.tgz", + "integrity": "sha512-xgZgJtKEa9YmDqXodIgl7Fl1C8yNXr8w6gXjqK3LW4GcEiYT+6AQfJSE/8SPsEpLLmcvbv8YU+qet94UewHxqg==" + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2538,6 +2790,18 @@ "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==" }, + "node_modules/bigint-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.3.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/bigint-crypto-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/bigint-crypto-utils/-/bigint-crypto-utils-3.3.0.tgz", @@ -2562,6 +2826,14 @@ "node": ">=8" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/blakejs": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", @@ -3787,6 +4059,14 @@ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -3841,6 +4121,11 @@ "reusify": "^1.0.4" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", diff --git a/package.json b/package.json index 87cd8749..0f8e600c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "test": "test" }, "dependencies": { + "@lodestar/api": "^1.23.0", "abbrev": "^1.0.9", "abstract-level": "^1.0.3", "acorn": "^8.11.2", @@ -386,6 +387,7 @@ "scripts": { "test": "mocha" }, + "type": "module", "keywords": [], "author": "", "license": "ISC" diff --git a/script/generate.js b/script/generate.js index 9b680cc5..8ef2597b 100644 --- a/script/generate.js +++ b/script/generate.js @@ -11,14 +11,16 @@ const clientChainInfo = { }; // this must be in the same order as whitelistTokens const tokenMetaInfos = [ - 'Exocore testnet ETH', // first we did push exoETH - 'Lido wrapped staked ETH', // then push wstETH + 'Staked ETH', + 'Exocore testnet ETH', + 'Lido wrapped staked ETH', ]; // this must be in the same order as whitelistTokens // they are provided because the symbol may not match what we are using from the price feeder. // for example, exoETH is not a real token and we are using the price feed for ETH. +// the script will take care of mapping the duplicates to a common token for x/oracle params. const tokenNamesForOracle = [ - 'ETH', 'wstETH' // not case sensitive + 'ETH', 'ETH', 'wstETH' // not case sensitive ] const nativeChain = { "name": "Exocore", @@ -39,14 +41,19 @@ const nativeAsset = { }, "staking_total_amount": "0" }; +const EXOCORE_BECH32_PREFIX = 'exo'; +const VIRTUAL_STAKED_ETH_ADDR = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" +const GWEI_TO_WEI = new Decimal(1e9); -const exocoreBech32Prefix = 'exo'; +import dotenv from 'dotenv'; +dotenv.config(); +import { decode } from 'bech32'; +import { promises as fs } from 'fs'; +import { Web3 } from 'web3'; +import Decimal from 'decimal.js'; -require('dotenv').config(); -let { decode } = require('bech32'); -const fs = require('fs').promises; -const { Web3 } = require('web3'); -const Decimal = require('decimal.js'); +import { getClient } from "@lodestar/api"; +import { config } from "@lodestar/config/default"; const isValidBech32 = (address) => { try { @@ -54,7 +61,7 @@ const isValidBech32 = (address) => { if (!prefix || !words.length) { return false; } - return prefix === exocoreBech32Prefix; + return prefix === EXOCORE_BECH32_PREFIX; } catch (error) { // If there's any error in decoding, return false return false; @@ -63,10 +70,14 @@ const isValidBech32 = (address) => { // Load variables from .env file -const { CLIENT_CHAIN_RPC, BOOTSTRAP_ADDRESS, BASE_GENESIS_FILE_PATH, RESULT_GENESIS_FILE_PATH, EXCHANGE_RATES } = process.env; +const { BEACON_CHAIN_ENDPOINT, CLIENT_CHAIN_RPC, BOOTSTRAP_ADDRESS, BASE_GENESIS_FILE_PATH, RESULT_GENESIS_FILE_PATH, EXCHANGE_RATES } = process.env; + +import pkg from 'js-sha3'; +const { keccak256 } = pkg; + +import JSONbig from 'json-bigint'; +const jsonBig = JSONbig({ useNativeBigInt: true }); -const { keccak256 } = require('js-sha3'); -const JSONbig = require('json-bigint')({ "useNativeBigInt": true }); function getChainIDWithoutPrevision(chainID) { const splitStr = chainID.split('-'); @@ -96,13 +107,27 @@ async function updateGenesisFile() { // Create contract instance const myContract = new web3.eth.Contract(contractABI, BOOTSTRAP_ADDRESS); + // Create beacon API client + const api = getClient({baseUrl: BEACON_CHAIN_ENDPOINT}, {config}); + const spec = (await api.config.getSpec()).value(); + const maxEffectiveBalance = new Decimal(spec.MAX_EFFECTIVE_BALANCE).mul(GWEI_TO_WEI); + const ejectIonBalance = new Decimal(spec.EJECTION_BALANCE).mul(GWEI_TO_WEI); + const slotsPerEpoch = spec.SLOTS_PER_EPOCH; + let lastHeader = (await api.beacon.getBlockHeader({blockId: "finalized"})).value(); + const finalizedSlot = lastHeader.header.message.slot; + const finalizedEpoch = Math.floor(finalizedSlot / slotsPerEpoch); + if (finalizedSlot % slotsPerEpoch != 0) { + // change the header + lastHeader = (await api.beacon.getBlockHeader({blockId: finalizedEpoch * slotsPerEpoch})).value(); + } + const stateRoot = web3.utils.bytesToHex(lastHeader.header.message.stateRoot); // Read exchange rates const exchangeRates = EXCHANGE_RATES.split(',').map(Decimal); // Read the genesis file const genesisData = await fs.readFile(BASE_GENESIS_FILE_PATH); - const genesisJSON = JSONbig.parse(genesisData); + const genesisJSON = jsonBig.parse(genesisData); const height = parseInt(genesisJSON.initial_height, 10); const bootstrapped = await myContract.methods.bootstrapped().call(); @@ -182,6 +207,9 @@ async function updateGenesisFile() { const decimals = []; const supportedTokens = []; const assetIds = []; + const oracleTokens = {}; + const oracleTokenFeeders = []; + let offset = 0; for (let i = 0; i < supportedTokensCount; i++) { let token = await myContract.methods.getWhitelistedTokenAtIndex(i).call(); const deposit_amount = await myContract.methods.depositsByToken(token.tokenAddress).call(); @@ -203,24 +231,34 @@ async function updateGenesisFile() { assetIds.push(token.tokenAddress.toLowerCase() + clientChainSuffix); const oracleToken = { name: tokenNamesForOracle[i], - chain_id: 1, // constant intentionally, representing the first chain in the list + chain_id: 1, // constant intentionally, representing the first chain in the list (after the reserved blank one) contract_address: token.tokenAddress, active: true, asset_id: token.tokenAddress.toLowerCase() + clientChainSuffix, decimal: 8, // price decimals, not token decimals } - genesisJSON.app_state.oracle.params.tokens.push(oracleToken); const oracleTokenFeeder = { - token_id: (i + 1).toString(), // first is reserved + index: offset, + token_id: (i + 1 - offset).toString(), // first is reserved rule_id: "1", start_round_id: "1", start_base_block: (height + 10000).toString(), interval: "30", end_block: "0", } - genesisJSON.app_state.oracle.params.token_feeders.push(oracleTokenFeeder); + if (oracleTokens.name in oracleTokens) { + oracleTokens[oracleTokens.name].asset_id += ',' + oracleToken.asset_id; + offset += 1; + } else { + oracleTokens[oracleTokens.name] = oracleToken; + oracleTokenFeeders.push(oracleTokenFeeder); + } // break; } + genesisJSON.app_state.oracle.params.tokens = Object.values(oracleTokens) + .sort((a, b) => {a.index - b.uindex}) + .map(({index, ...rest}) => rest); + genesisJSON.app_state.oracle.params.token_feeders = oracleTokenFeeders; supportedTokens.sort((a, b) => { if (a.asset_basic_info.symbol < b.asset_basic_info.symbol) { return -1; @@ -240,6 +278,9 @@ async function updateGenesisFile() { } const depositorsCount = await myContract.methods.getDepositorsCount().call(); const deposits = []; + const nativeTokenDepositors = []; + const staker_infos = []; + let staker_index_counter = 0; for (let i = 0; i < depositorsCount; i++) { const stakerAddress = await myContract.methods.depositors(i).call(); const depositsByStaker = []; @@ -247,15 +288,98 @@ async function updateGenesisFile() { // do not reuse the older array since it has been sorted. const tokenAddress = (await myContract.methods.getWhitelistedTokenAtIndex(j).call()).tokenAddress; - const depositValue = await myContract.methods.totalDepositAmounts( + let depositValue = new Decimal((await myContract.methods.totalDepositAmounts( stakerAddress, tokenAddress - ).call(); - const withdrawableValue = await myContract.methods.withdrawableAmounts( + ).call()).toString()); + let withdrawableValue = new Decimal((await myContract.methods.withdrawableAmounts( stakerAddress, tokenAddress - ).call(); + ).call()).toString()); + if ((tokenAddress == VIRTUAL_STAKED_ETH_ADDR) && (depositValue > 0)) { + // we have to use the effective balance calculation + nativeTokenDepositors.push(stakerAddress.toLowerCase()); + const pubKeyCount = await myContract.methods.getPubkeysCount(stakerAddress).call(); + const pubKeys = []; + for(let k = 0; k < pubKeyCount; k++) { + // TODO: the contract stores not pubkeys but pubkey hashes. figure out where + // to get the correct value. it affects ClientChainGateway and the network + // overall as well. + pubKeys.push("0x98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9"); + } + const validatorStates = (await api.beacon.getStateValidators({stateId: stateRoot, validatorIds: pubKeys})).value(); + let totalEffectiveBalance = new Decimal(0);; + for(let k = 0; k < validatorStates.length; k++) { + const validator = validatorStates[k]; + // https://hackmd.io/@protolambda/validator_status + // it is sufficient to check for active_ongoing + if (validator.status != "active_ongoing") { + console.log(`Skipping staker ${stakerAddress} due to inactive validator ${pubKeys[k]}`); + continue; + } + const valEffectiveBalance = new Decimal(validator.validator.effectiveBalance).mul(GWEI_TO_WEI); + if (valEffectiveBalance.gt(maxEffectiveBalance)) { + throw new Error(`The effective balance of staker ${stakerAddress} exceeds the maximum effective balance.`); + } + if (valEffectiveBalance.lt(ejectIonBalance)) { + console.log(`Skipping staker ${stakerAddress} due to low validator balance ${valEffectiveBalance}`); + continue; + } + totalEffectiveBalance = totalEffectiveBalance.plus(valEffectiveBalance); + } + console.log(`Total effective balance for staker ${stakerAddress}: ${totalEffectiveBalance}`); + console.log(`Total deposit value for staker ${stakerAddress}: ${depositValue}`); + if (depositValue > totalEffectiveBalance) { + console.log("Staker has more deposit than effective balance."); + // deposited 32 ETH and left with 31 ETH, aka downtime slashing + let toSlash = depositValue.minus(totalEffectiveBalance); + // if withdrawableValue can take the full slashing, do it. + if (withdrawableValue.gt(toSlash)) { + withdrawableValue = withdrawableValue.minus(toSlash); + } else { + // if not, only do it partially. + toSlash = toSlash.minus(withdrawableValue); + withdrawableValue = new Decimal(0); + } + // there is still some left, so do it from the deposit. + if (toSlash.gt(0)) { + if (depositValue.gt(toSlash)) { + depositValue = depositValue.minus(toSlash); + } else { + console.log(`Skipping staker ${stakerAddress} due to insufficient deposit ${depositValue}`); + continue; + } + } + let pendingSlashAmount = toSlash.sub(withdrawableValue); + if (pendingSlashAmount.gt(0)) { + withdrawableValue = withdrawableValue.minus(pendingSlashAmount); + depositValue = depositValue.minus(pendingSlashAmount); + } + } else if (depositValue < totalEffectiveBalance) { + // deposited 32 ETH and left with 33 ETH, aka rewards + const delta = totalEffectiveBalance.minus(depositValue); + depositValue = depositValue.plus(delta); + withdrawableValue = withdrawableValue.plus(delta); + } + staker_infos.push({ + staker_addr: stakerAddress.toLowerCase(), + staker_index: staker_index_counter, + validator_pubkey_list: pubKeys, + balance_list: [ + { + // TODO: check these values with Qing + round_id: 0, + block: height, + index: 0, + change: 0, + balance: depositValue.toString(), + } + ] + }); + staker_index_counter += 1; + } const depositByStakerForAsset = { asset_id: tokenAddress.toLowerCase() + clientChainSuffix, info: { + // adjusted for slashing by ETH beacon chain total_deposit_amount: depositValue.toString(), withdrawable_amount: withdrawableValue.toString(), pending_undelegation_amount: "0", @@ -677,6 +801,20 @@ async function updateGenesisFile() { genesisJSON.app_state.delegation.delegation_states = delegation_states; genesisJSON.app_state.delegation.stakers_by_operator = stakers_by_operator; + // x/oracle - native restaking for ETH + genesisJSON.app_state.oracle.staker_list_assets = [ + { + asset_id: VIRTUAL_STAKED_ETH_ADDR.toLowerCase() + clientChainSuffix, + staker_list: { + staker_addrs: nativeTokenDepositors, + } + } + ]; + genesisJSON.app_state.oracle.staker_infos_assets = { + asset_id: VIRTUAL_STAKED_ETH_ADDR.toLowerCase() + clientChainSuffix, + staker_infos: staker_infos, + }; + // add the native chain and at the end so that count-related issues don't arise. genesisJSON.app_state.assets.client_chains.push(nativeChain); genesisJSON.app_state.assets.tokens.push(nativeAsset); @@ -686,12 +824,12 @@ async function updateGenesisFile() { nativeAsset.asset_basic_info.layer_zero_chain_id.toString(16) ); - await fs.writeFile(RESULT_GENESIS_FILE_PATH, JSONbig.stringify(genesisJSON, null, 2)); + await fs.writeFile(RESULT_GENESIS_FILE_PATH, jsonBig.stringify(genesisJSON, null, 2)); console.log('Genesis file updated successfully.'); } catch (error) { console.error('Error updating genesis file:', error.message); console.error('Stack trace:', error.stack); } -} +}; updateGenesisFile(); \ No newline at end of file diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index a38f4939..4f1aa1ce 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -123,11 +123,11 @@ contract DeployContracts is Script { beaconGenesisTimestamp = vm.envUint("INTEGRATION_BEACON_GENESIS_TIMESTAMP"); require(beaconGenesisTimestamp > 0, "Beacon timestamp must be set"); // can not read uint64 from env - uint256 secondsPerSlot_ = vm.envOr("INTEGRATION_SECONDS_PER_SLOT", uint256(4)); + uint256 secondsPerSlot_ = vm.envUint("INTEGRATION_SECONDS_PER_SLOT"); require(secondsPerSlot_ > 0, "Seconds per slot must be set"); require(secondsPerSlot_ <= type(uint64).max, "Seconds per slot must be less than or equal to uint64 max"); secondsPerSlot = uint64(secondsPerSlot_); - uint256 slotsPerEpoch_ = vm.envOr("INTEGRATION_SLOTS_PER_EPOCH", uint256(3)); + uint256 slotsPerEpoch_ = vm.envUint("INTEGRATION_SLOTS_PER_EPOCH"); require(slotsPerEpoch_ > 0, "Slots per epoch must be set"); require(slotsPerEpoch_ <= type(uint64).max, "Slots per epoch must be less than or equal to uint64 max"); slotsPerEpoch = uint64(slotsPerEpoch_); diff --git a/script/integration/2_VerifyDepositNST.s.sol b/script/integration/2_VerifyDepositNST.s.sol index 9a8a5453..01191c6c 100644 --- a/script/integration/2_VerifyDepositNST.s.sol +++ b/script/integration/2_VerifyDepositNST.s.sol @@ -19,7 +19,6 @@ contract VerifyDepositNST is Script { using Endian for bytes32; using stdJson for string; - bytes32[] validatorContainer; BeaconChainProofs.ValidatorContainerProof validatorProof; bytes32 beaconBlockRoot; @@ -40,7 +39,9 @@ contract VerifyDepositNST is Script { } function run() external { + bytes32[] memory validatorContainer; vm.startBroadcast(nstDepositor); + Bootstrap bootstrap = Bootstrap(bootstrapAddress); string memory data = vm.readFile("script/integration/proof.json"); // load the validator container validatorContainer = data.readBytes32Array(".validatorContainer"); @@ -55,9 +56,12 @@ contract VerifyDepositNST is Script { // since the oracle is not necessarily active during integration testing, trigger it manually BeaconOracle oracle = BeaconOracle(beaconOracleAddress); oracle.addTimestamp(validatorProof.beaconBlockTimestamp); - // now, the transaction - Bootstrap bootstrap = Bootstrap(bootstrapAddress); + // now, the transactions bootstrap.verifyAndDepositNativeStake(validatorContainer, validatorProof); + // delegate only a small portion of the deposit for our test + bootstrap.delegateTo( + "exo1rtg0cgw94ep744epyvanc0wdd5kedwql73vlmr", address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), 18 ether + ); vm.stopBroadcast(); } diff --git a/script/integration/deposit.sh b/script/integration/deposit.sh index 9b9d60d8..bc8f2458 100755 --- a/script/integration/deposit.sh +++ b/script/integration/deposit.sh @@ -4,24 +4,35 @@ SCRIPT_DIR=$(dirname "$(readlink -f "$0")") # Fetch the validator details and save them to container.json -curl -s -X GET "http://localhost:3500/eth/v1/beacon/genesis" -H "accept: application/json" | jq > "$SCRIPT_DIR/genesis.json" +curl -s -X GET "http://localhost:3500/eth/v1/beacon/genesis" -H "accept: application/json" | jq >"$SCRIPT_DIR/genesis.json" + +# Fetch the spec sheet and save it to spec.json +curl -s http://localhost:3500/eth/v1/config/spec | jq >"$SCRIPT_DIR/spec.json" # Ensure the request was successful if [ $? -ne 0 ]; then - echo "Error: Failed to fetch genesis data of the beacon chain." - exit 1 + echo "Error: Failed to fetch genesis data of the beacon chain." + exit 1 fi timestamp=$(jq -r .data.genesis_time "$SCRIPT_DIR/genesis.json") -# Since the devnet uses `--fork=deneb` and `DENEB_FORK_EPOCH: 0`, the deneb time is equal to the beacon genesis time - private_key=${NST_DEPOSITOR:-"0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd"} sender=$(cast wallet a $private_key) +deposit_address=$(jq -r .data.DEPOSIT_CONTRACT_ADDRESS "$SCRIPT_DIR/genesis.json") +slots_per_epoch=$(jq -r .data.SLOTS_PER_EPOCH "$SCRIPT_DIR/spec.json") +seconds_per_slot=$(jq -r .data.SECONDS_PER_SLOT "$SCRIPT_DIR/spec.json") + +# Since the devnet uses `--fork=deneb` and `DENEB_FORK_EPOCH: 0`, the deneb time is equal to the beacon genesis time +export INTEGRATION_BEACON_GENESIS_TIMESTAMP=$timestamp +export INTEGRATION_DENEB_TIMESTAMP=$timestamp +export INTEGRATION_SECONDS_PER_SLOT=$seconds_per_slot +export INTEGRATION_SLOTS_PER_EPOCH=$slots_per_epoch +export INTEGRATION_DEPOSIT_ADDRESS=$deposit_address +export SENDER=$sender -INTEGRATION_BEACON_GENESIS_TIMESTAMP=$timestamp \ -INTEGRATION_DENEB_TIMESTAMP=$timestamp \ -SENDER=$sender \ - forge script --skip-simulation script/integration/1_DeployBootstrap.s.sol \ - --rpc-url $CLIENT_CHAIN_RPC \ - --broadcast -vvvv --slow \ - --sender $SENDER \ No newline at end of file +forge script \ + --skip-simulation script/integration/1_DeployBootstrap.s.sol \ + --rpc-url $CLIENT_CHAIN_RPC \ + --broadcast -vvvv \ + --sender $SENDER \ + --evm-version cancun diff --git a/script/integration/prove.sh b/script/integration/prove.sh index 294a9122..5f02b510 100755 --- a/script/integration/prove.sh +++ b/script/integration/prove.sh @@ -4,21 +4,21 @@ SCRIPT_DIR=$(dirname "$(readlink -f "$0")") # Fetch the validator details and save them to container.json -curl -s -X GET "http://localhost:3500/eth/v1/beacon/states/head/validators/0x98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9" -H "accept: application/json" | jq > "$SCRIPT_DIR/container.json" +curl -s -X GET "http://localhost:3500/eth/v1/beacon/states/head/validators/0x98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9" -H "accept: application/json" | jq >"$SCRIPT_DIR/container.json" # Ensure the request was successful if [ $? -ne 0 ]; then - echo "Error: Failed to fetch validator details." - exit 1 + echo "Error: Failed to fetch validator details." + exit 1 fi # Fetch slots per epoch from the spec -slots_per_epoch=$(curl -s http://localhost:3500/eth/v1/config/spec | jq -r .data.SLOTS_PER_EPOCH) +slots_per_epoch=$(jq -r .data.SLOTS_PER_EPOCH "$SCRIPT_DIR/spec.json") # Ensure slots_per_epoch was fetched successfully if [ -z "$slots_per_epoch" ]; then - echo "Error: Failed to fetch SLOTS_PER_EPOCH." - exit 1 + echo "Error: Failed to fetch SLOTS_PER_EPOCH." + exit 1 fi # Extract the validator index and activation epoch from container.json @@ -27,18 +27,29 @@ epoch=$(jq -r .data.validator.activation_eligibility_epoch "$SCRIPT_DIR/containe # Ensure epoch value is valid if [ -z "$epoch" ] || [ "$epoch" == "null" ]; then - echo "Error: Activation epoch not found for the validator." - exit 1 + echo "Error: Activation epoch not found for the validator." + exit 1 fi # Calculate the slot number slot=$((slots_per_epoch * epoch)) +# # Wait till the slot is reached +# seconds_per_slot=$(jq -r .data.SECONDS_PER_SLOT "$SCRIPT_DIR/spec.json") +# while true; do +# current_slot=$(curl http://localhost:3500/eth/v1/beacon/headers | jq -r '.data[0].header.message.slot') +# if (( current_slot > slot )); then +# break +# fi +# echo "Waiting for slot $slot, current slot is $current_slot" +# sleep $seconds_per_slot +# done + # Now derive the proof using the proof generation binary, which must already be running configured to the localnet curl -X POST -H "Content-Type: application/json" \ - -d "{\"slot\": $slot, \"validator_index\": $validator_index}" \ - http://localhost:8989/v1/validator-proof | jq > "$SCRIPT_DIR/proof.json" + -d "{\"slot\": $slot, \"validator_index\": $validator_index}" \ + http://localhost:8989/v1/validator-proof | jq >"$SCRIPT_DIR/proof.json" forge script script/integration/2_VerifyDepositNST.s.sol --skip-simulation \ - --rpc-url $CLIENT_CHAIN_RPC --broadcast \ - --evm-version cancun # required, otherwise you get EvmError: NotActtivated + --rpc-url $CLIENT_CHAIN_RPC --broadcast \ + --evm-version cancun # required, otherwise you get EvmError: NotActivated diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index 383857b3..c2831fac 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -26,6 +26,7 @@ import {IVault} from "../interfaces/IVault.sol"; import {BeaconChainProofs} from "../libraries/BeaconChainProofs.sol"; import {Errors} from "../libraries/Errors.sol"; +import {ValidatorContainer} from "../libraries/ValidatorContainer.sol"; import {BootstrapStorage} from "../storage/BootstrapStorage.sol"; import {Action} from "../storage/GatewayStorage.sol"; @@ -48,6 +49,8 @@ contract Bootstrap is BootstrapLzReceiver { + using ValidatorContainer for bytes32[]; + /// @notice Constructor for the Bootstrap contract. /// @param endpoint_ is the address of the layerzero endpoint on Exocore chain /// @param config is the struct containing the values for immutable state variables @@ -687,6 +690,15 @@ contract Bootstrap is revert Errors.IndexOutOfBounds(); } address tokenAddress = whitelistTokens[index]; + if (tokenAddress == VIRTUAL_NST_ADDRESS) { + return TokenInfo({ + name: "Native Staked ETH", + symbol: "ETH", + tokenAddress: tokenAddress, + decimals: 18, + depositAmount: depositsByToken[tokenAddress] + }); + } ERC20 token = ERC20(tokenAddress); return TokenInfo({ name: token.name(), @@ -781,6 +793,7 @@ contract Bootstrap is totalDepositAmounts[msg.sender][VIRTUAL_NST_ADDRESS] += depositValue; withdrawableAmounts[msg.sender][VIRTUAL_NST_ADDRESS] += depositValue; depositsByToken[VIRTUAL_NST_ADDRESS] += depositValue; + stakerToPubkeys[msg.sender].push(validatorContainer.getPubkey()); emit DepositResult(true, VIRTUAL_NST_ADDRESS, msg.sender, depositValue); } @@ -811,4 +824,13 @@ contract Bootstrap is capsule.withdrawNonBeaconChainETHBalance(recipient, amountToWithdraw); } + /// @notice Returns the number of pubkeys (across all validators) deposited + /// by a staker. The deposit must include deposit + verification for inclusion + /// into the beacon chain. + /// @param stakerAddress the address of the staker. + /// @return the number of pubkeys deposited by the staker. + function getPubkeysCount(address stakerAddress) external view returns (uint256) { + return stakerToPubkeys[stakerAddress].length; + } + } diff --git a/src/libraries/NetworkConstants.sol b/src/libraries/NetworkConstants.sol index 56044cee..5161c3ed 100644 --- a/src/libraries/NetworkConstants.sol +++ b/src/libraries/NetworkConstants.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// solhint-disable max-line-length pragma solidity ^0.8.0; import {NetworkParams} from "../interfaces/INetworkConfig.sol"; diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index 6b1cb635..60c4cf16 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -177,6 +177,10 @@ contract BootstrapStorage is GatewayStorage { /// contracts and we put it after __gap to maintain the storage layout compatible with deployed contracts. mapping(address owner => IExoCapsule capsule) public ownerToCapsule; + /// @notice Mapping of staker addresses to their corresponding public keys. + /// @dev Maps staker addresses to their corresponding public keys used on the beacon chain. + mapping(address staker => bytes32[]) public stakerToPubkeys; + /* -------------------------------------------------------------------------- */ /* Events */ /* -------------------------------------------------------------------------- */ diff --git a/src/storage/ExoCapsuleStorage.sol b/src/storage/ExoCapsuleStorage.sol index c8d6f71b..50ff8281 100644 --- a/src/storage/ExoCapsuleStorage.sol +++ b/src/storage/ExoCapsuleStorage.sol @@ -58,7 +58,7 @@ contract ExoCapsuleStorage { /// @notice The address of the NetworkConfig contract. /// @dev If it is set to the 0 address, the NetworkConstants library is used instead. - address public immutable networkConfig; + address public immutable NETWORK_CONFIG; /// @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) @@ -93,42 +93,42 @@ contract ExoCapsuleStorage { /// @notice Sets the network configuration contract address for the ExoCapsule contract. /// @param networkConfig_ The address of the NetworkConfig contract. constructor(address networkConfig_) { - networkConfig = networkConfig_; + NETWORK_CONFIG = networkConfig_; } /// @dev Gets the deneb hard fork timestamp, either from the NetworkConfig contract or the NetworkConstants library. function getDenebHardForkTimestamp() internal view returns (uint256) { - if (networkConfig == address(0)) { + if (NETWORK_CONFIG == address(0)) { return NetworkConstants.getDenebHardForkTimestamp(); } else { - return INetworkConfig(networkConfig).getDenebHardForkTimestamp(); + return INetworkConfig(NETWORK_CONFIG).getDenebHardForkTimestamp(); } } /// @dev Gets the slots per epoch, either from the NetworkConfig contract or the NetworkConstants library. function getSlotsPerEpoch() internal view returns (uint64) { - if (networkConfig == address(0)) { + if (NETWORK_CONFIG == address(0)) { return NetworkConstants.getSlotsPerEpoch(); } else { - return INetworkConfig(networkConfig).getSlotsPerEpoch(); + return INetworkConfig(NETWORK_CONFIG).getSlotsPerEpoch(); } } /// @dev Gets the seconds per slot, either from the NetworkConfig contract or the NetworkConstants library. function getSecondsPerEpoch() internal view returns (uint64) { - if (networkConfig == address(0)) { + if (NETWORK_CONFIG == address(0)) { return NetworkConstants.getSecondsPerEpoch(); } else { - return INetworkConfig(networkConfig).getSecondsPerEpoch(); + return INetworkConfig(NETWORK_CONFIG).getSecondsPerEpoch(); } } /// @dev Gets the beacon genesis timestamp, either from the NetworkConfig contract or the NetworkConstants library. function getBeaconGenesisTimestamp() internal view returns (uint256) { - if (networkConfig == address(0)) { + if (NETWORK_CONFIG == address(0)) { return NetworkConstants.getBeaconGenesisTimestamp(); } else { - return INetworkConfig(networkConfig).getBeaconGenesisTimestamp(); + return INetworkConfig(NETWORK_CONFIG).getBeaconGenesisTimestamp(); } } diff --git a/test/foundry/unit/NetworkConfig.t.sol b/test/foundry/unit/NetworkConfig.t.sol index fc2de668..89affbf1 100644 --- a/test/foundry/unit/NetworkConfig.t.sol +++ b/test/foundry/unit/NetworkConfig.t.sol @@ -64,7 +64,7 @@ contract NetworkConfigTest is Test { function testRevertUnsupportedChainId() public { // Change the chain ID to something other than 31337 vm.chainId(1); - vm.expectRevert("unsupported network"); + vm.expectRevert("only the 31337 chain ID is supported for integration tests"); new NetworkConfig( DEPOSIT_CONTRACT_ADDRESS, DENEB_TIMESTAMP, SLOTS_PER_EPOCH, SECONDS_PER_SLOT, BEACON_GENESIS_TIMESTAMP ); From 183dfabbc42addad7285f903680a7dc073294e8a Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Fri, 15 Nov 2024 03:12:48 +0000 Subject: [PATCH 05/22] change workflow cause --- .github/workflows/forge-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index a5e0e735..8dc8f5c6 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -2,7 +2,7 @@ name: Forge CI to build, test, format and compare storage layout on: merge_group: - pull_request: + pull_request_target: push: branches: - main From bd81e0a9a679d41b2e7d0d1d00509e2351fb189c Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Mon, 18 Nov 2024 03:17:49 +0000 Subject: [PATCH 06/22] Revert "change workflow cause" This reverts commit 183dfabbc42addad7285f903680a7dc073294e8a. --- .github/workflows/forge-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/forge-ci.yml b/.github/workflows/forge-ci.yml index 8dc8f5c6..a5e0e735 100644 --- a/.github/workflows/forge-ci.yml +++ b/.github/workflows/forge-ci.yml @@ -2,7 +2,7 @@ name: Forge CI to build, test, format and compare storage layout on: merge_group: - pull_request_target: + pull_request: push: branches: - main From 7e48df8e7d51a0ae95d21b5932927482ef0b6e29 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:27:42 +0000 Subject: [PATCH 07/22] fix: respond to AI comments --- .env.example | 34 ++++++---- package.json | 1 - script/{generate.js => generate.mjs} | 64 +++++++++--------- script/integration/1_DeployBootstrap.s.sol | 54 +++++++++------- script/integration/2_VerifyDepositNST.s.sol | 17 +++-- script/integration/BeaconOracle.sol | 3 + script/integration/deposit.sh | 72 ++++++++++++++++++--- script/integration/prove.sh | 68 ++++++++++++++----- test/foundry/unit/NetworkConfig.t.sol | 1 + 9 files changed, 212 insertions(+), 102 deletions(-) rename script/{generate.js => generate.mjs} (94%) diff --git a/.env.example b/.env.example index 56ed6081..66a893b6 100644 --- a/.env.example +++ b/.env.example @@ -12,25 +12,31 @@ EXOCORE_GENESIS_PRIVATE_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3f USE_ENDPOINT_MOCK=true USE_EXOCORE_PRECOMPILE_MOCK=true -VALIDATOR_KEYS= -EXO_ADDRESSES= -NAMES= -CONS_KEYS= - -# The following are used by generate.js, in addition to CLIENT_CHAIN_RPC above. -BOOTSTRAP_ADDRESS= -EXCHANGE_RATES= -BASE_GENESIS_FILE_PATH= -RESULT_GENESIS_FILE_PATH= -BEACON_CHAIN_ENDPOINT= - # For contract verification ETHERSCAN_API_KEY= -# These are used for integration testing ETH PoS +# These are used for integration testing the Bootstrap contract, in addition to +# CLIENT_CHAIN_RPC and BEACON_CHAIN_ENDPOINT above. +INTEGRATION_VALIDATOR_KEYS=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80,0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d,0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a +INTEGRATION_STAKERS=0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6,0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a,0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba,0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e,0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356,0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97,0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6 +INTEGRATION_TOKEN_DEPLOYERS=0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82,0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1 +INTEGRATION_CONTRACT_DEPLOYER=0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897 +# Specially ETH PoS related parameters. INTEGRATION_DEPOSIT_ADDRESS=0x6969696969696969696969696969696969696969 INTEGRATION_SECONDS_PER_SLOT=4 INTEGRATION_SLOTS_PER_EPOCH=3 INTEGRATION_BEACON_GENESIS_TIMESTAMP= INTEGRATION_DENEB_TIMESTAMP= -NST_DEPOSITOR= \ No newline at end of file +INTEGRATION_NST_DEPOSITOR=0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd +# margin tank lunch prison top episode peanut approve dish seat nominee illness +INTEGRATION_PUBKEY=0x98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9 +INTEGRATION_SIGNATURE=0x922a316bdc3516bfa66e88259d5e93e339ef81bc85b70e6c715542222025a28fa1e3644c853beb8c3ba76a2c5c03b726081bf605bde3a16e1f33f902cc1b6c01093c19609de87da9383fa4b1f347bd2d4222e1ae5428727a7896c8e553cc8071 +# derived from pubkey + network params == chain id + genesis {fork version + validators root} +INTEGRATION_DEPOSIT_DATA_ROOT=0x456934ced8f08ff106857418a6d885ba69d31e1b7fab9a931be06da25490cd1d +INTEGRATION_BEACON_CHAIN_ENDPOINT=http://localhost:3500 +INTEGRATION_PROVE_ENDPOINT=http://localhost:8989 +# for generate.js +INTEGRATION_BOOTSTRAP_ADDRESS=0xF801fc13AA08876F343fEBf50dFfA52A78180811 +INTEGRATION_EXCHANGE_RATES=1000.123,2000.123,1799.345345 +INTEGRATION_BASE_GENESIS_FILE_PATH= +INTEGRATION_RESULT_GENESIS_FILE_PATH= diff --git a/package.json b/package.json index 0f8e600c..c68bc3b8 100644 --- a/package.json +++ b/package.json @@ -387,7 +387,6 @@ "scripts": { "test": "mocha" }, - "type": "module", "keywords": [], "author": "", "license": "ISC" diff --git a/script/generate.js b/script/generate.mjs similarity index 94% rename from script/generate.js rename to script/generate.mjs index 8ef2597b..552e1323 100644 --- a/script/generate.js +++ b/script/generate.mjs @@ -49,7 +49,7 @@ import dotenv from 'dotenv'; dotenv.config(); import { decode } from 'bech32'; import { promises as fs } from 'fs'; -import { Web3 } from 'web3'; +import Web3 from 'web3'; import Decimal from 'decimal.js'; import { getClient } from "@lodestar/api"; @@ -70,7 +70,14 @@ const isValidBech32 = (address) => { // Load variables from .env file -const { BEACON_CHAIN_ENDPOINT, CLIENT_CHAIN_RPC, BOOTSTRAP_ADDRESS, BASE_GENESIS_FILE_PATH, RESULT_GENESIS_FILE_PATH, EXCHANGE_RATES } = process.env; +const { + INTEGRATION_BEACON_CHAIN_ENDPOINT, + CLIENT_CHAIN_RPC, + INTEGRATION_BOOTSTRAP_ADDRESS, + INTEGRATION_BASE_GENESIS_FILE_PATH, + INTEGRATION_RESULT_GENESIS_FILE_PATH, + INTEGRATION_EXCHANGE_RATES +} = process.env; import pkg from 'js-sha3'; const { keccak256 } = pkg; @@ -106,9 +113,9 @@ async function updateGenesisFile() { const web3 = new Web3(CLIENT_CHAIN_RPC); // Create contract instance - const myContract = new web3.eth.Contract(contractABI, BOOTSTRAP_ADDRESS); + const myContract = new web3.eth.Contract(contractABI, INTEGRATION_BOOTSTRAP_ADDRESS); // Create beacon API client - const api = getClient({baseUrl: BEACON_CHAIN_ENDPOINT}, {config}); + const api = getClient({baseUrl: INTEGRATION_BEACON_CHAIN_ENDPOINT}, {config}); const spec = (await api.config.getSpec()).value(); const maxEffectiveBalance = new Decimal(spec.MAX_EFFECTIVE_BALANCE).mul(GWEI_TO_WEI); const ejectIonBalance = new Decimal(spec.EJECTION_BALANCE).mul(GWEI_TO_WEI); @@ -123,10 +130,10 @@ async function updateGenesisFile() { const stateRoot = web3.utils.bytesToHex(lastHeader.header.message.stateRoot); // Read exchange rates - const exchangeRates = EXCHANGE_RATES.split(',').map(Decimal); + const exchangeRates = INTEGRATION_EXCHANGE_RATES.split(',').map(Decimal); // Read the genesis file - const genesisData = await fs.readFile(BASE_GENESIS_FILE_PATH); + const genesisData = await fs.readFile(INTEGRATION_BASE_GENESIS_FILE_PATH); const genesisJSON = jsonBig.parse(genesisData); const height = parseInt(genesisJSON.initial_height, 10); @@ -207,7 +214,8 @@ async function updateGenesisFile() { const decimals = []; const supportedTokens = []; const assetIds = []; - const oracleTokens = {}; + // start with the initial value + const oracleTokens = genesisJSON.app_state.oracle.params.tokens; const oracleTokenFeeders = []; let offset = 0; for (let i = 0; i < supportedTokensCount; i++) { @@ -231,6 +239,7 @@ async function updateGenesisFile() { assetIds.push(token.tokenAddress.toLowerCase() + clientChainSuffix); const oracleToken = { name: tokenNamesForOracle[i], + index: offset, chain_id: 1, // constant intentionally, representing the first chain in the list (after the reserved blank one) contract_address: token.tokenAddress, active: true, @@ -238,7 +247,6 @@ async function updateGenesisFile() { decimal: 8, // price decimals, not token decimals } const oracleTokenFeeder = { - index: offset, token_id: (i + 1 - offset).toString(), // first is reserved rule_id: "1", start_round_id: "1", @@ -246,17 +254,17 @@ async function updateGenesisFile() { interval: "30", end_block: "0", } - if (oracleTokens.name in oracleTokens) { - oracleTokens[oracleTokens.name].asset_id += ',' + oracleToken.asset_id; + if (oracleToken.name in oracleTokens) { + oracleTokens[oracleToken.name].asset_id += ',' + oracleToken.asset_id; offset += 1; } else { - oracleTokens[oracleTokens.name] = oracleToken; + oracleTokens[oracleToken.name] = oracleToken; oracleTokenFeeders.push(oracleTokenFeeder); } // break; } genesisJSON.app_state.oracle.params.tokens = Object.values(oracleTokens) - .sort((a, b) => {a.index - b.uindex}) + .sort((a, b) => {a.index - b.index}) .map(({index, ...rest}) => rest); genesisJSON.app_state.oracle.params.token_feeders = oracleTokenFeeders; supportedTokens.sort((a, b) => { @@ -325,8 +333,6 @@ async function updateGenesisFile() { } totalEffectiveBalance = totalEffectiveBalance.plus(valEffectiveBalance); } - console.log(`Total effective balance for staker ${stakerAddress}: ${totalEffectiveBalance}`); - console.log(`Total deposit value for staker ${stakerAddress}: ${depositValue}`); if (depositValue > totalEffectiveBalance) { console.log("Staker has more deposit than effective balance."); // deposited 32 ETH and left with 31 ETH, aka downtime slashing @@ -348,11 +354,6 @@ async function updateGenesisFile() { continue; } } - let pendingSlashAmount = toSlash.sub(withdrawableValue); - if (pendingSlashAmount.gt(0)) { - withdrawableValue = withdrawableValue.minus(pendingSlashAmount); - depositValue = depositValue.minus(pendingSlashAmount); - } } else if (depositValue < totalEffectiveBalance) { // deposited 32 ETH and left with 33 ETH, aka rewards const delta = totalEffectiveBalance.minus(depositValue); @@ -363,16 +364,8 @@ async function updateGenesisFile() { staker_addr: stakerAddress.toLowerCase(), staker_index: staker_index_counter, validator_pubkey_list: pubKeys, - balance_list: [ - { - // TODO: check these values with Qing - round_id: 0, - block: height, - index: 0, - change: 0, - balance: depositValue.toString(), - } - ] + // the balance list represents the history of the balance. for bootstrap, that is empty. + balance_list: [] }); staker_index_counter += 1; } @@ -380,8 +373,8 @@ async function updateGenesisFile() { asset_id: tokenAddress.toLowerCase() + clientChainSuffix, info: { // adjusted for slashing by ETH beacon chain - total_deposit_amount: depositValue.toString(), - withdrawable_amount: withdrawableValue.toString(), + total_deposit_amount: depositValue.toFixed(), + withdrawable_amount: withdrawableValue.toFixed(), pending_undelegation_amount: "0", } }; @@ -810,10 +803,10 @@ async function updateGenesisFile() { } } ]; - genesisJSON.app_state.oracle.staker_infos_assets = { + genesisJSON.app_state.oracle.staker_infos_assets = [{ asset_id: VIRTUAL_STAKED_ETH_ADDR.toLowerCase() + clientChainSuffix, staker_infos: staker_infos, - }; + }]; // add the native chain and at the end so that count-related issues don't arise. genesisJSON.app_state.assets.client_chains.push(nativeChain); @@ -824,7 +817,10 @@ async function updateGenesisFile() { nativeAsset.asset_basic_info.layer_zero_chain_id.toString(16) ); - await fs.writeFile(RESULT_GENESIS_FILE_PATH, jsonBig.stringify(genesisJSON, null, 2)); + await fs.writeFile( + INTEGRATION_RESULT_GENESIS_FILE_PATH, + jsonBig.stringify(genesisJSON, null, 2) + ); console.log('Genesis file updated successfully.'); } catch (error) { console.error('Error updating genesis file:', error.message); diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index 4f1aa1ce..1cd803f8 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -47,19 +47,12 @@ contract DeployContracts is Script { // neither is the ownership of the contract being tested here address exocoreValidatorSet = vm.addr(uint256(0x8)); - // assumes 3 validators, to add more - change registerValidators and delegate. uint256[] validators; uint256[] stakers; + string[] exos; uint256 contractDeployer; uint256 nstDepositor; Bootstrap bootstrap; - // to add more tokens, - // 0. add deployer private keys - // 1. update the decimals - // 2. increase the size of MyToken - // 3. add information about tokens to deployTokens. - // 4. update deposit and delegate amounts in fundAndApprove and delegate. - // everywhere else we use the length of the myTokens array. uint256[] tokenDeployers; uint8[2] decimals = [18, 6]; address[] whitelistTokens; @@ -82,6 +75,9 @@ contract DeployContracts is Script { uint64 secondsPerSlot; uint64 slotsPerEpoch; uint256 beaconGenesisTimestamp; + bytes pubkey; + bytes signature; + bytes32 depositDataRoot; function setUp() private { // placate the pre-simulation runner @@ -106,15 +102,21 @@ contract DeployContracts is Script { ANVIL_TOKEN_DEPLOYERS[0] = uint256(0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82); ANVIL_TOKEN_DEPLOYERS[1] = uint256(0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1); - uint256 CONTRACT_DEPLOYER = uint256(0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897); + uint256 ANVIL_CONTRACT_DEPLOYER = uint256(0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897); - uint256 NST_DEPOSITOR = uint256(0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd); + uint256 ANVIL_NST_DEPOSITOR = uint256(0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd); - validators = vm.envOr("ANVIL_VALIDATORS", ",", ANVIL_VALIDATORS); - stakers = vm.envOr("ANVIL_STAKERS", ",", ANVIL_STAKERS); - tokenDeployers = vm.envOr("ANVIL_TOKEN_DEPLOYERS", ",", ANVIL_TOKEN_DEPLOYERS); - contractDeployer = vm.envOr("CONTRACT_DEPLOYER", CONTRACT_DEPLOYER); - nstDepositor = vm.envOr("NST_DEPOSITOR", NST_DEPOSITOR); + // load the keys for validators, stakers, token deployers, and the contract deployer + validators = vm.envOr("INTEGRATION_VALIDATOR_KEYS", ",", ANVIL_VALIDATORS); + // we don't validate the contents of the keys because vm.addr will throw if they are invalid + require(validators.length == 3, "Modify this script to support validators.length other than 3"); + stakers = vm.envOr("INTEGRATION_STAKERS", ",", ANVIL_STAKERS); + require(stakers.length == 7, "Modify this script to support stakers.length other than 7"); + tokenDeployers = vm.envOr("INTEGRATION_TOKEN_DEPLOYERS", ",", ANVIL_TOKEN_DEPLOYERS); + require(tokenDeployers.length == 2, "Modify this script to support tokenDeployers.length other than 2"); + require(decimals.length == tokenDeployers.length, "Decimals and tokenDeployers must have the same length"); + contractDeployer = vm.envOr("INTEGRATION_CONTRACT_DEPLOYER", ANVIL_CONTRACT_DEPLOYER); + nstDepositor = vm.envOr("INTEGRATION_NST_DEPOSITOR", ANVIL_NST_DEPOSITOR); // read the network configuration parameters and validate them depositAddress = vm.envOr("INTEGRATION_DEPOSIT_ADDRESS", address(0x6969696969696969696969696969696969696969)); @@ -131,6 +133,13 @@ contract DeployContracts is Script { require(slotsPerEpoch_ > 0, "Slots per epoch must be set"); require(slotsPerEpoch_ <= type(uint64).max, "Slots per epoch must be less than or equal to uint64 max"); slotsPerEpoch = uint64(slotsPerEpoch_); + // then, the Ethereum-native validator configuration + pubkey = vm.envBytes("INTEGRATION_PUBKEY"); + require(pubkey.length == 48, "Pubkey must be 48 bytes"); + signature = vm.envBytes("INTEGRATION_SIGNATURE"); + require(signature.length == 96, "Signature must be 96 bytes"); + depositDataRoot = vm.envBytes32("INTEGRATION_DEPOSIT_DATA_ROOT"); + require(depositDataRoot != bytes32(0), "Deposit data root must be set"); } function deployTokens() private { @@ -255,20 +264,11 @@ contract DeployContracts is Script { myAddress = bootstrap.createExoCapsule(); } console.log("ExoCapsule address", myAddress); - bootstrap.stake{value: 32 ether}( - // mnemonic: margin tank lunch prison top episode peanut approve dish seat nominee illness - hex"98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9", // pubkey - hex"922a316bdc3516bfa66e88259d5e93e339ef81bc85b70e6c715542222025a28fa1e3644c853beb8c3ba76a2c5c03b726081bf605bde3a16e1f33f902cc1b6c01093c19609de87da9383fa4b1f347bd2d4222e1ae5428727a7896c8e553cc8071", // signature - bytes32(0x456934ced8f08ff106857418a6d885ba69d31e1b7fab9a931be06da25490cd1d) // deposit data root - ); + bootstrap.stake{value: 32 ether}(pubkey, signature, depositDataRoot); vm.stopBroadcast(); } function registerValidators() private { - // the mnemonics corresponding to the consensus public keys are given here. to recover, - // echo "${MNEMONIC}" | exocored init localnet --chain-id exocorelocal_233-1 --recover - // the value in this script is this one - // exocored keys consensus-pubkey-to-bytes --output json | jq -r .bytes string[3] memory exos = [ // these addresses will accrue rewards but they are not needed to keep the chain // running. @@ -277,6 +277,10 @@ contract DeployContracts is Script { "exo1rtg0cgw94ep744epyvanc0wdd5kedwql73vlmr" ]; string[3] memory names = ["validator1", "validator2", "validator3"]; + // the mnemonics corresponding to the consensus public keys are given here. to recover, + // echo "${MNEMONIC}" | exocored init localnet --chain-id exocorelocal_233-1 --recover + // the value in this script is this one + // exocored keys consensus-pubkey-to-bytes --output json | jq -r .bytes bytes32[3] memory pubKeys = [ // wonder quality resource ketchup occur stadium vicious output situate plug second // monkey harbor vanish then myself primary feed earth story real soccer shove like diff --git a/script/integration/2_VerifyDepositNST.s.sol b/script/integration/2_VerifyDepositNST.s.sol index 01191c6c..9ec9c15f 100644 --- a/script/integration/2_VerifyDepositNST.s.sol +++ b/script/integration/2_VerifyDepositNST.s.sol @@ -27,25 +27,28 @@ contract VerifyDepositNST is Script { uint256 nstDepositor; function setUp() public virtual { - // vm.chainId(ALLOWED_CHAIN_ID); // obtain the address string memory deployments = vm.readFile("script/integration/deployments.json"); bootstrapAddress = deployments.readAddress(".bootstrapAddress"); require(bootstrapAddress != address(0), "Bootstrap address not found"); beaconOracleAddress = deployments.readAddress(".beaconOracleAddress"); require(beaconOracleAddress != address(0), "BeaconOracle address not found"); - nstDepositor = - vm.envOr("NST_DEPOSITOR", uint256(0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd)); + nstDepositor = vm.envOr( + "INTEGRATION_NST_DEPOSITOR", uint256(0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd) + ); + require(nstDepositor != 0, "INTEGRATION_NST_DEPOSITOR not set"); } function run() external { bytes32[] memory validatorContainer; vm.startBroadcast(nstDepositor); Bootstrap bootstrap = Bootstrap(bootstrapAddress); + require(vm.exists("script/integration/proof.json"), "Proof file not found"); string memory data = vm.readFile("script/integration/proof.json"); // load the validator container validatorContainer = data.readBytes32Array(".validatorContainer"); // load the validator proof + // we don't validate it; that task is left to the contract. it is a test, after all. validatorProof = BeaconChainProofs.ValidatorContainerProof({ stateRoot: data.readBytes32(".stateRoot"), stateRootProof: data.readBytes32Array(".stateRootProof"), @@ -58,9 +61,13 @@ contract VerifyDepositNST is Script { oracle.addTimestamp(validatorProof.beaconBlockTimestamp); // now, the transactions bootstrap.verifyAndDepositNativeStake(validatorContainer, validatorProof); - // delegate only a small portion of the deposit for our test bootstrap.delegateTo( - "exo1rtg0cgw94ep744epyvanc0wdd5kedwql73vlmr", address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), 18 ether + // a validator in 1_DeployBootstrap.s.sol + "exo1rtg0cgw94ep744epyvanc0wdd5kedwql73vlmr", + // the native token address + address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + // delegate only a small portion of the deposit for our test + 18 ether ); vm.stopBroadcast(); } diff --git a/script/integration/BeaconOracle.sol b/script/integration/BeaconOracle.sol index c74cb96d..78a08555 100644 --- a/script/integration/BeaconOracle.sol +++ b/script/integration/BeaconOracle.sol @@ -51,6 +51,9 @@ contract BeaconOracle is IBeaconChainOracle { } function addTimestamp(uint256 _targetTimestamp) external { + if (_targetTimestamp < GENESIS_BLOCK_TIMESTAMP) { + revert InvalidBlockTimestamp(); + } // If the targetTimestamp is not guaranteed to be within the beacon block root ring buffer, revert. if ((block.timestamp - _targetTimestamp) >= (BEACON_ROOTS_HISTORY_BUFFER_LENGTH * SECONDS_PER_SLOT)) { revert TimestampOutOfRange(); diff --git a/script/integration/deposit.sh b/script/integration/deposit.sh index bc8f2458..3fd23518 100755 --- a/script/integration/deposit.sh +++ b/script/integration/deposit.sh @@ -1,38 +1,94 @@ #!/usr/bin/env bash +set -e + # Get the directory of the script SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +# Check that all of the variables required exist. +vars=( + CLIENT_CHAIN_RPC + INTEGRATION_BEACON_CHAIN_ENDPOINT + INTEGRATION_CONTRACT_DEPLOYER + INTEGRATION_DEPOSIT_DATA_ROOT + INTEGRATION_NST_DEPOSITOR + INTEGRATION_PUBKEY + INTEGRATION_SIGNATURE + INTEGRATION_STAKERS + INTEGRATION_TOKEN_DEPLOYERS + INTEGRATION_VALIDATOR_KEYS +) +for var in "${vars[@]}"; do + if [ -z "${!var}" ]; then + echo "Error: $var must be set" + exit 1 + fi +done + # Fetch the validator details and save them to container.json -curl -s -X GET "http://localhost:3500/eth/v1/beacon/genesis" -H "accept: application/json" | jq >"$SCRIPT_DIR/genesis.json" +if ! curl -s -X GET "$INTEGRATION_BEACON_CHAIN_ENDPOINT/eth/v1/beacon/genesis" -H "accept: application/json" | jq . >"$SCRIPT_DIR/genesis.json"; then + echo "Error: Failed to fetch genesis data from the beacon chain" + exit 1 +fi + +if ! jq -e .data "$SCRIPT_DIR/genesis.json" >/dev/null; then + echo "Error: Invalid genesis data structure." + exit 1 +fi # Fetch the spec sheet and save it to spec.json -curl -s http://localhost:3500/eth/v1/config/spec | jq >"$SCRIPT_DIR/spec.json" +if ! curl -s -X GET "$INTEGRATION_BEACON_CHAIN_ENDPOINT/eth/v1/config/spec" | jq >"$SCRIPT_DIR/spec.json"; then + echo "Error: Failed to fetch spec data from the beacon chain" + exit 1 +fi -# Ensure the request was successful -if [ $? -ne 0 ]; then - echo "Error: Failed to fetch genesis data of the beacon chain." +if ! jq -e .data "$SCRIPT_DIR/spec.json" >/dev/null; then + echo "Error: Invalid spec data structure." exit 1 fi timestamp=$(jq -r .data.genesis_time "$SCRIPT_DIR/genesis.json") -private_key=${NST_DEPOSITOR:-"0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd"} +private_key=$INTEGRATION_NST_DEPOSITOR sender=$(cast wallet a $private_key) +if [ $? -ne 0 ]; then + echo "Error: Failed to derive sender address." + exit 1 +fi deposit_address=$(jq -r .data.DEPOSIT_CONTRACT_ADDRESS "$SCRIPT_DIR/genesis.json") slots_per_epoch=$(jq -r .data.SLOTS_PER_EPOCH "$SCRIPT_DIR/spec.json") +if ! [[ "$slots_per_epoch" =~ ^[0-9]+$ ]]; then + echo "Error: Invalid slots per epoch" + exit 1 +fi seconds_per_slot=$(jq -r .data.SECONDS_PER_SLOT "$SCRIPT_DIR/spec.json") +if ! [[ "$seconds_per_slot" =~ ^[0-9]+$ ]]; then + echo "Error: Invalid slots per epoch" + exit 1 +fi -# Since the devnet uses `--fork=deneb` and `DENEB_FORK_EPOCH: 0`, the deneb time is equal to the beacon genesis time +# Make the variables available to the forge script +export INTEGRATION_VALIDATOR_KEYS=$INTEGRATION_VALIDATOR_KEYS +export INTEGRATION_STAKERS=$INTEGRATION_STAKERS +export INTEGRATION_TOKEN_DEPLOYERS=$INTEGRATION_TOKEN_DEPLOYERS +export INTEGRATION_CONTRACT_DEPLOYER=$INTEGRATION_CONTRACT_DEPLOYER +export INTEGRATION_PUBKEY=$INTEGRATION_PUBKEY +export INTEGRATION_SIGNATURE=$INTEGRATION_SIGNATURE +export INTEGRATION_DEPOSIT_DATA_ROOT=$INTEGRATION_DEPOSIT_DATA_ROOT export INTEGRATION_BEACON_GENESIS_TIMESTAMP=$timestamp +# Since the devnet uses `--fork=deneb` and `DENEB_FORK_EPOCH: 0`, the deneb time is equal to the beacon genesis time export INTEGRATION_DENEB_TIMESTAMP=$timestamp export INTEGRATION_SECONDS_PER_SLOT=$seconds_per_slot export INTEGRATION_SLOTS_PER_EPOCH=$slots_per_epoch export INTEGRATION_DEPOSIT_ADDRESS=$deposit_address +export INTEGRATION_NST_DEPOSITOR=$INTEGRATION_NST_DEPOSITOR export SENDER=$sender +# Specify SENDER so that libraries can be deployed +# Use Cancun version because prove.sh needs it or it complains +# Better to recompile here than to recompile in prove.sh forge script \ --skip-simulation script/integration/1_DeployBootstrap.s.sol \ --rpc-url $CLIENT_CHAIN_RPC \ - --broadcast -vvvv \ + --broadcast -v \ --sender $SENDER \ --evm-version cancun diff --git a/script/integration/prove.sh b/script/integration/prove.sh index 5f02b510..d1e6f84e 100755 --- a/script/integration/prove.sh +++ b/script/integration/prove.sh @@ -1,10 +1,41 @@ #!/usr/bin/env bash +set -e + # Get the directory of the script SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +# Check that all of the variables required exist. +vars=( + CLIENT_CHAIN_RPC + INTEGRATION_BEACON_CHAIN_ENDPOINT + INTEGRATION_NST_DEPOSITOR + INTEGRATION_PROVE_ENDPOINT + INTEGRATION_PUBKEY +) +for var in "${vars[@]}"; do + if [ -z "${!var}" ]; then + echo "Error: $var must be set" + exit 1 + fi +done + +# Check for the files to exist +if [ ! -f "$SCRIPT_DIR/spec.json" ]; then + echo "Error: spec.json not found in $SCRIPT_DIR" + exit 1 +fi + +# Check for the files to exist +if [ ! -f "$SCRIPT_DIR/container.json" ]; then + echo "Error: container.json not found in $SCRIPT_DIR" + exit 1 +fi + # Fetch the validator details and save them to container.json -curl -s -X GET "http://localhost:3500/eth/v1/beacon/states/head/validators/0x98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9" -H "accept: application/json" | jq >"$SCRIPT_DIR/container.json" +curl -s -X GET \ + "$INTEGRATION_BEACON_CHAIN_ENDPOINT/eth/v1/beacon/states/head/validators/$INTEGRATION_PUBKEY" \ + -H "accept: application/json" | jq >"$SCRIPT_DIR/container.json" # Ensure the request was successful if [ $? -ne 0 ]; then @@ -34,22 +65,29 @@ fi # Calculate the slot number slot=$((slots_per_epoch * epoch)) -# # Wait till the slot is reached -# seconds_per_slot=$(jq -r .data.SECONDS_PER_SLOT "$SCRIPT_DIR/spec.json") -# while true; do -# current_slot=$(curl http://localhost:3500/eth/v1/beacon/headers | jq -r '.data[0].header.message.slot') -# if (( current_slot > slot )); then -# break -# fi -# echo "Waiting for slot $slot, current slot is $current_slot" -# sleep $seconds_per_slot -# done - # Now derive the proof using the proof generation binary, which must already be running configured to the localnet -curl -X POST -H "Content-Type: application/json" \ - -d "{\"slot\": $slot, \"validator_index\": $validator_index}" \ - http://localhost:8989/v1/validator-proof | jq >"$SCRIPT_DIR/proof.json" +echo $INTEGRATION_PROVE_ENDPOINT +response=$(curl -s -w "%{http_code}" -X POST -H "Content-Type: application/json" \ + -d "{\"slot\": $slot, \"validator_index\": $validator_index}" \ + $INTEGRATION_PROVE_ENDPOINT/v1/validator-proof) + +http_code=${response: -3} +body=${response:0:${#response}-3} + +if [ "$http_code" != "200" ]; then + echo "Error: Failed to generate proof. HTTP code: $http_code" + echo "Response: $body" + exit 1 +fi + +echo "$body" | jq . >"$SCRIPT_DIR/proof.json" + +if [ ! -s "$SCRIPT_DIR/proof.json" ]; then + echo "Error: Generated proof is empty" + exit 1 +fi +export INTEGRATION_NST_DEPOSITOR=$INTEGRATION_NST_DEPOSITOR forge script script/integration/2_VerifyDepositNST.s.sol --skip-simulation \ --rpc-url $CLIENT_CHAIN_RPC --broadcast \ --evm-version cancun # required, otherwise you get EvmError: NotActivated diff --git a/test/foundry/unit/NetworkConfig.t.sol b/test/foundry/unit/NetworkConfig.t.sol index 89affbf1..78e16623 100644 --- a/test/foundry/unit/NetworkConfig.t.sol +++ b/test/foundry/unit/NetworkConfig.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; +import {stdError} from "forge-std/StdError.sol"; import "forge-std/Test.sol"; import {NetworkConfig} from "script/integration/NetworkConfig.sol"; From d855bab8e269a5a69f544609d9eff3f430c04bb8 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:25:53 +0000 Subject: [PATCH 08/22] fix: respond to more AI comments --- script/generate.mjs | 39 +++++++++++++++++++++++--------- script/integration/deposit.sh | 2 +- script/integration/prove.sh | 1 - src/core/Bootstrap.sol | 4 ++-- src/storage/BootstrapStorage.sol | 6 ++--- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/script/generate.mjs b/script/generate.mjs index 552e1323..37dbf10e 100644 --- a/script/generate.mjs +++ b/script/generate.mjs @@ -79,6 +79,18 @@ const { INTEGRATION_EXCHANGE_RATES } = process.env; + +if ( + !INTEGRATION_BEACON_CHAIN_ENDPOINT || + !CLIENT_CHAIN_RPC || + !INTEGRATION_BOOTSTRAP_ADDRESS || + !INTEGRATION_BASE_GENESIS_FILE_PATH || + !INTEGRATION_RESULT_GENESIS_FILE_PATH || + !INTEGRATION_EXCHANGE_RATES +) { + throw new Error('One or more required environment variables are missing.'); +} + import pkg from 'js-sha3'; const { keccak256 } = pkg; @@ -118,7 +130,7 @@ async function updateGenesisFile() { const api = getClient({baseUrl: INTEGRATION_BEACON_CHAIN_ENDPOINT}, {config}); const spec = (await api.config.getSpec()).value(); const maxEffectiveBalance = new Decimal(spec.MAX_EFFECTIVE_BALANCE).mul(GWEI_TO_WEI); - const ejectIonBalance = new Decimal(spec.EJECTION_BALANCE).mul(GWEI_TO_WEI); + const ejectionBalance = new Decimal(spec.EJECTION_BALANCE).mul(GWEI_TO_WEI); const slotsPerEpoch = spec.SLOTS_PER_EPOCH; let lastHeader = (await api.beacon.getBlockHeader({blockId: "finalized"})).value(); const finalizedSlot = lastHeader.header.message.slot; @@ -196,7 +208,9 @@ async function updateGenesisFile() { ); } else if (genesisJSON.app_state.oracle.params.token_feeders.length > 1) { // remove the ETH default token - genesisJSON.app_state.oracle.params.token_feeders = genesisJSON.app_state.oracle.params.token_feeders.slice(0, 1); + genesisJSON.app_state.oracle.params.token_feeders = genesisJSON.app_state.oracle.params.token_feeders.slice( + 0, 1 + ); } const supportedTokensCount = await myContract.methods.getWhitelistedTokensCount().call(); if (supportedTokensCount != tokenMetaInfos.length) { @@ -264,7 +278,7 @@ async function updateGenesisFile() { // break; } genesisJSON.app_state.oracle.params.tokens = Object.values(oracleTokens) - .sort((a, b) => {a.index - b.index}) + .sort((a, b) => a.index - b.index) .map(({index, ...rest}) => rest); genesisJSON.app_state.oracle.params.token_feeders = oracleTokenFeeders; supportedTokens.sort((a, b) => { @@ -308,12 +322,11 @@ async function updateGenesisFile() { const pubKeyCount = await myContract.methods.getPubkeysCount(stakerAddress).call(); const pubKeys = []; for(let k = 0; k < pubKeyCount; k++) { - // TODO: the contract stores not pubkeys but pubkey hashes. figure out where - // to get the correct value. it affects ClientChainGateway and the network - // overall as well. - pubKeys.push("0x98db81971df910a5d46314d21320f897060d76fdf137d22f0eb91a8693a4767d2a22730a3aaa955f07d13ad604f968e9"); + pubKeys.push(await myContract.methods.stakerToPubkeyIDs(stakerAddress, k).call()); } - const validatorStates = (await api.beacon.getStateValidators({stateId: stateRoot, validatorIds: pubKeys})).value(); + const validatorStates = (await api.beacon.getStateValidators( + {stateId: stateRoot, validatorIds: pubKeys.map(pubKey => parseInt(pubKey, 16))} + )).value(); let totalEffectiveBalance = new Decimal(0);; for(let k = 0; k < validatorStates.length; k++) { const validator = validatorStates[k]; @@ -325,9 +338,11 @@ async function updateGenesisFile() { } const valEffectiveBalance = new Decimal(validator.validator.effectiveBalance).mul(GWEI_TO_WEI); if (valEffectiveBalance.gt(maxEffectiveBalance)) { - throw new Error(`The effective balance of staker ${stakerAddress} exceeds the maximum effective balance.`); + throw new Error( + `The effective balance of staker ${stakerAddress} exceeds the maximum effective balance.` + ); } - if (valEffectiveBalance.lt(ejectIonBalance)) { + if (valEffectiveBalance.lt(ejectionBalance)) { console.log(`Skipping staker ${stakerAddress} due to low validator balance ${valEffectiveBalance}`); continue; } @@ -563,7 +578,9 @@ async function updateGenesisFile() { for (let j = 0; j < supportedTokens.length; j++) { const tokenAddress = (await myContract.methods.getWhitelistedTokenAtIndex(j).call()).tokenAddress; - const selfDelegationAmount = await myContract.methods.delegations(opAddressHex, opAddressExo, tokenAddress).call(); + const selfDelegationAmount = await myContract.methods.delegations( + opAddressHex, opAddressExo, tokenAddress + ).call(); amount = amount.plus( new Decimal(selfDelegationAmount.toString()). div('1e' + decimals[j]). diff --git a/script/integration/deposit.sh b/script/integration/deposit.sh index 3fd23518..3eee36c7 100755 --- a/script/integration/deposit.sh +++ b/script/integration/deposit.sh @@ -62,7 +62,7 @@ if ! [[ "$slots_per_epoch" =~ ^[0-9]+$ ]]; then fi seconds_per_slot=$(jq -r .data.SECONDS_PER_SLOT "$SCRIPT_DIR/spec.json") if ! [[ "$seconds_per_slot" =~ ^[0-9]+$ ]]; then - echo "Error: Invalid slots per epoch" + echo "Error: Invalid seconds per slot" exit 1 fi diff --git a/script/integration/prove.sh b/script/integration/prove.sh index d1e6f84e..0415f0a3 100755 --- a/script/integration/prove.sh +++ b/script/integration/prove.sh @@ -66,7 +66,6 @@ fi slot=$((slots_per_epoch * epoch)) # Now derive the proof using the proof generation binary, which must already be running configured to the localnet -echo $INTEGRATION_PROVE_ENDPOINT response=$(curl -s -w "%{http_code}" -X POST -H "Content-Type: application/json" \ -d "{\"slot\": $slot, \"validator_index\": $validator_index}" \ $INTEGRATION_PROVE_ENDPOINT/v1/validator-proof) diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index c2831fac..68e058bd 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -793,7 +793,7 @@ contract Bootstrap is totalDepositAmounts[msg.sender][VIRTUAL_NST_ADDRESS] += depositValue; withdrawableAmounts[msg.sender][VIRTUAL_NST_ADDRESS] += depositValue; depositsByToken[VIRTUAL_NST_ADDRESS] += depositValue; - stakerToPubkeys[msg.sender].push(validatorContainer.getPubkey()); + stakerToPubkeyIDs[msg.sender].push(bytes32(proof.validatorIndex)); emit DepositResult(true, VIRTUAL_NST_ADDRESS, msg.sender, depositValue); } @@ -830,7 +830,7 @@ contract Bootstrap is /// @param stakerAddress the address of the staker. /// @return the number of pubkeys deposited by the staker. function getPubkeysCount(address stakerAddress) external view returns (uint256) { - return stakerToPubkeys[stakerAddress].length; + return stakerToPubkeyIDs[stakerAddress].length; } } diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index 60c4cf16..0e53202a 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -177,9 +177,9 @@ contract BootstrapStorage is GatewayStorage { /// contracts and we put it after __gap to maintain the storage layout compatible with deployed contracts. mapping(address owner => IExoCapsule capsule) public ownerToCapsule; - /// @notice Mapping of staker addresses to their corresponding public keys. - /// @dev Maps staker addresses to their corresponding public keys used on the beacon chain. - mapping(address staker => bytes32[]) public stakerToPubkeys; + /// @notice Mapping of staker addresses to their corresponding validator indexes. + /// @dev Maps staker addresses to their corresponding validator indexes used on the beacon chain. + mapping(address staker => bytes32[]) public stakerToPubkeyIDs; /* -------------------------------------------------------------------------- */ /* Events */ From 391affc26c0d41721c81f7bd50db1636324508d7 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:54:14 +0000 Subject: [PATCH 09/22] fix(integration): remove container.json check --- script/integration/deposit.sh | 9 ++++----- script/integration/prove.sh | 24 +++++++++--------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/script/integration/deposit.sh b/script/integration/deposit.sh index 3eee36c7..c62b8c22 100755 --- a/script/integration/deposit.sh +++ b/script/integration/deposit.sh @@ -25,7 +25,6 @@ for var in "${vars[@]}"; do fi done -# Fetch the validator details and save them to container.json if ! curl -s -X GET "$INTEGRATION_BEACON_CHAIN_ENDPOINT/eth/v1/beacon/genesis" -H "accept: application/json" | jq . >"$SCRIPT_DIR/genesis.json"; then echo "Error: Failed to fetch genesis data from the beacon chain" exit 1 @@ -57,13 +56,13 @@ fi deposit_address=$(jq -r .data.DEPOSIT_CONTRACT_ADDRESS "$SCRIPT_DIR/genesis.json") slots_per_epoch=$(jq -r .data.SLOTS_PER_EPOCH "$SCRIPT_DIR/spec.json") if ! [[ "$slots_per_epoch" =~ ^[0-9]+$ ]]; then - echo "Error: Invalid slots per epoch" - exit 1 + echo "Error: Invalid slots per epoch" + exit 1 fi seconds_per_slot=$(jq -r .data.SECONDS_PER_SLOT "$SCRIPT_DIR/spec.json") if ! [[ "$seconds_per_slot" =~ ^[0-9]+$ ]]; then - echo "Error: Invalid seconds per slot" - exit 1 + echo "Error: Invalid seconds per slot" + exit 1 fi # Make the variables available to the forge script diff --git a/script/integration/prove.sh b/script/integration/prove.sh index 0415f0a3..8c3c0fed 100755 --- a/script/integration/prove.sh +++ b/script/integration/prove.sh @@ -22,14 +22,8 @@ done # Check for the files to exist if [ ! -f "$SCRIPT_DIR/spec.json" ]; then - echo "Error: spec.json not found in $SCRIPT_DIR" - exit 1 -fi - -# Check for the files to exist -if [ ! -f "$SCRIPT_DIR/container.json" ]; then - echo "Error: container.json not found in $SCRIPT_DIR" - exit 1 + echo "Error: spec.json not found in $SCRIPT_DIR" + exit 1 fi # Fetch the validator details and save them to container.json @@ -67,23 +61,23 @@ slot=$((slots_per_epoch * epoch)) # Now derive the proof using the proof generation binary, which must already be running configured to the localnet response=$(curl -s -w "%{http_code}" -X POST -H "Content-Type: application/json" \ - -d "{\"slot\": $slot, \"validator_index\": $validator_index}" \ - $INTEGRATION_PROVE_ENDPOINT/v1/validator-proof) + -d "{\"slot\": $slot, \"validator_index\": $validator_index}" \ + $INTEGRATION_PROVE_ENDPOINT/v1/validator-proof) http_code=${response: -3} body=${response:0:${#response}-3} if [ "$http_code" != "200" ]; then - echo "Error: Failed to generate proof. HTTP code: $http_code" - echo "Response: $body" - exit 1 + echo "Error: Failed to generate proof. HTTP code: $http_code" + echo "Response: $body" + exit 1 fi echo "$body" | jq . >"$SCRIPT_DIR/proof.json" if [ ! -s "$SCRIPT_DIR/proof.json" ]; then - echo "Error: Generated proof is empty" - exit 1 + echo "Error: Generated proof is empty" + exit 1 fi export INTEGRATION_NST_DEPOSITOR=$INTEGRATION_NST_DEPOSITOR From e232ea712c7eb44583333f8bd9cb81d750985714 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:16:33 +0000 Subject: [PATCH 10/22] fix(env): clarify port usage --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index 66a893b6..1c267757 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ +# anvil --port 8646 or via docker compose up in eth-pos-devnet CLIENT_CHAIN_RPC=http://localhost:8646 EXOCORE_TESETNET_RPC=http://localhost:8545 EXOCORE_LOCAL_RPC=http://localhost:8545 From c9caf33ed79f53dabcecaa287da24da012e24df9 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Thu, 21 Nov 2024 08:17:01 +0000 Subject: [PATCH 11/22] fix(generate): respond to review comments --- script/generate.mjs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/script/generate.mjs b/script/generate.mjs index 37dbf10e..cbf08b10 100644 --- a/script/generate.mjs +++ b/script/generate.mjs @@ -327,7 +327,7 @@ async function updateGenesisFile() { const validatorStates = (await api.beacon.getStateValidators( {stateId: stateRoot, validatorIds: pubKeys.map(pubKey => parseInt(pubKey, 16))} )).value(); - let totalEffectiveBalance = new Decimal(0);; + let totalEffectiveBalance = new Decimal(0); for(let k = 0; k < validatorStates.length; k++) { const validator = validatorStates[k]; // https://hackmd.io/@protolambda/validator_status @@ -507,7 +507,7 @@ async function updateGenesisFile() { if (!genesisJSON.app_state.delegation.associations) { genesisJSON.app_state.delegation.associations = []; } - const validators = []; + let validators = []; const operators = []; const associations = []; const operatorsCount = await myContract.methods.getValidatorsCount().call(); @@ -715,7 +715,7 @@ async function updateGenesisFile() { return b.power.cmp(a.power); }); // pick top N by vote power - validators.slice(0, genesisJSON.app_state.dogfood.params.max_validators); + validators = validators.slice(0, genesisJSON.app_state.dogfood.params.max_validators); let totalPower = 0; validators.forEach((val) => { // truncate @@ -840,8 +840,9 @@ async function updateGenesisFile() { ); console.log('Genesis file updated successfully.'); } catch (error) { - console.error('Error updating genesis file:', error.message); - console.error('Stack trace:', error.stack); + console.error( + 'Error updating genesis file:', error.message, '\nstack trace:', error.stack + ); } }; From e8fdc53b4fa2e9894b183d25c21676b6c19450ec Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:00:48 +0000 Subject: [PATCH 12/22] fix(generate): update x/oracle data - The `nstETH` token needs to be added both as a token and as a feeder - The `nstETH` token's `asset_id` needs to map back to the native (non-staked) token's `asset_id` --- .env.example | 2 +- script/generate.mjs | 76 +++++++++++++++++++++++++------------ script/integration/prove.sh | 2 - 3 files changed, 53 insertions(+), 27 deletions(-) diff --git a/.env.example b/.env.example index 1c267757..345ab592 100644 --- a/.env.example +++ b/.env.example @@ -36,7 +36,7 @@ INTEGRATION_SIGNATURE=0x922a316bdc3516bfa66e88259d5e93e339ef81bc85b70e6c71554222 INTEGRATION_DEPOSIT_DATA_ROOT=0x456934ced8f08ff106857418a6d885ba69d31e1b7fab9a931be06da25490cd1d INTEGRATION_BEACON_CHAIN_ENDPOINT=http://localhost:3500 INTEGRATION_PROVE_ENDPOINT=http://localhost:8989 -# for generate.js +# for generate.mjs INTEGRATION_BOOTSTRAP_ADDRESS=0xF801fc13AA08876F343fEBf50dFfA52A78180811 INTEGRATION_EXCHANGE_RATES=1000.123,2000.123,1799.345345 INTEGRATION_BASE_GENESIS_FILE_PATH= diff --git a/script/generate.mjs b/script/generate.mjs index cbf08b10..81620de4 100644 --- a/script/generate.mjs +++ b/script/generate.mjs @@ -18,9 +18,10 @@ const tokenMetaInfos = [ // this must be in the same order as whitelistTokens // they are provided because the symbol may not match what we are using from the price feeder. // for example, exoETH is not a real token and we are using the price feed for ETH. -// the script will take care of mapping the duplicates to a common token for x/oracle params. +// the script will take care of mapping the nstETH asset_id to the ETH asset_id in the oracle +// tokens list. const tokenNamesForOracle = [ - 'ETH', 'ETH', 'wstETH' // not case sensitive + 'nstETH', 'ETH', 'wstETH' // not case sensitive ] const nativeChain = { "name": "Exocore", @@ -230,8 +231,8 @@ async function updateGenesisFile() { const assetIds = []; // start with the initial value const oracleTokens = genesisJSON.app_state.oracle.params.tokens; - const oracleTokenFeeders = []; - let offset = 0; + const oracleTokenFeeders = genesisJSON.app_state.oracle.params.token_feeders; + let hasNst = {}; for (let i = 0; i < supportedTokensCount; i++) { let token = await myContract.methods.getWhitelistedTokenAtIndex(i).call(); const deposit_amount = await myContract.methods.depositsByToken(token.tokenAddress).call(); @@ -251,35 +252,62 @@ async function updateGenesisFile() { supportedTokens[i] = tokenCleaned; decimals.push(token.decimals); assetIds.push(token.tokenAddress.toLowerCase() + clientChainSuffix); - const oracleToken = { - name: tokenNamesForOracle[i], - index: offset, - chain_id: 1, // constant intentionally, representing the first chain in the list (after the reserved blank one) - contract_address: token.tokenAddress, - active: true, - asset_id: token.tokenAddress.toLowerCase() + clientChainSuffix, - decimal: 8, // price decimals, not token decimals - } + let oracleToken; const oracleTokenFeeder = { - token_id: (i + 1 - offset).toString(), // first is reserved + token_id: (i + 1).toString(), // first is reserved rule_id: "1", start_round_id: "1", - start_base_block: (height + 10000).toString(), + start_base_block: (height + 20).toString(), interval: "30", end_block: "0", - } - if (oracleToken.name in oracleTokens) { - oracleTokens[oracleToken.name].asset_id += ',' + oracleToken.asset_id; - offset += 1; + }; + if (tokenNamesForOracle[i].toLowerCase().startsWith('nst')) { + if (token.tokenAddress != VIRTUAL_STAKED_ETH_ADDR) { + throw new Error('Oracle name refers to NST token but this is LST'); + } + oracleToken = { + name: tokenNamesForOracle[i], + chain_id: 1, // first chain in the list + contract_address: '', + active: true, + asset_id: '', + decimal: 8, // price decimals, not token decimals + }; } else { - oracleTokens[oracleToken.name] = oracleToken; - oracleTokenFeeders.push(oracleTokenFeeder); + if (token.tokenAddress == VIRTUAL_STAKED_ETH_ADDR) { + throw new Error('Oracle name refers to LST token but this is NST'); + } + oracleToken = { + name: tokenNamesForOracle[i], + chain_id: 1, + contract_address: token.tokenAddress, + active: true, + asset_id: token.tokenAddress.toLowerCase() + clientChainSuffix, + decimal: 8, + }; + } + oracleTokens.push(oracleToken); + oracleTokenFeeders.push(oracleTokenFeeder); + if (oracleToken.name.toLowerCase().startsWith('nst')) { + if (hasNst.status) { + throw new Error('Multiple NST tokens found.'); + } + hasNst = { + // only used for tracking multiple NST tokens + status: true, + asset_id: token.tokenAddress.toLowerCase() + clientChainSuffix, + remainder: oracleToken.name.slice(3), + }; } // break; } - genesisJSON.app_state.oracle.params.tokens = Object.values(oracleTokens) - .sort((a, b) => a.index - b.index) - .map(({index, ...rest}) => rest); + // bind nstETH asset_id to the ETH token, if nstETH is found. + genesisJSON.app_state.oracle.params.tokens = oracleTokens.map((token) => { + if (token.name == hasNst.remainder) { + token.asset_id += "," + hasNst.asset_id; + } + return token; + }); genesisJSON.app_state.oracle.params.token_feeders = oracleTokenFeeders; supportedTokens.sort((a, b) => { if (a.asset_basic_info.symbol < b.asset_basic_info.symbol) { diff --git a/script/integration/prove.sh b/script/integration/prove.sh index 8c3c0fed..2235092a 100755 --- a/script/integration/prove.sh +++ b/script/integration/prove.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash -set -e - # Get the directory of the script SCRIPT_DIR=$(dirname "$(readlink -f "$0")") From 6a270bf85d67822fadeb143dc67c9bbdbc86868a Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:20:01 +0000 Subject: [PATCH 13/22] fix(generate): support whitelist `nstETH` + !`ETH` In the event that the Bootstrap contract has whitelisted `nstETH` but not `ETH`, the `nstETH` entry will only fetch the effective balance and the token price from the `ETH` entry will be missing. This commit fixes that by manually adding an `ETH` entry to the oracle state in such an event. --- script/generate.mjs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/script/generate.mjs b/script/generate.mjs index 81620de4..3eaab51a 100644 --- a/script/generate.mjs +++ b/script/generate.mjs @@ -43,7 +43,7 @@ const nativeAsset = { "staking_total_amount": "0" }; const EXOCORE_BECH32_PREFIX = 'exo'; -const VIRTUAL_STAKED_ETH_ADDR = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE" +const VIRTUAL_STAKED_ETH_ADDR = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; const GWEI_TO_WEI = new Decimal(1e9); import dotenv from 'dotenv'; @@ -302,12 +302,35 @@ async function updateGenesisFile() { // break; } // bind nstETH asset_id to the ETH token, if nstETH is found. + let found = false; genesisJSON.app_state.oracle.params.tokens = oracleTokens.map((token) => { if (token.name == hasNst.remainder) { + found = true; token.asset_id += "," + hasNst.asset_id; } return token; }); + if (!found && hasNst.status) { + // add `ETH` manually, if `nstETH` exists but not `ETH` in the oracle tokens. + // the former in `tokens` is to get the validator effective balance from beacon, denominated in ETH. + // the latter in `tokens` is to get the price of ETH in USD. + genesisJSON.app_state.oracle.params.tokens.push({ + name: hasNst.remainder, + chain_id: 1, + contract_address: VIRTUAL_STAKED_ETH_ADDR, + active: true, + asset_id: hasNst.asset_id, + decimal: 8, + }); + genesisJSON.app_state.oracle.params.token_feeders.push({ + token_id: (Number(supportedTokensCount) + 1).toString(), + rule_id: "1", + start_round_id: "1", + start_base_block: (height + 20).toString(), + interval: "30", + end_block: "0", + }); + } genesisJSON.app_state.oracle.params.token_feeders = oracleTokenFeeders; supportedTokens.sort((a, b) => { if (a.asset_basic_info.symbol < b.asset_basic_info.symbol) { From 2395890489ff6be7841d93b6e9e419777631c03e Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:12:45 +0000 Subject: [PATCH 14/22] update for effective balance --- script/generate.mjs | 326 ++++++++++++++++++++++++------- src/core/Bootstrap.sol | 15 +- src/storage/BootstrapStorage.sol | 4 + 3 files changed, 275 insertions(+), 70 deletions(-) diff --git a/script/generate.mjs b/script/generate.mjs index 3eaab51a..3ef6149c 100644 --- a/script/generate.mjs +++ b/script/generate.mjs @@ -44,7 +44,6 @@ const nativeAsset = { }; const EXOCORE_BECH32_PREFIX = 'exo'; const VIRTUAL_STAKED_ETH_ADDR = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; -const GWEI_TO_WEI = new Decimal(1e9); import dotenv from 'dotenv'; dotenv.config(); @@ -130,9 +129,9 @@ async function updateGenesisFile() { // Create beacon API client const api = getClient({baseUrl: INTEGRATION_BEACON_CHAIN_ENDPOINT}, {config}); const spec = (await api.config.getSpec()).value(); - const maxEffectiveBalance = new Decimal(spec.MAX_EFFECTIVE_BALANCE).mul(GWEI_TO_WEI); - const ejectionBalance = new Decimal(spec.EJECTION_BALANCE).mul(GWEI_TO_WEI); - const slotsPerEpoch = spec.SLOTS_PER_EPOCH; + const maxEffectiveBalance = new Decimal(web3.utils.toWei(spec.MAX_EFFECTIVE_BALANCE, 'gwei')); + const ejectionBalance = new Decimal(web3.utils.toWei(spec.EJECTION_BALANCE, 'gwei')); + const slotsPerEpoch = parseInt(spec.SLOTS_PER_EPOCH, 10); let lastHeader = (await api.beacon.getBlockHeader({blockId: "finalized"})).value(); const finalizedSlot = lastHeader.header.message.slot; const finalizedEpoch = Math.floor(finalizedSlot / slotsPerEpoch); @@ -149,7 +148,16 @@ async function updateGenesisFile() { const genesisData = await fs.readFile(INTEGRATION_BASE_GENESIS_FILE_PATH); const genesisJSON = jsonBig.parse(genesisData); - const height = parseInt(genesisJSON.initial_height, 10); + // the initial height, when starting a new chain, is 1. + // however, when restarting an exported chain, it is 1 + the last block in the + // exported chain. to that end, we will set an initial_height of 0, if + // the genesis file has it set as 1. this will allow the first block to be + // free of any genesis state which depends on the height. + let height = parseInt(genesisJSON.initial_height, 10); + if (height == 1) { + height = 0; + } + const bootstrapped = await myContract.methods.bootstrapped().call(); if (bootstrapped) { throw new Error('The contract has already been bootstrapped.'); @@ -270,8 +278,8 @@ async function updateGenesisFile() { chain_id: 1, // first chain in the list contract_address: '', active: true, - asset_id: '', - decimal: 8, // price decimals, not token decimals + asset_id: "NST" + clientChainSuffix, + decimal: 0, }; } else { if (token.tokenAddress == VIRTUAL_STAKED_ETH_ADDR) { @@ -353,6 +361,7 @@ async function updateGenesisFile() { const deposits = []; const nativeTokenDepositors = []; const staker_infos = []; + let slashProportions = []; let staker_index_counter = 0; for (let i = 0; i < depositorsCount; i++) { const stakerAddress = await myContract.methods.depositors(i).call(); @@ -367,73 +376,200 @@ async function updateGenesisFile() { let withdrawableValue = new Decimal((await myContract.methods.withdrawableAmounts( stakerAddress, tokenAddress ).call()).toString()); + // for validator pubkey ids to be available, a deposit must have been made. + // hence, the depositValue > 0 condition is necessary. if ((tokenAddress == VIRTUAL_STAKED_ETH_ADDR) && (depositValue > 0)) { // we have to use the effective balance calculation nativeTokenDepositors.push(stakerAddress.toLowerCase()); const pubKeyCount = await myContract.methods.getPubkeysCount(stakerAddress).call(); + if (pubKeyCount == 0) { + throw new Error('No pubkeys found for the staker.'); + } const pubKeys = []; for(let k = 0; k < pubKeyCount; k++) { pubKeys.push(await myContract.methods.stakerToPubkeyIDs(stakerAddress, k).call()); } + if (pubKeys.length == 0) { + throw new Error('No pubkeys found for the staker.'); + } + const staker_info = { + staker_addr: stakerAddress.toLowerCase(), + staker_index: staker_index_counter, + validator_pubkey_list: pubKeys, + balance_list: [] // filled later. + }; + staker_index_counter += 1; const validatorStates = (await api.beacon.getStateValidators( {stateId: stateRoot, validatorIds: pubKeys.map(pubKey => parseInt(pubKey, 16))} )).value(); let totalEffectiveBalance = new Decimal(0); + let balances = []; + // remember that these validators are specific to the provided staker address. + // a validator is identified by its public key (or validator index), while a staker + // is identified by its address. each staker may have multiple validators. for(let k = 0; k < validatorStates.length; k++) { + // we cannot drop validators even though they may be slashed. this is because + // even after slashing, the validators will retain 16 ETH of total balance. + // this must be allowed to be withdrawn after Exocore is launched. since the + // withdrawal credentials point to the ExoCapsule, such a withdrawal will + // be permitted only via Exocore, which must, correspondingly, have this validator's + // state recorded. const validator = validatorStates[k]; - // https://hackmd.io/@protolambda/validator_status - // it is sufficient to check for active_ongoing - if (validator.status != "active_ongoing") { - console.log(`Skipping staker ${stakerAddress} due to inactive validator ${pubKeys[k]}`); - continue; + const effectiveBalance = new Decimal(web3.utils.toWei(validator.validator.effectiveBalance.toString(), "gwei")); + if (effectiveBalance.eq(0)) { + if (!validator.status.startsWith("withdrawal")) { + throw new Error( + `The effective balance of ${effectiveBalance} is zero for a validator that is not withdrawing.` + ); + } } - const valEffectiveBalance = new Decimal(validator.validator.effectiveBalance).mul(GWEI_TO_WEI); - if (valEffectiveBalance.gt(maxEffectiveBalance)) { + // even if max is 16, this will still hold + if (effectiveBalance.gt(maxEffectiveBalance)) { throw new Error( - `The effective balance of staker ${stakerAddress} exceeds the maximum effective balance.` + `The effective balance of ${effectiveBalance} is greater than the maximum effective balance.` ); } - if (valEffectiveBalance.lt(ejectionBalance)) { - console.log(`Skipping staker ${stakerAddress} due to low validator balance ${valEffectiveBalance}`); - continue; + if (validator.status == "pending_initialized") { + // the deposit has happened, but perhaps not enough, or churn limit is exceeded, + // or the simplest case, the epoch containing the deposit has not yet ended. + // ideally, the effective balance should be equal to the depositValue, which + // would sum all the deposits made to the beacon chain. + // however, if a proof for a deposit was not submitted to the Bootstrap contract, + // but a deposit was made, the effective balance > depositValue. + // in a live chain, either a proof submission is made, or, the price feeder + // performs such an update. we will handle the update here ourselves. + // here, if the epoch in which the deposit was made hasn't ended, the effective + // balance may possibly be equal to 32 ETH. hence, we cannot check any range + // for this case. + } else if (validator.status == "pending_queued") { + // the deposit has happened, but the validator is not yet active. in this case, + // the effective balance must be exactly 32 ETH. otherwise, it would never be + // activated. + if (effectiveBalance.ne(maxEffectiveBalance)) { + throw new Error( + `The effective balance of ${effectiveBalance} is not equal to the maximum effective balance.` + ); + } + } else if (validator.status.startsWith("active") || validator.status.startsWith("exited")) { + if (validator.status.endsWith("slashed")) { + // [8, 16] + if (effectiveBalance.gt(ejectionBalance)) { + throw new Error( + `The effective balance of ${effectiveBalance} is greater than the ejection balance.` + ); + } else if (effectiveBalance.lt(ejectionBalance.div(2))) { + throw new Error( + `The effective balance of ${effectiveBalance} is less than half the ejection balance.` + ); + } + } else { + // [16, 32], of which 32 is already checked. + if (effectiveBalance.lt(ejectionBalance)) { + throw new Error( + `The effective balance of ${effectiveBalance} is less than the ejection balance.` + ); + } + } + } else { + // beacon chain withdrawal, may or may not have landed on the execution layer. + // we will need to record this in state nevertheless, because withdrawal of the execution layer ETH + // must be permitted. + if (!effectiveBalance.isZero()) { + throw new Error( + `The effective balance of ${effectiveBalance} is not zero for a withdrawal status.` + ); + } + } + totalEffectiveBalance = totalEffectiveBalance.plus(effectiveBalance); + let new_balance = { + round_id: 0, + block: height, + index: 0, + balance: 0, + // since we are only considering the total amount after slashing and refunds, + // it is always a deposit. + change: "ACTION_DEPOSIT" + }; + if (balances.length > 0) { + new_balance = balances[balances.length - 1]; + new_balance.index += 1; } - totalEffectiveBalance = totalEffectiveBalance.plus(valEffectiveBalance); + new_balance.balance = web3.utils.fromWei(effectiveBalance.toFixed(), "ether"); + balances.push(new_balance); } - if (depositValue > totalEffectiveBalance) { - console.log("Staker has more deposit than effective balance."); - // deposited 32 ETH and left with 31 ETH, aka downtime slashing - let toSlash = depositValue.minus(totalEffectiveBalance); - // if withdrawableValue can take the full slashing, do it. - if (withdrawableValue.gt(toSlash)) { - withdrawableValue = withdrawableValue.minus(toSlash); + // now we have the totalEffectiveBalance across all validator pubkeys for this staker + // we will compare it with the depositValue. ideally, they should be equal. however, + // a deposit proof may not have been submitted or the validator might have been + // slashed, causing a deviation. it is also possible for a validator to have exited + // from the beacon chain (without attempting to submit a proof), causing a deviation. + if (totalEffectiveBalance.eq(depositValue)) { + // (1) they are equal; do nothing + } else if (totalEffectiveBalance.lt(depositValue)) { + // (2) totalEffectiveBalance > depositValue; add spare as deposit but not withdrawable + depositValue = totalEffectiveBalance; + } else { + // (3) lower effective balance means that the Ethereum validator was either downtime + // penalised or slashed. we follow the logic enshrined in update_native_restaking_balance.go + // store this value before making any adjustments to calculate the slash proportion accurately. + let totalDelegated = depositValue.minus(withdrawableValue); + let slashFromWithdrawable = depositValue.minus(totalEffectiveBalance); + let pendingSlashAmount = slashFromWithdrawable.minus(withdrawableValue); + if (pendingSlashAmount.isPositive()) { + slashFromWithdrawable = withdrawableValue; } else { - // if not, only do it partially. - toSlash = toSlash.minus(withdrawableValue); - withdrawableValue = new Decimal(0); + pendingSlashAmount = new Decimal(0); } - // there is still some left, so do it from the deposit. - if (toSlash.gt(0)) { - if (depositValue.gt(toSlash)) { - depositValue = depositValue.minus(toSlash); - } else { - console.log(`Skipping staker ${stakerAddress} due to insufficient deposit ${depositValue}`); - continue; + depositValue = depositValue.minus(slashFromWithdrawable); + withdrawableValue = withdrawableValue.minus(slashFromWithdrawable); + // we don't have any undelegations, so we will skip that step. + if (pendingSlashAmount.isPositive()) { + // slash across all delegations, propotionately. + // let's look at an example. + // effective balance = 16 ETH at the time of generate.mjs + // originally, deposit value = 32 ETH, withdrawable value = 8 ETH + // slash from withdrawable = 16 ETH + // pending slash amount = 8 ETH + // slash from withdrawable = 8 ETH + // deposit value = 24 ETH, withdrawable value = 0 ETH + // we still have to slash 8 ETH of total delegated 24 ETH, across all operators + // to which delegations exist. so, 1/3 needs to be slashed. it should be saved + // and applied to staker_asset and operator_asset etc. + // in addition, we will apply it to the depositValue here too. + // total delegated was originally 24 ETH. so, 8 ETH (=1/3) needs to be slashed. + // the slashing needs to be applied to + // -- staker + asset + {each validator to which that combination is delegated} + // it should be applied to the delegated value against each validator, + // and then it will flow automatically(?) to the share. + let slashProportion = pendingSlashAmount.div(totalDelegated); + if (slashProportion.greaterThan(new Decimal(1))) { + slashProportion = new Decimal(1); + } + depositValue = totalDelegated.mul((new Decimal(1)).minus(slashProportion)); + // a certain subset of the validators is impacted by this above slashing. + // our goal is to find that subset and save it such that it can be applied + // to the delegated value below. + let impactedValidators = []; + let impactedValidatorsCount = + await myContract.methods.getValidatorsCountForStakerToken(stakerAddress, tokenAddress).call(); + for(let k = 0; k < impactedValidatorsCount; k++) { + let impactedValidator = + await myContract.methods.stakerToTokenToValidators(stakerAddress, tokenAddress, k).call(); + impactedValidators.push(impactedValidator); + } + if ((impactedValidators.length == 0) && (!slashProportion.isZero())) { + slashProportions.push({ + staker: stakerAddress, + token: tokenAddress, + proportion: slashProportion, + impacted_validators: impactedValidators + }); } } - } else if (depositValue < totalEffectiveBalance) { - // deposited 32 ETH and left with 33 ETH, aka rewards - const delta = totalEffectiveBalance.minus(depositValue); - depositValue = depositValue.plus(delta); - withdrawableValue = withdrawableValue.plus(delta); } - staker_infos.push({ - staker_addr: stakerAddress.toLowerCase(), - staker_index: staker_index_counter, - validator_pubkey_list: pubKeys, - // the balance list represents the history of the balance. for bootstrap, that is empty. - balance_list: [] - }); - staker_index_counter += 1; + staker_info.balance_list = balances; + if (!totalEffectiveBalance.isZero()) { + staker_infos.push(staker_info); + } } const depositByStakerForAsset = { asset_id: tokenAddress.toLowerCase() + clientChainSuffix, @@ -489,22 +625,38 @@ async function updateGenesisFile() { // do not reuse the older array since it has been sorted. const tokenAddress = (await myContract.methods.getWhitelistedTokenAtIndex(j).call()).tokenAddress; - const delegationValue = await myContract.methods.delegationsByValidator( + let matchingEntries = slashProportions.filter( + (element) => element.token === tokenAddress && element.impacted_validators.includes(validatorExoAddress) + ); + let totalSlashing = new Decimal(0); + let selfSlashing = new Decimal(0); + for(let k = 0; k < matchingEntries.length; k++) { + let matchingEntry = matchingEntries[k]; + let delegation = await myContract.methods.delegations( + matchingEntry.staker, validatorExoAddress, tokenAddress + ).call(); + if (delegation > 0) { + let slashing = new Decimal(delegation).mul(matchingEntry.proportion); + totalSlashing = totalSlashing.plus(slashing); + if (matchingEntry.staker == validatorEthAddress) { + selfSlashing = slashing; + } + } + } + const delegationValue = new Decimal((await myContract.methods.delegationsByValidator( validatorExoAddress, tokenAddress - ).call(); - const totalShare = new Decimal(delegationValue.toString()); - const selfDelegation = await myContract.methods.delegations( + ).call()).toString()).minus(totalSlashing); + const selfDelegation = new Decimal((await myContract.methods.delegations( validatorEthAddress, validatorExoAddress, tokenAddress - ).call(); - const selfShare = new Decimal(selfDelegation.toString()); + ).call()).toString()).minus(selfSlashing); const assetsByOperatorForAsset = { asset_id: tokenAddress.toLowerCase() + clientChainSuffix, info: { - total_amount: delegationValue.toString(), + total_amount: delegationValue.toFixed(), pending_undelegation_amount: "0", - total_share: totalShare.toFixed(), - operator_share: selfShare.toFixed(), + total_share: delegationValue.toFixed(), + operator_share: selfDelegation.toFixed(), } }; assetsByOperator.push(assetsByOperatorForAsset); @@ -629,19 +781,38 @@ async function updateGenesisFile() { for (let j = 0; j < supportedTokens.length; j++) { const tokenAddress = (await myContract.methods.getWhitelistedTokenAtIndex(j).call()).tokenAddress; - const selfDelegationAmount = await myContract.methods.delegations( + let selfDelegationAmount = new Decimal((await myContract.methods.delegations( opAddressHex, opAddressExo, tokenAddress - ).call(); + ).call()).toString()); + let matchingEntries = slashProportions.filter( + (element) => element.token === tokenAddress && element.impacted_validators.includes(opAddressExo) + ); + let totalSlashing = new Decimal(0); + let selfSlashing = new Decimal(0); + for(let k = 0; k < matchingEntries.length; k++) { + let matchingEntry = matchingEntries[k]; + let delegation = await myContract.methods.delegations( + matchingEntry.staker, opAddressExo, tokenAddress + ).call(); + if (delegation > 0) { + let slashing = new Decimal(delegation).mul(matchingEntry.proportion); + totalSlashing = totalSlashing.plus(slashing); + if (matchingEntry.staker == opAddressHex) { + selfSlashing = slashing; + } + } + } + selfDelegationAmount = selfDelegationAmount.minus(selfSlashing); amount = amount.plus( - new Decimal(selfDelegationAmount.toString()). + selfDelegationAmount. div('1e' + decimals[j]). mul(exchangeRates[j].toFixed()) ); - const perTokenDelegation = await myContract.methods.delegationsByValidator( + const perTokenDelegation = new Decimal((await myContract.methods.delegationsByValidator( opAddressExo, tokenAddress - ).call(); + ).call()).toString()).minus(totalSlashing); totalAmount = totalAmount.plus( - new Decimal(perTokenDelegation.toString()). + perTokenDelegation. div('1e' + decimals[j]). mul(exchangeRates[j].toFixed()) ); @@ -808,16 +979,33 @@ async function updateGenesisFile() { console.log(`Skipping operator with invalid bech32 address: ${operator}`); continue; } - const amount = await myContract.methods.delegations( + let matchingEntries = slashProportions.filter( + (element) => element.token === tokenAddress && element.impacted_validators.includes(operator) + ); + let totalSlashing = new Decimal(0); + let selfSlashing = new Decimal(0); + for(let k = 0; k < matchingEntries.length; k++) { + let matchingEntry = matchingEntries[k]; + let delegation = await myContract.methods.delegations( + matchingEntry.staker, validatorExoAddress, tokenAddress + ).call(); + if (delegation > 0) { + let slashing = new Decimal(delegation).mul(matchingEntry.proportion); + totalSlashing = totalSlashing.plus(slashing); + if (matchingEntry.staker == validatorEthAddress) { + selfSlashing = slashing; + } + } + } + const amount = new Decimal((await myContract.methods.delegations( staker, operator, tokenAddress - ).call(); - if (amount.toString() > 0) { + ).call()).toString()).minus(totalSlashing); + if (amount.isPositive()) { const key = getJoinedStoreKey(stakerId, assetId, operator); - const share = new Decimal(amount.toString()); delegation_states.push({ key: key, states: { - undelegatable_share: share.toFixed(), + undelegatable_share: amount.toFixed(), wait_undelegation_amount: "0" }, }); diff --git a/src/core/Bootstrap.sol b/src/core/Bootstrap.sol index 68e058bd..b13057f1 100644 --- a/src/core/Bootstrap.sol +++ b/src/core/Bootstrap.sol @@ -514,6 +514,11 @@ contract Bootstrap is if (withdrawable < amount) { revert Errors.BootstrapInsufficientWithdrawableBalance(); } + + if (delegations[user][validator][token] == 0) { + // if this amount later becomes 0, it is ok. we don't worry about removing it. + stakerToTokenToValidators[user][token].push(validator); + } delegations[user][validator][token] += amount; delegationsByValidator[validator][token] += amount; withdrawableAmounts[user][token] -= amount; @@ -828,9 +833,17 @@ contract Bootstrap is /// by a staker. The deposit must include deposit + verification for inclusion /// into the beacon chain. /// @param stakerAddress the address of the staker. - /// @return the number of pubkeys deposited by the staker. + /// @return uint256 The number of pubkeys deposited by the staker. function getPubkeysCount(address stakerAddress) external view returns (uint256) { return stakerToPubkeyIDs[stakerAddress].length; } + /// @notice Returns the number of validators to whom a staker has delegated a token. + /// @param stakerAddress The address of the staker. + /// @param token The address of the token. + /// @return uint256 The number of validators to whom the staker has delegated the token. + function getValidatorsCountForStakerToken(address stakerAddress, address token) external view returns (uint256) { + return stakerToTokenToValidators[stakerAddress][token].length; + } + } diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index 0e53202a..9d7d3fcc 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -181,6 +181,10 @@ contract BootstrapStorage is GatewayStorage { /// @dev Maps staker addresses to their corresponding validator indexes used on the beacon chain. mapping(address staker => bytes32[]) public stakerToPubkeyIDs; + /// @notice Mapping of staker address to token to list of validators. + /// @dev Maps staker addresses to a mapping of token addresses to a list of validators. + mapping(address staker => mapping(address token => string[])) public stakerToTokenToValidators; + /* -------------------------------------------------------------------------- */ /* Events */ /* -------------------------------------------------------------------------- */ From d6d7fe2d8f712f59417340b0d132a89ba72beab9 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Thu, 28 Nov 2024 23:42:31 +0000 Subject: [PATCH 15/22] respond to AI comments --- script/generate.mjs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/script/generate.mjs b/script/generate.mjs index 3ef6149c..16efdf32 100644 --- a/script/generate.mjs +++ b/script/generate.mjs @@ -983,18 +983,14 @@ async function updateGenesisFile() { (element) => element.token === tokenAddress && element.impacted_validators.includes(operator) ); let totalSlashing = new Decimal(0); - let selfSlashing = new Decimal(0); for(let k = 0; k < matchingEntries.length; k++) { let matchingEntry = matchingEntries[k]; let delegation = await myContract.methods.delegations( - matchingEntry.staker, validatorExoAddress, tokenAddress + matchingEntry.staker, operator, tokenAddress ).call(); if (delegation > 0) { let slashing = new Decimal(delegation).mul(matchingEntry.proportion); totalSlashing = totalSlashing.plus(slashing); - if (matchingEntry.staker == validatorEthAddress) { - selfSlashing = slashing; - } } } const amount = new Decimal((await myContract.methods.delegations( From e3b7a9e40112636a1fd049669e7548b14ea9a427 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Fri, 29 Nov 2024 03:24:53 +0000 Subject: [PATCH 16/22] update for 31 ETH test --- script/generate.mjs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/script/generate.mjs b/script/generate.mjs index 16efdf32..9075397f 100644 --- a/script/generate.mjs +++ b/script/generate.mjs @@ -97,6 +97,7 @@ const { keccak256 } = pkg; import JSONbig from 'json-bigint'; const jsonBig = JSONbig({ useNativeBigInt: true }); +const ZERO_DECIMAL = new Decimal(0); function getChainIDWithoutPrevision(chainID) { const splitStr = chainID.split('-'); @@ -416,6 +417,7 @@ async function updateGenesisFile() { // state recorded. const validator = validatorStates[k]; const effectiveBalance = new Decimal(web3.utils.toWei(validator.validator.effectiveBalance.toString(), "gwei")); + // const effectiveBalance = new Decimal(web3.utils.toWei(32, "ether")); if (effectiveBalance.eq(0)) { if (!validator.status.startsWith("withdrawal")) { throw new Error( @@ -504,7 +506,7 @@ async function updateGenesisFile() { // from the beacon chain (without attempting to submit a proof), causing a deviation. if (totalEffectiveBalance.eq(depositValue)) { // (1) they are equal; do nothing - } else if (totalEffectiveBalance.lt(depositValue)) { + } else if (totalEffectiveBalance.gt(depositValue)) { // (2) totalEffectiveBalance > depositValue; add spare as deposit but not withdrawable depositValue = totalEffectiveBalance; } else { @@ -514,7 +516,7 @@ async function updateGenesisFile() { let totalDelegated = depositValue.minus(withdrawableValue); let slashFromWithdrawable = depositValue.minus(totalEffectiveBalance); let pendingSlashAmount = slashFromWithdrawable.minus(withdrawableValue); - if (pendingSlashAmount.isPositive()) { + if (pendingSlashAmount.gt(ZERO_DECIMAL)) { slashFromWithdrawable = withdrawableValue; } else { pendingSlashAmount = new Decimal(0); @@ -522,7 +524,7 @@ async function updateGenesisFile() { depositValue = depositValue.minus(slashFromWithdrawable); withdrawableValue = withdrawableValue.minus(slashFromWithdrawable); // we don't have any undelegations, so we will skip that step. - if (pendingSlashAmount.isPositive()) { + if (pendingSlashAmount.gt(ZERO_DECIMAL)) { // slash across all delegations, propotionately. // let's look at an example. // effective balance = 16 ETH at the time of generate.mjs @@ -556,7 +558,7 @@ async function updateGenesisFile() { await myContract.methods.stakerToTokenToValidators(stakerAddress, tokenAddress, k).call(); impactedValidators.push(impactedValidator); } - if ((impactedValidators.length == 0) && (!slashProportion.isZero())) { + if ((impactedValidators.length > 0) && (!slashProportion.isZero())) { slashProportions.push({ staker: stakerAddress, token: tokenAddress, @@ -636,7 +638,7 @@ async function updateGenesisFile() { matchingEntry.staker, validatorExoAddress, tokenAddress ).call(); if (delegation > 0) { - let slashing = new Decimal(delegation).mul(matchingEntry.proportion); + let slashing = new Decimal(delegation.toString()).mul(matchingEntry.proportion); totalSlashing = totalSlashing.plus(slashing); if (matchingEntry.staker == validatorEthAddress) { selfSlashing = slashing; @@ -795,7 +797,7 @@ async function updateGenesisFile() { matchingEntry.staker, opAddressExo, tokenAddress ).call(); if (delegation > 0) { - let slashing = new Decimal(delegation).mul(matchingEntry.proportion); + let slashing = new Decimal(delegation.toString()).mul(matchingEntry.proportion); totalSlashing = totalSlashing.plus(slashing); if (matchingEntry.staker == opAddressHex) { selfSlashing = slashing; @@ -989,14 +991,14 @@ async function updateGenesisFile() { matchingEntry.staker, operator, tokenAddress ).call(); if (delegation > 0) { - let slashing = new Decimal(delegation).mul(matchingEntry.proportion); + let slashing = new Decimal(delegation.toString()).mul(matchingEntry.proportion); totalSlashing = totalSlashing.plus(slashing); } } const amount = new Decimal((await myContract.methods.delegations( staker, operator, tokenAddress ).call()).toString()).minus(totalSlashing); - if (amount.isPositive()) { + if (amount.gt(ZERO_DECIMAL)) { const key = getJoinedStoreKey(stakerId, assetId, operator); delegation_states.push({ key: key, From dd17faf375a28e11ed78700fd2ccd2618917ab9f Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Fri, 29 Nov 2024 04:31:02 +0000 Subject: [PATCH 17/22] fix storage layout --- src/storage/BootstrapStorage.sol | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/storage/BootstrapStorage.sol b/src/storage/BootstrapStorage.sol index 9d7d3fcc..e32d92d4 100644 --- a/src/storage/BootstrapStorage.sol +++ b/src/storage/BootstrapStorage.sol @@ -167,16 +167,6 @@ contract BootstrapStorage is GatewayStorage { /// @dev A mapping of validator names to a boolean indicating whether the name has been used. mapping(string name => bool used) public validatorNameInUse; - /// @dev Storage gap to allow for future upgrades. - // slither-disable-next-line shadowing-state - uint256[40] private __gap; - - /// @notice Mapping of owner addresses to their corresponding ExoCapsule contracts. - /// @dev Maps owner addresses to their corresponding ExoCapsule contracts. - /// @dev This state has been moved from ClientChainGatewayStorage to BootstrapStorage since it is shared by both - /// contracts and we put it after __gap to maintain the storage layout compatible with deployed contracts. - mapping(address owner => IExoCapsule capsule) public ownerToCapsule; - /// @notice Mapping of staker addresses to their corresponding validator indexes. /// @dev Maps staker addresses to their corresponding validator indexes used on the beacon chain. mapping(address staker => bytes32[]) public stakerToPubkeyIDs; @@ -185,6 +175,17 @@ contract BootstrapStorage is GatewayStorage { /// @dev Maps staker addresses to a mapping of token addresses to a list of validators. mapping(address staker => mapping(address token => string[])) public stakerToTokenToValidators; + /// @dev Storage gap to allow for future upgrades. + // slither-disable-next-line shadowing-state + uint256[38] private __gap; + + /// @notice Mapping of owner addresses to their corresponding ExoCapsule contracts. + /// @dev Maps owner addresses to their corresponding ExoCapsule contracts. + /// @dev This state has been moved from ClientChainGatewayStorage to BootstrapStorage since it is shared by both + /// contracts and we put it after __gap to maintain the storage layout compatible with deployed contracts. It was, + /// before the move, at the top of the storage layout of ClientChainGatewayStorage. + mapping(address owner => IExoCapsule capsule) public ownerToCapsule; + /* -------------------------------------------------------------------------- */ /* Events */ /* -------------------------------------------------------------------------- */ From e49502ed8f0e5bfeb1966058c62e47f5d34f375f Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Fri, 29 Nov 2024 08:57:12 +0000 Subject: [PATCH 18/22] fix(integration): add client chain gateway --- script/integration/1_DeployBootstrap.s.sol | 24 ++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index 1cd803f8..b65aa332 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -13,6 +13,8 @@ import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transpa import {EndpointV2Mock} from "../../test/mocks/EndpointV2Mock.sol"; import {Bootstrap} from "../../src/core/Bootstrap.sol"; +import {ClientChainGateway} from "../../src/core/ClientChainGateway.sol"; +import {RewardVault} from "../../src/core/RewardVault.sol"; import {BootstrapStorage} from "../../src/storage/BootstrapStorage.sol"; @@ -22,6 +24,8 @@ import {ALLOWED_CHAIN_ID, NetworkConfig} from "./NetworkConfig.sol"; import {ExoCapsule} from "../../src/core/ExoCapsule.sol"; import {Vault} from "../../src/core/Vault.sol"; import {IExoCapsule} from "../../src/interfaces/IExoCapsule.sol"; + +import {IRewardVault} from "../../src/interfaces/IRewardVault.sol"; import {IValidatorRegistry} from "../../src/interfaces/IValidatorRegistry.sol"; import {IVault} from "../../src/interfaces/IVault.sol"; @@ -44,12 +48,11 @@ contract DeployContracts is Script { // no cross-chain communication is part of this test so these are not relevant uint16 exocoreChainId = 1; uint16 clientChainId = 2; - // neither is the ownership of the contract being tested here - address exocoreValidatorSet = vm.addr(uint256(0x8)); uint256[] validators; uint256[] stakers; string[] exos; + // also the owner of the contracts uint256 contractDeployer; uint256 nstDepositor; Bootstrap bootstrap; @@ -62,9 +65,11 @@ contract DeployContracts is Script { BeaconOracle beaconOracle; IVault vaultImplementation; + IRewardVault rewardVaultImplementation; IExoCapsule capsuleImplementation; IBeacon vaultBeacon; IBeacon capsuleBeacon; + IBeacon rewardVaultBeacon; BeaconProxyBytecode beaconProxyBytecode; NetworkConfig networkConfig; @@ -198,6 +203,15 @@ contract DeployContracts is Script { Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); + rewardVaultImplementation = new RewardVault(); + rewardVaultBeacon = new UpgradeableBeacon(address(rewardVaultImplementation)); + ClientChainGateway clientGatewayLogic = + new ClientChainGateway(address(clientChainLzEndpoint), config, address(rewardVaultBeacon)); + + address[] memory emptyList; + bytes memory initialization = + abi.encodeWithSelector(clientGatewayLogic.initialize.selector, vm.addr(contractDeployer), emptyList); + bootstrap = Bootstrap( payable( address( @@ -209,15 +223,13 @@ contract DeployContracts is Script { ( vm.addr(contractDeployer), // keep a large buffer because we are going to be depositing a lot of tokens - // and we do one tx per block block.timestamp + 24 hours, 1 seconds, whitelistTokens, tvlLimits, address(proxyAdmin), - // the implementation upgrade and data don't matter for this test - address(0x1), - bytes("123456") + address(clientGatewayLogic), + initialization ) ) ) From 5a93e6ee7c0f710892f466ac27fdc01018e8a3d4 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:41:01 +0000 Subject: [PATCH 19/22] fix(integration): make Bootstrap address constant --- script/integration/1_DeployBootstrap.s.sol | 30 ++++++++++++++-------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index b65aa332..630b0d37 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -203,15 +203,6 @@ contract DeployContracts is Script { Bootstrap bootstrapLogic = new Bootstrap(address(clientChainLzEndpoint), config); - rewardVaultImplementation = new RewardVault(); - rewardVaultBeacon = new UpgradeableBeacon(address(rewardVaultImplementation)); - ClientChainGateway clientGatewayLogic = - new ClientChainGateway(address(clientChainLzEndpoint), config, address(rewardVaultBeacon)); - - address[] memory emptyList; - bytes memory initialization = - abi.encodeWithSelector(clientGatewayLogic.initialize.selector, vm.addr(contractDeployer), emptyList); - bootstrap = Bootstrap( payable( address( @@ -228,14 +219,31 @@ contract DeployContracts is Script { whitelistTokens, tvlLimits, address(proxyAdmin), - address(clientGatewayLogic), - initialization + // not needed for creating the contract + address(0x1), + bytes("123456") ) ) ) ) ) ); + + // to keep bootstrap address constant, deploy client chain gateway + associated contracts later + // the default deposit params are created using exocapsule address 0x90618D1cDb01bF37c24FC012E70029DA20fCe971 + // which is made using the default NST_DEPOSITOR + bootstrap address 0xF801fc13AA08876F343fEBf50dFfA52A78180811 + // if you get a DepositDataRoot or related error, check these addresses first. + rewardVaultImplementation = new RewardVault(); + rewardVaultBeacon = new UpgradeableBeacon(address(rewardVaultImplementation)); + ClientChainGateway clientGatewayLogic = + new ClientChainGateway(address(clientChainLzEndpoint), config, address(rewardVaultBeacon)); + + address[] memory emptyList; + bytes memory initialization = + abi.encodeWithSelector(clientGatewayLogic.initialize.selector, vm.addr(contractDeployer), emptyList); + + bootstrap.setClientChainGatewayLogic(address(clientGatewayLogic), initialization); + vm.stopBroadcast(); console.log("Bootstrap address: ", address(bootstrap)); From f0879534fb52e521ac76f35e00d37f8054862cbc Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Fri, 29 Nov 2024 11:41:39 +0000 Subject: [PATCH 20/22] fix(generate): truncate the values --- script/generate.mjs | 54 +++++++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/script/generate.mjs b/script/generate.mjs index 9075397f..7e3abab3 100644 --- a/script/generate.mjs +++ b/script/generate.mjs @@ -98,6 +98,7 @@ import JSONbig from 'json-bigint'; const jsonBig = JSONbig({ useNativeBigInt: true }); const ZERO_DECIMAL = new Decimal(0); +const ONE_DECIMAL = new Decimal(1); function getChainIDWithoutPrevision(chainID) { const splitStr = chainID.split('-'); @@ -403,7 +404,7 @@ async function updateGenesisFile() { const validatorStates = (await api.beacon.getStateValidators( {stateId: stateRoot, validatorIds: pubKeys.map(pubKey => parseInt(pubKey, 16))} )).value(); - let totalEffectiveBalance = new Decimal(0); + let totalEffectiveBalance = ZERO_DECIMAL; let balances = []; // remember that these validators are specific to the provided staker address. // a validator is identified by its public key (or validator index), while a staker @@ -417,7 +418,6 @@ async function updateGenesisFile() { // state recorded. const validator = validatorStates[k]; const effectiveBalance = new Decimal(web3.utils.toWei(validator.validator.effectiveBalance.toString(), "gwei")); - // const effectiveBalance = new Decimal(web3.utils.toWei(32, "ether")); if (effectiveBalance.eq(0)) { if (!validator.status.startsWith("withdrawal")) { throw new Error( @@ -513,15 +513,26 @@ async function updateGenesisFile() { // (3) lower effective balance means that the Ethereum validator was either downtime // penalised or slashed. we follow the logic enshrined in update_native_restaking_balance.go // store this value before making any adjustments to calculate the slash proportion accurately. + // An example case wherein not all the 32 ETH is staked to an Exocore validator. + // Effective balance = 29 ETH + // Deposited 32, of which 2 is free and 30 is delegated. So withdrawable is 2. + // DepositValue = 32 + // WithdrawableValue = 2 + // TotalDelegated = 30 let totalDelegated = depositValue.minus(withdrawableValue); + // SlashFromWithdrawable = 32 - 29 = 3 let slashFromWithdrawable = depositValue.minus(totalEffectiveBalance); + // PendingSlashAmount = 3 - 2 = 1 let pendingSlashAmount = slashFromWithdrawable.minus(withdrawableValue); if (pendingSlashAmount.gt(ZERO_DECIMAL)) { + // SlashFromWithdrawable = 2 slashFromWithdrawable = withdrawableValue; } else { - pendingSlashAmount = new Decimal(0); + pendingSlashAmount = ZERO_DECIMAL; } + // DepositValue = 30 depositValue = depositValue.minus(slashFromWithdrawable); + // WithdrawableValue = 0 withdrawableValue = withdrawableValue.minus(slashFromWithdrawable); // we don't have any undelegations, so we will skip that step. if (pendingSlashAmount.gt(ZERO_DECIMAL)) { @@ -542,19 +553,20 @@ async function updateGenesisFile() { // -- staker + asset + {each validator to which that combination is delegated} // it should be applied to the delegated value against each validator, // and then it will flow automatically(?) to the share. + // SlashProportion = 1/9, so we will need to handle truncation. let slashProportion = pendingSlashAmount.div(totalDelegated); - if (slashProportion.greaterThan(new Decimal(1))) { - slashProportion = new Decimal(1); + if (slashProportion.greaterThan(ONE_DECIMAL)) { + slashProportion = ONE_DECIMAL; } - depositValue = totalDelegated.mul((new Decimal(1)).minus(slashProportion)); + depositValue = totalDelegated.minus(pendingSlashAmount); // a certain subset of the validators is impacted by this above slashing. // our goal is to find that subset and save it such that it can be applied // to the delegated value below. let impactedValidators = []; - let impactedValidatorsCount = + let impactedValidatorsCount = await myContract.methods.getValidatorsCountForStakerToken(stakerAddress, tokenAddress).call(); for(let k = 0; k < impactedValidatorsCount; k++) { - let impactedValidator = + let impactedValidator = await myContract.methods.stakerToTokenToValidators(stakerAddress, tokenAddress, k).call(); impactedValidators.push(impactedValidator); } @@ -630,8 +642,8 @@ async function updateGenesisFile() { let matchingEntries = slashProportions.filter( (element) => element.token === tokenAddress && element.impacted_validators.includes(validatorExoAddress) ); - let totalSlashing = new Decimal(0); - let selfSlashing = new Decimal(0); + let totalSlashing = ZERO_DECIMAL; + let selfSlashing = ZERO_DECIMAL; for(let k = 0; k < matchingEntries.length; k++) { let matchingEntry = matchingEntries[k]; let delegation = await myContract.methods.delegations( @@ -647,10 +659,10 @@ async function updateGenesisFile() { } const delegationValue = new Decimal((await myContract.methods.delegationsByValidator( validatorExoAddress, tokenAddress - ).call()).toString()).minus(totalSlashing); + ).call()).toString()).minus(totalSlashing).truncated(); const selfDelegation = new Decimal((await myContract.methods.delegations( validatorEthAddress, validatorExoAddress, tokenAddress - ).call()).toString()).minus(selfSlashing); + ).call()).toString()).minus(selfSlashing).truncated(); const assetsByOperatorForAsset = { asset_id: tokenAddress.toLowerCase() + clientChainSuffix, @@ -716,7 +728,7 @@ async function updateGenesisFile() { const operators = []; const associations = []; const operatorsCount = await myContract.methods.getValidatorsCount().call(); - let dogfoodUSDValue = new Decimal(0); + let dogfoodUSDValue = ZERO_DECIMAL; const operator_records = []; const opt_states = []; const avs_usd_values = []; @@ -772,8 +784,8 @@ async function updateGenesisFile() { // and instead, load the asset prices into the oracle module genesis // and let the dogfood module pull the vote power from the rest of the system // at genesis. - let amount = new Decimal(0); - let totalAmount = new Decimal(0); + let amount = ZERO_DECIMAL; + let totalAmount = ZERO_DECIMAL; if (exchangeRates.length != supportedTokens.length) { throw new Error( `The number of exchange rates (${exchangeRates.length}) @@ -789,8 +801,8 @@ async function updateGenesisFile() { let matchingEntries = slashProportions.filter( (element) => element.token === tokenAddress && element.impacted_validators.includes(opAddressExo) ); - let totalSlashing = new Decimal(0); - let selfSlashing = new Decimal(0); + let totalSlashing = ZERO_DECIMAL; + let selfSlashing = ZERO_DECIMAL; for(let k = 0; k < matchingEntries.length; k++) { let matchingEntry = matchingEntries[k]; let delegation = await myContract.methods.delegations( @@ -804,7 +816,7 @@ async function updateGenesisFile() { } } } - selfDelegationAmount = selfDelegationAmount.minus(selfSlashing); + selfDelegationAmount = selfDelegationAmount.minus(selfSlashing).truncated(); amount = amount.plus( selfDelegationAmount. div('1e' + decimals[j]). @@ -812,7 +824,7 @@ async function updateGenesisFile() { ); const perTokenDelegation = new Decimal((await myContract.methods.delegationsByValidator( opAddressExo, tokenAddress - ).call()).toString()).minus(totalSlashing); + ).call()).toString()).minus(totalSlashing).truncated(); totalAmount = totalAmount.plus( perTokenDelegation. div('1e' + decimals[j]). @@ -984,7 +996,7 @@ async function updateGenesisFile() { let matchingEntries = slashProportions.filter( (element) => element.token === tokenAddress && element.impacted_validators.includes(operator) ); - let totalSlashing = new Decimal(0); + let totalSlashing = ZERO_DECIMAL; for(let k = 0; k < matchingEntries.length; k++) { let matchingEntry = matchingEntries[k]; let delegation = await myContract.methods.delegations( @@ -997,7 +1009,7 @@ async function updateGenesisFile() { } const amount = new Decimal((await myContract.methods.delegations( staker, operator, tokenAddress - ).call()).toString()).minus(totalSlashing); + ).call()).toString()).minus(totalSlashing).truncated(); if (amount.gt(ZERO_DECIMAL)) { const key = getJoinedStoreKey(stakerId, assetId, operator); delegation_states.push({ From 3e96e75ae3484c573d34f09534c1901c67806750 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Mon, 2 Dec 2024 06:25:44 +0000 Subject: [PATCH 21/22] fix: make bootstrapper --- script/integration/1_DeployBootstrap.s.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index 630b0d37..01064148 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -229,10 +229,12 @@ contract DeployContracts is Script { ) ); - // to keep bootstrap address constant, deploy client chain gateway + associated contracts later + // to keep bootstrap address constant, we must keep its nonce unchanged. hence, further transactions are sent + // after the bare minimum bootstrap and associated deployments. // the default deposit params are created using exocapsule address 0x90618D1cDb01bF37c24FC012E70029DA20fCe971 // which is made using the default NST_DEPOSITOR + bootstrap address 0xF801fc13AA08876F343fEBf50dFfA52A78180811 // if you get a DepositDataRoot or related error, check these addresses first. + proxyAdmin.initialize(address(bootstrap)); rewardVaultImplementation = new RewardVault(); rewardVaultBeacon = new UpgradeableBeacon(address(rewardVaultImplementation)); ClientChainGateway clientGatewayLogic = From 923c420d0b34491aa2172628b3d7d7eb48d4c287 Mon Sep 17 00:00:00 2001 From: MaxMustermann2 <82761650+MaxMustermann2@users.noreply.github.com> Date: Mon, 2 Dec 2024 06:37:47 +0000 Subject: [PATCH 22/22] fix: add validation for deposit address (ai) --- script/integration/1_DeployBootstrap.s.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/script/integration/1_DeployBootstrap.s.sol b/script/integration/1_DeployBootstrap.s.sol index 01064148..154f0cdf 100644 --- a/script/integration/1_DeployBootstrap.s.sol +++ b/script/integration/1_DeployBootstrap.s.sol @@ -125,6 +125,7 @@ contract DeployContracts is Script { // read the network configuration parameters and validate them depositAddress = vm.envOr("INTEGRATION_DEPOSIT_ADDRESS", address(0x6969696969696969696969696969696969696969)); + require(depositAddress != address(0), "Deposit address must be set"); denebTimestamp = vm.envUint("INTEGRATION_DENEB_TIMESTAMP"); require(denebTimestamp > 0, "Deneb timestamp must be set"); beaconGenesisTimestamp = vm.envUint("INTEGRATION_BEACON_GENESIS_TIMESTAMP");