From 0934ca06743f957ca245af551800469ade841b58 Mon Sep 17 00:00:00 2001 From: GitGuru7 <128375421+GitGuru7@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:32:29 +0530 Subject: [PATCH 1/6] feat: add VotingPowerAggregator and DatawareHouse contracts for BNB chain --- .../Cross-chain/Voting/DataWarehouse.sol | 92 +++++ .../Voting/VotingPowerAggregator.sol | 213 +++++++++++ .../Cross-chain/interfaces/IDataWarehouse.sol | 91 +++++ .../libs/MerklePatriciaProofVerifier.sol | 251 +++++++++++++ contracts/Cross-chain/libs/RLPReader.sol | 355 ++++++++++++++++++ .../Cross-chain/libs/StateProofVerifier.sol | 130 +++++++ contracts/Utils/SlotUtils.sol | 34 ++ 7 files changed, 1166 insertions(+) create mode 100644 contracts/Cross-chain/Voting/DataWarehouse.sol create mode 100644 contracts/Cross-chain/Voting/VotingPowerAggregator.sol create mode 100644 contracts/Cross-chain/interfaces/IDataWarehouse.sol create mode 100644 contracts/Cross-chain/libs/MerklePatriciaProofVerifier.sol create mode 100644 contracts/Cross-chain/libs/RLPReader.sol create mode 100644 contracts/Cross-chain/libs/StateProofVerifier.sol create mode 100644 contracts/Utils/SlotUtils.sol diff --git a/contracts/Cross-chain/Voting/DataWarehouse.sol b/contracts/Cross-chain/Voting/DataWarehouse.sol new file mode 100644 index 00000000..973bd2cb --- /dev/null +++ b/contracts/Cross-chain/Voting/DataWarehouse.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +import { IDataWarehouse } from "../interfaces/IDataWarehouse.sol"; +import { StateProofVerifier } from "../libs/StateProofVerifier.sol"; +import { RLPReader } from "../libs/RLPReader.sol"; + +/** + * @title DataWarehouse + * @author BGD Labs + * @notice This contract stores account state roots and allows proving against them + */ +contract DataWarehouse is IDataWarehouse { + using RLPReader for bytes; + using RLPReader for RLPReader.RLPItem; + + // account address => (block hash => Account state root hash) + mapping(address => mapping(bytes32 => bytes32)) internal _storageRoots; + + // account address => (block hash => (slot => slot value)) + mapping(address => mapping(bytes32 => mapping(bytes32 => uint256))) internal _slotsRegistered; + + /// @inheritdoc IDataWarehouse + function getStorageRoots(address account, bytes32 blockHash) external view returns (bytes32) { + return _storageRoots[account][blockHash]; + } + + /// @inheritdoc IDataWarehouse + function getRegisteredSlot(bytes32 blockHash, address account, bytes32 slot) external view returns (uint256) { + return _slotsRegistered[account][blockHash][slot]; + } + + /// @inheritdoc IDataWarehouse + function processStorageRoot( + address account, + bytes32 blockHash, + bytes memory blockHeaderRLP, + bytes memory accountStateProofRLP + ) external returns (bytes32) { + StateProofVerifier.BlockHeader memory decodedHeader = StateProofVerifier.verifyBlockHeader( + blockHeaderRLP, + blockHash + ); + // The path for an account in the state trie is the hash of its address + bytes32 proofPath = keccak256(abi.encodePacked(account)); + StateProofVerifier.Account memory accountData = StateProofVerifier.extractAccountFromProof( + proofPath, + decodedHeader.stateRootHash, + accountStateProofRLP.toRlpItem().toList() + ); + + _storageRoots[account][blockHash] = accountData.storageRoot; + + emit StorageRootProcessed(msg.sender, account, blockHash); + + return accountData.storageRoot; + } + + /// @inheritdoc IDataWarehouse + function getStorage( + address account, + bytes32 blockHash, + bytes32 slot, + bytes memory storageProof + ) public view returns (StateProofVerifier.SlotValue memory) { + bytes32 root = _storageRoots[account][blockHash]; + require(root != bytes32(0), "DataWarehouse: storage root not processed"); + // The path for a storage value is the hash of its slot + bytes32 proofPath = keccak256(abi.encodePacked(slot)); + StateProofVerifier.SlotValue memory slotData = StateProofVerifier.extractSlotValueFromProof( + proofPath, + root, + storageProof.toRlpItem().toList() + ); + + return slotData; + } + + /// @inheritdoc IDataWarehouse + function processStorageSlot( + address account, + bytes32 blockHash, + bytes32 slot, + bytes calldata storageProof + ) external { + StateProofVerifier.SlotValue memory storageSlot = getStorage(account, blockHash, slot, storageProof); + + _slotsRegistered[account][blockHash][slot] = storageSlot.value; + + emit StorageSlotProcessed(msg.sender, account, blockHash, slot, storageSlot.value); + } +} diff --git a/contracts/Cross-chain/Voting/VotingPowerAggregator.sol b/contracts/Cross-chain/Voting/VotingPowerAggregator.sol new file mode 100644 index 00000000..4f2bd39a --- /dev/null +++ b/contracts/Cross-chain/Voting/VotingPowerAggregator.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; +import { IDataWarehouse } from "../interfaces/IDataWarehouse.sol"; +import { SlotUtils } from "../../Utils/SlotUtils.sol"; +import { StateProofVerifier } from "../libs/StateProofVerifier.sol"; +import { ExcessivelySafeCall } from "@layerzerolabs/solidity-examples/contracts/libraries/ExcessivelySafeCall.sol"; + +import { NonblockingLzApp } from "@layerzerolabs/solidity-examples/contracts/lzApp/NonblockingLzApp.sol"; +import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; + +import { Pausable } from "@openzeppelin/contracts/security/Pausable.sol"; + +contract VotingPowerAggregator is NonblockingLzApp, Pausable { + using ExcessivelySafeCall for address; + + uint8 public constant CHECKPOINTS_SLOT = 16; + uint8 public constant NUM_CHECKPOINTS_SLOT = 17; + IDataWarehouse public warehouse; + + // Array of remote chain ids for iteration on mapping + uint16[] public remoteChainIds; + + // remote identifier => chainId => blockHash + mapping(uint256 => mapping(uint16 => bytes32)) public remoteBlockHash; + + // List of supported remote chain Id + mapping(uint16 => bool) public isSupportedRemote; + + // Address of XVS vault wrt to chain Id + mapping(uint16 => address) public xvsVault; + + /** + * @notice Emitted when proposal failed + */ + event ReceivePayloadFailed(uint16 indexed remoteChainId, bytes indexed remoteAddress, uint64 nonce, bytes reason); + + /** + * @notice Emitted when block hash of remote chain is received + */ + event HashReceived(uint256 remoteIdentifier, uint16 remoteChainId, bytes blockHash); + + /** + * @notice Emitted when remote configurations are updated + */ + event UpdateRemoteConfigurations(uint16 remoteChainId, address xvsVault, bool isSupported); + + constructor(address endpoint, address warehouseAddress) NonblockingLzApp(endpoint) { + ensureNonzeroAddress(warehouseAddress); + warehouse = IDataWarehouse(warehouseAddress); + } + + /** + * @notice Updates the configuration of remote chain ids, marking them as supported or unsupported and setting the corresponding XVS vault addresses + * @param remoteChainId An array of remote chain ids to update + * @param isSupported An array indicating whether each remote chain id is supported or not + * @param xvsVaultAddress An array of XVS vault addresses corresponding to each remote chain id + * @custom:access Only owner + * @custom:emit UpdateRemoteConfigurations Emitted when the configuration of a remote chain id is updated, along with its vault address and support status + */ + function updateRemoteConfigurations( + uint16[] calldata remoteChainId, + bool[] calldata isSupported, + address[] calldata xvsVaultAddress + ) external onlyOwner { + for (uint16 i; i < remoteChainId.length; ++i) { + ensureNonzeroAddress(xvsVaultAddress[i]); + require(remoteChainId[i] != 0, "Invalid chain id"); + isSupportedRemote[remoteChainId[i]] = isSupported[i]; + if (isSupported[i]) { + remoteChainIds.push(remoteChainId[i]); + xvsVault[remoteChainId[i]] = xvsVaultAddress[i]; + } else { + delete remoteChainIds[remoteChainId[i]]; + delete xvsVault[remoteChainId[i]]; + } + + emit UpdateRemoteConfigurations(remoteChainId[i], xvsVaultAddress[i], isSupported[i]); + } + } + + /** + * @notice Calculates the total voting power of a voter across multiple remote chains + * @param voter The address of the voter for whom to calculate the voting power + * @param remoteIdentifier The identifier that links to remote chain-specific data + * @param numCheckpointsProof The proof data needed to verify the number of checkpoints + * @param checkpointsProof The proof data needed to verify the actual voting power from the checkpoints + * @return power The total voting power of the voter across all supported remote chains + */ + function getVotingPower( + address voter, + uint256 remoteIdentifier, + bytes calldata numCheckpointsProof, + bytes calldata checkpointsProof + ) external view returns (uint96 power) { + uint96 totalVotingPower; + for (uint16 i; i < remoteChainIds.length; ++i) { + totalVotingPower += _getVotingPower( + remoteChainIds[i], + remoteIdentifier, + numCheckpointsProof, + checkpointsProof, + voter + ); + } + + return totalVotingPower; + } + + /** + * @dev Calculates the total voting power of a voter for a remote chain + * @param voter The address of the voter for whom to calculate the voting power + * @param remoteIdentifier The identifier that links to remote chain-specific data + * @param numCheckpointsProof The proof data needed to verify the number of checkpoints + * @param checkpointsProof The proof data needed to verify the actual voting power from the checkpoints + * @return power The total voting power of supported remote chains + */ + function _getVotingPower( + uint16 remoteChainId, + uint256 remoteIdentifier, + bytes calldata numCheckpointsProof, + bytes calldata checkpointsProof, + address voter + ) internal view returns (uint96) { + bytes32 blockHash = remoteBlockHash[remoteIdentifier][remoteChainId]; + address vault = xvsVault[remoteChainId]; + + StateProofVerifier.SlotValue memory latestCheckpoint = warehouse.getStorage( + vault, + blockHash, + SlotUtils.getAccountSlotHash(voter, NUM_CHECKPOINTS_SLOT), + numCheckpointsProof + ); + + if (latestCheckpoint.exists) { + StateProofVerifier.SlotValue memory votingPower = warehouse.getStorage( + vault, + blockHash, + SlotUtils.getCheckpointSlotHash(voter, uint32(latestCheckpoint.value - 1), uint32(CHECKPOINTS_SLOT)), + checkpointsProof + ); + if (votingPower.exists) { + return votingPower.value >> 32; + } + return 0; + } + return 0; + } + + /** + * @notice Process blocking LayerZero receive request + * @param remoteChainId Remote chain Id + * @param remoteAddress Remote address from which payload is received + * @param nonce Nonce associated with the payload to prevent replay attacks + * @param payload Encoded payload containing block hash and remote identifier of remote chains + * @custom:event Emit ReceivePayloadFailed if call fails + */ + function _blockingLzReceive( + uint16 remoteChainId, + bytes memory remoteAddress, + uint64 nonce, + bytes memory payload + ) internal override { + // remoteChainId should belongs to supported chainIds + require(isSupportedRemote[remoteChainId], "source chain id not supported"); + bytes32 hashedPayload = keccak256(payload); + bytes memory callData = abi.encodeCall( + this.nonblockingLzReceive, + (remoteChainId, remoteAddress, nonce, payload) + ); + + (bool success, bytes memory reason) = address(this).excessivelySafeCall(gasleft() - 30000, 150, callData); + // try-catch all errors/exceptions + if (!success) { + failedMessages[remoteChainId][remoteAddress][nonce] = hashedPayload; + emit ReceivePayloadFailed(remoteChainId, remoteAddress, nonce, reason); // Retrieve payload from the remote chain if needed to clear + } + } + + /** + * @notice Process non blocking LayerZero receive request + * @param payload Encoded payload containing block hash and remote identifier of remote chains + * @custom:event Emit ProposalReceived + */ + function _nonblockingLzReceive( + uint16 remoteChainId, + bytes memory, + uint64, + bytes memory payload + ) internal override whenNotPaused { + // remoteIdentifier is unique identifier generated at the time of createProposal + (uint256 remoteIdentifier, bytes memory blockHash) = abi.decode(payload, (uint256, bytes)); + + // Prevent Overriding hash + require(remoteBlockHash[remoteIdentifier][remoteChainId] == bytes32(0), "block hash already exists"); + + emit HashReceived(remoteIdentifier, remoteChainId, blockHash); + } + + /** + * @dev Removes a specified remote chain id from the list of supported remote chain ids. + * @param remoteChainId The chain id to be removed from the list of supported remote chains. + */ + function _removeRemoteChainId(uint16 remoteChainId) internal { + uint256 length = remoteChainIds.length; + for (uint256 i = 0; i < length; i++) { + if (remoteChainIds[i] == remoteChainId) { + remoteChainIds[i] = remoteChainIds[length - 1]; + remoteChainIds.pop(); + break; + } + } + } +} diff --git a/contracts/Cross-chain/interfaces/IDataWarehouse.sol b/contracts/Cross-chain/interfaces/IDataWarehouse.sol new file mode 100644 index 00000000..fcf4c076 --- /dev/null +++ b/contracts/Cross-chain/interfaces/IDataWarehouse.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity ^0.8.0; + +import { StateProofVerifier } from "../libs/StateProofVerifier.sol"; + +/** + * @title IDataWarehouse + * @author BGD Labs + * @notice interface containing the methods definitions of the DataWarehouse contract + */ +interface IDataWarehouse { + /** + * @notice event emitted when a storage root has been processed successfully + * @param caller address that called the processStorageRoot method + * @param account address where the root is generated + * @param blockHash hash of the block where the root was generated + */ + event StorageRootProcessed(address indexed caller, address indexed account, bytes32 indexed blockHash); + + /** + * @notice event emitted when a storage root has been processed successfully + * @param caller address that called the processStorageSlot method + * @param account address where the slot is processed + * @param blockHash hash of the block where the storage proof was generated + * @param slot storage location to search + * @param value storage information on the specified location + */ + event StorageSlotProcessed( + address indexed caller, + address indexed account, + bytes32 indexed blockHash, + bytes32 slot, + uint256 value + ); + + /** + * @notice method to get the storage roots of an account (token) in a certain block hash + * @param account address of the token to get the storage roots from + * @param blockHash hash of the block from where the roots are generated + * @return state root hash of the account on the block hash specified + */ + function getStorageRoots(address account, bytes32 blockHash) external view returns (bytes32); + + /** + * @notice method to process the storage root from an account on a block hash. + * @param account address of the token to get the storage roots from + * @param blockHash hash of the block from where the roots are generated + * @param blockHeaderRLP rlp encoded block header. At same block where the block hash was taken + * @param accountStateProofRLP rlp encoded account state proof, taken in same block as block hash + * @return the storage root + */ + function processStorageRoot( + address account, + bytes32 blockHash, + bytes memory blockHeaderRLP, + bytes memory accountStateProofRLP + ) external returns (bytes32); + + /** + * @notice method to get the storage value at a certain slot and block hash for a certain address + * @param account address of the token to get the storage roots from + * @param blockHash hash of the block from where the roots are generated + * @param slot hash of the explicit storage placement where the value to get is found. + * @param storageProof generated proof containing the storage, at block hash + * @return an object containing the slot value at the specified storage slot + */ + function getStorage( + address account, + bytes32 blockHash, + bytes32 slot, + bytes memory storageProof + ) external view returns (StateProofVerifier.SlotValue memory); + + /** + * @notice method to register the storage value at a certain slot and block hash for a certain address + * @param account address of the token to get the storage roots from + * @param blockHash hash of the block from where the roots are generated + * @param slot hash of the explicit storage placement where the value to get is found. + * @param storageProof generated proof containing the storage, at block hash + */ + function processStorageSlot(address account, bytes32 blockHash, bytes32 slot, bytes calldata storageProof) external; + + /** + * @notice method to get the value from storage at a certain block hash, previously registered. + * @param blockHash hash of the block from where the roots are generated + * @param account address of the token to get the storage roots from + * @param slot hash of the explicit storage placement where the value to get is found. + * @return numeric slot value of the slot. The value must be decoded to get the actual stored information + */ + function getRegisteredSlot(bytes32 blockHash, address account, bytes32 slot) external view returns (uint256); +} diff --git a/contracts/Cross-chain/libs/MerklePatriciaProofVerifier.sol b/contracts/Cross-chain/libs/MerklePatriciaProofVerifier.sol new file mode 100644 index 00000000..ddfce92d --- /dev/null +++ b/contracts/Cross-chain/libs/MerklePatriciaProofVerifier.sol @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: MIT + +/** + * Copied from https://github.com/lidofinance/curve-merkle-oracle/blob/main/contracts/MerklePatriciaProofVerifier.sol + */ +pragma solidity ^0.8.0; + +import { RLPReader } from "./RLPReader.sol"; + +library MerklePatriciaProofVerifier { + using RLPReader for RLPReader.RLPItem; + using RLPReader for bytes; + + /// @dev Validates a Merkle-Patricia-Trie proof. + /// If the proof proves the inclusion of some key-value pair in the + /// trie, the value is returned. Otherwise, i.e. if the proof proves + /// the exclusion of a key from the trie, an empty byte array is + /// returned. + /// @param rootHash is the Keccak-256 hash of the root node of the MPT. + /// @param path is the key of the node whose inclusion/exclusion we are + /// proving. + /// @param stack is the stack of MPT nodes (starting with the root) that + /// need to be traversed during verification. + /// @return value whose inclusion is proved or an empty byte array for + /// a proof of exclusion + function extractProofValue( + bytes32 rootHash, + bytes memory path, + RLPReader.RLPItem[] memory stack + ) internal pure returns (bytes memory value) { + bytes memory mptKey = _decodeNibbles(path, 0); + uint256 mptKeyOffset = 0; + + bytes32 nodeHashHash; + RLPReader.RLPItem[] memory node; + + RLPReader.RLPItem memory rlpValue; + + if (stack.length == 0) { + // Root hash of empty Merkle-Patricia-Trie + require(rootHash == 0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421); + return new bytes(0); + } + + // Traverse stack of nodes starting at root. + for (uint256 i = 0; i < stack.length; i++) { + // We use the fact that an rlp encoded list consists of some + // encoding of its length plus the concatenation of its + // *rlp-encoded* items. + + // The root node is hashed with Keccak-256 ... + if (i == 0 && rootHash != stack[i].rlpBytesKeccak256()) { + revert(); + } + // ... whereas all other nodes are hashed with the MPT + // hash function. + if (i != 0 && nodeHashHash != _mptHashHash(stack[i])) { + revert(); + } + // We verified that stack[i] has the correct hash, so we + // may safely decode it. + node = stack[i].toList(); + + if (node.length == 2) { + // Extension or Leaf node + + bool isLeaf; + bytes memory nodeKey; + (isLeaf, nodeKey) = _merklePatriciaCompactDecode(node[0].toBytes()); + + uint256 prefixLength = _sharedPrefixLength(mptKeyOffset, mptKey, nodeKey); + mptKeyOffset += prefixLength; + + if (prefixLength < nodeKey.length) { + // Proof claims divergent extension or leaf. (Only + // relevant for proofs of exclusion.) + // An Extension/Leaf node is divergent iff it "skips" over + // the point at which a Branch node should have been had the + // excluded key been included in the trie. + // Example: Imagine a proof of exclusion for path [1, 4], + // where the current node is a Leaf node with + // path [1, 3, 3, 7]. For [1, 4] to be included, there + // should have been a Branch node at [1] with a child + // at 3 and a child at 4. + + // Sanity check + if (i < stack.length - 1) { + // divergent node must come last in proof + revert(); + } + + return new bytes(0); + } + + if (isLeaf) { + // Sanity check + if (i < stack.length - 1) { + // leaf node must come last in proof + revert(); + } + + if (mptKeyOffset < mptKey.length) { + return new bytes(0); + } + + rlpValue = node[1]; + return rlpValue.toBytes(); + } else { + // extension + // Sanity check + if (i == stack.length - 1) { + // shouldn't be at last level + revert(); + } + + if (!node[1].isList()) { + // rlp(child) was at least 32 bytes. node[1] contains + // Keccak256(rlp(child)). + nodeHashHash = node[1].payloadKeccak256(); + } else { + // rlp(child) was less than 32 bytes. node[1] contains + // rlp(child). + nodeHashHash = node[1].rlpBytesKeccak256(); + } + } + } else if (node.length == 17) { + // Branch node + + if (mptKeyOffset != mptKey.length) { + // we haven't consumed the entire path, so we need to look at a child + uint8 nibble = uint8(mptKey[mptKeyOffset]); + mptKeyOffset += 1; + if (nibble >= 16) { + // each element of the path has to be a nibble + revert(); + } + + if (_isEmptyBytesequence(node[nibble])) { + // Sanity + if (i != stack.length - 1) { + // leaf node should be at last level + revert(); + } + + return new bytes(0); + } else if (!node[nibble].isList()) { + nodeHashHash = node[nibble].payloadKeccak256(); + } else { + nodeHashHash = node[nibble].rlpBytesKeccak256(); + } + } else { + // we have consumed the entire mptKey, so we need to look at what's contained in this node. + + // Sanity + if (i != stack.length - 1) { + // should be at last level + revert(); + } + + return node[16].toBytes(); + } + } + } + } + + /// @dev Computes the hash of the Merkle-Patricia-Trie hash of the RLP item. + /// Merkle-Patricia-Tries use a weird "hash function" that outputs + /// *variable-length* hashes: If the item is shorter than 32 bytes, + /// the MPT hash is the item. Otherwise, the MPT hash is the + /// Keccak-256 hash of the item. + /// The easiest way to compare variable-length byte sequences is + /// to compare their Keccak-256 hashes. + /// @param item The RLP item to be hashed. + /// @return Keccak-256(MPT-hash(item)) + function _mptHashHash(RLPReader.RLPItem memory item) private pure returns (bytes32) { + if (item.len < 32) { + return item.rlpBytesKeccak256(); + } else { + return keccak256(abi.encodePacked(item.rlpBytesKeccak256())); + } + } + + function _isEmptyBytesequence(RLPReader.RLPItem memory item) private pure returns (bool) { + if (item.len != 1) { + return false; + } + uint8 b; + uint256 memPtr = item.memPtr; + assembly { + b := byte(0, mload(memPtr)) + } + return b == 0x80 /* empty byte string */; + } + + function _merklePatriciaCompactDecode( + bytes memory compact + ) private pure returns (bool isLeaf, bytes memory nibbles) { + require(compact.length > 0); + uint256 first_nibble = (uint8(compact[0]) >> 4) & 0xF; + uint256 skipNibbles; + if (first_nibble == 0) { + skipNibbles = 2; + isLeaf = false; + } else if (first_nibble == 1) { + skipNibbles = 1; + isLeaf = false; + } else if (first_nibble == 2) { + skipNibbles = 2; + isLeaf = true; + } else if (first_nibble == 3) { + skipNibbles = 1; + isLeaf = true; + } else { + // Not supposed to happen! + revert(); + } + return (isLeaf, _decodeNibbles(compact, skipNibbles)); + } + + function _decodeNibbles(bytes memory compact, uint256 skipNibbles) private pure returns (bytes memory nibbles) { + require(compact.length > 0); + + uint256 length = compact.length * 2; + require(skipNibbles <= length); + length -= skipNibbles; + + nibbles = new bytes(length); + uint256 nibblesLength = 0; + + for (uint256 i = skipNibbles; i < skipNibbles + length; i += 1) { + if (i % 2 == 0) { + nibbles[nibblesLength] = bytes1((uint8(compact[i / 2]) >> 4) & 0xF); + } else { + nibbles[nibblesLength] = bytes1((uint8(compact[i / 2]) >> 0) & 0xF); + } + nibblesLength += 1; + } + + assert(nibblesLength == nibbles.length); + } + + function _sharedPrefixLength(uint256 xsOffset, bytes memory xs, bytes memory ys) private pure returns (uint256) { + uint256 i; + for (i = 0; i + xsOffset < xs.length && i < ys.length; i++) { + if (xs[i + xsOffset] != ys[i]) { + return i; + } + } + return i; + } +} diff --git a/contracts/Cross-chain/libs/RLPReader.sol b/contracts/Cross-chain/libs/RLPReader.sol new file mode 100644 index 00000000..6d3934eb --- /dev/null +++ b/contracts/Cross-chain/libs/RLPReader.sol @@ -0,0 +1,355 @@ +// SPDX-License-Identifier: Apache-2.0 + +/* + * @author Hamdi Allam hamdi.allam97@gmail.com + * Please reach out with any questions or concerns + * Code copied from: https://github.com/hamdiallam/Solidity-RLP/blob/master/contracts/RLPReader.sol + */ +pragma solidity ^0.8.0; + +library RLPReader { + uint8 constant internal STRING_SHORT_START = 0x80; + uint8 constant internal STRING_LONG_START = 0xb8; + uint8 constant internal LIST_SHORT_START = 0xc0; + uint8 constant internal LIST_LONG_START = 0xf8; + uint8 constant internal WORD_SIZE = 32; + + struct RLPItem { + uint256 len; + uint256 memPtr; + } + + struct Iterator { + RLPItem item; // Item that's being iterated over. + uint256 nextPtr; // Position of the next item in the list. + } + + /* + * @dev Returns the next element in the iteration. Reverts if it has not next element. + * @param self The iterator. + * @return The next element in the iteration. + */ + function next(Iterator memory self) internal pure returns (RLPItem memory) { + require(hasNext(self)); + + uint256 ptr = self.nextPtr; + uint256 itemLength = _itemLength(ptr); + self.nextPtr = ptr + itemLength; + + return RLPItem(itemLength, ptr); + } + + /* + * @dev Returns true if the iteration has more elements. + * @param self The iterator. + * @return true if the iteration has more elements. + */ + function hasNext(Iterator memory self) internal pure returns (bool) { + RLPItem memory item = self.item; + return self.nextPtr < item.memPtr + item.len; + } + + /* + * @param item RLP encoded bytes + */ + function toRlpItem(bytes memory item) internal pure returns (RLPItem memory) { + uint256 memPtr; + assembly { + memPtr := add(item, 0x20) + } + + return RLPItem(item.length, memPtr); + } + + /* + * @dev Create an iterator. Reverts if item is not a list. + * @param self The RLP item. + * @return An 'Iterator' over the item. + */ + function iterator(RLPItem memory self) internal pure returns (Iterator memory) { + require(isList(self)); + + uint256 ptr = self.memPtr + _payloadOffset(self.memPtr); + return Iterator(self, ptr); + } + + /* + * @param the RLP item. + */ + function rlpLen(RLPItem memory item) internal pure returns (uint256) { + return item.len; + } + + /* + * @param the RLP item. + * @return (memPtr, len) pair: location of the item's payload in memory. + */ + function payloadLocation(RLPItem memory item) internal pure returns (uint256, uint256) { + uint256 offset = _payloadOffset(item.memPtr); + uint256 memPtr = item.memPtr + offset; + uint256 len = item.len - offset; // data length + return (memPtr, len); + } + + /* + * @param the RLP item. + */ + function payloadLen(RLPItem memory item) internal pure returns (uint256) { + (, uint256 len) = payloadLocation(item); + return len; + } + + /* + * @param the RLP item containing the encoded list. + */ + function toList(RLPItem memory item) internal pure returns (RLPItem[] memory) { + require(isList(item)); + + uint256 items = numItems(item); + RLPItem[] memory result = new RLPItem[](items); + + uint256 memPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 dataLen; + for (uint256 i = 0; i < items; i++) { + dataLen = _itemLength(memPtr); + result[i] = RLPItem(dataLen, memPtr); + memPtr = memPtr + dataLen; + } + + return result; + } + + // @return indicator whether encoded payload is a list. negate this function call for isData. + function isList(RLPItem memory item) internal pure returns (bool) { + if (item.len == 0) return false; + + uint8 byte0; + uint256 memPtr = item.memPtr; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < LIST_SHORT_START) return false; + return true; + } + + /* + * @dev A cheaper version of keccak256(toRlpBytes(item)) that avoids copying memory. + * @return keccak256 hash of RLP encoded bytes. + */ + function rlpBytesKeccak256(RLPItem memory item) internal pure returns (bytes32) { + uint256 ptr = item.memPtr; + uint256 len = item.len; + bytes32 result; + assembly { + result := keccak256(ptr, len) + } + return result; + } + + /* + * @dev A cheaper version of keccak256(toBytes(item)) that avoids copying memory. + * @return keccak256 hash of the item payload. + */ + function payloadKeccak256(RLPItem memory item) internal pure returns (bytes32) { + (uint256 memPtr, uint256 len) = payloadLocation(item); + bytes32 result; + assembly { + result := keccak256(memPtr, len) + } + return result; + } + + /** RLPItem conversions into data types **/ + + // @returns raw rlp encoding in bytes + function toRlpBytes(RLPItem memory item) internal pure returns (bytes memory) { + bytes memory result = new bytes(item.len); + if (result.length == 0) return result; + + uint256 ptr; + assembly { + ptr := add(0x20, result) + } + + copy(item.memPtr, ptr, item.len); + return result; + } + + // any non-zero byte except "0x80" is considered true + function toBoolean(RLPItem memory item) internal pure returns (bool) { + require(item.len == 1); + uint256 result; + uint256 memPtr = item.memPtr; + assembly { + result := byte(0, mload(memPtr)) + } + + // SEE Github Issue #5. + // Summary: Most commonly used RLP libraries (i.e Geth) will encode + // "0" as "0x80" instead of as "0". We handle this edge case explicitly + // here. + if (result == 0 || result == STRING_SHORT_START) { + return false; + } else { + return true; + } + } + + function toAddress(RLPItem memory item) internal pure returns (address) { + // 1 byte for the length prefix + require(item.len == 21); + + return address(uint160(toUint(item))); + } + + function toUint(RLPItem memory item) internal pure returns (uint256) { + require(item.len > 0 && item.len <= 33); + + (uint256 memPtr, uint256 len) = payloadLocation(item); + + uint256 result; + assembly { + result := mload(memPtr) + + // shift to the correct location if neccesary + if lt(len, 32) { + result := div(result, exp(256, sub(32, len))) + } + } + + return result; + } + + // enforces 32 byte length + function toUintStrict(RLPItem memory item) internal pure returns (uint256) { + // one byte prefix + require(item.len == 33); + + uint256 result; + uint256 memPtr = item.memPtr + 1; + assembly { + result := mload(memPtr) + } + + return result; + } + + function toBytes(RLPItem memory item) internal pure returns (bytes memory) { + require(item.len > 0); + + (uint256 memPtr, uint256 len) = payloadLocation(item); + bytes memory result = new bytes(len); + + uint256 destPtr; + assembly { + destPtr := add(0x20, result) + } + + copy(memPtr, destPtr, len); + return result; + } + + /* + * Private Helpers + */ + + // @return number of payload items inside an encoded list. + function numItems(RLPItem memory item) private pure returns (uint256) { + if (item.len == 0) return 0; + + uint256 count = 0; + uint256 currPtr = item.memPtr + _payloadOffset(item.memPtr); + uint256 endPtr = item.memPtr + item.len; + while (currPtr < endPtr) { + currPtr = currPtr + _itemLength(currPtr); // skip over an item + count++; + } + + return count; + } + + // @return entire rlp item byte length + function _itemLength(uint256 memPtr) private pure returns (uint256) { + uint256 itemLen; + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) { + itemLen = 1; + } else if (byte0 < STRING_LONG_START) { + itemLen = byte0 - STRING_SHORT_START + 1; + } else if (byte0 < LIST_SHORT_START) { + assembly { + let byteLen := sub(byte0, 0xb7) // # of bytes the actual length is + memPtr := add(memPtr, 1) // skip over the first byte + + /* 32 byte word size */ + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to get the len + itemLen := add(dataLen, add(byteLen, 1)) + } + } else if (byte0 < LIST_LONG_START) { + itemLen = byte0 - LIST_SHORT_START + 1; + } else { + assembly { + let byteLen := sub(byte0, 0xf7) + memPtr := add(memPtr, 1) + + let dataLen := div(mload(memPtr), exp(256, sub(32, byteLen))) // right shifting to the correct length + itemLen := add(dataLen, add(byteLen, 1)) + } + } + + return itemLen; + } + + // @return number of bytes until the data + function _payloadOffset(uint256 memPtr) private pure returns (uint256) { + uint256 byte0; + assembly { + byte0 := byte(0, mload(memPtr)) + } + + if (byte0 < STRING_SHORT_START) { + return 0; + } else if (byte0 < STRING_LONG_START || (byte0 >= LIST_SHORT_START && byte0 < LIST_LONG_START)) { + return 1; + } else if (byte0 < LIST_SHORT_START) { + // being explicit + return byte0 - (STRING_LONG_START - 1) + 1; + } else { + return byte0 - (LIST_LONG_START - 1) + 1; + } + } + + /* + * @param src Pointer to source + * @param dest Pointer to destination + * @param len Amount of memory to copy from the source + */ + function copy(uint256 src, uint256 dest, uint256 len) private pure { + if (len == 0) return; + + // copy as many word sizes as possible + for (; len >= WORD_SIZE; len -= WORD_SIZE) { + assembly { + mstore(dest, mload(src)) + } + + src += WORD_SIZE; + dest += WORD_SIZE; + } + + if (len > 0) { + // left over bytes. Mask is used to remove unwanted bytes from the word + uint256 mask = 256 ** (WORD_SIZE - len) - 1; + assembly { + let srcpart := and(mload(src), not(mask)) // zero out src + let destpart := and(mload(dest), mask) // retrieve the bytes + mstore(dest, or(destpart, srcpart)) + } + } + } +} diff --git a/contracts/Cross-chain/libs/StateProofVerifier.sol b/contracts/Cross-chain/libs/StateProofVerifier.sol new file mode 100644 index 00000000..70b7901b --- /dev/null +++ b/contracts/Cross-chain/libs/StateProofVerifier.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import { RLPReader } from "./RLPReader.sol"; +import { MerklePatriciaProofVerifier } from "./MerklePatriciaProofVerifier.sol"; + +/** + * @title A helper library for verification of Merkle Patricia account and state proofs. + */ +library StateProofVerifier { + using RLPReader for RLPReader.RLPItem; + using RLPReader for bytes; + + uint256 constant internal HEADER_STATE_ROOT_INDEX = 3; + uint256 constant internal HEADER_NUMBER_INDEX = 8; + uint256 constant internal HEADER_TIMESTAMP_INDEX = 11; + + struct BlockHeader { + bytes32 hash; + bytes32 stateRootHash; + uint256 number; + uint256 timestamp; + } + + struct Account { + bool exists; + uint256 nonce; + uint256 balance; + bytes32 storageRoot; + bytes32 codeHash; + } + + struct SlotValue { + bool exists; + uint96 value; + } + + /** + * @notice Parses block header and verifies its presence onchain within the latest 256 blocks. + * @param _headerRlpBytes RLP-encoded block header. + */ + function verifyBlockHeader( + bytes memory _headerRlpBytes, + bytes32 _blockHash + ) internal pure returns (BlockHeader memory) { + BlockHeader memory header = parseBlockHeader(_headerRlpBytes); + require(header.hash == _blockHash, "blockhash mismatch"); + return header; + } + + /** + * @notice Parses RLP-encoded block header. + * @param _headerRlpBytes RLP-encoded block header. + */ + function parseBlockHeader(bytes memory _headerRlpBytes) internal pure returns (BlockHeader memory) { + BlockHeader memory result; + RLPReader.RLPItem[] memory headerFields = _headerRlpBytes.toRlpItem().toList(); + + require(headerFields.length > HEADER_TIMESTAMP_INDEX); + + result.stateRootHash = bytes32(headerFields[HEADER_STATE_ROOT_INDEX].toUint()); + result.number = headerFields[HEADER_NUMBER_INDEX].toUint(); + result.timestamp = headerFields[HEADER_TIMESTAMP_INDEX].toUint(); + result.hash = keccak256(_headerRlpBytes); + + return result; + } + + /** + * @notice Verifies Merkle Patricia proof of an account and extracts the account fields. + * + * @param _addressHash Keccak256 hash of the address corresponding to the account. + * @param _stateRootHash MPT root hash of the Ethereum state trie. + */ + function extractAccountFromProof( + bytes32 _addressHash, // keccak256(abi.encodePacked(address)) + bytes32 _stateRootHash, + RLPReader.RLPItem[] memory _proof + ) internal pure returns (Account memory) { + bytes memory acctRlpBytes = MerklePatriciaProofVerifier.extractProofValue( + _stateRootHash, + abi.encodePacked(_addressHash), + _proof + ); + Account memory account; + + if (acctRlpBytes.length == 0) { + return account; + } + + RLPReader.RLPItem[] memory acctFields = acctRlpBytes.toRlpItem().toList(); + require(acctFields.length == 4); + + account.exists = true; + account.nonce = acctFields[0].toUint(); + account.balance = acctFields[1].toUint(); + account.storageRoot = bytes32(acctFields[2].toUint()); + account.codeHash = bytes32(acctFields[3].toUint()); + + return account; + } + + /** + * @notice Verifies Merkle Patricia proof of a slot and extracts the slot's value. + * + * @param _slotHash Keccak256 hash of the slot position. + * @param _storageRootHash MPT root hash of the account's storage trie. + */ + function extractSlotValueFromProof( + bytes32 _slotHash, + bytes32 _storageRootHash, + RLPReader.RLPItem[] memory _proof + ) internal pure returns (SlotValue memory) { + bytes memory valueRlpBytes = MerklePatriciaProofVerifier.extractProofValue( + _storageRootHash, + abi.encodePacked(_slotHash), + _proof + ); + + SlotValue memory value; + + if (valueRlpBytes.length != 0) { + value.exists = true; + value.value = uint96(valueRlpBytes.toRlpItem().toUint()); + } + + return value; + } +} diff --git a/contracts/Utils/SlotUtils.sol b/contracts/Utils/SlotUtils.sol new file mode 100644 index 00000000..591c3674 --- /dev/null +++ b/contracts/Utils/SlotUtils.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +library SlotUtils { + /** + * @notice method to calculate the slot hash of the a mapping indexed by account + * @param account address of the balance holder + * @param balanceMappingPosition base position of the storage slot of the balance on a token contract + * @return the slot hash + */ + function getAccountSlotHash(address account, uint256 balanceMappingPosition) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(bytes32(uint256(uint160(account))), balanceMappingPosition)); + } + + /** + * @notice method to calculate the slot hash of the a mapping indexed by voter and chainId + * @param voter address of the voter + * @param numCheckpoint id of the chain of the votingMachine + * @param representativesMappingPosition base position of the storage slot of the representatives on governance contract + * @return the slot hash + * @dev mapping(address => mapping(uint256 => address)) + */ + function getCheckpointSlotHash( + address voter, + uint32 numCheckpoint, + uint32 representativesMappingPosition + ) internal pure returns (bytes32) { + bytes memory firstLevelEncoded = abi.encode(bytes32(uint256(uint160(voter))), representativesMappingPosition); + + bytes memory secondLevelEncoded = abi.encode(numCheckpoint); + + return keccak256(abi.encodePacked(secondLevelEncoded, (keccak256(firstLevelEncoded)))); + } +} From 40483790254c7459aaaa77135f072d1372dc702c Mon Sep 17 00:00:00 2001 From: GitGuru7 <128375421+GitGuru7@users.noreply.github.com> Date: Fri, 25 Oct 2024 18:30:54 +0530 Subject: [PATCH 2/6] feat: add BlockHashDispatcher for remote chains --- .../Voting/BlockHashDispatcher.sol | 228 ++++++++++++++++++ .../Voting/VotingPowerAggregator.sol | 18 +- 2 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 contracts/Cross-chain/Voting/BlockHashDispatcher.sol diff --git a/contracts/Cross-chain/Voting/BlockHashDispatcher.sol b/contracts/Cross-chain/Voting/BlockHashDispatcher.sol new file mode 100644 index 00000000..e48c0fbb --- /dev/null +++ b/contracts/Cross-chain/Voting/BlockHashDispatcher.sol @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.25; + +import { ILayerZeroEndpoint } from "@layerzerolabs/solidity-examples/contracts/lzApp/interfaces/ILayerZeroEndpoint.sol"; +import { Pausable } from "@openzeppelin/contracts/security/Pausable.sol"; +import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol"; +import { IAccessControlManagerV8 } from "./../../Governance/IAccessControlManagerV8.sol"; + +contract BlockHashDispatcher is Pausable { + /** + * @notice ACM (Access Control Manager) contract address + */ + address public accessControlManager; + + /** @notice Proposal chain id on which block hash will be send (BNB) */ + + uint16 public proposalChainId; + + /** @notice Counter to keep track of hahses dispatched */ + uint256 public dispatchedHashes; + + /** + * @notice LayerZero endpoint for sending messages to remote chains + */ + ILayerZeroEndpoint public immutable LZ_ENDPOINT; + + /** + * @notice Specifies the allowed path for sending messages (remote chainId => remote app address + local app address) + */ + mapping(uint16 => bytes) public trustedRemoteLookup; + + /** + * @notice Execution hashes of failed messages + * @dev [identifier] -> [executionHash] + */ + mapping(uint256 => bytes32) public storedExecutionHashes; + + /** + * @notice Emitted when a remote message receiver is set for the remote chain + */ + event SetTrustedRemoteAddress(uint16 indexed chainId, bytes oldRemoteAddress, bytes newRemoteAddress); + /** + * @notice Emitted when block hash is send to proposal chain (BNB) + */ + event HashDispatched(uint256 indexed remoteIdentifier, bytes payload); + + /** + * @notice Emitted when an execution hash of a failed message is saved + */ + event HashStored(uint256 indexed remoteIdentifier, bytes payload, bytes adapterParams, uint256 value, bytes reason); + + /** + * @notice Event emitted when trusted remote sets to empty + */ + event TrustedRemoteRemoved(uint16 indexed chainId); + + /** + * @notice Emitted when a previously failed message is successfully sent to the proposal chain (BNB) + */ + event ClearPayload(uint256 indexed remoteIdentifier, bytes32 executionHash); + + /* + * @notice Emitted when the address of ACM is updated + */ + event NewAccessControlManager(address indexed oldAccessControlManager, address indexed newAccessControlManager); + + constructor(ILayerZeroEndpoint lzEndpoint_, address accessControlManager_, uint16 proposalChainId_) { + ensureNonzeroAddress(address(lzEndpoint_)); + require(proposalChainId_ != 0, "chain id can't be zero"); + LZ_ENDPOINT = lzEndpoint_; + proposalChainId = proposalChainId_; + accessControlManager = accessControlManager_; + } + + /** + * @notice Triggers the paused state of the controller + * @custom:access Controlled by AccessControlManager + */ + function pause() external { + _ensureAllowed("pause()"); + _pause(); + } + + /** + * @notice Triggers the resume state of the controller + * @custom:access Controlled by AccessControlManager + */ + function unpause() external { + _ensureAllowed("unpause()"); + _unpause(); + } + + /** + * @notice Sets the address of Access Control Manager (ACM) + * @param newAccessControlManager The new address of the Access Control Manager + * @custom:access Only owner + * @custom:event Emits NewAccessControlManager with old and new access control manager addresses + */ + function setAccessControlManager(address newAccessControlManager) external { + _ensureAllowed("setAccessControlManager(address)"); + ensureNonzeroAddress(newAccessControlManager); + emit NewAccessControlManager(accessControlManager, newAccessControlManager); + accessControlManager = newAccessControlManager; + } + + /** + * @notice Estimates LayerZero fees for cross-chain message delivery to the proposal chain + * @dev The estimated fees are the minimum required; it's recommended to increase the fees amount when sending a message. The unused amount will be refunded + * @param payload The payload to be sent to the proposal chain. It's computed as follows: + * payload = abi.encode(remoteIdentifier, blockHash) + * @param useZro Bool that indicates whether to pay in ZRO tokens or not + * @param adapterParams The params used to specify the custom amount of gas required for the execution on the proposal chain + * @return nativeFee The amount of fee in the native gas token (e.g. ETH) + * @return zroFee The amount of fee in ZRO token + */ + function estimateFees( + bytes calldata payload, + bool useZro, + bytes calldata adapterParams + ) external view returns (uint256, uint256) { + return LZ_ENDPOINT.estimateFees(proposalChainId, address(this), payload, useZro, adapterParams); + } + + /** + * @notice Remove trusted remote from storage + * @param chainId The chain's id corresponds to setting the trusted remote to empty + * @custom:access Controlled by Access Control Manager + * @custom:event Emit TrustedRemoteRemoved with remote chain id + */ + function removeTrustedRemote(uint16 chainId) external { + _ensureAllowed("removeTrustedRemote(uint16)"); + require(trustedRemoteLookup[chainId].length != 0, "trusted remote not found"); + delete trustedRemoteLookup[chainId]; + emit TrustedRemoteRemoved(chainId); + } + + /** + * @notice Dispatches a block hash along with a remote identifier to proposal chain(BNB) + * @param remoteIdentifier A unique identifier to identify the proposal + * @param zroPaymentAddress The address for payment using ZRO tokens + * @param adapterParams The params used to specify the custom amount of gas required for the execution on the destination + */ + function dispatchHash( + uint256 remoteIdentifier, + address zroPaymentAddress, + bytes calldata adapterParams + ) external payable { + bytes32 blockHash = blockhash(block.number - 1); + bytes memory payload = abi.encode(remoteIdentifier, blockHash); + uint16 proposalChainId_ = proposalChainId; + bytes memory trustedRemote = trustedRemoteLookup[proposalChainId_]; + require(trustedRemote.length != 0, "proposal chain id is not set"); + + // A zero value will result in a failed message; therefore, a positive value is required to send a message across the chain. + require(msg.value > 0, "value cannot be zero"); + + dispatchedHashes++; + try + LZ_ENDPOINT.send{ value: msg.value }( + proposalChainId_, + trustedRemote, + payload, + payable(msg.sender), + zroPaymentAddress, + adapterParams + ) + { + emit HashDispatched(remoteIdentifier, payload); + } catch (bytes memory reason) { + storedExecutionHashes[remoteIdentifier] = keccak256(abi.encode(payload, adapterParams, msg.value)); + emit HashStored(remoteIdentifier, payload, adapterParams, msg.value, reason); + } + } + + /** + * @notice Resends a previously failed message + * @dev Allows providing more fees if needed. The extra fees will be refunded to the caller + * @param remoteIdentifier The unique remote identifier identify a failed message + * @param payload The payload to be sent to the remote chain + * It's computed as follows: payload = abi.encode(remoteIdentifier, blockHash) + * @param adapterParams The params used to specify the custom amount of gas required for the execution on the destination + * @param zroPaymentAddress The address of the ZRO token holder who would pay for the transaction. + * @param originalValue The msg.value passed when dispatchHash() function was called + * @custom:event Emits ClearPayload with proposal id and hash + * @custom:access Controlled by Access Control Manager + */ + function retryExecute( + uint256 remoteIdentifier, + bytes calldata payload, + bytes calldata adapterParams, + address zroPaymentAddress, + uint256 originalValue + ) external payable whenNotPaused { + _ensureAllowed("retryExecute(uint256,uint16,bytes,bytes,address,uint256)"); + uint16 proposalChainId_ = proposalChainId; + + bytes memory trustedRemote = trustedRemoteLookup[proposalChainId_]; + require(trustedRemote.length != 0, "proposal chain id is not a trusted source"); + bytes32 hash = storedExecutionHashes[remoteIdentifier]; + require(hash != bytes32(0), "no stored payload"); + + require(keccak256(abi.encode(payload, adapterParams, originalValue)) == hash, "invalid execution params"); + + delete storedExecutionHashes[remoteIdentifier]; + + emit ClearPayload(remoteIdentifier, hash); + + LZ_ENDPOINT.send{ value: originalValue + msg.value }( + proposalChainId_, + trustedRemote, + payload, + payable(msg.sender), + zroPaymentAddress, + adapterParams + ); + } + + /** + * @notice Ensure that the caller has permission to execute a specific function + * @param functionSig Function signature to be checked for permission + */ + function _ensureAllowed(string memory functionSig) internal view { + require( + IAccessControlManagerV8(accessControlManager).isAllowedToCall(msg.sender, functionSig), + "access denied" + ); + } +} diff --git a/contracts/Cross-chain/Voting/VotingPowerAggregator.sol b/contracts/Cross-chain/Voting/VotingPowerAggregator.sol index 4f2bd39a..65ce3f46 100644 --- a/contracts/Cross-chain/Voting/VotingPowerAggregator.sol +++ b/contracts/Cross-chain/Voting/VotingPowerAggregator.sol @@ -27,7 +27,7 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { mapping(uint16 => bool) public isSupportedRemote; // Address of XVS vault wrt to chain Id - mapping(uint16 => address) public xvsVault; + mapping(uint16 => address) public xvsVault; /** * @notice Emitted when proposal failed @@ -49,6 +49,22 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { warehouse = IDataWarehouse(warehouseAddress); } + /** + * @notice Triggers the paused state of the aggregator + * @custom:access Only owner + */ + function pause() external onlyOwner { + _pause(); + } + + /** + * @notice Triggers the resume state of the aggregator + * @custom:access Only owner + */ + function unpause() external onlyOwner { + _unpause(); + } + /** * @notice Updates the configuration of remote chain ids, marking them as supported or unsupported and setting the corresponding XVS vault addresses * @param remoteChainId An array of remote chain ids to update From 8838697bb8b6900bf71ed00cd765de85306f6339 Mon Sep 17 00:00:00 2001 From: GitGuru7 <128375421+GitGuru7@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:34:17 +0530 Subject: [PATCH 3/6] refactor: VotingPowerAggregator and BlockHashDispatcher --- .../Voting/BlockHashDispatcher.sol | 16 +- .../Voting/VotingPowerAggregator.sol | 141 ++++++++++-------- 2 files changed, 92 insertions(+), 65 deletions(-) diff --git a/contracts/Cross-chain/Voting/BlockHashDispatcher.sol b/contracts/Cross-chain/Voting/BlockHashDispatcher.sol index e48c0fbb..8cda3cb9 100644 --- a/contracts/Cross-chain/Voting/BlockHashDispatcher.sol +++ b/contracts/Cross-chain/Voting/BlockHashDispatcher.sol @@ -12,12 +12,14 @@ contract BlockHashDispatcher is Pausable { */ address public accessControlManager; - /** @notice Proposal chain id on which block hash will be send (BNB) */ + /** + * @notice Proposal chain id on which block hash will be send (BNB) + */ uint16 public proposalChainId; - /** @notice Counter to keep track of hahses dispatched */ - uint256 public dispatchedHashes; + // remote identifier => blockHash + mapping(uint256 => bytes32) public blockHash; /** * @notice LayerZero endpoint for sending messages to remote chains @@ -145,8 +147,11 @@ contract BlockHashDispatcher is Pausable { address zroPaymentAddress, bytes calldata adapterParams ) external payable { - bytes32 blockHash = blockhash(block.number - 1); - bytes memory payload = abi.encode(remoteIdentifier, blockHash); + // Send hash only once for each unique identifier + require(blockHash[remoteIdentifier] == bytes32(0), "block hash already exists"); + + bytes32 blockHash_ = blockhash(block.number - 1); + bytes memory payload = abi.encode(remoteIdentifier, blockHash_); uint16 proposalChainId_ = proposalChainId; bytes memory trustedRemote = trustedRemoteLookup[proposalChainId_]; require(trustedRemote.length != 0, "proposal chain id is not set"); @@ -154,7 +159,6 @@ contract BlockHashDispatcher is Pausable { // A zero value will result in a failed message; therefore, a positive value is required to send a message across the chain. require(msg.value > 0, "value cannot be zero"); - dispatchedHashes++; try LZ_ENDPOINT.send{ value: msg.value }( proposalChainId_, diff --git a/contracts/Cross-chain/Voting/VotingPowerAggregator.sol b/contracts/Cross-chain/Voting/VotingPowerAggregator.sol index 65ce3f46..c7c56a7a 100644 --- a/contracts/Cross-chain/Voting/VotingPowerAggregator.sol +++ b/contracts/Cross-chain/Voting/VotingPowerAggregator.sol @@ -13,20 +13,23 @@ import { Pausable } from "@openzeppelin/contracts/security/Pausable.sol"; contract VotingPowerAggregator is NonblockingLzApp, Pausable { using ExcessivelySafeCall for address; + struct Proofs { + uint16 remoteChainId; + bytes numCheckpointsProof; + bytes checkpointsProof; + } + uint8 public constant CHECKPOINTS_SLOT = 16; uint8 public constant NUM_CHECKPOINTS_SLOT = 17; IDataWarehouse public warehouse; - // Array of remote chain ids for iteration on mapping - uint16[] public remoteChainIds; + // containing deactivation record of chain id + mapping(uint16 => bool) public isdeactived; // remote identifier => chainId => blockHash mapping(uint256 => mapping(uint16 => bytes32)) public remoteBlockHash; - // List of supported remote chain Id - mapping(uint16 => bool) public isSupportedRemote; - - // Address of XVS vault wrt to chain Id + // Address of XVS vault corresponding to remote chain Id mapping(uint16 => address) public xvsVault; /** @@ -37,12 +40,27 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { /** * @notice Emitted when block hash of remote chain is received */ - event HashReceived(uint256 remoteIdentifier, uint16 remoteChainId, bytes blockHash); + event HashReceived(uint256 indexed remoteIdentifier, uint16 indexed remoteChainId, bytes blockHash); /** * @notice Emitted when remote configurations are updated */ - event UpdateRemoteConfigurations(uint16 remoteChainId, address xvsVault, bool isSupported); + event UpdateDeactivatedRemoteChainId(uint16 indexed remoteChainId, bool isSupported); + + /** + * @notice Emitted when vault addressis updated + */ + event UpdateVaultAddress(uint16 indexed remoteChainId, address xvsVault); + + /** + * @notice Thrown when invalid chain id is provided + */ + error InvalidChainId(uint16 chainId); + + /** + * @notice Thrown when chain id is deactivated + */ + error DeactivatedChainId(uint16 chainId); constructor(address endpoint, address warehouseAddress) NonblockingLzApp(endpoint) { ensureNonzeroAddress(warehouseAddress); @@ -66,31 +84,45 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { } /** - * @notice Updates the configuration of remote chain ids, marking them as supported or unsupported and setting the corresponding XVS vault addresses - * @param remoteChainId An array of remote chain ids to update - * @param isSupported An array indicating whether each remote chain id is supported or not + * @notice Update xvs vault address corresponding to the remote chain id + * @param remoteChainId An array of remote chain ids * @param xvsVaultAddress An array of XVS vault addresses corresponding to each remote chain id * @custom:access Only owner - * @custom:emit UpdateRemoteConfigurations Emitted when the configuration of a remote chain id is updated, along with its vault address and support status + * @custom:event UpdateVaultAddress with remote chain id and its corresponding xvs vault address */ - function updateRemoteConfigurations( + function updateVaultAddress( uint16[] calldata remoteChainId, - bool[] calldata isSupported, address[] calldata xvsVaultAddress ) external onlyOwner { + require(remoteChainId.length == xvsVaultAddress.length, "invalid params"); for (uint16 i; i < remoteChainId.length; ++i) { ensureNonzeroAddress(xvsVaultAddress[i]); - require(remoteChainId[i] != 0, "Invalid chain id"); - isSupportedRemote[remoteChainId[i]] = isSupported[i]; - if (isSupported[i]) { - remoteChainIds.push(remoteChainId[i]); - xvsVault[remoteChainId[i]] = xvsVaultAddress[i]; - } else { - delete remoteChainIds[remoteChainId[i]]; - delete xvsVault[remoteChainId[i]]; + if (remoteChainId[i] == 0) { + revert InvalidChainId(remoteChainId[i]); } - emit UpdateRemoteConfigurations(remoteChainId[i], xvsVaultAddress[i], isSupported[i]); + xvsVault[remoteChainId[i]] = xvsVaultAddress[i]; + emit UpdateVaultAddress(remoteChainId[i], xvsVaultAddress[i]); + } + } + + /** + * @notice Updates the deactivedRemoteChainIds array + * @param remoteChainId An array of remote chain ids to update + * @param isDeactivated An array indicating whether each remote chain id is deactivated or not + * @custom:access Only owner + * @custom:emit UpdateDeactivatedRemoteChainId emitted with remote chain id & its deactivation status + */ + function updateDeactivatedRemoteChainId( + uint16[] calldata remoteChainId, + bool[] calldata isDeactivated + ) external onlyOwner { + for (uint16 i; i < remoteChainId.length; ++i) { + if (remoteChainId[i] == 0) { + revert InvalidChainId(remoteChainId[i]); + } + isdeactived[remoteChainId[i]] = isDeactivated[i]; + emit UpdateDeactivatedRemoteChainId(remoteChainId[i], isDeactivated[i]); } } @@ -98,23 +130,27 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { * @notice Calculates the total voting power of a voter across multiple remote chains * @param voter The address of the voter for whom to calculate the voting power * @param remoteIdentifier The identifier that links to remote chain-specific data - * @param numCheckpointsProof The proof data needed to verify the number of checkpoints - * @param checkpointsProof The proof data needed to verify the actual voting power from the checkpoints + * @param proofs Array of proofs containing remote chain id with their corresponding proofs (numCheckpointsProof, checkpointsProof) where + * numCheckpointsProof is the proof data needed to verify the number of checkpoints and + * checkpointsProof is the proof data needed to verify the actual voting power from the checkpoints * @return power The total voting power of the voter across all supported remote chains */ function getVotingPower( address voter, uint256 remoteIdentifier, - bytes calldata numCheckpointsProof, - bytes calldata checkpointsProof + Proofs[] calldata proofs ) external view returns (uint96 power) { uint96 totalVotingPower; - for (uint16 i; i < remoteChainIds.length; ++i) { + for (uint16 i; i < proofs.length; ++i) { + // remoteChainId must be active + if (isdeactived[proofs[i].remoteChainId]) { + revert DeactivatedChainId(proofs[i].remoteChainId); + } totalVotingPower += _getVotingPower( - remoteChainIds[i], + proofs[i].remoteChainId, remoteIdentifier, - numCheckpointsProof, - checkpointsProof, + proofs[i].numCheckpointsProof, + proofs[i].checkpointsProof, voter ); } @@ -147,19 +183,23 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { numCheckpointsProof ); - if (latestCheckpoint.exists) { - StateProofVerifier.SlotValue memory votingPower = warehouse.getStorage( - vault, - blockHash, - SlotUtils.getCheckpointSlotHash(voter, uint32(latestCheckpoint.value - 1), uint32(CHECKPOINTS_SLOT)), - checkpointsProof - ); - if (votingPower.exists) { - return votingPower.value >> 32; - } + // Reverts if latest checkpoint not exists + require(latestCheckpoint.exists, "Invalid num checkpoint proof"); + + if (latestCheckpoint.value == 0) { return 0; } - return 0; + StateProofVerifier.SlotValue memory votingPower = warehouse.getStorage( + vault, + blockHash, + SlotUtils.getCheckpointSlotHash(voter, uint32(latestCheckpoint.value - 1), uint32(CHECKPOINTS_SLOT)), + checkpointsProof + ); + + // Reverts if voting power not exists + require(votingPower.exists, "Invalid checkpoint proof"); + + return votingPower.value >> 32; } /** @@ -176,8 +216,6 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { uint64 nonce, bytes memory payload ) internal override { - // remoteChainId should belongs to supported chainIds - require(isSupportedRemote[remoteChainId], "source chain id not supported"); bytes32 hashedPayload = keccak256(payload); bytes memory callData = abi.encodeCall( this.nonblockingLzReceive, @@ -211,19 +249,4 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { emit HashReceived(remoteIdentifier, remoteChainId, blockHash); } - - /** - * @dev Removes a specified remote chain id from the list of supported remote chain ids. - * @param remoteChainId The chain id to be removed from the list of supported remote chains. - */ - function _removeRemoteChainId(uint16 remoteChainId) internal { - uint256 length = remoteChainIds.length; - for (uint256 i = 0; i < length; i++) { - if (remoteChainIds[i] == remoteChainId) { - remoteChainIds[i] = remoteChainIds[length - 1]; - remoteChainIds.pop(); - break; - } - } - } } From 5ce043e334a5bf81bb79ea2727147a40061e3e3b Mon Sep 17 00:00:00 2001 From: GitGuru7 <128375421+GitGuru7@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:34:45 +0530 Subject: [PATCH 4/6] feat: add generateProof script --- hardhat.config.ts | 1 + hardhat.config.zksync.ts | 1 + package.json | 4 +- script/generateProofs.ts | 175 ++++++++++++++++++++++ script/utils.ts | 85 +++++++++++ tests/Syncing-of-votes/sepoliaProofs.json | 1 + yarn.lock | 65 +++++--- 7 files changed, 307 insertions(+), 25 deletions(-) create mode 100644 script/generateProofs.ts create mode 100644 script/utils.ts create mode 100644 tests/Syncing-of-votes/sepoliaProofs.json diff --git a/hardhat.config.ts b/hardhat.config.ts index f585ada2..6bf479e3 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,6 +1,7 @@ import "module-alias/register"; import "@nomicfoundation/hardhat-chai-matchers"; +import "@nomiclabs/hardhat-ethers"; import "@nomiclabs/hardhat-etherscan"; import "@openzeppelin/hardhat-upgrades"; import "@typechain/hardhat"; diff --git a/hardhat.config.zksync.ts b/hardhat.config.zksync.ts index 904d53f1..8cc09cde 100644 --- a/hardhat.config.zksync.ts +++ b/hardhat.config.zksync.ts @@ -4,6 +4,7 @@ import "@matterlabs/hardhat-zksync"; import "@matterlabs/hardhat-zksync-solc"; import "@matterlabs/hardhat-zksync-verify"; import "@nomicfoundation/hardhat-chai-matchers"; +import "@nomiclabs/hardhat-ethers"; import "hardhat-dependency-compiler"; import "hardhat-deploy"; import { HardhatUserConfig, task } from "hardhat/config"; diff --git a/package.json b/package.json index 0bf23a0a..c764fc14 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "@nomicfoundation/hardhat-network-helpers": "^1.0.6", "@nomicfoundation/hardhat-toolbox": "^2.0.0", "@nomicfoundation/hardhat-verify": "^2.0.8", - "@nomiclabs/hardhat-ethers": "^2.2.1", + "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomiclabs/hardhat-etherscan": "^3.1.2", "@openzeppelin/contracts": "^4.8.2", "@openzeppelin/contracts-upgradeable": "^4.8.2", @@ -77,6 +77,7 @@ "@types/chai": "^4.3.4", "@types/debug": "^4.1.12", "@types/fs-extra": "^9.0.13", + "@types/json-stable-stringify": "^1.0.36", "@types/mocha": "^10.0.0", "@types/node": "^18.16.3", "@typescript-eslint/eslint-plugin": "^5.44.0", @@ -98,6 +99,7 @@ "hardhat-docgen": "^1.3.0", "hardhat-gas-reporter": "^1.0.9", "husky": "^8.0.3", + "json-stable-stringify": "^1.1.1", "lint-staged": "^13.0.4", "lodash": "^4.17.21", "mocha": "^10.1.0", diff --git a/script/generateProofs.ts b/script/generateProofs.ts new file mode 100644 index 00000000..c515813d --- /dev/null +++ b/script/generateProofs.ts @@ -0,0 +1,175 @@ +import "dotenv/config"; +import { BigNumber, utils } from "ethers"; +import { hexStripZeros, hexZeroPad } from "ethers/lib/utils.js"; +import * as fs from "fs"; +import { network } from "hardhat"; +import stringify from "json-stable-stringify"; + +import { + formatToProofRLP, + getExtendedBlock, + getProof, + getSolidityStorageSlotBytes, + getSolidityTwoLevelStorageSlotHash, + prepareBLockRLP, +} from "./utils.ts"; + +export type ProofData = { + blockNumber?: number; + blockHash?: string; + accountStateProofRLP?: string; + blockHeaderRLP?: string; + xvsVaultAddress?: string; + checkpointsSlot?: string; + numCheckpointsSlot?: string; + checkpointsSlotHash?: string; + numCheckpointsSlotHash?: string; + numCheckpoints?: number; + xvsVaultNumCheckpointsStorageProofRlp?: string; + checkpoint?: { + fromBlockNumber?: string; + votes?: string; + }; + xvsVaultCheckpointsStorageProofRlp?: string; +}; + +const xvsVault = { + sepolia: "0x1129f882eAa912aE6D4f6D445b2E2b1eCbA99fd5", + ethereum: "0xA0882C2D5DF29233A092d2887A258C2b90e9b994", + opbnbtestnet: "0xB14A0e72C5C202139F78963C9e89252c1ad16f01", + opbnbmainnet: "0x7dc969122450749A8B0777c0e324522d67737988", + arbitrumone: "0x8b79692AAB2822Be30a6382Eb04763A74752d5B4", + arbitrumsepolia: "0x407507DC2809D3aa31D54EcA3BEde5C5c4C8A17F", + zksyncsepolia: "0x825f9EE3b2b1C159a5444A111A70607f3918564e", + zksyncmainnet: "0xbbB3C88192a5B0DB759229BeF49DcD1f168F326F", + opsepolia: "0x4d344e48F02234E82D7D1dB84d0A4A18Aa43Dacc", + opmainnet: "0x133120607C018c949E91AE333785519F6d947e01", +}; + +const SLOTS = { + checkpoints: 16, + numCheckpoint: 17, +}; + +const saveJson = (stringifiedJson: string) => { + fs.writeFileSync(`./tests/Syncing-of-votes/${process.env.REMOTE_NETWORK}Proofs.json`, stringifiedJson); +}; + +export const getProofsJson = (): ProofData => { + try { + const file = fs.readFileSync(`./tests/Syncing-of-votes/${process.env.REMOTE_NETWORK}Proofs.json`); + return JSON.parse(file.toString()); + } catch (error) { + return {}; + } +}; + +const generateRoots = async (xvsVaultAddress: string, numCheckpointSlotRaw: number, checkpointSlotRaw: number) => { + const proofsJson = getProofsJson(); + + if (!proofsJson["xvsVaultAddress"]) { + proofsJson["xvsVaultAddress"] = xvsVaultAddress; + } + + // calculate blockHeaderRLP + const blockData = await getExtendedBlock(parseInt(process.env.BLOCK as string)); + const blockHeaderRLP = prepareBLockRLP(blockData); + proofsJson.blockHash = blockData.hash; + proofsJson.blockNumber = BigNumber.from(blockData.number).toNumber(); + proofsJson.blockHeaderRLP = blockHeaderRLP; + + // calculate slots + const slots: string[] = []; + + const checkpointSlot = hexZeroPad(utils.hexlify(checkpointSlotRaw), 32); + slots.push(checkpointSlot); + proofsJson.checkpointsSlot = checkpointSlot; + + const numCheckpointSlot = hexZeroPad(utils.hexlify(numCheckpointSlotRaw), 32); + slots.push(numCheckpointSlot); + proofsJson.numCheckpointsSlot = numCheckpointSlot; + + // get account state proof rlp + const rawAccountProofData = await getProof(xvsVaultAddress, slots, proofsJson.blockNumber); + const accountStateProofRLP = formatToProofRLP(rawAccountProofData.accountProof); + proofsJson.accountStateProofRLP = accountStateProofRLP; + saveJson(stringify(proofsJson)); +}; + +const generateProofsNumCheckpointsSlot = async (vault: string, rawSlot: number, voter: string) => { + const proofsJson = getProofsJson(); + const hexSlot = utils.hexlify(rawSlot); + const slot = getSolidityStorageSlotBytes(hexSlot, voter); + if (!proofsJson.blockNumber) { + throw new Error("blockNumber is not set"); + } + + const numCheckpointsHex = await network.provider.send("eth_getStorageAt", [ + vault, + slot, + hexStripZeros(utils.hexlify(proofsJson.blockNumber)), + ]); + + const numCheckpoints = BigNumber.from(numCheckpointsHex).toNumber(); + + const rawProofData = await getProof(vault, [slot], proofsJson.blockNumber); + + const storageProofRlp = formatToProofRLP(rawProofData.storageProof[0].proof); + proofsJson.numCheckpointsSlotHash = slot; + proofsJson.numCheckpoints = numCheckpoints; + proofsJson.xvsVaultNumCheckpointsStorageProofRlp = storageProofRlp; + + saveJson(stringify(proofsJson)); +}; + +const generateXvsVaultProofsByCheckpoint = async (vault: string, rawSlot: number, voter: string) => { + const hexSlot = utils.hexlify(rawSlot); + const proofsJson = getProofsJson(); + + if (!proofsJson.numCheckpoints) { + throw new Error("numCheckpoints is zero"); + } + + const slot = getSolidityTwoLevelStorageSlotHash(hexSlot, voter, proofsJson.numCheckpoints - 1); + + if (!proofsJson.blockNumber) { + throw new Error("blockNumber is not set"); + } + + const checkpointData = await network.provider.send("eth_getStorageAt", [ + vault, + slot, + hexStripZeros(utils.hexlify(proofsJson.blockNumber)), + ]); + + const fromBlockNumberHex = "0x" + checkpointData.slice(-8); + const votesHex = checkpointData.slice(0, -8); + const fromBlockNumber = BigNumber.from(fromBlockNumberHex).toString(); + const votes = BigNumber.from(votesHex).toString(); + + const rawProofData = await getProof(vault, [slot], proofsJson.blockNumber); + + const storageProofRlp = formatToProofRLP(rawProofData.storageProof[0].proof); + + proofsJson.checkpointsSlotHash = slot; + proofsJson.checkpoint = { + fromBlockNumber, + votes, + }; + proofsJson.xvsVaultCheckpointsStorageProofRlp = storageProofRlp; + saveJson(stringify(proofsJson)); +}; + +const generateJson = async () => { + const XVS_VAULT = xvsVault[process.env.REMOTE_NETWORK as string]; + + await generateRoots(XVS_VAULT, SLOTS.checkpoints, SLOTS.numCheckpoint); + + await generateProofsNumCheckpointsSlot(XVS_VAULT, SLOTS.numCheckpoint, process.env.VOTER as string); + + await generateXvsVaultProofsByCheckpoint(XVS_VAULT, SLOTS.checkpoints, process.env.VOTER as string); +}; + +(async () => { + await generateJson(); +})(); diff --git a/script/utils.ts b/script/utils.ts new file mode 100644 index 00000000..c2d8660e --- /dev/null +++ b/script/utils.ts @@ -0,0 +1,85 @@ +import { ethers, providers, utils } from "ethers"; +import { defaultAbiCoder, hexStripZeros, hexZeroPad, keccak256 } from "ethers/lib/utils.js"; + +const provider = new providers.StaticJsonRpcProvider(process.env[`ARCHIVE_NODE_${process.env.REMOTE_NETWORK}`]); + +const RLP = require("rlp"); + +export function formatToProofRLP(rawData) { + return ethers.utils.RLP.encode(rawData.map(d => ethers.utils.RLP.decode(d))); +} + +export const getProof = async (address: string, storageKeys: string[], blockNumber: number) => { + return await provider.send("eth_getProof", [address, storageKeys, hexStripZeros(utils.hexlify(blockNumber))]); +}; + +export const getExtendedBlock = async (blockNumber: number) => { + const blockNumber_ = blockNumber ? hexStripZeros(utils.hexlify(blockNumber)) : "latest"; + return provider.send("eth_getBlockByNumber", [blockNumber_, false]); +}; +export function prepareBLockRLP(rawBlock) { + const blockHeader = [ + rawBlock.parentHash, + rawBlock.sha3Uncles, + rawBlock.miner, + rawBlock.stateRoot, + rawBlock.transactionsRoot, + rawBlock.receiptsRoot, + rawBlock.logsBloom, + rawBlock.difficulty === "0x0" ? "0x" : rawBlock.difficulty, + rawBlock.number, + rawBlock.gasLimit === "0x0" ? "0x" : rawBlock.gasLimit, + rawBlock.gasUsed === "0x0" ? "0x" : rawBlock.gasUsed, + rawBlock.timestamp, + rawBlock.extraData, + rawBlock.mixHash, + rawBlock.nonce, + ]; + if (rawBlock.baseFeePerGas) { + blockHeader.push(rawBlock.baseFeePerGas === "0x0" ? "0x" : rawBlock.baseFeePerGas); + } + if (rawBlock.withdrawalsRoot) { + blockHeader.push(rawBlock.withdrawalsRoot); + } + if (rawBlock.blobGasUsed) { + blockHeader.push(rawBlock.blobGasUsed === "0x0" ? "0x" : rawBlock.blobGasUsed); + } + if (rawBlock.excessBlobGas) { + blockHeader.push(rawBlock.excessBlobGas === "0x0" ? "0x" : rawBlock.excessBlobGas); + } + if (rawBlock.parentBeaconBlockRoot) { + blockHeader.push(rawBlock.parentBeaconBlockRoot); + } + const encodedHeader = RLP.encode(blockHeader); + const encodedHeaderHex = "0x" + Buffer.from(encodedHeader).toString("hex"); + + return encodedHeaderHex; +} + +export function getSolidityStorageSlotBytes( + mappingSlot, //: BytesLike, + key, //: string +) { + const slot = hexZeroPad(mappingSlot, 32); + return hexStripZeros(keccak256(defaultAbiCoder.encode(["address", "uint256"], [key, slot]))); +} + +export function getSolidityTwoLevelStorageSlotHash( + rawSlot, // number + voter, // string + numCheckpoints, // number +) { + const abiCoder = new ethers.utils.AbiCoder(); + // ABI Encode the first level of the mapping + // abi.encode(address(voter), uint256(MAPPING_SLOT)) + // The keccak256 of this value will be the "slot" of the inner mapping + const firstLevelEncoded = abiCoder.encode(["address", "uint256"], [voter, ethers.BigNumber.from(rawSlot)]); + + // ABI Encode the second level of the mapping + // abi.encode(uint256(numCheckpoints)) + const secondLevelEncoded = abiCoder.encode(["uint256"], [ethers.BigNumber.from(numCheckpoints)]); + + // Compute the storage slot of [address][uint256] + // keccak256(abi.encode(uint256(numCheckpoints)) . abi.encode(address(voter), uint256(MAPPING_SLOT))) + return ethers.utils.keccak256(ethers.utils.concat([secondLevelEncoded, ethers.utils.keccak256(firstLevelEncoded)])); +} diff --git a/tests/Syncing-of-votes/sepoliaProofs.json b/tests/Syncing-of-votes/sepoliaProofs.json new file mode 100644 index 00000000..72bbb2fb --- /dev/null +++ b/tests/Syncing-of-votes/sepoliaProofs.json @@ -0,0 +1 @@ +{"accountStateProofRLP":"0xf90d66f90211a0bffd482e8cf690c90e7cad7998b35e09e24daac6e7c66f10b9f18e9c267b62a8a0fdbd043ff6c77ebe9a83890040e0194b294c8f82ae172d93745988d473ca0002a041482adccca0cdf5ff8ada6017b0eb8fc54d0d25fea1f7a6b4d33497256aea18a024ea31005ae5b2fb211a5a82e2904484a725a2dae7c455dbbbac7915fb7bf6a7a068f5a279a7f9fcf0b1247bcfdc29515e47b8ab12a81c2864c1d83db036d3cbfda0d762c1e70b0db4f40c131c7db6e8764dd1f7e0fbe59be35cf3f2e12cf69b7c9fa05e8bfa0e8c4391463c995679f0319e735f73ff3a92239324cfda93ecba8ff1e9a0684f281be3bb9d63dee6f0a3b85b2cce9f6bbebd936d97d3ec2f60fa4314f78ba02ff4108e6de5d8e445d5c324cce1ccbdc6197093fe92bf848f0ee4d4a97b64cda00070c4c28c3b15b9e6d1e7140b24dbbafe14f2912f02df48a2707c83712ac34aa0047bc6c91836ad024e886b2f81e106c382b14d3af71afdb09c1668da2b506d3ba0bf469e1788e762b0f34917bdfb3183d59733eb46868767544aa0c26d11cd9285a0778020d9f09ada2c8246e4802dcc81a9b57c8d082449e184ab116d8169813a60a07284a2482be2ae2e95372ac6f28b2783b1b36a91fea3ce540ea8d0ef7706724ea099ed9720a0f7a6cdb617717438afabc009114baaef7dc4eb14019f4f9ee4d770a0818aee63967096e292fad0a261893957f938e55cd3203a0343c1595d125e1d4080f90211a04f2006e6e3a934ad0dc8ee5a679619d36be8529445359b69006d27c4181bf158a06ecd13afe9631e04ff1bba0f6daa92fd49a13a0507c8da2eb01e3c862bc9fea1a04097a1e46d41c0556235422b1bcf4f40c7c5df958f29c5baf2097ff494d05995a09b3a61d9ee9bc6f40db87f7164ba48bf0422b68deedac2c3134d7c873d10b19ca0b957dc3effe9acc6a77cb8bc437f17908a534ced350898060ff33ca0fcebec95a018d25cd477a8052a12a96624b04069d86fcb1862a2ba09adfce3e0abe1cc426ba0293a07bb8be587c10de909c58aca184b3c95ee772babf75e53cba210d85659fca0ba51326c6c20db5e2139d60797cd3d30b0baef4c5da2adf431f06ec0f6c9fc90a03f84cab88a59fd85437bae600dbc817c74873773b74ea03c15f2c7c8718ce2afa023a553bd861f88211a248b19c5ff61a87e6b191d6148c27cde2e3cfdc3889fada0103059e7d1f39acc9ff52699a9a802289a90b8b7e5c0022e334a37a91dee5965a0fdc1baa338ec64528e2697628571103d4e4ead3d613942e07c0dc563fc0f6b7ca03232d0f9fbb6637b5671298c76adfa604125c6f494b6ac117d5eeb950c408424a0bfe86b828927d849b85c8a7c609fee646fc309f8bb67dcba011177c41aac271ca028c4fffb65a3f7f4151f4c3d5957d6b82b7a16a1a97cfb4be7af50e1abcdf60da029f4c2a52f266da2c56cfd6b030f0f23ed448fb245f03e99e897be648b84c78280f90211a05ce5f36e7b7bc7fc75f54078b2e1120b66d684c612528dc65b5b1e7005bd572ba02a5b98498cf71626740e17dfe5afb0a82ebc57c44c16b0e448b6ff44d6fc8bdfa0e17e9f5405d79a718ece4bcb338e61872000c21dae7dd64b7842b337ab5fdb14a002b30b5afecee94adbff1cc236840dabbc18fb1da1456b38bf7850284b6174e6a05cc2e4dde3eff7e2c75c453c11b8fece1b41bdb6090ea8627d576b1e832db828a07709d58e2bba23879bdcd7b8aa05d6bb20545ac87eb48ba5057e9a626e92dfc5a0e24436fade0021cb2fc0a7fc6c73d811361643604897451125b079dd138a897ea045f9c86ac0e893cde57833c6f5ae09553b28ca1afafca8ae2b3845bc25b34beca0985e4bc4b513176bf0f35a322ed93deefbcb1adb352ae41da0963e8283f26806a0150a60420b74add256c71f8ab0b317eab14da37ab2cc457edb91356cdcdf7aeea022d2d32fc7127330dc81871391b0604d4b0ff8d3ca1b4efe9dcb37481ecb6d6ca0e5869aaa00519d2afb3ad359fe6410edb0303bd6858d1b53da17a17d09ee96faa084e0fd6bad4e858e10da00073b38b731765445c75e553eb35221e8fd3e90dc2aa0c1f00d927970581cec0dd7e1552a07783ea4ee9dc0e393731cce6f9efc37abc7a0a241a2b6f90d1d7a6d4725bb2ce0e5e72f095747eff9c54dca78be4e85c252faa0985f79232dfcb1258237e26f2cff97a92edd90d772dcfe72fd3bf2101f86707f80f90211a04ad5cd0ae4d11d2f376a69e078eae424cfaccb95768aa8006ddc7fbf13ca4f03a086a86f7be7185cbece6733a640066459721e099c007f44d8f8bb596335926210a018e98a8487ce49b95e495de2e13d9e5b968c3cdcd8053bef4fadfd3087d61718a0b371218bdcdc42277010bd5bd13b69cdc55b6b72f66c0856dfcdc1b9c75944b0a0646bac97dd32742dc5f3e1d4bfafa0c371f5c8f4fb02f2d14adfb0093ce76ddaa01361d43d0cd74ab9f91b4404e2bb78320d2c256bff7d4578f5bbdf94e3108987a0419e3065d7c205531f349910f9b5e6028e77b731c5cab0d92d6ff3182cf3bdf2a0ee0d0537c2bd7d441ef92dbaec11c670dfa0a45f9aa8b10964fcd2bec9b980d1a059644ea6cc3b6d8b9f2e86ed0f85fe01480420396b68ab84f4f7ab35e835209fa026b4189f6b97801198e60a5504c9e72e45b858d46f54a42ab813477332242f00a0b5105f1c69c24daa343d154aa76dde988ed45264d29af893285ab2634cbff9d6a0be4607a85ed4d39a5e49d0e26a41497261e203d4037dfcec76345874b326b3eaa053e4d03ab09d7d9828458531ee50e283c2e6c6044f97627993fdda9d745f430ca0c688ce84423b6e624ccebc3a1a88ed175177cc47e8f36269e9621ece69fcaa5ba0caa77f081255a2b5623aad5e762a0654411841c4d957b69f24e73c8be08bbdb1a04ce24402babac9914ebe645ff0bc66d34d50dd161f04701e4b099ea934b7b21780f90211a0660423b2d895f2aba3813807e74edac3fb9538266bdb42babe1e5dd25c4fd161a04150b5a2aa96406d0177153c353d16867cbd34a08aae35de53107a0a0b25658fa073c31b0b234014004029140c8dd63f8f281fe5ecb996cc38444efaf9f58a6b97a0da59ee8a17b488874e1949304dd3fdac7cb82c69a2c97365f0d6197696e2d054a049ab40adfe47ee2651086f0b722eb6f9bae40357438352fb34d9e6973c058c86a0a9fd2355826bcb94e5060af6ee6a7af1c60a2fecbab3bb809bea3c9b60b2b16ba026f23be81e08afdcfe85483c7fe217a7a391380e6f1e60078fe1d978b3b9fab4a06d60fbb088071b0f783972eda652a900f52cc85a81fe5f85243f7ed94143ba43a0910d149ab3be02efd52b2f07582acf8e1f80dc8565f7a0a6c659446369f2f3a2a09d6abd23e6aa8665b109e26ae13ef2f07a74bfb7d88d2de4e8a5faf2a48cecfea01f2e48683177e9da2910e7b523222b3cf5dc793caaf5c7f606a5395a0cf4af5ea0fc7d5dcc4a69a762dbd014cf35704f079673eabfc15e33bcfe453e35261e4258a048893be539b2cd2832d2ee746c1dce63979f29c2c3661e68cdfe477919fd4fcda08134f7abcbd812300070e34101cb818cd6a929b92825794df7687c13793e1798a0c9c310d767aaeaef86f7001d8c603aca2f3a4fbdd9fc997b42021328d59a9200a006ea99e6b8ce70f2d160744de1dc3eef851ffb16c2e907669f9b20e692049c7780f901d1a0dc46d5c082d2f2c80e0b5182cc2329ceec32b68584efb95ae474ab6996b16405a0cb3b5857ff3e418aa100a37fd76ea07f11e9d20a8a25de15e7c5cf047f9bb1efa09539eb3d48355167d413df7084c6e4e2d16249fae03a426d58a23240c5eb6050a0b52f82379833886dcf5bc370050b201a6ca9ae6f8349377f37581b0fbd83ccbfa02ead36534a30cf5639013c89cbf5f287d126226d3215c293448f2dd1bc86efeea0477bdca5ac4012c525920e4c48a221d90bdd7e095839d8521a975d39575ede04a0f4c68a529743bb06e731591dc32d22fe6bfa147b7afaab71a7b60f4dc122537ea01ae2e537cff6e6e3ae2489049e230cc5e3de39a0a6fd1e5ccf5d6363284b31ada056cbeef705e4a661cb1e829854e8f7c695dfa7522137924b65a582e6f30df837a0889deb18807278808e153de8ef29a518bb3352f91c6debd63d0126309bbe7e5c80a09c2c9d8df97055c3594088cdaa0d105ef463073b68c5f9ed317f280d00276545a0c91b1d73b891626ea90d6301139f21f8270555ad92bf6614d8eec619c51623c480a0bcb691927c8fe0073e0dde266e63976dfd449de4a827d29937f7f2d28b9798b4a0ca3a8558ef41a0ed62d11d38c907e439aa75f94ee490f98d135e0a5bbfc1e58f80f8718080a023b74a8c299a6e63cbd07b7b96f314cb3eb8b881efd645478cd1fb4a8ce6effca08f7176c73f5dfb1f19eab4fb00e33ed9541e40aee865cad1c7a291bdcf0600fb80808080808080a0915964c5f883d8ba02b81498e8ad3876f750fd3a19efb52f5553409a90f400fc8080808080f85180808080a0152357eb75eaa46b66aa3d777243bdee179a4c62ed78ecebe95e52611551d0ca8080a0851ce61f6802498c2096b2151bf87a67bdc89a105f75a4b2e7df77213e1f1e42808080808080808080f8669d2071b037cfc7f1b352780f45be6b97b02cca1ea9adf861c3a66e3f6953b846f8440180a042af08184d99bd2aa1b39b35e2764bda2d3a8690e7a8a15203db1c30f51fa350a04ab3ab54c1f8633aa6651140bc32f687c11f416b07ab8cedac30b8a9a01f7b11","blockHash":"0xa62c59f7a251084cdc91ddea474cbd004bd7313b8bef9bd9238420115b4c3a0c","blockHeaderRLP":"0xf9025ca05a1865fd898fdd7ba4652f12e1bc71bb3650badec9ebcbcc345f527897353803a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794670b24610df99b1685aeac0dfd5307b92e0cf4d7a027530029980a77dd08dcc982fe062d7c7c1479408e79ff58340be45806909f4ca02d74de20e58ff7fa9e7009f65f1091a54539b666041c8a62241ba47d9e935b80a07ca2ce0da426d57e7efc9686a5eb2b2831e8c04b1e08ee367616d7185600bab3b901000924129ccd48f0200411485287202a203048e5605363c0506008a9104488404241889004020424150082040b3234018844104510030264250022124e24248000502f410b1c46000a5408011f8030d801440089068104104c841018a6903012070a2440c70a01a81322222c0a5185092c800c00c90041080042201050118803050b6214000b842d216d006020020a00f2983044d1f22016040085010505485001228ca003340060560a40aa45000004a9094942b9c686010b08e0220222ca718581101a464001110238870201dcaa02194044000081018a8211775a22070865b12190a208458ee022094230402892dc0a808a99b2481022d004080d080021404580836a3b718401c9c38083a97fc184671f52f0914e65746865726d696e642d312e32392e31a013fd858a6063ad8bfe7125dcb9bca7b460cfba667716fcbdf84453415c3918fe88000000000000000085014a912a02a0e0ae2f315d9dfed0cd1b60580c6c7115721eef628f2b84b69df66f775ad34abf830c000083020000a0897a3d563c5a17bb4988089facb2dfb5eef7d385db64def7db91dfba1ee126c8","blockNumber":6962033,"checkpoint":{"fromBlockNumber":"6962032","votes":"100000000000000000"},"checkpointsSlot":"0x0000000000000000000000000000000000000000000000000000000000000011","checkpointsSlotHash":"0x7a18e34a98abd3f75f7fae004fd3e105ecbbe8069d15fd681ac333c6a9361dc2","numCheckpoints":1,"numCheckpointsSlot":"0x0000000000000000000000000000000000000000000000000000000000000010","numCheckpointsSlotHash":"0x3dbec2fe4782522edad30a15deeb9d7945f002ab48536039e7c5159f681d0130","xvsVaultAddress":"0x1129f882eAa912aE6D4f6D445b2E2b1eCbA99fd5","xvsVaultCheckpointsStorageProofRlp":"0xf90244f90211a06b13e146c561442f5920b14e0c951980e1c49792134a2f821cc66ef3ecf073e6a0ceb2d033e6f06dd212af278d3a1c262e16ebb70ef71cfa2601ba60a425b5ecf3a036e1c719852d320874025f801ce37c93acc8778f7239675b1c72459d7b930e67a068d03bce23cfdda3592d5b53c76a17b6f9c4c2a5416d20b05c6a4885773c5f2ba0cfe85b098ae712457e24637cf7ebdbbdbba54f20cbdaae7a66937dcfa5d1ffd3a0dce6d96764249dcde6972317b88f2030d5fb0ea6ded0c19eb7b3285c5731c4faa018291384f807f66b96a2865fb9575241f6e0dbcae05c2434919275a2f1016e10a0f87cf04cbe6a230538306e2df8fd748807b6a7133ed2927d9a017b1d54a46082a076fe24e853626849b26cdd63176eaf83ca0a8998426a4a4bb8b943d10bd48b38a0de9681719c686751a4700c73b75b9355405547b60396d82d0b384c8c9be0cacca044c58811a4c259cabeb868913e5d4ae85a02b22424fa6af60d6994c1ce259485a054eedfa0796a2f85b4b853146c92187494d9569bfc8a20ee0e0af5d6872bea82a033930ec3992aafbbcab7d6d4672efb9c10cca49c0dd3f104f4708efebe6fbaf1a0bb74af010cde0a9568239e7053b011d132d700b47cf07948f2de9b6a23a4c0c0a08e4256e246c746a690dae3dcd5befb78fa7972f5caae08089f937f000960cf12a0254817906244bcdac3379f661298ed4a7b44b53fd5ad836c37d30a712471c3b580efa03ca5c5f1f9d0578d08b165798fb3a2a1059bd2fcc284d792d71e57a8957197b78d8c016345785d8a0000006a3b70","xvsVaultNumCheckpointsStorageProofRlp":"0xf9033cf90211a06b13e146c561442f5920b14e0c951980e1c49792134a2f821cc66ef3ecf073e6a0ceb2d033e6f06dd212af278d3a1c262e16ebb70ef71cfa2601ba60a425b5ecf3a036e1c719852d320874025f801ce37c93acc8778f7239675b1c72459d7b930e67a068d03bce23cfdda3592d5b53c76a17b6f9c4c2a5416d20b05c6a4885773c5f2ba0cfe85b098ae712457e24637cf7ebdbbdbba54f20cbdaae7a66937dcfa5d1ffd3a0dce6d96764249dcde6972317b88f2030d5fb0ea6ded0c19eb7b3285c5731c4faa018291384f807f66b96a2865fb9575241f6e0dbcae05c2434919275a2f1016e10a0f87cf04cbe6a230538306e2df8fd748807b6a7133ed2927d9a017b1d54a46082a076fe24e853626849b26cdd63176eaf83ca0a8998426a4a4bb8b943d10bd48b38a0de9681719c686751a4700c73b75b9355405547b60396d82d0b384c8c9be0cacca044c58811a4c259cabeb868913e5d4ae85a02b22424fa6af60d6994c1ce259485a054eedfa0796a2f85b4b853146c92187494d9569bfc8a20ee0e0af5d6872bea82a033930ec3992aafbbcab7d6d4672efb9c10cca49c0dd3f104f4708efebe6fbaf1a0bb74af010cde0a9568239e7053b011d132d700b47cf07948f2de9b6a23a4c0c0a08e4256e246c746a690dae3dcd5befb78fa7972f5caae08089f937f000960cf12a0254817906244bcdac3379f661298ed4a7b44b53fd5ad836c37d30a712471c3b580f891a0c85136f04b3eacd0306912504cdf10ff91fc78ca6c1a2175f67cbae67c363e13a08207b298cd904700083965f169a6588c9eae2c35772d2074610b2a5e71d54f8e808080808080808080a070be13b403c36b215538a036d87ec87d8ea4f553b8f7ecb6a4355202b4e68bb2a02317eb09881e705352a221f27a0da7e922563414a8e88d8b88cfbaea1dee315380808080f87180808080a0bf9e3745641ec8321cab5c304d342a00cee6e036ef6f9f57c31e19d6379e1b8880808080808080a024d549fe7f4b14475328474853c3046bb5faaa1b4b180c2cf295241266e16c08a054739297fafd81d064ea90688cb00a1b92f83bb1b9430f53efccab07f51bf682808080e19f3230382301bc735e3d2fadbef69d56b6ebf99d47ba075f53929dadb7f7324001"} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3d286289..1b66dda6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3178,6 +3178,13 @@ __metadata: languageName: node linkType: hard +"@types/json-stable-stringify@npm:^1.0.36": + version: 1.0.36 + resolution: "@types/json-stable-stringify@npm:1.0.36" + checksum: 765b07589e11a3896c3d06bb9e3a9be681e7edd95adf27370df0647a91bd2bfcfaf0e091fd4a13729343b388973f73f7e789d6cc62ab988240518a2d27c4a4e2 + languageName: node + linkType: hard + "@types/lru-cache@npm:^5.1.0": version: 5.1.1 resolution: "@types/lru-cache@npm:5.1.1" @@ -3450,29 +3457,7 @@ __metadata: languageName: node linkType: hard -"@venusprotocol/governance-contracts@npm:^1.4.0": - version: 1.4.0 - resolution: "@venusprotocol/governance-contracts@npm:1.4.0" - dependencies: - "@venusprotocol/solidity-utilities": ^1.1.0 - hardhat-deploy-ethers: ^0.3.0-beta.13 - module-alias: ^2.2.2 - checksum: 85c6b6a815edb0befa4c38e3652a58464827d390620210b99575c16960ee6505e95e7c2192ebc972da7ed758d3c62e150d32fbdd1f01acab1731f29b11d1884e - languageName: node - linkType: hard - -"@venusprotocol/governance-contracts@npm:^2.0.0": - version: 2.3.0 - resolution: "@venusprotocol/governance-contracts@npm:2.3.0" - dependencies: - "@venusprotocol/solidity-utilities": 2.0.0 - hardhat-deploy-ethers: ^0.3.0-beta.13 - module-alias: ^2.2.2 - checksum: b7c6054d36f435e6360acbe5ef31816901377bffb119147a519c7aa93f8ad4205dfbb85deb706a5fbc37ab144dfdb42284f18c8e95bc729a780303c828221891 - languageName: node - linkType: hard - -"@venusprotocol/governance-contracts@workspace:.": +"@venusprotocol/governance-contracts@^2.0.0, @venusprotocol/governance-contracts@workspace:.": version: 0.0.0-use.local resolution: "@venusprotocol/governance-contracts@workspace:." dependencies: @@ -3493,7 +3478,7 @@ __metadata: "@nomicfoundation/hardhat-network-helpers": ^1.0.6 "@nomicfoundation/hardhat-toolbox": ^2.0.0 "@nomicfoundation/hardhat-verify": ^2.0.8 - "@nomiclabs/hardhat-ethers": ^2.2.1 + "@nomiclabs/hardhat-ethers": ^2.2.3 "@nomiclabs/hardhat-etherscan": ^3.1.2 "@openzeppelin/contracts": ^4.8.2 "@openzeppelin/contracts-upgradeable": ^4.8.2 @@ -3506,6 +3491,7 @@ __metadata: "@types/chai": ^4.3.4 "@types/debug": ^4.1.12 "@types/fs-extra": ^9.0.13 + "@types/json-stable-stringify": ^1.0.36 "@types/mocha": ^10.0.0 "@types/node": ^18.16.3 "@typescript-eslint/eslint-plugin": ^5.44.0 @@ -3529,6 +3515,7 @@ __metadata: hardhat-docgen: ^1.3.0 hardhat-gas-reporter: ^1.0.9 husky: ^8.0.3 + json-stable-stringify: ^1.1.1 lint-staged: ^13.0.4 lodash: ^4.17.21 mocha: ^10.1.0 @@ -3551,6 +3538,17 @@ __metadata: languageName: unknown linkType: soft +"@venusprotocol/governance-contracts@npm:^1.4.0": + version: 1.4.0 + resolution: "@venusprotocol/governance-contracts@npm:1.4.0" + dependencies: + "@venusprotocol/solidity-utilities": ^1.1.0 + hardhat-deploy-ethers: ^0.3.0-beta.13 + module-alias: ^2.2.2 + checksum: 85c6b6a815edb0befa4c38e3652a58464827d390620210b99575c16960ee6505e95e7c2192ebc972da7ed758d3c62e150d32fbdd1f01acab1731f29b11d1884e + languageName: node + linkType: hard + "@venusprotocol/protocol-reserve@npm:^1.4.0": version: 1.5.0 resolution: "@venusprotocol/protocol-reserve@npm:1.5.0" @@ -9862,6 +9860,18 @@ __metadata: languageName: node linkType: hard +"json-stable-stringify@npm:^1.1.1": + version: 1.1.1 + resolution: "json-stable-stringify@npm:1.1.1" + dependencies: + call-bind: ^1.0.5 + isarray: ^2.0.5 + jsonify: ^0.0.1 + object-keys: ^1.1.1 + checksum: e1ba06600fd278767eeff53f28e408e29c867e79abf564e7aadc3ce8f31f667258f8db278ef28831e45884dd687388fa1910f46e599fc19fb94c9afbbe3a4de8 + languageName: node + linkType: hard + "json-stringify-nice@npm:^1.1.4": version: 1.1.4 resolution: "json-stringify-nice@npm:1.1.4" @@ -9933,6 +9943,13 @@ __metadata: languageName: node linkType: hard +"jsonify@npm:^0.0.1": + version: 0.0.1 + resolution: "jsonify@npm:0.0.1" + checksum: 027287e1c0294fce15f18c0ff990cfc2318e7f01fb76515f784d5cd0784abfec6fc5c2355c3a2f2cb0ad7f4aa2f5b74ebbfe4e80476c35b2d13cabdb572e1134 + languageName: node + linkType: hard + "jsonparse@npm:^1.2.0, jsonparse@npm:^1.3.1": version: 1.3.1 resolution: "jsonparse@npm:1.3.1" From 3f299c402a98731b08ab3ec312ee646a006fc0cc Mon Sep 17 00:00:00 2001 From: GitGuru7 <128375421+GitGuru7@users.noreply.github.com> Date: Wed, 8 Jan 2025 20:36:35 +0530 Subject: [PATCH 5/6] [wip]: use lzread to get remote block hashes --- .../Voting/BlockHashDispatcher.sol | 5 + .../Voting/VotingPowerAggregator.sol | 190 +++++++++++++----- .../Governance/GovernorBravoDelegate.sol | 85 ++++++-- package.json | 2 + yarn.lock | 29 +++ 5 files changed, 250 insertions(+), 61 deletions(-) diff --git a/contracts/Cross-chain/Voting/BlockHashDispatcher.sol b/contracts/Cross-chain/Voting/BlockHashDispatcher.sol index 8cda3cb9..260b46f0 100644 --- a/contracts/Cross-chain/Voting/BlockHashDispatcher.sol +++ b/contracts/Cross-chain/Voting/BlockHashDispatcher.sol @@ -229,4 +229,9 @@ contract BlockHashDispatcher is Pausable { "access denied" ); } + + function getHash(uint256 blockTimestamp) external view returns (uint256, bytes32) { + bytes32 blockHash_ = blockhash(blockTimestamp); + return (blockTimestamp, blockHash_); + } } diff --git a/contracts/Cross-chain/Voting/VotingPowerAggregator.sol b/contracts/Cross-chain/Voting/VotingPowerAggregator.sol index c7c56a7a..a565f593 100644 --- a/contracts/Cross-chain/Voting/VotingPowerAggregator.sol +++ b/contracts/Cross-chain/Voting/VotingPowerAggregator.sol @@ -10,7 +10,16 @@ import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contract import { Pausable } from "@openzeppelin/contracts/security/Pausable.sol"; -contract VotingPowerAggregator is NonblockingLzApp, Pausable { +import { BlockHashDispatcher } from "./BlockHashDispatcher.sol"; + +import { MessagingFee, Origin } from "@layerzerolabs/oapp-evm/contracts/oapp/OApp.sol"; +import { OAppRead } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppRead.sol"; +import { MessagingReceipt } from "@layerzerolabs/oapp-evm/contracts/oapp/OAppSender.sol"; +import { IOAppMapper } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppMapper.sol"; +import { IOAppReducer } from "@layerzerolabs/oapp-evm/contracts/oapp/interfaces/IOAppReducer.sol"; +import { ReadCodecV1, EVMCallComputeV1, EVMCallRequestV1 } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/ReadCodecV1.sol"; + +contract VotingPowerAggregator is NonblockingLzApp, Pausable, OAppRead { using ExcessivelySafeCall for address; struct Proofs { @@ -19,15 +28,32 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { bytes checkpointsProof; } + enum Status { + REJECTED, + PENDING, + ACTIVE + } + uint8 public constant CHECKPOINTS_SLOT = 16; uint8 public constant NUM_CHECKPOINTS_SLOT = 17; IDataWarehouse public warehouse; + address public governanceBravo; + mapping(uint256 => uint16[]) private requiredChainIds; + mapping(uint256 => uint16[]) public remoteChainIds; + // block timestamp => proposal ID + mapping(uint256 => uint256) public proposalId; + + mapping(uint256 => bytes[]) private remoteBlockHeaders; + + // blockTimestamp => proof + mapping(uint256 => Proofs[]) public proposerVotingProof; + // containing deactivation record of chain id mapping(uint16 => bool) public isdeactived; - // remote identifier => chainId => blockHash - mapping(uint256 => mapping(uint16 => bytes32)) public remoteBlockHash; + // chainId -> block number -> block hash + mapping(uint16 => mapping(uint256 => bytes32)) public remoteBlockHash; // Address of XVS vault corresponding to remote chain Id mapping(uint16 => address) public xvsVault; @@ -62,9 +88,11 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { */ error DeactivatedChainId(uint16 chainId); - constructor(address endpoint, address warehouseAddress) NonblockingLzApp(endpoint) { + constructor(address endpoint, address warehouseAddress, address bravo) NonblockingLzApp(endpoint) { ensureNonzeroAddress(warehouseAddress); + ensureNonzeroAddress(bravo); warehouse = IDataWarehouse(warehouseAddress); + governanceBravo = bravo; } /** @@ -126,10 +154,73 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { } } + function getRemoteBlockHashes( + uint16[] remoteChainId, + uint16 appLabel, + Proofs[] proposerVotingProofs, + bytes[] memory remoteBlockheader + ) external { + uint32 channelId = getChannelId(); + EVMReadRequest[] requests = getRequests(remoteChainIds); + EVMComputeRequest computeRequest = getComputeRequests(remoteChainIds); + bytes calldata options = getOptions(); + + // decode blockTimestamp from remoteBlockHeader + for (uint256 i; i < remoteBlockHeader.length; i++) { + blockTimestamp = decode(remoteBlockheader); + requiredChainIds[blockTimestamp].push(remoteChainIds); + remoteBlockHeaders[blockTimestamp] = remoteBlockHeader; + } + remoteChainIds[blockTimestamp] = remoteChainId; + + proposerVotingProof[tx.origin] = proposerVotingProofs; + bytes memory cmd = buildCmd(_appLabel, _requests, _computeRequest); + receipt = _lzSend(_channelId, cmd, _options, MessagingFee(msg.value, 0), payable(tx.origin)); + } + + /** + * @notice Builds the command to be sent + * @param appLabel The application label to use for the message. + * @param _readRequests An array of `EvmReadRequest` structs containing the read requests to be made. + * @param _computeRequest A `EvmComputeRequest` struct containing the compute request to be made. + * @return cmd The encoded command to be sent to to the channel. + */ + function buildCmd( + uint16 appLabel, + EvmReadRequestV1[] memory _readRequests, + EvmComputeRequest memory _computeRequest + ) public pure returns (bytes memory) { + require(_readRequests.length > 0, "LzReadCounter: empty requests"); + // build read requests + EVMCallRequestV1[] memory readRequests = new EVMCallRequestV1[](_readRequests.length); + for (uint256 i = 0; i < _readRequests.length; i++) { + EvmReadRequestV1 memory req = _readRequests[i]; + readRequests[i] = EVMCallRequestV1({ + appRequestLabel: req.appRequestLabel, + targetEid: req.targetEid, + isBlockNum: req.isBlockNum, + blockNumOrTimestamp: req.blockNumOrTimestamp, + confirmations: req.confirmations, + to: req.to, + callData: abi.encodeWithSelector(this.myInformation.selector) + }); + } + require(_computeRequest.computeSetting <= COMPUTE_SETTING_NONE, "LzReadCounter: invalid compute type"); + EVMCallComputeV1 memory evmCompute = EVMCallComputeV1({ + computeSetting: _computeRequest.computeSetting, + targetEid: _computeRequest.computeSetting == COMPUTE_SETTING_NONE ? 0 : _computeRequest.targetEid, + isBlockNum: _computeRequest.isBlockNum, + blockNumOrTimestamp: _computeRequest.blockNumOrTimestamp, + confirmations: _computeRequest.confirmations, + to: _computeRequest.to + }); + bytes memory cmd = ReadCodecV1.encode(appLabel, readRequests, evmCompute); + return cmd; + } + /** * @notice Calculates the total voting power of a voter across multiple remote chains * @param voter The address of the voter for whom to calculate the voting power - * @param remoteIdentifier The identifier that links to remote chain-specific data * @param proofs Array of proofs containing remote chain id with their corresponding proofs (numCheckpointsProof, checkpointsProof) where * numCheckpointsProof is the proof data needed to verify the number of checkpoints and * checkpointsProof is the proof data needed to verify the actual voting power from the checkpoints @@ -137,7 +228,7 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { */ function getVotingPower( address voter, - uint256 remoteIdentifier, + uint256 blockTimestamp, Proofs[] calldata proofs ) external view returns (uint96 power) { uint96 totalVotingPower; @@ -148,7 +239,7 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { } totalVotingPower += _getVotingPower( proofs[i].remoteChainId, - remoteIdentifier, + blockTimestamp, proofs[i].numCheckpointsProof, proofs[i].checkpointsProof, voter @@ -203,50 +294,53 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable { } /** - * @notice Process blocking LayerZero receive request - * @param remoteChainId Remote chain Id - * @param remoteAddress Remote address from which payload is received - * @param nonce Nonce associated with the payload to prevent replay attacks - * @param payload Encoded payload containing block hash and remote identifier of remote chains - * @custom:event Emit ReceivePayloadFailed if call fails + * @dev Internal function override to handle incoming messages from another chain. + * @param payload The encoded message payload being received. This is the resolved command from the DVN + * + * @dev The following params are unused in the current implementation of the OApp. + * @dev _origin A struct containing information about the message sender. + * @dev _guid A unique global packet identifier for the message. + * @dev _executor The address of the Executor responsible for processing the message. + * @dev _extraData Arbitrary data appended by the Executor to the message. + * + * Decodes the received payload and processes it as per the business logic defined in the function. */ - function _blockingLzReceive( - uint16 remoteChainId, - bytes memory remoteAddress, - uint64 nonce, - bytes memory payload + function _lzReceive( + Origin calldata origin, + bytes32 /*_guid*/, + bytes calldata payload, + address /*_executor*/, + bytes calldata /*_extraData*/ ) internal override { - bytes32 hashedPayload = keccak256(payload); - bytes memory callData = abi.encodeCall( - this.nonblockingLzReceive, - (remoteChainId, remoteAddress, nonce, payload) - ); - - (bool success, bytes memory reason) = address(this).excessivelySafeCall(gasleft() - 30000, 150, callData); - // try-catch all errors/exceptions - if (!success) { - failedMessages[remoteChainId][remoteAddress][nonce] = hashedPayload; - emit ReceivePayloadFailed(remoteChainId, remoteAddress, nonce, reason); // Retrieve payload from the remote chain if needed to clear - } - } - - /** - * @notice Process non blocking LayerZero receive request - * @param payload Encoded payload containing block hash and remote identifier of remote chains - * @custom:event Emit ProposalReceived - */ - function _nonblockingLzReceive( - uint16 remoteChainId, - bytes memory, - uint64, - bytes memory payload - ) internal override whenNotPaused { - // remoteIdentifier is unique identifier generated at the time of createProposal - (uint256 remoteIdentifier, bytes memory blockHash) = abi.decode(payload, (uint256, bytes)); + uint256 status; + (uint256 blockTimestamp, bytes memory blockHash) = abi.decode(payload); + remoteBlockHash[origin.srcEid][blockTimestamp] = blockHash; + requiredChainIds[blockTimestamp].remove(origin.srcEid); + + // Once all the block hashes has been synced + if (requiredChainIds[blockTimestamp].length == 0) { + // verify all block hashes + for (uint256 i; i < remoteBlockheaders[blockTimestamp].length; i++) { + bytes memory decodedBlockHash = abi.decode(remoteBlockheaders[blockTimestamp][i]); + // compare with the block hashes got from above steps + uint chainId = remoteChainIds[blockTimestamp]; + if (decodedBlockHash != remoteBlockHash[chainId][blockTimestamp]) { + status = Status.REJECTED; + break; + } + } - // Prevent Overriding hash - require(remoteBlockHash[remoteIdentifier][remoteChainId] == bytes32(0), "block hash already exists"); + if (proposerVotingProof[blockTimestamp][0].remoteChainId != 0 && status != Status.REJECTED) { + uint96 votes = getVotingPower(msg.sender, blocknumber, proposerVotingProof); + // compare proposer votes with the threshold + if (votes > threshold) { + status = Status.ACTIVE; + } else { + status = Status.PENDING; + } + } - emit HashReceived(remoteIdentifier, remoteChainId, blockHash); + IGovernanceBravoDelegate(governanceBravo).InternalActivateProposal(proposalId[blockTimestamp], status); + } } } diff --git a/contracts/Governance/GovernorBravoDelegate.sol b/contracts/Governance/GovernorBravoDelegate.sol index 71694239..3458ee95 100644 --- a/contracts/Governance/GovernorBravoDelegate.sol +++ b/contracts/Governance/GovernorBravoDelegate.sol @@ -102,6 +102,22 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE /// @notice The EIP-712 typehash for the ballot struct used by the contract bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)"); + // It will be removed and instead of this we will add one more state + uint256 bridgeStatus; + + address public votingPowerAggregator; + + struct VotingProof { + uint16 chainId; + bytes numCheckPointproof; + bytes checkpointProof; + } + struct SyncingParams { + uint16 remoteIds; + bytes proofs; + bytes remoteBlockheaders; // block hash, stateRoot, timestamp, blockNumber + } + /** * @notice Used to initialize the contract during delegator contructor * @param xvsVault_ The address of the XvsVault @@ -110,6 +126,7 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE */ function initialize( address xvsVault_, + address votingPowerAggregator_, ProposalConfig[] memory proposalConfigs_, TimelockInterface[] memory timelocks, address guardian_ @@ -118,6 +135,7 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE require(msg.sender == admin, "GovernorBravo::initialize: admin only"); require(xvsVault_ != address(0), "GovernorBravo::initialize: invalid xvs address"); require(guardian_ != address(0), "GovernorBravo::initialize: invalid guardian"); + require(votingPowerAggregator_ != address(0), "GovernorBravo::initialize: invalid votingPowerAggregator"); require( timelocks.length == uint8(ProposalType.CRITICAL) + 1, "GovernorBravo::initialize:number of timelocks should match number of governance routes" @@ -130,6 +148,7 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE xvsVault = XvsVaultInterface(xvsVault_); proposalMaxOperations = 10; guardian = guardian_; + votingPowerAggregator = votingPowerAggregator_; //Set parameters for each Governance Route uint256 arrLength = proposalConfigs_.length; @@ -178,21 +197,22 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE * @param proposalType the type of the proposal (e.g NORMAL, FASTTRACK, CRITICAL) * @return Proposal id of new proposal */ - function propose( + function createProposal( address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description, - ProposalType proposalType - ) public returns (uint) { + ProposalType proposalType, + address proposer, + VotingProof[] memory proposerVotingProof, + uint16[] memory remoteChainIds, + bytes[] memory proofs, + bytes[] memory remoteBlockheaders + ) public payable returns (uint) { // Reject proposals before initiating as Governor require(initialProposalId != 0, "GovernorBravo::propose: Governor Bravo not active"); - require( - xvsVault.getPriorVotes(msg.sender, sub256(block.number, 1)) >= - proposalConfigs[uint8(proposalType)].proposalThreshold, - "GovernorBravo::propose: proposer votes below proposal threshold" - ); + require( targets.length == values.length && targets.length == signatures.length && @@ -202,7 +222,7 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE require(targets.length != 0, "GovernorBravo::propose: must provide actions"); require(targets.length <= proposalMaxOperations, "GovernorBravo::propose: too many actions"); - uint latestProposalId = latestProposalIds[msg.sender]; + uint latestProposalId = latestProposalIds[proposer]; if (latestProposalId != 0) { ProposalState proposersLatestProposalState = state(latestProposalId); require( @@ -215,13 +235,23 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE ); } - uint startBlock = add256(block.number, proposalConfigs[uint8(proposalType)].votingDelay); - uint endBlock = add256(startBlock, proposalConfigs[uint8(proposalType)].votingPeriod); + // Verify remoteBlockheaders + // TO DO proposalCount++; + // Fetch all block hashes + IVotingPowerAggregator(votingPowerAggregator).getRemoteBlockHashes.value(msg.value)( + remoteChainIds, + proposalCount, + proposerVotingProof + ); + + uint startBlock = 0; + uint endBlock = 0; + Proposal memory newProposal = Proposal({ id: proposalCount, - proposer: msg.sender, + proposer: proposer, eta: 0, targets: targets, values: values, @@ -242,7 +272,7 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE emit ProposalCreated( newProposal.id, - msg.sender, + proposer, targets, values, signatures, @@ -255,6 +285,34 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE return newProposal.id; } + // due to different msg.sender we have to make another activate proposal function + function InternalActivateProposal(uint256 proposalId, uint256 status) external { + require(msg.sender == votingPowerAggregator, "Invalid owner"); + + bridgeStatus = status; + // If proposal status is Active + if (status == 3) { + Proposal[proposalId].startBlock = block.number; + proposal[proposalId].endBlock = block.number + delayBlocks; + } + } + + function ActivateProposalByProposer(uint256 proposalId, VotingProof[] memory proposerVotingProof) public { + require(bridgeStatus == 1, "Invalid proposal"); + + if (proposerVotingProof[0].remoteIds != 0) { + uint96 votes = IVotingPowerAggregator(votingPowerAggregator).getVotingPower( + msg.sender, + blocknumber, + proposerVotingProof + ); + // comare proposer votes with the threshold + require(votes > threshold, "Insufficient proposer's balance"); + } + Proposal[proposalId].startBlock = block.number; + proposal[proposalId].endBlock = block.number + delayBlocks; + } + /** * @notice Queues a proposal of state succeeded * @param proposalId The id of the proposal to queue @@ -389,6 +447,7 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE Proposal storage proposal = proposals[proposalId]; if (proposal.canceled) { return ProposalState.Canceled; + // If not activated yet return expired } else if (block.number <= proposal.startBlock) { return ProposalState.Pending; } else if (block.number <= proposal.endBlock) { diff --git a/package.json b/package.json index c764fc14..726ce427 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,8 @@ "clean": "hardhat clean && hardhat clean --config hardhat.config.zksync.ts" }, "dependencies": { + "@layerzerolabs/lz-evm-protocol-v2": "^3.0.38", + "@layerzerolabs/oapp-evm": "^0.3.0", "@venusprotocol/solidity-utilities": "2.0.0", "hardhat-deploy-ethers": "^0.3.0-beta.13", "module-alias": "^2.2.2" diff --git a/yarn.lock b/yarn.lock index 1b66dda6..140c32a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1056,6 +1056,18 @@ __metadata: languageName: node linkType: hard +"@layerzerolabs/lz-evm-protocol-v2@npm:^3.0.38": + version: 3.0.38 + resolution: "@layerzerolabs/lz-evm-protocol-v2@npm:3.0.38" + peerDependencies: + "@openzeppelin/contracts": ^4.8.1 || ^5.0.0 + "@openzeppelin/contracts-upgradeable": ^4.8.1 || ^5.0.0 + hardhat-deploy: ^0.12.4 + solidity-bytes-utils: ^0.8.0 + checksum: d2560a745fb96c57f93fd083ffafa9f9640fb0c39a31c42d15eb0c095329764bb541c64cb388dfdbb115d628debdfa4fa1ac70198094809c00bc5bcca8ad5235 + languageName: node + linkType: hard + "@layerzerolabs/lz-evm-sdk-v1-0.7@npm:^1.5.14": version: 1.5.16 resolution: "@layerzerolabs/lz-evm-sdk-v1-0.7@npm:1.5.16" @@ -1066,6 +1078,21 @@ __metadata: languageName: node linkType: hard +"@layerzerolabs/oapp-evm@npm:^0.3.0": + version: 0.3.0 + resolution: "@layerzerolabs/oapp-evm@npm:0.3.0" + dependencies: + ethers: ^5.7.2 + peerDependencies: + "@layerzerolabs/lz-evm-messagelib-v2": ^3.0.12 + "@layerzerolabs/lz-evm-protocol-v2": ^3.0.12 + "@layerzerolabs/lz-evm-v1-0.7": ^3.0.12 + "@openzeppelin/contracts": ^4.8.1 || ^5.0.0 + "@openzeppelin/contracts-upgradeable": ^4.8.1 || ^5.0.0 + checksum: 308751307f4082818f6607be0f99842788a2d889a2570e5365d5d46ee407f2404128458051928ddb1af9e485ebc01c4b60fe15b7fb12abc77378f8780a5073ba + languageName: node + linkType: hard + "@layerzerolabs/solidity-examples@npm:^1.0.0": version: 1.1.0 resolution: "@layerzerolabs/solidity-examples@npm:1.1.0" @@ -3469,6 +3496,8 @@ __metadata: "@ethersproject/bignumber": ^5.7.0 "@ethersproject/bytes": ^5.7.0 "@ethersproject/providers": ^5.7.2 + "@layerzerolabs/lz-evm-protocol-v2": ^3.0.38 + "@layerzerolabs/oapp-evm": ^0.3.0 "@layerzerolabs/solidity-examples": ^1.0.0 "@matterlabs/hardhat-zksync": ^0.2.0 "@matterlabs/hardhat-zksync-deploy": ^0.11.0 From f05ece088f0ecce9d78a0fd05078be927c026005 Mon Sep 17 00:00:00 2001 From: GitGuru7 Date: Fri, 10 Jan 2025 19:55:22 +0530 Subject: [PATCH 6/6] [wip]: add PendingSync state --- .../Voting/BlockHashDispatcher.sol | 23 ++-- .../Cross-chain/Voting/DataWarehouse.sol | 13 +- .../Voting/VotingPowerAggregator.sol | 126 +++++++++++------- .../Cross-chain/interfaces/IDataWarehouse.sol | 2 +- .../Governance/GovernorBravoDelegate.sol | 99 ++++++++------ 5 files changed, 157 insertions(+), 106 deletions(-) diff --git a/contracts/Cross-chain/Voting/BlockHashDispatcher.sol b/contracts/Cross-chain/Voting/BlockHashDispatcher.sol index 260b46f0..caad83b7 100644 --- a/contracts/Cross-chain/Voting/BlockHashDispatcher.sol +++ b/contracts/Cross-chain/Voting/BlockHashDispatcher.sol @@ -138,20 +138,21 @@ contract BlockHashDispatcher is Pausable { /** * @notice Dispatches a block hash along with a remote identifier to proposal chain(BNB) - * @param remoteIdentifier A unique identifier to identify the proposal + * @param pId A unique identifier to identify the proposal * @param zroPaymentAddress The address for payment using ZRO tokens * @param adapterParams The params used to specify the custom amount of gas required for the execution on the destination */ function dispatchHash( - uint256 remoteIdentifier, + uint256 pId, + uint256 blockNumber, address zroPaymentAddress, bytes calldata adapterParams ) external payable { // Send hash only once for each unique identifier - require(blockHash[remoteIdentifier] == bytes32(0), "block hash already exists"); + require(blockHash[pId] == bytes32(0), "block hash already exists"); - bytes32 blockHash_ = blockhash(block.number - 1); - bytes memory payload = abi.encode(remoteIdentifier, blockHash_); + bytes32 blockHash_ = blockhash(blockNumber); + bytes memory payload = abi.encode(pId, blockHash_); uint16 proposalChainId_ = proposalChainId; bytes memory trustedRemote = trustedRemoteLookup[proposalChainId_]; require(trustedRemote.length != 0, "proposal chain id is not set"); @@ -169,10 +170,10 @@ contract BlockHashDispatcher is Pausable { adapterParams ) { - emit HashDispatched(remoteIdentifier, payload); + emit HashDispatched(pId, payload); } catch (bytes memory reason) { - storedExecutionHashes[remoteIdentifier] = keccak256(abi.encode(payload, adapterParams, msg.value)); - emit HashStored(remoteIdentifier, payload, adapterParams, msg.value, reason); + storedExecutionHashes[pId] = keccak256(abi.encode(payload, adapterParams, msg.value)); + emit HashStored(pId, payload, adapterParams, msg.value, reason); } } @@ -230,8 +231,8 @@ contract BlockHashDispatcher is Pausable { ); } - function getHash(uint256 blockTimestamp) external view returns (uint256, bytes32) { - bytes32 blockHash_ = blockhash(blockTimestamp); - return (blockTimestamp, blockHash_); + function getHash(uint256 blockNumber, uint256 pId) external view returns (uint256, uint256, bytes32) { + bytes32 blockHash_ = blockhash(blockNumber); + return (pId, blockNumber, blockHash_); } } diff --git a/contracts/Cross-chain/Voting/DataWarehouse.sol b/contracts/Cross-chain/Voting/DataWarehouse.sol index 973bd2cb..f33e4d12 100644 --- a/contracts/Cross-chain/Voting/DataWarehouse.sol +++ b/contracts/Cross-chain/Voting/DataWarehouse.sol @@ -14,6 +14,7 @@ contract DataWarehouse is IDataWarehouse { using RLPReader for bytes; using RLPReader for RLPReader.RLPItem; + uint256 public delay; // account address => (block hash => Account state root hash) mapping(address => mapping(bytes32 => bytes32)) internal _storageRoots; @@ -30,17 +31,25 @@ contract DataWarehouse is IDataWarehouse { return _slotsRegistered[account][blockHash][slot]; } + function setDelay(uint256 _delay) external onlyOwner { + delay = _delay; + } + /// @inheritdoc IDataWarehouse function processStorageRoot( address account, bytes32 blockHash, bytes memory blockHeaderRLP, bytes memory accountStateProofRLP - ) external returns (bytes32) { + ) external returns (StateProofVerifier.BlockHeader memory) { StateProofVerifier.BlockHeader memory decodedHeader = StateProofVerifier.verifyBlockHeader( blockHeaderRLP, blockHash ); + + // Verifies blockTimestamp + require(decodedHeader.blockTimestamp < block.timestamp + delay && decoded.blockTimestamp > block.timestamp + delay, "Invalid block timestamp"); + // The path for an account in the state trie is the hash of its address bytes32 proofPath = keccak256(abi.encodePacked(account)); StateProofVerifier.Account memory accountData = StateProofVerifier.extractAccountFromProof( @@ -53,7 +62,7 @@ contract DataWarehouse is IDataWarehouse { emit StorageRootProcessed(msg.sender, account, blockHash); - return accountData.storageRoot; + return decodedHeader; } /// @inheritdoc IDataWarehouse diff --git a/contracts/Cross-chain/Voting/VotingPowerAggregator.sol b/contracts/Cross-chain/Voting/VotingPowerAggregator.sol index a565f593..3e3e206e 100644 --- a/contracts/Cross-chain/Voting/VotingPowerAggregator.sol +++ b/contracts/Cross-chain/Voting/VotingPowerAggregator.sol @@ -28,23 +28,28 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable, OAppRead { bytes checkpointsProof; } - enum Status { + enum BridgeStatus { REJECTED, PENDING, - ACTIVE + SYNCED } uint8 public constant CHECKPOINTS_SLOT = 16; uint8 public constant NUM_CHECKPOINTS_SLOT = 17; IDataWarehouse public warehouse; + uint256 public status; + + /// @notice The minimum setable voting period + uint public constant MIN_VOTING_PERIOD = 20 * 60 * 3; // About 3 hours, 3 secs per block + + /// @notice The max setable voting period + uint public constant MAX_VOTING_PERIOD = 20 * 60 * 24 * 14; // About 2 weeks, 3 secs per block + address public governanceBravo; mapping(uint256 => uint16[]) private requiredChainIds; mapping(uint256 => uint16[]) public remoteChainIds; - // block timestamp => proposal ID - mapping(uint256 => uint256) public proposalId; - - mapping(uint256 => bytes[]) private remoteBlockHeaders; + mapping(uint16 => address) public proposer; // blockTimestamp => proof mapping(uint256 => Proofs[]) public proposerVotingProof; @@ -53,11 +58,17 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable, OAppRead { mapping(uint16 => bool) public isdeactived; // chainId -> block number -> block hash - mapping(uint16 => mapping(uint256 => bytes32)) public remoteBlockHash; + mapping(uint256 => mapping(uint16 => mapping(uint256 => bytes32))) public remoteBlockHash; // Address of XVS vault corresponding to remote chain Id mapping(uint16 => address) public xvsVault; + // proposalId => remoteChainId => blockNumber + mapping(uint256 => mapping(uint16 => uint256)) public remoteBlockNumber; + + // pId => remoteChainId => decodedHeader + mapping(uint256 => mapping(uint16 => StateProofVerifier.BlockHeader)) decodedBlockHeader; + /** * @notice Emitted when proposal failed */ @@ -153,29 +164,44 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable, OAppRead { emit UpdateDeactivatedRemoteChainId(remoteChainId[i], isDeactivated[i]); } } - - function getRemoteBlockHashes( - uint16[] remoteChainId, - uint16 appLabel, - Proofs[] proposerVotingProofs, - bytes[] memory remoteBlockheader - ) external { - uint32 channelId = getChannelId(); - EVMReadRequest[] requests = getRequests(remoteChainIds); - EVMComputeRequest computeRequest = getComputeRequests(remoteChainIds); - bytes calldata options = getOptions(); - - // decode blockTimestamp from remoteBlockHeader - for (uint256 i; i < remoteBlockHeader.length; i++) { - blockTimestamp = decode(remoteBlockheader); - requiredChainIds[blockTimestamp].push(remoteChainIds); - remoteBlockHeaders[blockTimestamp] = remoteBlockHeader; + function startVotingPowerSync( + uint256 pId, + uint16[] memory remoteChainId, + bytes[] memory blockHash, + bytes[] memory remoteBlockheaderRLP, + bytes[] memory xvsVaultStateProofRLP, + Proofs[] memory proposerVotingProofs + ) external { + // Verify headers + for(uint256 i; i < remoteChainId.length; i++ ){ + StateProofVerifier.BlockHeader memory decodedHeader = warehouse.processStorageRoot(xvsVault[remoteChainId[i]], blockHash[i], remoteBlockHeaderRLP[i], xvsVaultStateProofRLP[i]); + decodedBlockHeader[pId][remoteChainId] = decodedHeader; } - remoteChainIds[blockTimestamp] = remoteChainId; + proposer[pId] = tx.origin; + _startVotingPowerSync(pId, remoteChainId, blockHash, remoteBlockheaderRLP, xvsVaultStateProofRLP, proposerVotingProofs ); + } - proposerVotingProof[tx.origin] = proposerVotingProofs; - bytes memory cmd = buildCmd(_appLabel, _requests, _computeRequest); - receipt = _lzSend(_channelId, cmd, _options, MessagingFee(msg.value, 0), payable(tx.origin)); + function _startVotingPowerSync( + uint256 pId, + uint16[] memory remoteChainId, + bytes[] memory blockHash, + bytes[] memory remoteBlockheaderRLP, + bytes[] memory xvsVaultStateProofRLP, + Proofs[] memory proposerVotingProofs + ) internal { + remoteChainIds[pId] = remoteChainId; + requiredChainIds[pId] = remoteChainIds; + + for(uint256 i; i < remoteChainId.length; i++){ + uint32 channelId = getChannelId(remoteChainId[i]); + EVMReadRequest[] requests = getRequests(remoteChainId[i]); + EVMComputeRequest computeRequest = getComputeRequests(remoteChainId[i]); + bytes calldata options = getOptions(remoteChainId[i]); + blockNumber[pId][remoteChainId[i]] = decodedHeader.blockNumber; + proposerVotingProof[proposer] = proposerVotingProofs; + bytes memory cmd = buildCmd(_appLabel, _requests, _computeRequest); + receipt = _lzSend(_channelId, cmd, _options, MessagingFee(msg.value, 0), payable(tx.origin)); + } } /** @@ -228,7 +254,7 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable, OAppRead { */ function getVotingPower( address voter, - uint256 blockTimestamp, + uint256 pId, Proofs[] calldata proofs ) external view returns (uint96 power) { uint96 totalVotingPower; @@ -239,7 +265,7 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable, OAppRead { } totalVotingPower += _getVotingPower( proofs[i].remoteChainId, - blockTimestamp, + pId, proofs[i].numCheckpointsProof, proofs[i].checkpointsProof, voter @@ -252,19 +278,20 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable, OAppRead { /** * @dev Calculates the total voting power of a voter for a remote chain * @param voter The address of the voter for whom to calculate the voting power - * @param remoteIdentifier The identifier that links to remote chain-specific data + * @param pId The identifier that links to remote chain-specific data * @param numCheckpointsProof The proof data needed to verify the number of checkpoints * @param checkpointsProof The proof data needed to verify the actual voting power from the checkpoints * @return power The total voting power of supported remote chains */ function _getVotingPower( uint16 remoteChainId, - uint256 remoteIdentifier, + uint256 pId, bytes calldata numCheckpointsProof, bytes calldata checkpointsProof, address voter ) internal view returns (uint96) { - bytes32 blockHash = remoteBlockHash[remoteIdentifier][remoteChainId]; + uint256 blockNumber = remoteBlockNumber[pId][remoteChainId]; + bytes32 blockHash = remoteBlockHash[pId][remoteChainId][blockNumber]; address vault = xvsVault[remoteChainId]; StateProofVerifier.SlotValue memory latestCheckpoint = warehouse.getStorage( @@ -312,35 +339,36 @@ contract VotingPowerAggregator is NonblockingLzApp, Pausable, OAppRead { address /*_executor*/, bytes calldata /*_extraData*/ ) internal override { - uint256 status; - (uint256 blockTimestamp, bytes memory blockHash) = abi.decode(payload); - remoteBlockHash[origin.srcEid][blockTimestamp] = blockHash; - requiredChainIds[blockTimestamp].remove(origin.srcEid); + (uint256 pId, uint256 blockNumber, bytes memory blockHash) = abi.decode(payload); + require(remoteBlockNumber[pId][origin.srcEid] == blockNumber, "Invalid blockNumber"); + remoteBlockHash[pId][origin.srcEid][blockNumber] = blockHash; + requiredChainIds[pId].remove(origin.srcEid); // Once all the block hashes has been synced - if (requiredChainIds[blockTimestamp].length == 0) { + if (requiredChainIds[pId].length == 0) { // verify all block hashes - for (uint256 i; i < remoteBlockheaders[blockTimestamp].length; i++) { - bytes memory decodedBlockHash = abi.decode(remoteBlockheaders[blockTimestamp][i]); + for (uint256 i; i < decodedBlockHeader[pId][remoteChainIds].length; i++) { + bytes memory decodedBlockHash = decodedBlockHeader[pId][remoteChainIds].blockHash ; // compare with the block hashes got from above steps - uint chainId = remoteChainIds[blockTimestamp]; - if (decodedBlockHash != remoteBlockHash[chainId][blockTimestamp]) { - status = Status.REJECTED; + uint chainId = remoteChainIds[i]; + uint256 _blockNumber = remoteBlockNumber[pId][origin.srcEid]; + if(decodedBlockHash != remoteBlockHash[pId][chainId][_blockNumber]) { + status = BridgeStatus.REJECTED; break; } } - if (proposerVotingProof[blockTimestamp][0].remoteChainId != 0 && status != Status.REJECTED) { - uint96 votes = getVotingPower(msg.sender, blocknumber, proposerVotingProof); + if (proposerVotingProof[pId][0].remoteChainId != 0 && status != BridgeStatus.REJECTED) { + uint96 votes = getVotingPower(proposer[pId], blocknumber, proposerVotingProof); // compare proposer votes with the threshold - if (votes > threshold) { - status = Status.ACTIVE; + if (votes > MIN_VOTING_PERIOD && votes < MAX_VOTING_PERIOD) { + status = BridgeStatus.SYNCED; } else { - status = Status.PENDING; + status = BridgeStatus.PENDING; } } - IGovernanceBravoDelegate(governanceBravo).InternalActivateProposal(proposalId[blockTimestamp], status); + IGovernanceBravoDelegate(governanceBravo).activateProposal(pId, status, proposer[pId]); } } } diff --git a/contracts/Cross-chain/interfaces/IDataWarehouse.sol b/contracts/Cross-chain/interfaces/IDataWarehouse.sol index fcf4c076..0738871f 100644 --- a/contracts/Cross-chain/interfaces/IDataWarehouse.sol +++ b/contracts/Cross-chain/interfaces/IDataWarehouse.sol @@ -54,7 +54,7 @@ interface IDataWarehouse { bytes32 blockHash, bytes memory blockHeaderRLP, bytes memory accountStateProofRLP - ) external returns (bytes32); + ) external returns (StateProofVerifier.BlockHeader memory); /** * @notice method to get the storage value at a certain slot and block hash for a certain address diff --git a/contracts/Governance/GovernorBravoDelegate.sol b/contracts/Governance/GovernorBravoDelegate.sol index 3458ee95..e4e902d5 100644 --- a/contracts/Governance/GovernorBravoDelegate.sol +++ b/contracts/Governance/GovernorBravoDelegate.sol @@ -71,6 +71,13 @@ import "./GovernorBravoInterfaces.sol"; * vote delegation by calling the same function with a value of `0`. */ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoEvents { + + enum BridgeStatus { + REJECTED, + PENDING, + SYNCED + } + /// @notice The name of this contract string public constant name = "Venus Governor Bravo"; @@ -102,11 +109,14 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE /// @notice The EIP-712 typehash for the ballot struct used by the contract bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)"); - // It will be removed and instead of this we will add one more state - uint256 bridgeStatus; + // Is proposal bridged + mapping(uint256 => bool) public isBridged; address public votingPowerAggregator; + // proposal id => block number + mapping(uint256 => uint256) public proposalSubmissionTimestamp; + struct VotingProof { uint16 chainId; bytes numCheckPointproof; @@ -197,18 +207,18 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE * @param proposalType the type of the proposal (e.g NORMAL, FASTTRACK, CRITICAL) * @return Proposal id of new proposal */ - function createProposal( + function submitProposalPayload( address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description, ProposalType proposalType, - address proposer, VotingProof[] memory proposerVotingProof, uint16[] memory remoteChainIds, - bytes[] memory proofs, - bytes[] memory remoteBlockheaders + bytes[] memory blockHash, + bytes[] memory remoteBlockHeadersRLP, + bytes[] memory xvsVaultStateProofRLP ) public payable returns (uint) { // Reject proposals before initiating as Governor require(initialProposalId != 0, "GovernorBravo::propose: Governor Bravo not active"); @@ -222,7 +232,7 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE require(targets.length != 0, "GovernorBravo::propose: must provide actions"); require(targets.length <= proposalMaxOperations, "GovernorBravo::propose: too many actions"); - uint latestProposalId = latestProposalIds[proposer]; + uint latestProposalId = latestProposalIds[msg.sender]; if (latestProposalId != 0) { ProposalState proposersLatestProposalState = state(latestProposalId); require( @@ -235,23 +245,35 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE ); } - // Verify remoteBlockheaders - // TO DO - proposalCount++; - // Fetch all block hashes - IVotingPowerAggregator(votingPowerAggregator).getRemoteBlockHashes.value(msg.value)( - remoteChainIds, - proposalCount, - proposerVotingProof - ); + proposalSubmissionTimestamp[proposalCount] = block.number; uint startBlock = 0; uint endBlock = 0; + // Backward compatibility + if(remoteChainIds.length == 0) { + uint96 proposerVotes = IVotingPowerAggregator(votingPowerAggregator).getVotingPower(msg.sender); + require(proposerVotes > MIN_PROPOSAL_THRESHOLD && proposerVotes < MAX_PROPOSAL_THRESHOLD); + isBridged[proposalCount] = true; + Proposal[proposalCount].startBlock = block.number; + Proposal[proposalCount].endBlock = block.number + delayBlocks; + } + else { + // Fetch all block hashes + IVotingPowerAggregator(votingPowerAggregator).startVotingPowerSync.value(msg.value)( + proposalCount, + remoteChainIds, + blockHash, + remoteBlockHeadersRLP, + xvsVaultStateProofRLP, + proposerVotingProof + ); + } + Proposal memory newProposal = Proposal({ id: proposalCount, - proposer: proposer, + proposer: msg.sender, eta: 0, targets: targets, values: values, @@ -272,7 +294,7 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE emit ProposalCreated( newProposal.id, - proposer, + msg.sender, targets, values, signatures, @@ -285,32 +307,21 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE return newProposal.id; } - // due to different msg.sender we have to make another activate proposal function - function InternalActivateProposal(uint256 proposalId, uint256 status) external { - require(msg.sender == votingPowerAggregator, "Invalid owner"); - - bridgeStatus = status; - // If proposal status is Active - if (status == 3) { - Proposal[proposalId].startBlock = block.number; - proposal[proposalId].endBlock = block.number + delayBlocks; + function activateProposal(uint256 proposalId, uint256 status, address proposer) external { + // Either votes not synced or proposer has not sufficient power + if(msg.sender == votingPowerAggregator) { + require(status == BridgeStatus.SYNCED, "Not synced yet"); } - } - - function ActivateProposalByProposer(uint256 proposalId, VotingProof[] memory proposerVotingProof) public { - require(bridgeStatus == 1, "Invalid proposal"); - - if (proposerVotingProof[0].remoteIds != 0) { - uint96 votes = IVotingPowerAggregator(votingPowerAggregator).getVotingPower( - msg.sender, - blocknumber, - proposerVotingProof - ); - // comare proposer votes with the threshold - require(votes > threshold, "Insufficient proposer's balance"); + else { + require(IVotingPowerAggregator(votingPowerAggregator).status == BridgeStatus.SYNCED, "Not synced yet"); } + + // If Votes get Synced Proposal[proposalId].startBlock = block.number; - proposal[proposalId].endBlock = block.number + delayBlocks; + Proposal[proposalId].endBlock = block.number + delayBlocks; + isBridged[proposalId] = true; + + emit ProposalActivated(proposalId); } /** @@ -448,9 +459,11 @@ contract GovernorBravoDelegate is GovernorBravoDelegateStorageV2, GovernorBravoE if (proposal.canceled) { return ProposalState.Canceled; // If not activated yet return expired - } else if (block.number <= proposal.startBlock) { + } else if(proposal.startBlock == 0 && !isBridged[proposalId] && proposalSubmissionTimestamp[proposalId] > 0 && block.number > proposalSubmissionTimestamp[proposalId] ){ + return ProposalState.PendingSync; + }else if (block.number <= proposal.startBlock) { return ProposalState.Pending; - } else if (block.number <= proposal.endBlock) { + } else if (block.number <= proposal.endBlock && isBridged[proposalId]) { return ProposalState.Active; } else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < quorumVotes) { return ProposalState.Defeated;