diff --git a/.gitmodules b/.gitmodules index c42cdd9..92c9785 100644 --- a/.gitmodules +++ b/.gitmodules @@ -30,3 +30,6 @@ url = https://github.com/foundry-rs/forge-std branch = "v1.3.0" path = lib/osx url = https://github.com/aragon/osx +[submodule "lib/optimism"] + path = lib/optimism + url = https://github.com/ethereum-optimism/optimism diff --git a/lib/optimism b/lib/optimism new file mode 160000 index 0000000..5662448 --- /dev/null +++ b/lib/optimism @@ -0,0 +1 @@ +Subproject commit 5662448279e4fb16e073e00baeb6e458b12a59b2 diff --git a/remappings.txt b/remappings.txt index c719bfe..e37fd83 100644 --- a/remappings.txt +++ b/remappings.txt @@ -36,3 +36,7 @@ solidity-bytes-utils/=lib/solidity-bytes-utils/ @execution-chain/=src/execution-chain/ @utils/=src/utils/ @helpers/=test/helpers/ + +@eth-optimism/contracts-bedrock/=lib/optimism/packages/contracts-bedrock/ + +@hashi/contracts/=src/hashi/ \ No newline at end of file diff --git a/src/execution-chain/crosschain/ActionRelay.sol b/src/execution-chain/crosschain/ActionRelay.sol index cfe8100..44dd1c4 100644 --- a/src/execution-chain/crosschain/ActionRelay.sol +++ b/src/execution-chain/crosschain/ActionRelay.sol @@ -2,115 +2,52 @@ pragma solidity ^0.8.0; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; -import {MessagingFee, MessagingReceipt} from "@layerzerolabs/lz-evm-protocol-v2/contracts/interfaces/ILayerZeroEndpointV2.sol"; - -import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import {OptionsBuilder} from "@lz-oapp/libs/OptionsBuilder.sol"; - -import {OAppSenderUpgradeable, MessagingFee} from "@oapp-upgradeable/aragon-oapp/OAppSenderUpgradeable.sol"; -import {bytes32ToAddress} from "@utils/converters.sol"; +import {DaoAuthorizableUpgradeable} from "@aragon/osx/core/plugin/dao-authorizable/DaoAuthorizableUpgradeable.sol"; /// @title ActionRelay /// @author Aragon -/// @notice A LayerZero-compatible OApp that allows for sending arbitrary action data across chains. -contract ActionRelay is OAppSenderUpgradeable, UUPSUpgradeable { - using OptionsBuilder for bytes; - using SafeCast for uint256; - +/// @notice A contract that allows for sending arbitrary action data across chains using Hashi pull flow. +contract ActionRelay is UUPSUpgradeable, DaoAuthorizableUpgradeable { /// @notice Holders of this role are allowed to relay actions to another chain. bytes32 public constant XCHAIN_ACTION_RELAYER_ID = keccak256("XCHAIN_ACTION_RELAYER"); - /// @notice Additional Layer Zero params required to send a cross chain message. - /// @param dstEid The LayerZero endpoint ID of the execution chain. - /// @param gasLimit The additional gas needed on the execution chain to process the message, surplus will be refunded. - /// @param fee The messaging fee required to send the message, this is sent to LayerZero. - /// @param options Additional options required to send the message, these are encoded as bytes. - struct LzSendParams { - uint32 dstEid; - uint128 gasLimit; - MessagingFee fee; - bytes options; - } + /// @notice Holders of this role are allowed to upgrade the contract + bytes32 public constant OAPP_ADMINISTRATOR_ID = keccak256("OAPP_ADMINISTRATOR_ID"); + + /// @notice Variable used to ensure commitment uniqueness + uint256 private _nonce; /// @notice Emitted when actions have been successfully relayed to another chain. /// @param callId A unique identifier for the relayed actions, such as a proposal ID. - /// @param destinationEid The LayerZero endpoint ID of the destination chain. - event ActionsRelayed( - uint256 indexed callId, - uint256 indexed destinationEid, - MessagingReceipt receipt - ); + /// @param destinationChainId The destination chain ID. + /// @param commitment The commitment of the message to execute on the destination chain. + event ActionsRelayed(uint256 indexed callId, uint256 indexed destinationChainId, bytes32 commitment); constructor() { _disableInitializers(); } - /// @notice Initialize the OApp with the LayerZero endpoint and DAO. - /// @param _lzEndpoint The LayerZero endpoint address on this chain. - /// @param _dao The DAO address, will be the delegate for this OApp. - function initialize(address _lzEndpoint, address _dao) external initializer { - __OAppCore_init({_endpoint: _lzEndpoint, _dao: _dao}); - } - - /// @notice The refund address will receive extra gas on the destination chain. - /// @param _dstEid The layerZero endpoint ID of the destination chain. - /// @dev Encoded as a 256bit integer in case we want to change the implementation to a different chain Id. - /// @return The address that will receive the refund. By default this is the LayerZero peer address. - /// which should implement a sweep function to recover the funds. - function refundAddress(uint256 _dstEid) public view virtual returns (address) { - return bytes32ToAddress(peers[_dstEid.toUint32()]); - } - - /// @notice Quote the messaging fee required to relay actions to another chain. - /// @param _callId The unique identifier for the relayed actions, such as a proposal ID. - /// @param _actions The actions to relay to the destination chain, including value, target and calldata. - /// @param _allowFailureMap A bitmap of actions that are allowed to fail. - /// @param _dstEid The LayerZero endpoint ID of the destination chain. - /// @param _gasLimit The additional gas needed on the destination chain to process the message, surplus will be refunded. - function quote( - uint256 _callId, - IDAO.Action[] memory _actions, - uint256 _allowFailureMap, - uint32 _dstEid, - uint128 _gasLimit - ) external view returns (LzSendParams memory params) { - bytes memory message = abi.encode(_callId, _actions, _allowFailureMap); - bytes memory options = OptionsBuilder.newOptions().addExecutorLzReceiveOption({ - _gas: _gasLimit, - _value: 0 - }); - MessagingFee memory fee = _quote({ - _dstEid: _dstEid, - _message: message, - _options: options, - _payInLzToken: false - }); - return LzSendParams({dstEid: _dstEid, gasLimit: _gasLimit, fee: fee, options: options}); - } + function initialize() external initializer {} /// @notice Relay actions to another chain. Requires the sender to be authorized and the peer OApp to be set. /// @param _callId The unique identifier for the relayed actions, such as a proposal ID. /// @param _actions The actions to relay to the destination chain, including value, target and calldata. /// @param _allowFailureMap A bitmap of actions that are allowed to fail. - /// @param _params Additional Layer Zero params required to send a cross chain message, use the `quote` function to get these. + /// @param _destinationChainId The destination chain ID. function relayActions( uint256 _callId, IDAO.Action[] memory _actions, uint256 _allowFailureMap, - LzSendParams memory _params - ) external payable auth(XCHAIN_ACTION_RELAYER_ID) returns (MessagingReceipt memory receipt) { - bytes memory message = abi.encode(_callId, _actions, _allowFailureMap); - - receipt = _lzSend({ - _dstEid: _params.dstEid, - _message: message, - _options: _params.options, - _fee: _params.fee, - _refundAddress: refundAddress(_params.dstEid) - }); - - emit ActionsRelayed(_callId, _params.dstEid, receipt); + uint256 _destinationChainId + ) external payable auth(XCHAIN_ACTION_RELAYER_ID) returns (bytes32 commitment) { + bytes memory message = + abi.encode(block.chainid, _destinationChainId, msg.sender, _nonce, _callId, _actions, _allowFailureMap); + commitment = keccak256(message); + unchecked { + ++_nonce; + } + emit ActionsRelayed(_callId, _destinationChainId, commitment); } /// @notice Returns the address of the implementation contract in the [proxy storage slot](https://eips.ethereum.org/EIPS/eip-1967) slot the [UUPS proxy](https://eips.ethereum.org/EIPS/eip-1822) is pointing to. diff --git a/src/hashi/HashiProverLib.sol b/src/hashi/HashiProverLib.sol new file mode 100644 index 0000000..ef23ec7 --- /dev/null +++ b/src/hashi/HashiProverLib.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import {SecureMerkleTrie} from "@eth-optimism/contracts-bedrock/src/libraries/trie/SecureMerkleTrie.sol"; +import {MerkleTrie} from "@eth-optimism/contracts-bedrock/src/libraries/trie/MerkleTrie.sol"; +import {RLPReader} from "@eth-optimism/contracts-bedrock/src/libraries/rlp/RLPReader.sol"; +import {AccountAndStorageProof, ReceiptProof} from "./HashiProverStructs.sol"; +import {IShoyuBashi} from "./interfaces/IShoyuBashi.sol"; + +library HashiProverLib { + using RLPReader for RLPReader.RLPItem; + using RLPReader for bytes; + + error BlockHeaderNotFound(); + error ConflictingBlockHeader(uint256 blockNumber, bytes32 ancestralBlockHeaderHash, bytes32 blockHeaderHash); + error InvalidAccount(); + error InvalidLogIndex(); + error InvalidReceipt(); + error InvalidStorageHash(); + error InvalidStorageProofParams(); + error UnsupportedTxType(); + + /** + * @dev Verifies and retrieves a specific event from a transaction receipt in a foreign blockchain. + * + * @param proof A `ReceiptProof` struct containing proof details: + * - chainId: The chain ID of the foreign blockchain. + * - blockNumber: If ancestralBlockNumber is 0, then blockNumber represents the block where the transaction occurred and is available in Hashi. + * - blockHeader: The header of the specified block. + * - ancestralBlockNumber: If provided, this is the block number where the transaction took place. In this case, blockNumber is the block whose header is accessible in Hashi. + * - ancestralBlockHeaders: Array of block headers to prove the ancestry of the specified block. + * - receiptProof: Proof data for locating the receipt in the Merkle Trie. + * - transactionIndex: Index of the transaction within the block. + * - logIndex: The specific log index within the transaction receipt. + * @param shoyuBashi The address of ShoyuBashi contract + * + * @return bytes The RLP-encoded event corresponding to the specified `logIndex`. + */ + function verifyForeignEvent(ReceiptProof calldata proof, address shoyuBashi) internal view returns (bytes memory) { + bytes memory blockHeader = checkBlockHeaderAgainstHashi( + proof.chainId, + proof.blockNumber, + proof.blockHeader, + proof.ancestralBlockNumber, + proof.ancestralBlockHeaders, + shoyuBashi + ); + RLPReader.RLPItem[] memory blockHeaderFields = blockHeader.toRLPItem().readList(); + bytes32 receiptsRoot = bytes32(blockHeaderFields[5].readBytes()); + + bytes memory value = MerkleTrie.get(proof.transactionIndex, proof.receiptProof, receiptsRoot); + RLPReader.RLPItem[] memory receiptFields = extractReceiptFields(value); + if (receiptFields.length != 4) revert InvalidReceipt(); + + RLPReader.RLPItem[] memory logs = receiptFields[3].readList(); + if (proof.logIndex >= logs.length) revert InvalidLogIndex(); + return logs[proof.logIndex].readRawBytes(); + } + + /** + * @dev Verifies foreign storage data for a specified account on a foreign blockchain. + * + * @param proof An `AccountAndStorageProof` struct containing proof details: + * - chainId: The chain ID of the foreign blockchain. + * - blockNumber: If ancestralBlockNumber is 0, then blockNumber represents the block where the transaction occurred and is available in Hashi. + * - blockHeader: The header of the specified block. + * - ancestralBlockNumber: If provided, this is the block number where the transaction took place. In this case, blockNumber is the block whose header is accessible in Hashi. + * - ancestralBlockHeaders: Array of block headers proving ancestry of the specified block. + * - account: The account address whose storage is being verified. + * - accountProof: Proof data for locating the account in the state trie. + * - storageKeys: Array of storage keys for which data is being verified. + * - storageProof: Proof data for locating the storage values in the storage trie. + * @param shoyuBashi The address of ShoyuBashi contract + * + * @return bytes[] An array of storage values corresponding to the specified `storageKeys`. + */ + function verifyForeignStorage(AccountAndStorageProof calldata proof, address shoyuBashi) + internal + view + returns (bytes[] memory) + { + bytes memory blockHeader = checkBlockHeaderAgainstHashi( + proof.chainId, + proof.blockNumber, + proof.blockHeader, + proof.ancestralBlockNumber, + proof.ancestralBlockHeaders, + shoyuBashi + ); + RLPReader.RLPItem[] memory blockHeaderFields = blockHeader.toRLPItem().readList(); + bytes32 stateRoot = bytes32(blockHeaderFields[3].readBytes()); + (,, bytes32 storageHash,) = verifyAccountProof(proof.account, stateRoot, proof.accountProof); + return verifyStorageProof(storageHash, proof.storageKeys, proof.storageProof); + } + + /** + * @notice Verifies a block header against the Hashi contract by checking its hash and, if needed, traversing ancestral blocks. + * @dev This function first checks if the provided block header hash matches the threshold hash stored in the ShoyuBashi contract. + * If it doesn't match directly, it will verify the block by traversing ancestral blocks until a matching block header or ancestor is found. + * If no match is found, it reverts with `BlockHeaderNotFound`. + * @param chainId The chain ID associated with the block. + * @param blockNumber The number of the block to be checked. + * @param blockHeader The RLP-encoded header of the block. + * @param ancestralBlockNumber The block number of the ancestral block to be verified, if applicable. + * @param ancestralBlockHeaders An array of RLP-encoded headers for ancestral blocks. + * @param shoyuBashi The address of ShoyuBashi contract. + * @return bytes The RLP-encoded block header if successfully verified. + */ + function checkBlockHeaderAgainstHashi( + uint256 chainId, + uint256 blockNumber, + bytes memory blockHeader, + uint256 ancestralBlockNumber, + bytes[] memory ancestralBlockHeaders, + address shoyuBashi + ) internal view returns (bytes memory) { + bytes32 blockHeaderHash = keccak256(blockHeader); + bytes32 currentBlockHeaderHash = IShoyuBashi(shoyuBashi).getThresholdHash(chainId, blockNumber); + if (currentBlockHeaderHash == blockHeaderHash && ancestralBlockHeaders.length == 0) return blockHeader; + + for (uint256 i = 0; i < ancestralBlockHeaders.length; i++) { + RLPReader.RLPItem[] memory ancestralBlockHeaderFields = ancestralBlockHeaders[i].readList(); + + bytes32 blockParentHash = bytes32(ancestralBlockHeaderFields[0].readBytes()); + uint256 currentAncestralBlockNumber = bytesToUint(ancestralBlockHeaderFields[8].readBytes()); + + bytes32 ancestralBlockHeaderHash = keccak256(ancestralBlockHeaders[i]); + if (ancestralBlockHeaderHash != currentBlockHeaderHash) { + revert ConflictingBlockHeader( + currentAncestralBlockNumber, ancestralBlockHeaderHash, currentBlockHeaderHash + ); + } + + if (ancestralBlockNumber == currentAncestralBlockNumber) { + return ancestralBlockHeaders[i]; + } else { + currentBlockHeaderHash = blockParentHash; + } + } + + revert BlockHeaderNotFound(); + } + + /** + * @notice Extracts the fields of a transaction receipt from its RLP-encoded data. + * @dev This function handles different transaction types by setting the appropriate offset for RLP parsing. + * It adjusts the starting point based on the transaction type byte, then uses RLPReader to parse the fields. + * @param value The RLP-encoded transaction receipt. + * @return RLPReader.RLPItem[] An array of RLP items representing the fields of the receipt. + */ + function extractReceiptFields(bytes memory value) internal pure returns (RLPReader.RLPItem[] memory) { + bytes1 txTypeOrFirstByte = value[0]; + + uint256 offset; + if ( + txTypeOrFirstByte == 0x01 || txTypeOrFirstByte == 0x02 || txTypeOrFirstByte == 0x03 + || txTypeOrFirstByte == 0x7e // EIP-2718 (https://eips.ethereum.org/EIPS/eip-2718) transaction + ) { + offset = 1; + } else if (txTypeOrFirstByte >= 0xc0) { + offset = 0; + } else { + revert UnsupportedTxType(); + } + + uint256 ptr; + assembly { + ptr := add(value, 32) + } + + return RLPReader.RLPItem({length: value.length - offset, ptr: RLPReader.MemoryPointer.wrap(ptr + offset)}) + .readList(); + } + + /** + * @notice Verifies an account proof and extracts account fields from it. + * @dev This function uses a Merkle proof to verify the account state in a given state root. + * It retrieves and decodes the account data, checking the storage root and account structure. + * @param account The address of the account to verify. + * @param stateRoot The state root against which the account proof is verified. + * @param proof A Merkle proof required to verify the account. + * @return uint256 The nonce of the account. + * @return uint256 The balance of the account. + * @return bytes32 The storage root of the account. + * @return bytes32 The code hash of the account. + */ + function verifyAccountProof(address account, bytes32 stateRoot, bytes[] memory proof) + internal + pure + returns (uint256, uint256, bytes32, bytes32) + { + bytes memory accountRlp = SecureMerkleTrie.get(abi.encodePacked(account), proof, stateRoot); + + bytes32 accountStorageRoot = bytes32(accountRlp.toRLPItem().readList()[2].readBytes()); + if (accountStorageRoot.length == 0) revert InvalidStorageHash(); + RLPReader.RLPItem[] memory accountFields = accountRlp.toRLPItem().readList(); + if (accountFields.length != 4) revert InvalidAccount(); + // [nonce, balance, storageHash, codeHash] + return ( + bytesToUint(accountFields[0].readBytes()), + bytesToUint(accountFields[1].readBytes()), + bytes32(accountFields[2].readBytes()), + bytes32(accountFields[3].readBytes()) + ); + } + + /** + * @notice Verifies multiple storage proofs and retrieves the storage values associated with given keys. + * @dev This function iterates over provided storage keys and their respective proofs, + * using a Merkle proof to verify each storage value against the specified storage hash. + * @param storageHash The root hash of the storage trie for the account being verified. + * @param storageKeys An array of storage keys for which values need to be verified. + * @param proof A 2D array of Merkle proof elements for each storage key. + * @return bytes[] An array of storage values corresponding to each storage key. + */ + function verifyStorageProof(bytes32 storageHash, bytes32[] memory storageKeys, bytes[][] memory proof) + internal + pure + returns (bytes[] memory) + { + bytes[] memory results = new bytes[](proof.length); + if (storageKeys.length == 0 || proof.length == 0 || storageKeys.length != proof.length) { + revert InvalidStorageProofParams(); + } + for (uint256 i = 0; i < proof.length;) { + RLPReader.RLPItem memory item = + RLPReader.toRLPItem(SecureMerkleTrie.get(abi.encode(storageKeys[i]), proof[i], storageHash)); + results[i] = item.readBytes(); + unchecked { + ++i; + } + } + return results; + } + + /** + * @notice Converts a byte array to an unsigned integer (uint256). + * @param b The byte array to convert to an unsigned integer. + * @return uint256 The resulting unsigned integer from the byte array. + */ + function bytesToUint(bytes memory b) internal pure returns (uint256) { + uint256 number; + for (uint256 i = 0; i < b.length; i++) { + number = number + uint256(uint8(b[i])) * (2 ** (8 * (b.length - (i + 1)))); + } + return number; + } +} diff --git a/src/hashi/HashiProverStructs.sol b/src/hashi/HashiProverStructs.sol new file mode 100644 index 0000000..3b0e7f7 --- /dev/null +++ b/src/hashi/HashiProverStructs.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +/** + * @notice Represents a proof structure for verifying both account and storage data within a specific blockchain state. + * @dev This struct includes all necessary components to verify account existence and storage values in a specified block. + */ +struct AccountAndStorageProof { + uint256 chainId; // The ID of the blockchain where the proof is applicable. + uint256 blockNumber; // The block number at which the proof is generated. + bytes blockHeader; // The RLP-encoded header of the block containing the account state. + uint256 ancestralBlockNumber; // The block number of an ancestral block if needed for verification. + bytes[] ancestralBlockHeaders; // An array of RLP-encoded headers for ancestral blocks (used if the proof requires it). + address account; // The address of the account being proven. + bytes[] accountProof; // Merkle proof for verifying the account's state in the specified block. + bytes32[] storageKeys; // An array of storage keys for which values are being proven. + bytes[][] storageProof; // A 2D array of Merkle proofs for each storage key, verifying each key-value pair in the storage trie. +} + +/** + * @notice Represents a proof structure for verifying a transaction receipt and its corresponding log entry within a specific block. + * @dev This struct includes all necessary components to verify the validity of a transaction receipt and the log it produced in a specified block. + */ +struct ReceiptProof { + uint256 chainId; // The ID of the blockchain where the proof is applicable. + uint256 blockNumber; // The block number at which the transaction receipt is included. + bytes blockHeader; // The RLP-encoded header of the block containing the transaction receipt. + uint256 ancestralBlockNumber; // The block number of an ancestral block, if needed for receipt verification. + bytes[] ancestralBlockHeaders; // An array of RLP-encoded headers for ancestral blocks (used if the proof requires them). + bytes[] receiptProof; // Merkle proof for verifying the transaction receipt in the block's receipt trie. + bytes transactionIndex; // The index of the transaction within the block's transaction list (RLP-encoded). + uint256 logIndex; // The index of the log entry within the transaction receipt being proven. +} diff --git a/src/hashi/HashiProverUpgradeable.sol b/src/hashi/HashiProverUpgradeable.sol new file mode 100644 index 0000000..0ecca63 --- /dev/null +++ b/src/hashi/HashiProverUpgradeable.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {HashiProverLib} from "./HashiProverLib.sol"; +import {AccountAndStorageProof, ReceiptProof} from "./HashiProverStructs.sol"; + +contract HashiProverUpgradeable is Initializable, OwnableUpgradeable { + /// @notice Stores the address of the ShoyuBashi contract. + /// @dev This address can be updated by the owner using the `setShoyuBashi` function. + address public shoyuBashi; + + /** + * @notice Emitted when the ShoyuBashi contract address is updated. + * @param shoyuBashi The new address of the ShoyuBashi contract. + */ + event ShoyuBashiSet(address shoyuBashi); + + function __HashiProverUpgradeable_init(address shoyuBashi_) public onlyInitializing { + __Ownable_init(); + shoyuBashi = shoyuBashi_; + } + + /** + * @notice Sets the address of the ShoyuBashi contract. + * @dev This function can only be called by the contract owner. + * It updates the `shoyuBashi` address and emits an event to record the change. + * @param shoyuBashi_ The new address for the ShoyuBashi contract. + */ + function setShoyuBashi(address shoyuBashi_) external onlyOwner { + shoyuBashi = shoyuBashi_; + emit ShoyuBashiSet(shoyuBashi_); + } + + /** + * @dev Verifies and retrieves a specific event from a transaction receipt in a foreign blockchain. + * + * @param proof A `ReceiptProof` struct containing proof details: + * - chainId: The chain ID of the foreign blockchain. + * - blockNumber: If ancestralBlockNumber is 0, then blockNumber represents the block where the transaction occurred and is available in Hashi. + * - blockHeader: The header of the specified block. + * - ancestralBlockNumber: If provided, this is the block number where the transaction took place. In this case, blockNumber is the block whose header is accessible in Hashi. + * - ancestralBlockHeaders: Array of block headers to prove the ancestry of the specified block. + * - receiptProof: Proof data for locating the receipt in the Merkle Trie. + * - transactionIndex: Index of the transaction within the block. + * - logIndex: The specific log index within the transaction receipt. + * + * @return bytes The RLP-encoded event corresponding to the specified `logIndex`. + */ + function verifyForeignEvent(ReceiptProof calldata proof) internal view returns (bytes memory) { + return HashiProverLib.verifyForeignEvent(proof, shoyuBashi); + } + + /** + * @dev Verifies foreign storage data for a specified account on a foreign blockchain. + * + * @param proof An `AccountAndStorageProof` struct containing proof details: + * - chainId: The chain ID of the foreign blockchain. + * - blockNumber: If ancestralBlockNumber is 0, then blockNumber represents the block where the transaction occurred and is available in Hashi. + * - blockHeader: The header of the specified block. + * - ancestralBlockNumber: If provided, this is the block number where the transaction took place. In this case, blockNumber is the block whose header is accessible in Hashi. + * - ancestralBlockHeaders: Array of block headers proving ancestry of the specified block. + * - account: The account address whose storage is being verified. + * - accountProof: Proof data for locating the account in the state trie. + * - storageHash: Expected hash of the storage root for the account. + * - storageKeys: Array of storage keys for which data is being verified. + * - storageProof: Proof data for locating the storage values in the storage trie. + * + * @return bytes[] An array of storage values corresponding to the specified `storageKeys`. + */ + function verifyForeignStorage(AccountAndStorageProof calldata proof) internal view returns (bytes[] memory) { + return HashiProverLib.verifyForeignStorage(proof, shoyuBashi); + } + + /// @notice This empty reserved space is put in place to allow future versions to add new variables without shifting down storage in the inheritance chain (see [OpenZeppelin's guide about storage gaps](https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps)). + uint256[49] private __gap; +} diff --git a/src/hashi/interfaces/IAdapter.sol b/src/hashi/interfaces/IAdapter.sol new file mode 100644 index 0000000..72b9c7f --- /dev/null +++ b/src/hashi/interfaces/IAdapter.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +/** + * @title IAdapter + */ +interface IAdapter { + error ConflictingBlockHeader(uint256 blockNumber, bytes32 blockHash, bytes32 storedBlockHash); + error InvalidBlockHeaderRLP(); + + /** + * @dev Emitted when a hash is stored. + * @param id - The ID of the stored hash. + * @param hash - The stored hash as bytes32 values. + */ + event HashStored(uint256 indexed id, bytes32 indexed hash); + + /** + * @dev Returns the hash for a given ID. + * @param domain - Identifier for the domain to query. + * @param id - Identifier for the ID to query. + * @return hash Bytes32 hash for the given ID on the given domain. + * @notice MUST return bytes32(0) if the hash is not present. + */ + function getHash(uint256 domain, uint256 id) external view returns (bytes32 hash); +} diff --git a/src/hashi/interfaces/IHashi.sol b/src/hashi/interfaces/IHashi.sol new file mode 100644 index 0000000..e0d0c53 --- /dev/null +++ b/src/hashi/interfaces/IHashi.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import {IAdapter} from "./IAdapter.sol"; + +/** + * @title IHashi + */ +interface IHashi { + error AdaptersDisagree(IAdapter adapterOne, IAdapter adapterTwo); + error HashNotAvailableInAdapter(IAdapter adapter); + error InvalidThreshold(uint256 threshold, uint256 maxThreshold); + error NoAdaptersGiven(); + + /** + * @dev Checks whether the threshold is reached for a message given a set of adapters. + * @param domain - ID of the domain to query. + * @param id - ID for which to return hash. + * @param threshold - Threshold to use. + * @param adapters - Array of addresses for the adapters to query. + * @notice If the threshold is 1, it will always return true. + * @return result A boolean indicating if a threshold for a given message has been reached. + */ + function checkHashWithThresholdFromAdapters( + uint256 domain, + uint256 id, + uint256 threshold, + IAdapter[] calldata adapters + ) external view returns (bool); + + /** + * @dev Returns the hash stored by a given adapter for a given ID. + * @param domain - ID of the domain to query. + * @param id - ID for which to return a hash. + * @param adapter - Address of the adapter to query. + * @return hash stored by the given adapter for the given ID. + */ + function getHashFromAdapter(uint256 domain, uint256 id, IAdapter adapter) external view returns (bytes32); + + /** + * @dev Returns the hashes for a given ID stored by a given set of adapters. + * @param domain - The ID of the domain to query. + * @param id - The ID for which to return hashes. + * @param adapters - An array of addresses for the adapters to query. + * @return hashes An array of hashes stored by the given adapters for the specified ID. + */ + function getHashesFromAdapters(uint256 domain, uint256 id, IAdapter[] calldata adapters) + external + view + returns (bytes32[] memory); + + /** + * @dev Returns the hash unanimously agreed upon by a given set of adapters. + * @param domain - The ID of the domain to query. + * @param id - The ID for which to return a hash. + * @param adapters - An array of addresses for the adapters to query. + * @return hash agreed on by the given set of adapters. + * @notice MUST revert if adapters disagree on the hash or if an adapter does not report. + */ + function getHash(uint256 domain, uint256 id, IAdapter[] calldata adapters) external view returns (bytes32); +} diff --git a/src/hashi/interfaces/IShoyuBashi.sol b/src/hashi/interfaces/IShoyuBashi.sol new file mode 100644 index 0000000..1880147 --- /dev/null +++ b/src/hashi/interfaces/IShoyuBashi.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import {IHashi} from "./IHashi.sol"; +import {IAdapter} from "./IAdapter.sol"; +import {IShuSho} from "./IShuSho.sol"; + +/** + * @title IShoyuBashi + */ +interface IShoyuBashi is IShuSho { + /** + * @dev Disables the given adapters for a given domain. + * @param domain - Uint256 identifier for the domain for which to set adapters. + * @param adapters - Array of adapter addresses. + * @notice Only callable by the owner of this contract. + * @notice Reverts if adapters are out of order or contain duplicates. + */ + function disableAdapters(uint256 domain, IAdapter[] memory adapters) external; + + /** + * @dev Enables the given adapters for a given domain. + * @param domain - Uint256 identifier for the domain for which to set adapters. + * @param adapters - Array of adapter addresses. + * @param threshold - Uint256 threshold to set for the given domain. + * @notice Only callable by the owner of this contract. + * @notice Reverts if adapters are out of order, contain duplicates or if the threshold is not higher than half the count of the adapters + */ + function enableAdapters(uint256 domain, IAdapter[] memory adapters, uint256 threshold) external; + + /** + * @dev Returns the hash unanimously agreed upon by ALL of the enabled adapters. + * @param domain - Uint256 identifier for the domain to query. + * @param id - Uint256 identifier to query. + * @return Bytes32 hash agreed upon by the adapters for the given domain. + * @notice Revert if the adapters do not yet have the hash for the given ID. + * @notice Reverts if adapters disagree. + * @notice Reverts if no adapters are set for the given domain. + */ + function getUnanimousHash(uint256 domain, uint256 id) external view returns (bytes32); + + /** + * @dev Returns the hash agreed upon by a threshold of the enabled adapters. + * @param domain - Uint256 identifier for the domain to query. + * @param id - Uint256 identifier to query. + * @return Bytes32 hash agreed upon by a threshold of the adapters for the given domain. + * @notice Reverts if the threshold is not reached. + * @notice Reverts if no adapters are set for the given domain. + */ + function getThresholdHash(uint256 domain, uint256 id) external view returns (bytes32); + + /** + * @dev Returns the hash unanimously agreed upon by all of the given adapters. + * @param domain - Uint256 identifier for the domain to query. + * @param adapters - Array of adapter addresses to query. + * @param id - Uint256 identifier to query. + * @return Bytes32 hash agreed upon by the adapters for the given domain. + * @notice adapters must be in numerical order from smallest to largest and contain no duplicates. + * @notice Reverts if adapters are out of order or contain duplicates. + * @notice Reverts if adapters disagree. + * @notice Revert if the adapters do not yet have the hash for the given ID. + * @notice Reverts if no adapters are set for the given domain. + */ + function getHash(uint256 domain, uint256 id, IAdapter[] memory adapters) external view returns (bytes32); + + /** + * @dev Sets the threshold of adapters required for a given domain. + * @param domain - Uint256 identifier for the domain for which to set the threshold. + * @param threshold - Uint256 threshold to set for the given domain. + * @notice Only callable by the owner of this contract. + * @notice Reverts if the threshold is already set to the given value. + */ + function setThreshold(uint256 domain, uint256 threshold) external; + + /** + * @dev Sets the address of the IHashi contract. + * @param hashi - Address of the hashi contract. + * @notice Only callable by the owner of this contract. + */ + function setHashi(IHashi hashi) external; +} diff --git a/src/hashi/interfaces/IShuSho.sol b/src/hashi/interfaces/IShuSho.sol new file mode 100644 index 0000000..e41bca0 --- /dev/null +++ b/src/hashi/interfaces/IShuSho.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import {IHashi} from "./IHashi.sol"; +import {IAdapter} from "./IAdapter.sol"; + +/** + * @title IShuSho + */ +interface IShuSho { + struct Domain { + uint256 threshold; + uint256 count; + } + + struct Link { + IAdapter previous; + IAdapter next; + } + + error AdapterNotEnabled(IAdapter adapter); + error AdapterAlreadyEnabled(IAdapter adapter); + error CountCannotBeZero(); + error DuplicateHashiAddress(IHashi hashi); + error DuplicateOrOutOfOrderAdapters(IAdapter adapterOne, IAdapter adapterTwo); + error DuplicateThreshold(uint256 threshold); + error InvalidAdapter(IAdapter adapter); + error InvalidThreshold(uint256 threshold); + error NoAdaptersEnabled(uint256 domain); + error NoAdaptersGiven(); + error ThresholdNotMet(); + + /** + * @dev Emitted when adapters are disabled for a specific domain. + * @param domain - The domain associated with the disabled adapters. + * @param adapters - An array of disabled adapter addresses associated with this event. + */ + event AdaptersDisabled(uint256 indexed domain, IAdapter[] adapters); + + /** + * @dev Emitted when adapters are enabled for a specific domain. + * @param domain - The domain associated with the enabled adapters. + * @param adapters - An array of enabled adapter addresses associated with this event. + */ + event AdaptersEnabled(uint256 indexed domain, IAdapter[] adapters); + + /** + * @dev Emitted when the address of the IHashi contract is set. + * @param hashi - The address of the IHashi contract associated with this event. + */ + event HashiSet(IHashi indexed hashi); + + /** + * @dev Emitted when initialization occurs with the owner's address and the IHashi contract address. + * @param owner - The address of the owner associated with this event. + * @param hashi - The address of the IHashi contract associated with this event. + */ + event Init(address indexed owner, IHashi indexed hashi); + + /** + * @dev Emitted when the threshold is set for a specific domain. + * @param domain - The domain associated with the set threshold. + * @param threshold - The new threshold value associated with this event. + */ + event ThresholdSet(uint256 domain, uint256 threshold); + + /** + * @dev Checks the order and validity of adapters for a given domain. + * @param domain - The Uint256 identifier for the domain. + * @param _adapters - An array of adapter instances. + */ + function checkAdapterOrderAndValidity(uint256 domain, IAdapter[] memory _adapters) external view; + + /** + * @dev Get the previous and the next adapter given a domain and an adapter. + * @param domain - Uint256 identifier for the domain. + * @param adapter - IAdapter value for the adapter. + * @return link - The Link struct containing the previous and the next adapter. + */ + function getAdapterLink(uint256 domain, IAdapter adapter) external view returns (Link memory); + + /** + * @dev Returns an array of enabled adapters for a given domain. + * @param domain - Uint256 identifier for the domain for which to list adapters. + * @return adapters - The adapters for a given domain. + */ + function getAdapters(uint256 domain) external view returns (IAdapter[] memory); + + /** + * @dev Get the current configuration for a given domain. + * @param domain - Uint256 identifier for the domain. + * @return domain - The Domain struct containing the current configuration for a given domain. + */ + function getDomain(uint256 domain) external view returns (Domain memory); + + /** + * @dev Returns the threshold and count for a given domain. + * @param domain - Uint256 identifier for the domain. + * @return threshold - Uint256 adapters threshold for the given domain. + * @return count - Uint256 adapters count for the given domain. + * @notice If the threshold for a domain has not been set, or is explicitly set to 0, this function will return a threshold equal to the adapters count for the given domain. + */ + function getThresholdAndCount(uint256 domain) external view returns (uint256, uint256); + + /** + * @dev Returns the address of the specified Hashi. + * @return hashi - The Hashi address. + */ + function hashi() external view returns (IHashi); +} diff --git a/src/voting-chain/crosschain/AdminXChain.sol b/src/voting-chain/crosschain/AdminXChain.sol index d925cba..d345128 100644 --- a/src/voting-chain/crosschain/AdminXChain.sol +++ b/src/voting-chain/crosschain/AdminXChain.sol @@ -3,51 +3,70 @@ pragma solidity ^0.8.8; import {IDAO} from "@aragon/osx/core/dao/IDAO.sol"; -import {IOAppReceiver} from "@lz-oapp/interfaces/IOAppReceiver.sol"; - import {SafeCastUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/SafeCastUpgradeable.sol"; - import {ProposalUpgradeable} from "@aragon/osx/core/plugin/proposal/ProposalUpgradeable.sol"; -import {DaoUnauthorized} from "@aragon/osx/core/utils/auth.sol"; import {PluginUUPSUpgradeable} from "@aragon/osx/core/plugin/PluginUUPSUpgradeable.sol"; -import {DAO, PermissionManager} from "@aragon/osx/core/dao/DAO.sol"; - -import {OAppReceiverUpgradeable, Origin} from "@oapp-upgradeable/aragon-oapp/OAppReceiverUpgradeable.sol"; +import {DAO} from "@aragon/osx/core/dao/DAO.sol"; +import {HashiProverLib} from "@hashi/contracts/HashiProverLib.sol"; +import {AccountAndStorageProof} from "@hashi/contracts/HashiProverStructs.sol"; import {SweeperUpgradeable} from "@utils/SweeperUpgradeable.sol"; -import {bytes32ToAddress} from "@utils/converters.sol"; /// @title AdminXChain /// @author Aragon X /// @notice The admin governance plugin giving execution permission on the DAO to a trusted relayer. /// This allows a parent DAO on a foreign chain to control the DAO on this chain. -/// @dev The security model for this contract is entirely reliant on the message broker, and that the peer is trusted. -/// In the case of LayerZero, we trust that both the origin EID and the sender address are validated, and that the peer is corectly set. +/// @dev The security model for this contract is entirely reliant on the configuration you choose within Hashi. /// @custom:security-contact sirt@aragon.org -contract AdminXChain is - PluginUUPSUpgradeable, - ProposalUpgradeable, - OAppReceiverUpgradeable, - SweeperUpgradeable -{ +contract AdminXChain is PluginUUPSUpgradeable, ProposalUpgradeable, SweeperUpgradeable { using SafeCastUpgradeable for uint256; + /// @notice Holders of this role are allowed to upgrade the contract + bytes32 public constant OAPP_ADMINISTRATOR_ID = keccak256("OAPP_ADMINISTRATOR_ID"); + + /// @notice Holders of this role are set actionRelayStorageKey + bytes32 public constant SET_ACTION_RELAY_STORAGE_KEY = keccak256("SET_ACTION_RELAY_STORAGE_KEY"); + + /// @notice Holders of this role are set shoyuBashi + bytes32 public constant SET_SHOYU_BASHI = keccak256("SET_SHOYU_BASHI"); + + /// @notice Thrown when an action has already been executed and cannot be repeated. + error AlreadyExecuted(); + + /// @notice Thrown when a wrong action relay is used for a given source chain id. + error InvalidActionRelay(); + + /// @notice Thrown when an invalid destination chain ID is provided. + error InvalidDestinationChainId(); + + /// @notice Thrown when a message is malformed or fails validation checks. + error InvalidMessage(); + + /// @notice Thrown when an incorrect storage key is used for accessing or verifying data. + error InvalidStorageKey(); + /// @notice Emitted when a cross chain execution event is successfully processed. event XChainExecuted( uint256 indexed proposalId, uint256 indexed foreignCallId, - uint32 indexed srcEid, + uint256 indexed srcChainid, address sender, uint256 failureMap ); + /// @notice Emitted when ShoyuBashi is changed. + event ShoyuBashiSet(address shoyuBashi); + + /// @notice Emitted when ShoyuBaactionRelayStorageKeyshi is changed. + event ActionRelayStorageKeySet(bytes32 actionRelayStorageKey); + /// @notice Metadata to identify a remote proposal. Logged on receipt. /// @param callId The ID of the proposal on the foreign chain. No guarantees of uniqueness. - /// @param srcEid The LayerZero foreign chain ID. + /// @param srcChainid The LayerZero foreign chain ID. /// @param sender The address of the sender on the foreign chain. /// @param received The timestamp when the proposal was received. struct XChainActionMetadata { uint256 callId; - uint32 srcEid; + uint256 srcChainid; address sender; uint32 received; } @@ -56,67 +75,97 @@ contract AdminXChain is /// @dev proposalId => XChainActionMetadata mapping(uint256 => XChainActionMetadata) internal _xChainActionMetadata; + /// @notice Mapping used to avoid multiple execution of the same request. + mapping(bytes32 => bool) internal _executedCommitments; + + /// @notice Mapping used to assign a specific action relay for a source chain id + mapping(uint256 => address) public actionRelays; + + /// @notice value of the expected storage key of ActionRelay. + bytes32 public actionRelayStorageKey; + + /// @notice address of the ShoyuBashi contract. This contract is used to define the oracles and the threshold used in Hashi. + address public shoyuBashi; + constructor() { _disableInitializers(); } /// @notice Initializes the contract by setting the owner and the delegate to this address. - /// @param _dao The associated DAO. - /// @param _lzEndpoint The address of the Layer Zero endpoint on this chain.abi - function initialize(address _dao, address _lzEndpoint) external initializer { - __OAppCore_init(_lzEndpoint, _dao); + /// @param _shoyuBashi The address of the ShoyuBashi contract + function initialize(address _shoyuBashi) external initializer { // do not init PluginCloneable, as it would reinit DAOAuthorizable + shoyuBashi = _shoyuBashi; } /// @notice Returns xChainActionMetadata for a given proposal ID as a struct. - function xChainActionMetadata( - uint256 _proposalId - ) external view returns (XChainActionMetadata memory) { + function xChainActionMetadata(uint256 _proposalId) external view returns (XChainActionMetadata memory) { return _xChainActionMetadata[_proposalId]; } + /// @notice Sets the storage key used to verify action relay commitments on the source chain. + /// @dev This function requires the caller to have the `SET_ACTION_RELAY_STORAGE_KEY` authorization role. + /// It updates the `actionRelayStorageKey` and emits an event to log this change. + /// @param _actionRelayStorageKey The new storage key for the action relay. + function setActionRelayStorageKey(bytes32 _actionRelayStorageKey) external auth(SET_ACTION_RELAY_STORAGE_KEY) { + actionRelayStorageKey = _actionRelayStorageKey; + emit ActionRelayStorageKeySet(_actionRelayStorageKey); + } + + /// @notice Sets the address of the ShoyuBashi contract. + /// @dev This function requires the caller to have the `SET_SHOYU_BASHI` authorization role. + /// It updates the `shoyuBashi` address and emits an event to log the change. + /// @param _shoyuBashi The new address for the ShoyuBashi contract. + function setShoyuBashi(address _shoyuBashi) external auth(SET_SHOYU_BASHI) { + shoyuBashi = _shoyuBashi; + emit ShoyuBashiSet(_shoyuBashi); + } + /// @notice Checks if this or the parent contract supports an interface by its ID. /// @param _interfaceId The ID of the interface. /// @return Returns `true` if the interface is supported. - function supportsInterface( - bytes4 _interfaceId - ) public view override(PluginUUPSUpgradeable, ProposalUpgradeable) returns (bool) { - return - _interfaceId == type(IOAppReceiver).interfaceId || - super.supportsInterface(_interfaceId); + function supportsInterface(bytes4 _interfaceId) + public + view + override(PluginUUPSUpgradeable, ProposalUpgradeable) + returns (bool) + { + return super.supportsInterface(_interfaceId); } /// @notice Entrypoint for executing a cross chain proposal. - /// @param _origin contains the source endpoint and sender address, passed from Layer Zero. + /// @param _proof contains the data to verify the proof. /// @param _message contains the execution instruction. - /// @dev The security model for this function is entirely reliant on the message broker, and that the peer is trusted. - /// @dev TODO: storing the message hash is an alternative option that could then be used to limit the calldata passed - /// between chains. This could then be re-construted and executed on the receiving chain. - function _lzReceive( - Origin calldata _origin, - bytes32 /* _guid */, - bytes calldata _message, - address /* _executor */, - bytes calldata /* _extraData */ - ) internal override { - (uint256 callId, IDAO.Action[] memory actions, uint256 allowFailureMap) = abi.decode( - _message, - (uint256, IDAO.Action[], uint256) - ); - address sender = bytes32ToAddress(_origin.sender); + function execute(AccountAndStorageProof calldata _proof, bytes calldata _message) internal { + bytes32 commitment = keccak256(_message); + if (_executedCommitments[commitment]) revert AlreadyExecuted(); + _executedCommitments[commitment] = true; + + bytes32 expectedCommitment = bytes32(HashiProverLib.verifyForeignStorage(_proof, shoyuBashi)[0]); + if (commitment != expectedCommitment) revert InvalidMessage(); + + ( + uint256 sourceChainId, + uint256 destinationChainId, + address sender, + , + uint256 callId, + IDAO.Action[] memory actions, + uint256 allowFailureMap + ) = abi.decode(_message, (uint256, uint256, address, uint256, uint256, IDAO.Action[], uint256)); + + if (_proof.account != actionRelays[sourceChainId]) revert InvalidActionRelay(); + if (_proof.storageKeys[0] != actionRelayStorageKey) revert InvalidStorageKey(); + if (destinationChainId != block.chainid) revert InvalidDestinationChainId(); // store the action metadata against the newly generated proposalId, ensuring it is unique - uint proposalId = _createProposalId(); - _xChainActionMetadata[proposalId] = XChainActionMetadata( - callId, - _origin.srcEid, - sender, - block.timestamp.toUint32() - ); + uint256 proposalId = _createProposalId(); + _xChainActionMetadata[proposalId] = + XChainActionMetadata(callId, sourceChainId, sender, block.timestamp.toUint32()); // execute the action(s) as a proposal on the DAO (, uint256 failureMap) = dao().execute(bytes32(callId), actions, allowFailureMap); - emit XChainExecuted(proposalId, callId, _origin.srcEid, sender, failureMap); + emit XChainExecuted(proposalId, callId, sourceChainId, sender, failureMap); } /// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~