From e63b05c8fccfe7790efce14b6187eeb0fc5e2d14 Mon Sep 17 00:00:00 2001 From: qedk <1994constant@gmail.com> Date: Tue, 7 May 2024 21:52:18 +0400 Subject: [PATCH] feat: implement V1 --- script/Deploy.s.sol | 9 +- src/AvailBridgeV1.sol | 430 ++++++++++++++++++++ test/AvailBridgeV1Test.t.sol | 763 +++++++++++++++++++++++++++++++++++ 3 files changed, 1198 insertions(+), 4 deletions(-) create mode 100644 src/AvailBridgeV1.sol create mode 100644 test/AvailBridgeV1Test.t.sol diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 53da990..04cae60 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.25; import {TransparentUpgradeableProxy} from "lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; -import {AvailBridge} from "src/AvailBridge.sol"; +import {AvailBridgeV1} from "src/AvailBridgeV1.sol"; import {Avail} from "src/Avail.sol"; import {IAvail} from "src/interfaces/IAvail.sol"; import {IVectorx} from "src/interfaces/IVectorx.sol"; @@ -13,11 +13,12 @@ contract Deploy is Script { function run() external { vm.startBroadcast(); address admin = vm.envAddress("ADMIN"); + address pauser = vm.envAddress("PAUSER"); address vectorx = vm.envAddress("VECTORX"); - address impl = address(new AvailBridge()); - AvailBridge bridge = AvailBridge(address(new TransparentUpgradeableProxy(impl, admin, ""))); + address impl = address(new AvailBridgeV1()); + AvailBridgeV1 bridge = AvailBridgeV1(address(new TransparentUpgradeableProxy(impl, admin, ""))); Avail avail = new Avail(address(bridge)); - bridge.initialize(0, admin, IAvail(address(avail)), admin, admin, IVectorx(vectorx)); + bridge.initialize(0, admin, IAvail(address(avail)), admin, pauser, IVectorx(vectorx)); vm.stopBroadcast(); } } diff --git a/src/AvailBridgeV1.sol b/src/AvailBridgeV1.sol new file mode 100644 index 0000000..ae81a93 --- /dev/null +++ b/src/AvailBridgeV1.sol @@ -0,0 +1,430 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.25; + +import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import {ReentrancyGuardUpgradeable} from + "lib/openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol"; +import {PausableUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol"; +import {AccessControlDefaultAdminRulesUpgradeable} from + "lib/openzeppelin-contracts-upgradeable/contracts/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol"; +import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; +import {Merkle} from "src/lib/Merkle.sol"; +import {IVectorx} from "src/interfaces/IVectorx.sol"; +import {IAvail} from "src/interfaces/IAvail.sol"; +import {IMessageReceiver} from "src/interfaces/IMessageReceiver.sol"; +import {IAvailBridge} from "src/interfaces/IAvailBridge.sol"; + +/** + * @author @QEDK (Avail) + * @title AvailBridgeV1 + * @notice An arbitrary message bridge between Avail <-> Ethereum + * @custom:security security@availproject.org + */ +contract AvailBridgeV1 is + Initializable, + ReentrancyGuardUpgradeable, + PausableUpgradeable, + AccessControlDefaultAdminRulesUpgradeable, + IAvailBridge +{ + using Merkle for bytes32[]; + using SafeERC20 for IERC20; + + bytes1 private constant MESSAGE_TX_PREFIX = 0x01; + bytes1 private constant TOKEN_TX_PREFIX = 0x02; + uint32 private constant AVAIL_DOMAIN = 1; + uint32 private constant ETH_DOMAIN = 2; + uint256 private constant MAX_DATA_LENGTH = 102_400; + // Derived from abi.encodePacked("ETH") + // slither-disable-next-line too-many-digits + bytes32 private constant ETH_ASSET_ID = 0x4554480000000000000000000000000000000000000000000000000000000000; + bytes32 private constant PAUSER_ROLE = keccak256("PAUSER_ROLE"); + // map store spent message hashes, used for Avail -> Ethereum messages + mapping(bytes32 => bool) public isBridged; + // map message hashes to their message ID, used for Ethereum -> Avail messages + mapping(uint256 => bytes32) public isSent; + // map Avail asset IDs to an Ethereum address + mapping(bytes32 => address) public tokens; + + IVectorx public vectorx; + IAvail public avail; + address public feeRecipient; + uint256 public fees; // total fees accumulated by bridge + uint256 public feePerByte; // in wei + uint256 public messageId; // next nonce + + error Unimplemented(); + + modifier onlySupportedDomain(uint32 originDomain, uint32 destinationDomain) { + if (originDomain != AVAIL_DOMAIN || destinationDomain != ETH_DOMAIN) { + revert InvalidDomain(); + } + _; + } + + modifier onlyTokenTransfer(bytes1 messageType) { + if (messageType != TOKEN_TX_PREFIX) { + revert InvalidFungibleTokenTransfer(); + } + _; + } + + modifier checkDestAmt(bytes32 dest, uint256 amount) { + if (dest == 0x0 || amount == 0 || amount > type(uint128).max) { + revert InvalidDestinationOrAmount(); + } + _; + } + + /** + * @notice Initializes the AvailBridge contract + * @param newFeePerByte New fee per byte value + * @param newFeeRecipient New fee recipient address + * @param newAvail Address of the AVAIL token contract + * @param governance Address of the governance multisig + * @param pauser Address of the pauser multisig + * @param newVectorx Address of the VectorX contract + */ + function initialize( + uint256 newFeePerByte, + address newFeeRecipient, + IAvail newAvail, + address governance, + address pauser, + IVectorx newVectorx + ) external initializer { + feePerByte = newFeePerByte; + // slither-disable-next-line missing-zero-check + feeRecipient = newFeeRecipient; + vectorx = newVectorx; + avail = newAvail; + __AccessControlDefaultAdminRules_init(0, governance); + _grantRole(PAUSER_ROLE, pauser); + __Pausable_init(); + __ReentrancyGuard_init(); + } + + /** + * @notice Updates pause status of the bridge + * @param status New pause status + */ + function setPaused(bool status) external onlyRole(PAUSER_ROLE) { + if (status) { + _pause(); + } else { + _unpause(); + } + } + + /** + * @notice Update the address of the VectorX contract + * @param newVectorx Address of new VectorX contract + */ + function updateVectorx(IVectorx newVectorx) external onlyRole(DEFAULT_ADMIN_ROLE) { + vectorx = newVectorx; + } + + /** + * @notice Function to update asset ID -> token address mapping + * @dev Only callable by governance + * @param assetIds Asset IDs to update + * @param tokenAddresses Token addresses to update + */ + function updateTokens(bytes32[] calldata assetIds, address[] calldata tokenAddresses) + external + onlyRole(DEFAULT_ADMIN_ROLE) + { + uint256 length = assetIds.length; + if (length != tokenAddresses.length) { + revert ArrayLengthMismatch(); + } + for (uint256 i = 0; i < length;) { + tokens[assetIds[i]] = tokenAddresses[i]; + unchecked { + ++i; + } + } + } + + /** + * @notice Function to update the fee per byte value + * @dev Only callable by governance + * @param newFeePerByte New fee per byte value + */ + function updateFeePerByte(uint256 newFeePerByte) external onlyRole(DEFAULT_ADMIN_ROLE) { + feePerByte = newFeePerByte; + } + + /** + * @notice Function to update the fee recipient + * @dev Only callable by governance + * @param newFeeRecipient New fee recipient address + */ + function updateFeeRecipient(address newFeeRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) { + // slither-disable-next-line missing-zero-check + feeRecipient = newFeeRecipient; + } + + /** + * @notice Function to withdraw fees to the fee recipient + * @dev Callable by anyone because all fees are always sent to the recipient + */ + function withdrawFees() external { + uint256 fee = fees; + delete fees; + // slither-disable-next-line low-level-calls + (bool success,) = feeRecipient.call{value: fee}(""); + if (!success) { + revert WithdrawFailed(); + } + } + + /** + * @notice Takes an arbitrary message and its proof of inclusion, verifies and executes it (if valid) + * @dev This function is used for passing arbitrary data from Avail to Ethereum + * @param message Message that is used to reconstruct the bridge leaf + * @param input Merkle tree proof of inclusion for the bridge leaf + */ + function receiveMessage(Message calldata message, MerkleProofInput calldata input) + external + whenNotPaused + onlySupportedDomain(message.originDomain, message.destinationDomain) + nonReentrant + { + if (message.messageType != MESSAGE_TX_PREFIX) { + revert InvalidMessage(); + } + + _checkBridgeLeaf(message, input); + + // downcast SCALE-encoded bytes to an Ethereum address + address dest = address(bytes20(message.to)); + IMessageReceiver(dest).onAvailMessage(message.from, message.data); + + emit MessageReceived(message.from, dest, message.messageId); + } + + /** + * @notice Takes an AVAIL transfer message and its proof of inclusion, verifies and executes it (if valid) + * @dev This function is used for AVAIL transfers from Avail to Ethereum + * @param message Message that is used to reconstruct the bridge leaf + * @param input Merkle tree proof of inclusion for the bridge leaf + */ + function receiveAVAIL(Message calldata message, MerkleProofInput calldata input) + external + whenNotPaused + onlySupportedDomain(message.originDomain, message.destinationDomain) + onlyTokenTransfer(message.messageType) + { + (bytes32 assetId, uint256 value) = abi.decode(message.data, (bytes32, uint256)); + if (assetId != 0x0) { + revert InvalidAssetId(); + } + + _checkBridgeLeaf(message, input); + + // downcast SCALE-encoded bytes to an Ethereum address + address dest = address(bytes20(message.to)); + + emit MessageReceived(message.from, dest, message.messageId); + + avail.mint(dest, value); + } + + /** + * @notice Takes an ETH transfer message and its proof of inclusion, verifies and executes it (if valid) + * @dev This function is used for ETH transfers from Avail to Ethereum + * @param message Message that is used to reconstruct the bridge leaf + */ + function receiveETH(Message calldata message, MerkleProofInput calldata) + external + whenNotPaused + onlySupportedDomain(message.originDomain, message.destinationDomain) + onlyTokenTransfer(message.messageType) + nonReentrant + { + revert Unimplemented(); // not implemented + } + + /** + * @notice Takes an ERC20 transfer message and its proof of inclusion, verifies and executes it (if valid) + * @dev This function is used for ERC20 transfers from Avail to Ethereum + * @param message Message that is used to reconstruct the bridge leaf + */ + function receiveERC20(Message calldata message, MerkleProofInput calldata) + external + whenNotPaused + onlySupportedDomain(message.originDomain, message.destinationDomain) + onlyTokenTransfer(message.messageType) + nonReentrant + { + revert Unimplemented(); + } + + /** + * @notice Emits a corresponding arbitrary messag event on Avail + * @dev This function is used for passing arbitrary data from Ethereum to Avail + * @param recipient Recipient of the message on Avail + * @param data Data to send + */ + function sendMessage(bytes32 recipient, bytes calldata data) external payable whenNotPaused { + uint256 length = data.length; + if (length == 0 || length > MAX_DATA_LENGTH) { + revert InvalidDataLength(); + } + // ensure that fee is above minimum amount + if (msg.value < getFee(length)) { + revert FeeTooLow(); + } + uint256 id; + unchecked { + id = messageId++; + } + fees += msg.value; + Message memory message = Message( + MESSAGE_TX_PREFIX, bytes32(bytes20(msg.sender)), recipient, ETH_DOMAIN, AVAIL_DOMAIN, data, uint64(id) + ); + // store message hash to be retrieved later by our light client + isSent[id] = keccak256(abi.encode(message)); + + emit MessageSent(msg.sender, recipient, id); + } + + /** + * @notice Burns amount worth of AVAIL tokens and bridges it to the specified recipient on Avail + * @dev This function is used for AVAIL transfers from Ethereum to Avail + * @param recipient Recipient of the AVAIL tokens on Avail + * @param amount Amount of AVAIL tokens to bridge + */ + function sendAVAIL(bytes32 recipient, uint256 amount) external whenNotPaused checkDestAmt(recipient, amount) { + uint256 id; + unchecked { + id = messageId++; + } + Message memory message = Message( + TOKEN_TX_PREFIX, + bytes32(bytes20(msg.sender)), + recipient, + ETH_DOMAIN, + AVAIL_DOMAIN, + abi.encode(bytes32(0), amount), + uint64(id) + ); + // store message hash to be retrieved later by our light client + isSent[id] = keccak256(abi.encode(message)); + + emit MessageSent(msg.sender, recipient, id); + + avail.burn(msg.sender, amount); + } + + /** + * @notice Bridges ETH to the specified recipient on Avail + * @dev This function is used for ETH transfers from Ethereum to Avail + * @param recipient Recipient of the ETH on Avail + */ + function sendETH(bytes32 recipient) external payable whenNotPaused checkDestAmt(recipient, msg.value) { + revert Unimplemented(); // not implemented + } + + /** + * @notice Bridges ERC20 tokens to the specified recipient on Avail + * @dev This function is used for ERC20 transfers from Ethereum to Avail + * @param recipient Recipient of the asset on Avail + * @param amount Amount of ERC20 tokens to bridge + */ + function sendERC20(bytes32, bytes32 recipient, uint256 amount) + external + view + whenNotPaused + checkDestAmt(recipient, amount) + { + revert Unimplemented(); // not implemented + } + + /** + * @notice Takes a Merkle tree proof of inclusion for a blob leaf and verifies it + * @dev This function is used for data attestation on Ethereum + * @param input Merkle tree proof of inclusion for the blob leaf + * @return bool Returns true if the blob leaf is valid, else false + */ + function verifyBlobLeaf(MerkleProofInput calldata input) external view returns (bool) { + if (input.blobRoot == 0x0) { + revert BlobRootEmpty(); + } + _checkDataRoot(input); + // leaf must be keccak(blob) + // we don't need to check that the leaf is non-zero because we hash the pre-image here + return input.leafProof.verify(input.blobRoot, input.leafIndex, keccak256(abi.encode(input.leaf))); + } + + /** + * @notice Takes a Merkle tree proof of inclusion for a bridge leaf and verifies it + * @dev This function does not validate that the leaf itself is valid, only that it's included + * @param input Merkle tree proof of inclusion for the bridge leaf + * @return bool Returns true if the bridge leaf is valid, else false + */ + function verifyBridgeLeaf(MerkleProofInput calldata input) public view returns (bool) { + if (input.bridgeRoot == 0x0) { + revert BridgeRootEmpty(); + } + _checkDataRoot(input); + // leaf must be keccak(message) + // we don't need to check that the leaf is non-zero because we check that the root is non-zero + return input.leafProof.verify(input.bridgeRoot, input.leafIndex, input.leaf); + } + + /** + * @notice Returns the minimum fee for a given message length + * @param length Length of the message (in bytes) + * @return uint256 The minimum fee + */ + function getFee(uint256 length) public view returns (uint256) { + return length * feePerByte; + } + + /** + * @notice Takes a message and its proof of inclusion, verifies and marks it as spent (if valid) + * @dev This function is used for verifying a message and marking it as spent (if valid) + * @param message Message that is used to reconstruct the bridge leaf + * @param input Merkle tree proof of inclusion for the bridge leaf + */ + function _checkBridgeLeaf(Message calldata message, MerkleProofInput calldata input) private { + bytes32 leaf = keccak256(abi.encode(message)); + if (isBridged[leaf]) { + revert AlreadyBridged(); + } + // validate that the leaf being proved is indeed the message hash! + if (input.leaf != leaf) { + revert InvalidLeaf(); + } + // check proof of inclusion + if (!verifyBridgeLeaf(input)) { + revert InvalidMerkleProof(); + } + // mark as spent + isBridged[leaf] = true; + } + + /** + * @notice Takes a Merkle proof of inclusion, and verifies it + * @dev This function is used for verifying a Merkle proof of inclusion for a data root + * @param input Merkle tree proof of inclusion for the data root + */ + function _checkDataRoot(MerkleProofInput calldata input) private view { + bytes32 dataRootCommitment = vectorx.dataRootCommitments(input.rangeHash); + if (dataRootCommitment == 0x0) { + revert DataRootCommitmentEmpty(); + } + // we construct the data root here internally, it is not possible to create an invalid data root that is + // also part of the commitment tree + if ( + !input.dataRootProof.verifySha2( + dataRootCommitment, input.dataRootIndex, keccak256(abi.encode(input.blobRoot, input.bridgeRoot)) + ) + ) { + revert InvalidDataRootProof(); + } + } +} diff --git a/test/AvailBridgeV1Test.t.sol b/test/AvailBridgeV1Test.t.sol new file mode 100644 index 0000000..dad4b08 --- /dev/null +++ b/test/AvailBridgeV1Test.t.sol @@ -0,0 +1,763 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.25; + +import {TransparentUpgradeableProxy} from + "lib/openzeppelin-contracts/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {IAccessControl} from "lib/openzeppelin-contracts/contracts/access/IAccessControl.sol"; +import {Pausable} from "lib/openzeppelin-contracts/contracts/utils/Pausable.sol"; +import {IAvailBridge, AvailBridgeV1} from "src/AvailBridgeV1.sol"; +import {Avail, IAvail} from "src/Avail.sol"; +import {VectorxMock, IVectorx} from "src/mocks/VectorxMock.sol"; +import {ERC20Mock} from "src/mocks/ERC20Mock.sol"; +import {MessageReceiverMock} from "src/mocks/MessageReceiverMock.sol"; +import {MurkyBase} from "lib/murky/src/common/MurkyBase.sol"; +import {Vm, Test, console} from "forge-std/Test.sol"; + +contract AvailBridgeV1Test is Test, MurkyBase { + AvailBridgeV1 public bridge; + Avail public avail; + VectorxMock public vectorx; + Sha2Merkle public sha2merkle; + address public owner; + address public pauser; + bytes public constant revertCode = "5F5FFD"; + + function setUp() external { + vectorx = new VectorxMock(); + pauser = makeAddr("pauser"); + sha2merkle = new Sha2Merkle(); + address impl = address(new AvailBridgeV1()); + bridge = AvailBridgeV1(address(new TransparentUpgradeableProxy(impl, msg.sender, ""))); + avail = new Avail(address(bridge)); + bridge.initialize(0, msg.sender, IAvail(address(avail)), msg.sender, pauser, IVectorx(vectorx)); + owner = msg.sender; + } + + function test_owner() external { + assertNotEq(bridge.owner(), address(0)); + assertEq(bridge.owner(), owner); + } + + function test_feeRecipient() external { + assertNotEq(bridge.feeRecipient(), address(0)); + assertEq(bridge.feeRecipient(), owner); + } + + function testRevertUnauthorizedAccount_setFeePerByte(uint256 feePerByte) external { + address rand = makeAddr("rand"); + vm.assume(rand != owner); + vm.expectRevert(abi.encodeWithSelector((IAccessControl.AccessControlUnauthorizedAccount.selector), rand, 0x0)); + vm.prank(rand); + bridge.updateFeePerByte(feePerByte); + } + + function test_setFeePerByte(uint256 feePerByte) external { + vm.prank(owner); + bridge.updateFeePerByte(feePerByte); + assertEq(bridge.feePerByte(), feePerByte); + } + + function testRevertUnauthorizedAccount_updateVectorx(IVectorx newVectorx) external { + address rand = makeAddr("rand"); + vm.assume(rand != owner); + vm.expectRevert(abi.encodeWithSelector((IAccessControl.AccessControlUnauthorizedAccount.selector), rand, 0x0)); + vm.prank(rand); + bridge.updateVectorx(newVectorx); + } + + function test_updateVectorx(IVectorx newVectorx) external { + vm.prank(owner); + bridge.updateVectorx(newVectorx); + assertEq(address(bridge.vectorx()), address(newVectorx)); + } + + function testRevertUnauthorizedAccount_updateFeeRecipient(address newFeeRecipient) external { + address rand = makeAddr("rand"); + vm.assume(rand != owner); + vm.expectRevert(abi.encodeWithSelector((IAccessControl.AccessControlUnauthorizedAccount.selector), rand, 0x0)); + vm.prank(rand); + bridge.updateFeeRecipient(newFeeRecipient); + } + + function test_updateFeeRecipient(address newFeeRecipient) external { + vm.prank(owner); + bridge.updateFeeRecipient(newFeeRecipient); + assertEq(bridge.feeRecipient(), newFeeRecipient); + } + + function testRevertUnauthorizedAccount_updateTokens() external { + address rand = makeAddr("rand"); + vm.assume(rand != owner); + vm.expectRevert(abi.encodeWithSelector((IAccessControl.AccessControlUnauthorizedAccount.selector), rand, 0x0)); + vm.prank(rand); + bridge.updateTokens(new bytes32[](0), new address[](0)); + } + + function testRevertArrayLengthMismatch_updateTokens(uint8 len1, uint8 len2) external { + // using len > uint8 slows tests by a *lot* + vm.assume(len1 != len2); + bytes32[] memory assetIds = new bytes32[](len1); + address[] memory addresses = new address[](len2); + vm.prank(owner); + vm.expectRevert(IAvailBridge.ArrayLengthMismatch.selector); + bridge.updateTokens(assetIds, addresses); + } + + function test_updateTokens(uint256 val1, uint256 val2, uint8 len) external { + // we do this to get unique assetIds and addresses + bytes32[] memory assetIds = new bytes32[](len); + address[] memory addresses = new address[](len); + for (uint256 i = 0; i < len;) { + assetIds[i] = keccak256(abi.encode(val1, i)); + addresses[i] = makeAddr(string(abi.encode(val2, i))); + unchecked { + ++i; + } + } + vm.prank(owner); + bridge.updateTokens(assetIds, addresses); + for (uint256 i = 0; i < len;) { + assertEq(bridge.tokens(assetIds[i]), addresses[i]); + unchecked { + ++i; + } + } + } + + function testRevertInvalidMessage_receiveMessage(bytes1 prefix) external { + vm.assume(prefix != 0x01); + IAvailBridge.Message memory message = IAvailBridge.Message(prefix, bytes32(0), bytes32(0), 1, 2, "", 0); + IAvailBridge.MerkleProofInput memory input = IAvailBridge.MerkleProofInput( + new bytes32[](0), new bytes32[](0), bytes32(0), 0, bytes32(0), bytes32(0), bytes32(0), 0 + ); + vm.expectRevert(IAvailBridge.InvalidMessage.selector); + bridge.receiveMessage(message, input); + assertFalse(bridge.isBridged(keccak256(abi.encode(message)))); + } + + function testRevertOnlyPauser_setPaused(bool status) external { + address rand = makeAddr("rand"); + vm.expectRevert( + abi.encodeWithSelector( + (IAccessControl.AccessControlUnauthorizedAccount.selector), rand, keccak256("PAUSER_ROLE") + ) + ); + vm.prank(rand); + bridge.setPaused(status); + } + + function test_setPaused() external { + vm.startPrank(pauser); + bridge.setPaused(true); + assertTrue(bridge.paused()); + bridge.setPaused(false); + assertFalse(bridge.paused()); + } + + function test_setPausedWithMessage(bytes32 rangeHash, bytes calldata data, bytes32 from, uint64 messageId) + external + { + vm.startPrank(pauser); + bridge.setPaused(true); + assertTrue(bridge.paused()); + MessageReceiverMock messageReceiver = new MessageReceiverMock(); + messageReceiver.initialize(address(bridge)); + + IAvailBridge.Message memory message = + IAvailBridge.Message(0x01, from, bytes32(bytes20(address(messageReceiver))), 1, 2, data, messageId); + bytes32 messageHash = keccak256(abi.encode(message)); + bytes32 dataRoot = keccak256(abi.encode(bytes32(0), messageHash)); + + vectorx.set(rangeHash, dataRoot); + + bytes32[] memory emptyArr; + IAvailBridge.MerkleProofInput memory input = + IAvailBridge.MerkleProofInput(emptyArr, emptyArr, rangeHash, 0, bytes32(0), messageHash, messageHash, 0); + + vm.expectRevert(Pausable.EnforcedPause.selector); + bridge.receiveMessage(message, input); + assertFalse(bridge.isBridged(messageHash)); + bridge.setPaused(false); + assertFalse(bridge.paused()); + vm.expectCall(address(messageReceiver), abi.encodeCall(messageReceiver.onAvailMessage, (from, data))); + bridge.receiveMessage(message, input); + assertTrue(bridge.isBridged(messageHash)); + } + + function test_receiveMessage(bytes32 rangeHash, bytes calldata data, bytes32 from, uint64 messageId) external { + MessageReceiverMock messageReceiver = new MessageReceiverMock(); + messageReceiver.initialize(address(bridge)); + + IAvailBridge.Message memory message = + IAvailBridge.Message(0x01, from, bytes32(bytes20(address(messageReceiver))), 1, 2, data, messageId); + bytes32 messageHash = keccak256(abi.encode(message)); + bytes32 dataRoot = keccak256(abi.encode(bytes32(0), messageHash)); + + vectorx.set(rangeHash, dataRoot); + + bytes32[] memory emptyArr; + IAvailBridge.MerkleProofInput memory input = + IAvailBridge.MerkleProofInput(emptyArr, emptyArr, rangeHash, 0, bytes32(0), messageHash, messageHash, 0); + + vm.expectCall(address(messageReceiver), abi.encodeCall(messageReceiver.onAvailMessage, (from, data))); + bridge.receiveMessage(message, input); + assertTrue(bridge.isBridged(messageHash)); + } + + function testRevertAlreadyBridged_receiveMessage( + bytes32 rangeHash, + bytes calldata data, + bytes32 from, + uint64 messageId + ) external { + MessageReceiverMock messageReceiver = new MessageReceiverMock(); + messageReceiver.initialize(address(bridge)); + + IAvailBridge.Message memory message = + IAvailBridge.Message(0x01, from, bytes32(bytes20(address(messageReceiver))), 1, 2, data, messageId); + bytes32 messageHash = keccak256(abi.encode(message)); + bytes32 dataRoot = keccak256(abi.encode(bytes32(0), messageHash)); + + vectorx.set(rangeHash, dataRoot); + + bytes32[] memory emptyArr; + IAvailBridge.MerkleProofInput memory input = + IAvailBridge.MerkleProofInput(emptyArr, emptyArr, rangeHash, 0, bytes32(0), messageHash, messageHash, 0); + + vm.expectCall(address(messageReceiver), abi.encodeCall(messageReceiver.onAvailMessage, (from, data))); + bridge.receiveMessage(message, input); + assertTrue(bridge.isBridged(messageHash)); + vm.expectRevert(IAvailBridge.AlreadyBridged.selector); + bridge.receiveMessage(message, input); + assertTrue(bridge.isBridged(messageHash)); + } + + function testRevertInvalidLeaf_receiveMessage( + bytes32 rangeHash, + bytes calldata data, + bytes32 from, + uint64 messageId + ) external { + MessageReceiverMock messageReceiver = new MessageReceiverMock(); + messageReceiver.initialize(address(bridge)); + + IAvailBridge.Message memory message = + IAvailBridge.Message(0x01, from, bytes32(bytes20(address(messageReceiver))), 1, 2, data, messageId); + bytes32 messageHash = keccak256(abi.encode(message)); + bytes32 dataRoot = keccak256(abi.encode(bytes32(0), messageHash)); + + vectorx.set(rangeHash, dataRoot); + + bytes32[] memory emptyArr; + // hash the hash to generate a wrong leaf + IAvailBridge.MerkleProofInput memory input = IAvailBridge.MerkleProofInput( + emptyArr, emptyArr, rangeHash, 0, bytes32(0), messageHash, keccak256(abi.encode(messageHash)), 0 + ); + + vm.expectRevert(IAvailBridge.InvalidLeaf.selector); + bridge.receiveMessage(message, input); + assertFalse(bridge.isBridged(messageHash)); + } + + function testRevertInvalidMerkleProof_receiveMessage( + bytes32 rangeHash, + bytes calldata data, + bytes32 from, + uint64 messageId, + bytes32[] calldata wrongProof, + uint256 wrongLeafIndex + ) external { + vm.assume(wrongLeafIndex != 0 && wrongProof.length != 0); + MessageReceiverMock messageReceiver = new MessageReceiverMock(); + messageReceiver.initialize(address(bridge)); + + IAvailBridge.Message memory message = + IAvailBridge.Message(0x01, from, bytes32(bytes20(address(messageReceiver))), 1, 2, data, messageId); + bytes32 messageHash = keccak256(abi.encode(message)); + bytes32 dataRoot = keccak256(abi.encode(bytes32(0), messageHash)); + + vectorx.set(rangeHash, dataRoot); + + bytes32[] memory emptyArr; + // give a fuzzed wrong index + IAvailBridge.MerkleProofInput memory input = IAvailBridge.MerkleProofInput( + emptyArr, emptyArr, rangeHash, 0, bytes32(0), messageHash, messageHash, wrongLeafIndex + ); + + vm.expectRevert(IAvailBridge.InvalidMerkleProof.selector); + bridge.receiveMessage(message, input); + assertFalse(bridge.isBridged(messageHash)); + // give a fuzzed wrong proof + input = + IAvailBridge.MerkleProofInput(emptyArr, wrongProof, rangeHash, 0, bytes32(0), messageHash, messageHash, 0); + + vm.expectRevert(IAvailBridge.InvalidMerkleProof.selector); + bridge.receiveMessage(message, input); + assertFalse(bridge.isBridged(messageHash)); + + // give a fuzzed wrong proof and index + input = IAvailBridge.MerkleProofInput( + emptyArr, wrongProof, rangeHash, 0, bytes32(0), messageHash, messageHash, wrongLeafIndex + ); + + vm.expectRevert(IAvailBridge.InvalidMerkleProof.selector); + bridge.receiveMessage(message, input); + assertFalse(bridge.isBridged(messageHash)); + } + + function testRevertDataRootCommitmentEmpty_receiveMessage( + bytes32 rangeHash, + bytes calldata data, + bytes32 from, + uint64 messageId + ) external { + MessageReceiverMock messageReceiver = new MessageReceiverMock(); + messageReceiver.initialize(address(bridge)); + + IAvailBridge.Message memory message = + IAvailBridge.Message(0x01, from, bytes32(bytes20(address(messageReceiver))), 1, 2, data, messageId); + bytes32 messageHash = keccak256(abi.encode(message)); + + bytes32[] memory emptyArr; + IAvailBridge.MerkleProofInput memory input = + IAvailBridge.MerkleProofInput(emptyArr, emptyArr, rangeHash, 0, bytes32(0), messageHash, messageHash, 0); + + // data root is not set in vectorx! + vm.expectRevert(IAvailBridge.DataRootCommitmentEmpty.selector); + bridge.receiveMessage(message, input); + assertFalse(bridge.isBridged(messageHash)); + } + + function testRevertInvalidDataRootProof_receiveMessage( + bytes32 rangeHash, + bytes calldata data, + bytes32 from, + uint64 messageId, + bytes32[] calldata wrongProof, + uint256 wrongIndex + ) external { + vm.assume(wrongIndex != 0 && wrongProof.length != 0); + MessageReceiverMock messageReceiver = new MessageReceiverMock(); + messageReceiver.initialize(address(bridge)); + + IAvailBridge.Message memory message = + IAvailBridge.Message(0x01, from, bytes32(bytes20(address(messageReceiver))), 1, 2, data, messageId); + bytes32 messageHash = keccak256(abi.encode(message)); + bytes32 dataRoot = keccak256(abi.encode(bytes32(0), messageHash)); + + vectorx.set(rangeHash, dataRoot); + + bytes32[] memory emptyArr; + // give fuzzed wrong proof + IAvailBridge.MerkleProofInput memory input = + IAvailBridge.MerkleProofInput(wrongProof, emptyArr, rangeHash, 0, bytes32(0), messageHash, messageHash, 0); + + vm.expectRevert(IAvailBridge.InvalidDataRootProof.selector); + bridge.receiveMessage(message, input); + assertFalse(bridge.isBridged(messageHash)); + + // give fuzzed wrong index + input = IAvailBridge.MerkleProofInput( + emptyArr, emptyArr, rangeHash, wrongIndex, bytes32(0), messageHash, messageHash, 0 + ); + + vm.expectRevert(IAvailBridge.InvalidDataRootProof.selector); + bridge.receiveMessage(message, input); + assertFalse(bridge.isBridged(messageHash)); + + // give fuzzed wrong proof and wrong index + input = IAvailBridge.MerkleProofInput( + wrongProof, emptyArr, rangeHash, wrongIndex, bytes32(0), messageHash, messageHash, 0 + ); + + vm.expectRevert(IAvailBridge.InvalidDataRootProof.selector); + bridge.receiveMessage(message, input); + assertFalse(bridge.isBridged(messageHash)); + } + + function testRevertInvalidAssetId_receiveAvail(bytes32 assetId) external { + vm.assume(assetId != 0x0); + IAvailBridge.Message memory message = + IAvailBridge.Message(0x02, bytes32(0), bytes32(0), 1, 2, abi.encode(assetId, 0), 0); + IAvailBridge.MerkleProofInput memory input = IAvailBridge.MerkleProofInput( + new bytes32[](0), new bytes32[](0), bytes32(0), 0, bytes32(0), bytes32(0), bytes32(0), 0 + ); + vm.expectRevert(IAvailBridge.InvalidAssetId.selector); + bridge.receiveAVAIL(message, input); + assertFalse(bridge.isBridged(keccak256(abi.encode(message)))); + } + + function test_receiveAVAIL(bytes32 rangeHash, bytes32 from, uint256 amount, uint64 messageId) external { + vm.assume(amount != 0); + address to = makeAddr("to"); + IAvailBridge.Message memory message = + IAvailBridge.Message(0x02, from, bytes32(bytes20(to)), 1, 2, abi.encode(bytes32(0), amount), messageId); + bytes32 messageHash = keccak256(abi.encode(message)); + bytes32 dataRoot = keccak256(abi.encode(bytes32(0), messageHash)); + + vectorx.set(rangeHash, dataRoot); + + bytes32[] memory emptyArr; + IAvailBridge.MerkleProofInput memory input = + IAvailBridge.MerkleProofInput(emptyArr, emptyArr, rangeHash, 0, bytes32(0), messageHash, messageHash, 0); + + vm.expectCall(address(avail), abi.encodeCall(avail.mint, (to, amount))); + bridge.receiveAVAIL(message, input); + assertTrue(bridge.isBridged(messageHash)); + assertEq(avail.totalSupply(), amount); + } + + function test_receiveAVAIL_2( + bytes32 rangeHash, + uint64 messageId, + bytes32[16] calldata c_leaves, + bytes32[16] calldata c_dataRoots, + uint256 rand, + bytes32 blobRoot + ) external { + // this function is a bit unreadable because forge coverage does not support IR compilation which results + // in stack too deep errors + bytes32[] memory dataRoots = new bytes32[](c_dataRoots.length); + bytes32[] memory leaves = new bytes32[](c_leaves.length); + for (uint256 i = 0; i < c_leaves.length;) { + dataRoots[i] = c_dataRoots[i]; + leaves[i] = c_leaves[i]; + unchecked { + ++i; + } + } + address to = makeAddr("to"); + leaves[rand % leaves.length] = keccak256( + abi.encode( + IAvailBridge.Message( + 0x02, bytes32("1"), bytes32(bytes20(to)), 1, 2, abi.encode(bytes32(0), 1), messageId + ) + ) + ); + // set dataRoot at this point in the array + dataRoots[rand % dataRoots.length] = hashLeafPairs(blobRoot, getRoot(leaves)); + vectorx.set(rangeHash, sha2merkle.getRoot(dataRoots)); + + vm.expectCall(address(avail), abi.encodeCall(avail.mint, (to, 1))); + { + bridge.receiveAVAIL( + IAvailBridge.Message( + 0x02, bytes32("1"), bytes32(bytes20(to)), 1, 2, abi.encode(bytes32(0), 1), messageId + ), + IAvailBridge.MerkleProofInput( + sha2merkle.getProof(dataRoots, rand % dataRoots.length), + getProof(leaves, rand % leaves.length), + rangeHash, + rand % dataRoots.length, + blobRoot, + getRoot(leaves), + keccak256( + abi.encode( + IAvailBridge.Message( + 0x02, bytes32("1"), bytes32(bytes20(to)), 1, 2, abi.encode(bytes32(0), 1), messageId + ) + ) + ), + rand % leaves.length + ) + ); + } + { + assertTrue( + bridge.isBridged( + keccak256( + abi.encode( + IAvailBridge.Message( + 0x02, bytes32("1"), bytes32(bytes20(to)), 1, 2, abi.encode(bytes32(0), 1), messageId + ) + ) + ) + ) + ); + } + { + assertEq(avail.totalSupply(), 1); + } + } + + function test_receiveETH(bytes32 rangeHash, bytes32 from, uint256 amount, uint64 messageId) external { + vm.assume(amount != 0); + address to = makeAddr("to"); + vm.deal(address(bridge), amount); + IAvailBridge.Message memory message = IAvailBridge.Message( + 0x02, + from, + bytes32(bytes20(to)), + 1, + 2, + abi.encode(0x4554480000000000000000000000000000000000000000000000000000000000, amount), + messageId + ); + bytes32 messageHash = keccak256(abi.encode(message)); + bytes32 dataRoot = keccak256(abi.encode(bytes32(0), messageHash)); + + vectorx.set(rangeHash, dataRoot); + + bytes32[] memory emptyArr; + IAvailBridge.MerkleProofInput memory input = + IAvailBridge.MerkleProofInput(emptyArr, emptyArr, rangeHash, 0, bytes32(0), messageHash, messageHash, 0); + + vm.expectRevert(AvailBridgeV1.Unimplemented.selector); + bridge.receiveETH(message, input); + } + + function test_receiveERC20(bytes32 rangeHash, bytes32 assetId, bytes32 from, uint256 amount, uint64 messageId) + external + { + vm.assume(amount != 0); + address to = makeAddr("to"); + ERC20Mock token = new ERC20Mock(); + token.mint(address(bridge), amount); + bytes32[] memory assetIdArr = new bytes32[](1); + assetIdArr[0] = assetId; + address[] memory tokenArr = new address[](1); + tokenArr[0] = address(token); + vm.prank(owner); + bridge.updateTokens(assetIdArr, tokenArr); + IAvailBridge.Message memory message = + IAvailBridge.Message(0x02, from, bytes32(bytes20(to)), 1, 2, abi.encode(assetId, amount), messageId); + bytes32 messageHash = keccak256(abi.encode(message)); + bytes32 dataRoot = keccak256(abi.encode(bytes32(0), messageHash)); + + vectorx.set(rangeHash, dataRoot); + + bytes32[] memory emptyArr; + IAvailBridge.MerkleProofInput memory input = + IAvailBridge.MerkleProofInput(emptyArr, emptyArr, rangeHash, 0, bytes32(0), messageHash, messageHash, 0); + + vm.expectRevert(); + bridge.receiveERC20(message, input); + } + + function testRevertInvalidDataLength_sendMessage(bytes32 to, bytes32[3201] calldata c_data, uint256 amount) + external + { + bytes memory data = abi.encodePacked(c_data); + address from = makeAddr("from"); + vm.prank(from); + vm.deal(from, amount); + vm.expectRevert(IAvailBridge.InvalidDataLength.selector); + bridge.sendMessage{value: amount}(to, data); + assertEq(bridge.isSent(0), 0x0); + assertEq(bridge.fees(), 0); + } + + function testRevertInvalidDataLength_sendMessage(bytes32 to, uint256 amount) external { + address from = makeAddr("from"); + vm.prank(from); + vm.deal(from, amount); + vm.expectRevert(IAvailBridge.InvalidDataLength.selector); + bridge.sendMessage{value: amount}(to, ""); + assertEq(bridge.isSent(0), 0x0); + assertEq(bridge.fees(), 0); + } + + function testRevertFeeTooLow_sendMessage(bytes32 to, bytes calldata data, uint32 feePerByte, uint256 amount) + external + { + vm.assume(feePerByte != 0 && data.length != 0 && data.length < 102_400); + vm.prank(owner); + bridge.updateFeePerByte(feePerByte); + vm.assume(amount < bridge.getFee(data.length)); + address from = makeAddr("from"); + vm.prank(from); + vm.deal(from, amount); + vm.expectRevert(IAvailBridge.FeeTooLow.selector); + bridge.sendMessage{value: amount}(to, data); + assertEq(bridge.isSent(0), 0x0); + assertEq(bridge.fees(), 0); + } + + function test_sendMessage(bytes32 to, bytes calldata data, uint32 feePerByte, uint256 amount) external { + vm.prank(owner); + bridge.updateFeePerByte(feePerByte); + vm.assume(data.length != 0 && data.length < 102_400 && amount >= bridge.getFee(data.length)); + address from = makeAddr("from"); + IAvailBridge.Message memory message = IAvailBridge.Message(0x01, bytes32(bytes20(from)), to, 2, 1, data, 0); + vm.prank(from); + vm.deal(from, amount); + bridge.sendMessage{value: amount}(to, data); + assertEq(bridge.isSent(0), keccak256(abi.encode(message))); + assertEq(bridge.fees(), amount); + } + + function testRevertWithdrawalFailed_withdrawFees(bytes32 to, bytes calldata data, uint32 feePerByte, uint256 amount) + external + { + vm.prank(owner); + bridge.updateFeePerByte(feePerByte); + vm.assume(data.length != 0 && data.length < 102_400 && amount >= bridge.getFee(data.length)); + address from = makeAddr("from"); + IAvailBridge.Message memory message = IAvailBridge.Message(0x01, bytes32(bytes20(from)), to, 2, 1, data, 0); + vm.prank(from); + vm.deal(from, amount); + bridge.sendMessage{value: amount}(to, data); + assertEq(bridge.isSent(0), keccak256(abi.encode(message))); + assertEq(bridge.fees(), amount); + + uint256 balance = bridge.feeRecipient().balance; + vm.etch(bridge.feeRecipient(), revertCode); + vm.expectRevert(IAvailBridge.WithdrawFailed.selector); + bridge.withdrawFees(); + assertEq(bridge.feeRecipient().balance, balance); + assertEq(bridge.fees(), amount); + } + + function test_withdrawFees(bytes32 to, bytes calldata data, uint32 feePerByte, uint248 amount) external { + vm.prank(owner); + bridge.updateFeePerByte(feePerByte); + vm.assume(data.length != 0 && data.length < 102_400 && amount >= bridge.getFee(data.length)); + address from = makeAddr("from"); + IAvailBridge.Message memory message = IAvailBridge.Message(0x01, bytes32(bytes20(from)), to, 2, 1, data, 0); + vm.prank(from); + vm.deal(from, amount); + bridge.sendMessage{value: amount}(to, data); + assertEq(bridge.isSent(0), keccak256(abi.encode(message))); + assertEq(bridge.fees(), amount); + + uint256 balance = bridge.feeRecipient().balance; + bridge.withdrawFees(); + assertEq(bridge.feeRecipient().balance, balance + amount); + assertEq(bridge.fees(), 0); + } + + function test_sendAVAIL(bytes32 to, uint128 amount) external { + vm.assume(to != bytes32(0) && amount != 0); + address from = makeAddr("from"); + vm.prank(address(bridge)); + avail.mint(from, amount); + IAvailBridge.Message memory message = + IAvailBridge.Message(0x02, bytes32(bytes20(from)), to, 2, 1, abi.encode(bytes32(0), amount), 0); + vm.expectCall(address(avail), abi.encodeCall(avail.burn, (from, amount))); + vm.prank(from); + bridge.sendAVAIL(to, amount); + assertEq(bridge.isSent(0), keccak256(abi.encode(message))); + assertEq(avail.balanceOf(from), 0); + assertEq(avail.totalSupply(), 0); + } + + function test_sendETH(bytes32 to, uint128 amount) external { + vm.assume(to != bytes32(0) && amount != 0); + address from = makeAddr("from"); + vm.deal(from, amount); + vm.prank(from); + vm.expectRevert(AvailBridgeV1.Unimplemented.selector); + bridge.sendETH{value: amount}(to); + } + + function test_sendERC20(bytes32 assetId, bytes32 to, uint128 amount) external { + vm.assume(to != bytes32(0) && amount != 0); + address from = makeAddr("from"); + ERC20Mock token = new ERC20Mock(); + token.mint(from, amount); + bytes32[] memory assetIdArr = new bytes32[](1); + assetIdArr[0] = assetId; + address[] memory tokenArr = new address[](1); + tokenArr[0] = address(token); + vm.prank(owner); + bridge.updateTokens(assetIdArr, tokenArr); + vm.startPrank(from); + token.approve(address(bridge), amount); + vm.expectRevert(AvailBridgeV1.Unimplemented.selector); + bridge.sendERC20(assetId, to, amount); + } + + function testRevertBlobRootEmpty_verifyBlobLeaf(IAvailBridge.MerkleProofInput memory input) external { + input.blobRoot = 0x0; + vm.expectRevert(IAvailBridge.BlobRootEmpty.selector); + bridge.verifyBlobLeaf(input); + } + + function test_verifyBlobLeaf( + bytes32[16] calldata preimages, + bytes32[16] calldata c_dataRoots, + bytes32 rangeHash, + uint256 rand, + bytes32 bridgeRoot + ) external { + // we use a fixed size array because the fuzzer rejects too many inputs with arbitrary lengths + bytes32[] memory dataRoots = new bytes32[](c_dataRoots.length); + bytes32[] memory leaves = new bytes32[](preimages.length); + for (uint256 i = 0; i < preimages.length;) { + dataRoots[i] = c_dataRoots[i]; + leaves[i] = keccak256(abi.encode(preimages[i])); + unchecked { + ++i; + } + } + bytes32 blobRoot = getRoot(leaves); + bytes32 dataRoot = hashLeafPairs(blobRoot, bridgeRoot); + // set dataRoot at this point in the array + dataRoots[rand % dataRoots.length] = dataRoot; + bytes32 dataRootCommitment = sha2merkle.getRoot(dataRoots); + bytes32[] memory dataRootProof = sha2merkle.getProof(dataRoots, rand % dataRoots.length); + vectorx.set(rangeHash, dataRootCommitment); + for (uint256 i = 0; i < leaves.length;) { + bytes32[] memory leafProof = getProof(leaves, i); + IAvailBridge.MerkleProofInput memory input = IAvailBridge.MerkleProofInput( + dataRootProof, leafProof, rangeHash, rand % dataRoots.length, blobRoot, bridgeRoot, preimages[i], i + ); + assertTrue(bridge.verifyBlobLeaf(input)); + unchecked { + ++i; + } + } + } + + function testRevertBridgeRootEmpty_verifyBridgeLeaf(IAvailBridge.MerkleProofInput memory input) external { + input.bridgeRoot = 0x0; + vm.expectRevert(IAvailBridge.BridgeRootEmpty.selector); + bridge.verifyBridgeLeaf(input); + } + + function test_verifyBridgeLeaf( + bytes32[16] calldata c_leaves, + bytes32[16] calldata c_dataRoots, + bytes32 rangeHash, + uint256 rand, + bytes32 blobRoot + ) external { + // we use a fixed size array because the fuzzer rejects too many inputs with arbitrary lengths + bytes32[] memory dataRoots = new bytes32[](c_dataRoots.length); + bytes32[] memory leaves = new bytes32[](c_leaves.length); + for (uint256 i = 0; i < c_leaves.length;) { + dataRoots[i] = c_dataRoots[i]; + leaves[i] = c_leaves[i]; + unchecked { + ++i; + } + } + bytes32 bridgeRoot = getRoot(leaves); + bytes32 dataRoot = hashLeafPairs(blobRoot, bridgeRoot); + // set dataRoot at this point in the array + dataRoots[rand % dataRoots.length] = dataRoot; + bytes32 dataRootCommitment = sha2merkle.getRoot(dataRoots); + bytes32[] memory dataRootProof = sha2merkle.getProof(dataRoots, rand % dataRoots.length); + vectorx.set(rangeHash, dataRootCommitment); + for (uint256 i = 0; i < leaves.length;) { + bytes32[] memory leafProof = getProof(leaves, i); + IAvailBridge.MerkleProofInput memory input = IAvailBridge.MerkleProofInput( + dataRootProof, leafProof, rangeHash, rand % dataRoots.length, blobRoot, bridgeRoot, leaves[i], i + ); + assertTrue(bridge.verifyBridgeLeaf(input)); + unchecked { + ++i; + } + } + } + + function hashLeafPairs(bytes32 left, bytes32 right) public pure override returns (bytes32) { + return keccak256(abi.encode(left, right)); + } +} + +contract Sha2Merkle is MurkyBase { + function hashLeafPairs(bytes32 left, bytes32 right) public pure override returns (bytes32) { + return sha256(abi.encode(left, right)); + } +}