diff --git a/.gitmodules b/.gitmodules index 8f5beb22c..a0cdd7880 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "crates/evm/src/evm/system_contracts/lib/forge-std"] path = crates/evm/src/evm/system_contracts/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "contracts/lib/bitcoin-spv"] + path = crates/evm/src/evm/system_contracts/lib/bitcoin-spv + url = https://github.com/keep-network/bitcoin-spv diff --git a/crates/evm/src/evm/system_contracts/lib/bitcoin-spv b/crates/evm/src/evm/system_contracts/lib/bitcoin-spv new file mode 160000 index 000000000..856849612 --- /dev/null +++ b/crates/evm/src/evm/system_contracts/lib/bitcoin-spv @@ -0,0 +1 @@ +Subproject commit 856849612ef49114af18c0f407eaa74afc2ee4be diff --git a/crates/evm/src/evm/system_contracts/src/Bridge.sol b/crates/evm/src/evm/system_contracts/src/Bridge.sol new file mode 100644 index 000000000..b593febcc --- /dev/null +++ b/crates/evm/src/evm/system_contracts/src/Bridge.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "bitcoin-spv/solidity/contracts/ValidateSPV.sol"; +import "bitcoin-spv/solidity/contracts/BTCUtils.sol"; + +import "./MerkleTree.sol"; +import "./L1BlockHashList.sol"; + +/// @title Bridge contract of Clementine +/// @author Citrea + +contract Bridge is MerkleTree, Ownable { + // TODO: Update this to be the actual address of the L1BlockHashList contract + L1BlockHashList public constant BLOCK_HASH_LIST = L1BlockHashList(address(0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD)); + + bytes public DEPOSIT_TXOUT_0 = hex"c2ddf50500000000225120fc6eb6fa4fd4ed1e8519a7edfa171eddcedfbd0e0be49b5e531ef36e7e66eb05"; + uint256 public constant DEPOSIT_AMOUNT = 1 ether; + address public operator; + mapping(bytes32 => bool) public blockHashes; + mapping(bytes32 => bool) public spentTxIds; + + event Deposit(bytes32 txId, uint256 timestamp); + event Withdrawal(bytes32 bitcoin_address, uint32 indexed leafIndex, uint256 timestamp); + event DepositTxOutUpdate(bytes oldExpectedVout0, bytes newExpectedVout0); + event BlockHashAdded(bytes32 block_hash); + event OperatorUpdated(address oldOperator, address newOperator); + + modifier onlyOperator() { + require(msg.sender == operator, "caller is not the operator"); + _; + } + + constructor(uint32 _levels) MerkleTree(_levels) {} + + /// @notice Sets the expected first transaction output of a deposit transaction on Bitcoin, which signifies the multisig address on Bitcoin + /// @dev TxOut0 is derived from the multisig on Bitcoin so it stays constant as long as the multisig doesn't change + /// @param _depositTxOut0 The new expected first transaction output of a deposit transaction on Bitcoin + function setDepositTxOut0(bytes calldata _depositTxOut0) external onlyOwner { + bytes memory oldDepositTxOut0 = DEPOSIT_TXOUT_0; + DEPOSIT_TXOUT_0 = _depositTxOut0; + emit DepositTxOutUpdate(oldDepositTxOut0, DEPOSIT_TXOUT_0); + } + + /// @notice Checks if funds 1 BTC is sent to the bridge multisig on Bitcoin, and if so, sends 1 cBTC to the receiver + /// @param version The version of the Bitcoin transaction + /// @param vin The transaction inputs + /// @param vout The transaction outputs + /// @param locktime Locktime of the Bitcoin transaction + /// @param intermediate_nodes - + /// @param block_header Block header of the Bitcoin block that the deposit transaction is in + /// @param index Index of the transaction in the block + function deposit( + bytes4 version, + bytes calldata vin, + bytes calldata vout, + bytes4 locktime, + bytes calldata intermediate_nodes, + bytes calldata block_header, + uint256 block_height, + uint index + ) external onlyOperator { + bytes32 block_hash = BTCUtils.hash256(block_header); + require(BLOCK_HASH_LIST.getBlockHash(block_height) == block_hash, "Incorrect block hash"); + + bytes32 extracted_merkle_root = BTCUtils.extractMerkleRootLE(block_header); + bytes32 txId = ValidateSPV.calculateTxId(version, vin, vout, locktime); + require(!spentTxIds[txId], "txId already spent"); + spentTxIds[txId] = true; + + bool result = ValidateSPV.prove(txId, extracted_merkle_root, intermediate_nodes, index); + require(result, "SPV Verification failed."); + + // First output is always the bridge utxo, so it should be constant + bytes memory output1 = BTCUtils.extractOutputAtIndex(vout, 0); + require(isBytesEqual(output1, DEPOSIT_TXOUT_0), "Incorrect Deposit TxOut"); + + // Second output is the receiver of tokens + bytes memory output2 = BTCUtils.extractOutputAtIndex(vout, 1); + bytes memory output2_ext = BTCUtils.extractOpReturnData(output2); + address receiver = address(bytes20(output2_ext)); + require(receiver != address(0), "Invalid receiver address"); + + emit Deposit(txId, block.timestamp); + (bool success, ) = receiver.call{value: DEPOSIT_AMOUNT}(""); + require(success, "Transfer failed"); + } + + /// @notice Accepts 1 cBTC from the sender and inserts this withdrawal request of 1 BTC on Bitcoin into the Merkle tree so that later on can be processed by the operator + /// @param bitcoin_address The Bitcoin address of the receiver + function withdraw(bytes32 bitcoin_address) external payable { + require(msg.value == DEPOSIT_AMOUNT, "Invalid withdraw amount"); + insertWithdrawalTree(bitcoin_address); + emit Withdrawal(bitcoin_address, nextIndex, block.timestamp); + } + + /// @notice Batch version of `withdraw` that can accept multiple cBTC + /// @dev Takes in multiple Bitcoin addresses as recipient addresses should be unique + /// @param bitcoin_addresses The Bitcoin addresses of the receivers + function batchWithdraw(bytes32[] calldata bitcoin_addresses) external payable { + require(msg.value == DEPOSIT_AMOUNT * bitcoin_addresses.length, "Invalid withdraw amount"); + for (uint i = 0; i < bitcoin_addresses.length; i++) { + insertWithdrawalTree(bitcoin_addresses[i]); + emit Withdrawal(bitcoin_addresses[i], nextIndex, block.timestamp); + } + } + + /// @notice Adds a block hash to the list of block hashes + /// @param block_hash The block hash to be added + function addBlockHash(bytes32 block_hash) external onlyOwner { + blockHashes[block_hash] = true; + emit BlockHashAdded(block_hash); + } + + /// @notice Sets the operator address that can process user deposits + /// @param _operator Address of the privileged operator + function setOperator(address _operator) external onlyOwner { + operator = _operator; + emit OperatorUpdated(operator, _operator); + } + + /// @notice Checks if two byte sequences are equal + /// @dev This is not efficient, and a better approach would be doing a hash based comparison but as this is ran in a zkEVM, hashing is inefficient + /// @param a First byte sequence + /// @param b Second byte sequence + function isBytesEqual(bytes memory a, bytes memory b) internal pure returns (bool result) { + require(a.length == b.length, "Lengths do not match"); + + // Cannot use keccak as its costly in ZK environment + uint length = a.length; + for (uint i = 0; i < length; i++) { + if (a[i] != b[i]) { + result = false; + return result; + } + } + result = true; + } +} diff --git a/crates/evm/src/evm/system_contracts/src/MerkleTree.sol b/crates/evm/src/evm/system_contracts/src/MerkleTree.sol new file mode 100644 index 000000000..59448f742 --- /dev/null +++ b/crates/evm/src/evm/system_contracts/src/MerkleTree.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +contract MerkleTree { + bytes32 public constant ZERO_VALUE = 0xcb0c9f4264546b15be9801ecb11df7e43bfc6841609fc1e4e9de5b3a5973af38; // keccak256("CITREA") + + uint32 public immutable levels; + mapping(uint256 => bytes32) filledSubtrees; + bytes32 root; + uint32 nextIndex; + + constructor(uint32 _levels) { + levels = _levels; + initializeTree(); + } + + function initializeTree() internal { + for (uint32 i = 0; i < levels; i++) { + filledSubtrees[i] = zeros(i); + } + root = zeros(levels); + } + + function hashLeftRight(bytes32 _left, bytes32 _right) public pure returns (bytes32 value) { + return sha256(abi.encodePacked(_left, _right)); + } + + function _insert(bytes32 _leaf) internal returns (uint32 index) { + uint32 _nextIndex = nextIndex; + require(_nextIndex != uint32(2) ** levels, "Merkle tree is full. No more leaves can be added"); + uint32 currentIndex = _nextIndex; + bytes32 currentLevelHash = _leaf; + bytes32 left; + bytes32 right; + + for (uint32 i = 0; i < levels; i++) { + if (currentIndex % 2 == 0) { + left = currentLevelHash; + right = zeros(i); + filledSubtrees[i] = currentLevelHash; + } else { + left = filledSubtrees[i]; + right = currentLevelHash; + } + currentLevelHash = hashLeftRight(left, right); + currentIndex /= 2; + } + + root = currentLevelHash; + nextIndex = _nextIndex + 1; + return _nextIndex; + } + + // Insert function + function insertWithdrawalTree(bytes32 _leaf) public returns (uint32 index) { + return _insert(_leaf); + } + + // Get root function + function getRootWithdrawalTree() public view returns (bytes32) { + return root; + } + + /// @dev provides Zero (Empty) elements for a MiMC MerkleTree. Up to 32 levels + function zeros(uint256 i) public pure returns (bytes32) { + if (i == 0) { + return bytes32(0xcb0c9f4264546b15be9801ecb11df7e43bfc6841609fc1e4e9de5b3a5973af38); + } else if (i == 1) { + return bytes32(0x455b22fd2c80e024797f8d83d31a0b6b11aa024ecffb905b1d691d657434b90a); + } else if (i == 2) { + return bytes32(0x480c45bfe41828a16e4bf0bd7a1db34d2ec4d239101daf0b774821ed2dfca761); + } else if (i == 3) { + return bytes32(0x29f2f8c3c9d792bd9ebdc71486151113b73af357a205dd41760f941555e26146); + } else if (i == 4) { + return bytes32(0xd0fb88bcc6243bb6021b49d377e1655d1e61fb08a659b9511680f83824d64197); + } else if (i == 5) { + return bytes32(0x5c7fac0890e73fb2e0d8fb3063c6786a2c63da95187efe29da4bce99d621d0ba); + } else if (i == 6) { + return bytes32(0xcb08837032cc923c34faece314b3e13158b7fda962321262618d4bcc46217eb2); + } else if (i == 7) { + return bytes32(0x229c0a76b756fc3db3266dc0f8e571d113ca44e0cb7326bee03d35331748091f); + } else if (i == 8) { + return bytes32(0x8270abf4c13bfeb13ee8967544be4d343424fa3910ec7ad995873ca0d86daf69); + } else if (i == 9) { + return bytes32(0x2d338a30d2a6bb5169df7cc8d61b6578d35568f6e1f09eda43f5bd36ad68be67); + } else if (i == 10) { + return bytes32(0x6609db0b330090c86c349ad31f986c5f2a346461eb6f160a0e0a865d39260df6); + } else if (i == 11) { + return bytes32(0x81861227312c9b868eb34b8cf216893c0db60b6b5c5e05f549f3ab6811b9f3d6); + } else if (i == 12) { + return bytes32(0x1f7af2d9c7e25a85c9035abd0d8c2c7b993754a4af7250924bc55071770f75ef); + } else if (i == 13) { + return bytes32(0x45d98c88c1c60a5d35eabe7a4cf3b2ba3bee0d8a4617f7bf485a5bd93749d3f0); + } else if (i == 14) { + return bytes32(0x11b8807d04fe98c0bd60dec1890026baa1d83f51b37e365a45b5c87028aaf6c9); + } else if (i == 15) { + return bytes32(0x6a43477876ca14f6d714ab184a94b3a8d324e29085646fd2bc3663427c512332); + } else if (i == 16) { + return bytes32(0x2e2ca637ce5b8e8b33629f637c7fd77c7ac300ae409e64898d38b4c177280e43); + } else if (i == 17) { + return bytes32(0x212db654d1d3a4e31fd8b86d3545babbf23ce592753c50936b30b1eb34d6db2a); + } else if (i == 18) { + return bytes32(0x7818b4041e25a0bcd1bf0528728f20c6976f0923d5038facff21524ccd971d75); + } else if (i == 19) { + return bytes32(0x538977a76a1b4557dd0e98d557d9a24d88349d83220ed84feb13c79f8ae83c7c); + } else if (i == 20) { + return bytes32(0x9f3efb03c31cf43571e1e5395e2f71fcaabc491ca72631b44ec9cf99110821be); + } else if (i == 21) { + return bytes32(0xfb29f69c3f45b34a9e0b7a4007a953190aa2618deef63b15ea4dd10998785734); + } else if (i == 22) { + return bytes32(0x3d12f73b1f682ed24aa0b1a39df347240a74a64b5e15312466368dd943e769bc); + } else if (i == 23) { + return bytes32(0x5bce4a357cef84ee3edf0669ec908843281b2d19072f58eff8d6503468b0c8d6); + } else if (i == 24) { + return bytes32(0x069835ff0850f45ad28f01399d6af695a96d056589cc0ac9259e66beae87cd3c); + } else if (i == 25) { + return bytes32(0xdbdece04a4bafc538705847f0c28d441f57c7b6dbf8a803ca0e5b097857e0513); + } else if (i == 26) { + return bytes32(0x020d43e87ea83e8ad54ca1eb11d826a2cd75823b5f6a91a6568839b42551f727); + } else if (i == 27) { + return bytes32(0x19a652f1c22915bd595b81d87a7dd5cc01ba7d82ba78882949373b6220d3a504); + } else if (i == 28) { + return bytes32(0x4138c0097adba86ca3c19d2181a21b8e331c42c1fdb3ce8cfd953a4553279ef1); + } else if (i == 29) { + return bytes32(0xfdc8ebd533132c3178ab8434060ae1007fc3b672fcf270d30b57f8e12ca7fa27); + } else if (i == 30) { + return bytes32(0xf0b266c6a0adb776bf7b9fafe1f02c99f35ba89067bcedb8f8f267002d51bceb); + } else if (i == 31) { + return bytes32(0x2afd595f486a771bf9653b9333d78bf101fad1f5ddb0db960c5a1450200061db); + } else if (i == 32) { + return bytes32(0x35c59abafcc58285f02e048ba62334323f15a2d2ea0a033f8df2fbee3344902d); + } else { + revert("Index out of bounds"); + } + } +} diff --git a/crates/evm/src/evm/system_contracts/test/Bridge.t.sol b/crates/evm/src/evm/system_contracts/test/Bridge.t.sol new file mode 100644 index 000000000..4ee7fbeb8 --- /dev/null +++ b/crates/evm/src/evm/system_contracts/test/Bridge.t.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import "../src/Bridge.sol"; +import "bitcoin-spv/solidity/contracts/BTCUtils.sol"; + +// !!! WARNINGS: +// !!! - Update `testDepositThenWithdraw` and `testBatchWithdraw` with proper testing of withdrawal tree root if this goes to production +// !!! - Write fuzz tests for deposit and withdraw actions with random Bitcoin txns if this goes to production + +contract BridgeHarness is Bridge { + constructor(uint32 _levels) Bridge(_levels) {} + // Overriding in harness is needed as internal functions are not accessible in the test + function isBytesEqual_(bytes memory a, bytes memory b) public pure returns (bool result) { + result = super.isBytesEqual(a, b); + } +} + +contract BridgeTest is Test { + uint256 constant DEPOSIT_AMOUNT = 1 ether; + BridgeHarness public bridge; + bytes4 version = hex"02000000"; + bytes vin = hex"01335d4a3454d976220232738ca03a7f3456f2e31625b31ae484696d2669083b720000000000fdffffff"; + bytes vout = hex"03c2ddf50500000000225120fc6eb6fa4fd4ed1e8519a7edfa171eddcedfbd0e0be49b5e531ef36e7e66eb050000000000000000166a14d5463b64bb3ecd7501283145600b763c3137b4d04a010000000000002200204ae81572f06e1b88fd5ced7a1a000945432e83e1551e6f721ee9c00b8cc33260"; + bytes4 locktime = hex"00000000"; + bytes intermediate_nodes = hex"b2fd785590896305ab9c3dd8453acfdb6d3d0538ce72f10e9e720e5c39ba1aa61918d0dd24910a182354cbf2f9e1c85e56e176afdc0763f04186f367d0d1434e936800c1e088f80a692cc8af3c6d3afa7f3d6fcead06b53739de44e67fce59533dffa19f80d5a8a0c9698bb096ae937d4a9a31640cf40da4c923e8833448de33"; + bytes block_header = hex"00000020bc9079764fe41a13327a9f1b99931b18b34d60d3947f956949eec5c1af5cb80d0a76a7d6a942436f382e259c20d0c5fee06b12799b491683f9c418311e83e224fe28d765ffff7f2001000000"; + uint index = 11; + + address operator = makeAddr("citrea_operator"); + address user = makeAddr("citrea_user"); + + uint256 constant INITIAL_BLOCK_NUMBER = 505050; + bytes32 randomMerkleRoot = bytes32(keccak256("CITREA")); + + function setUp() public { + bridge = new BridgeHarness(31); + vm.deal(address(bridge), 21_000_000 ether); + address block_hash_list_impl = address(new L1BlockHashList()); + L1BlockHashList l1BlockHashList = bridge.BLOCK_HASH_LIST(); + vm.etch(address(l1BlockHashList), block_hash_list_impl.code); + + address self = address(this); + vm.startPrank(address(0)); + l1BlockHashList.transferOwnership(self); + vm.stopPrank(); + l1BlockHashList.acceptOwnership(); + + l1BlockHashList.initializeBlockNumber(INITIAL_BLOCK_NUMBER); + bytes32 expected_blockhash = hex"b25d57f9acbf22e533b0963b47d91b11bdef9da9591002b1ef4e3ef856aec80e"; + l1BlockHashList.setBlockInfo(expected_blockhash, randomMerkleRoot); + } + + function testZeros() public { + bytes32 zero = bridge.ZERO_VALUE(); + assertEq(zero, bridge.zeros(0)); + assertEq(zero, keccak256("CITREA")); + for (uint32 i = 1; i < 33; i++) { + zero = bridge.hashLeftRight(zero, zero); + assertEq(zero, bridge.zeros(i)); + } + } + + function testDeposit() public { + // Operator makes a deposit for the `receiver` address specified in the second output of above Bitcoin txn + bridge.setOperator(operator); + vm.startPrank(operator); + bridge.deposit(version, vin, vout, locktime, intermediate_nodes, block_header, INITIAL_BLOCK_NUMBER, index); + + bytes memory output2 = BTCUtils.extractOutputAtIndex(vout, 1); + bytes memory output2_ext = BTCUtils.extractOpReturnData(output2); + address receiver = address(bytes20(output2_ext)); + + // Assert if asset transferred + assertEq(receiver.balance, DEPOSIT_AMOUNT); + vm.stopPrank(); + } + + // TODO: Replace the logic of testing the root of withdrawal tree in a more proper manner if this goes into production + function testDepositThenWithdraw() public { + // Operator makes a deposit for the `receiver` address specified in the second output of above Bitcoin txn + bridge.setOperator(operator); + vm.startPrank(operator); + bridge.deposit(version, vin, vout, locktime, intermediate_nodes, block_header, INITIAL_BLOCK_NUMBER, index); + + bytes memory output2 = BTCUtils.extractOutputAtIndex(vout, 1); + bytes memory output2_ext = BTCUtils.extractOpReturnData(output2); + address receiver = address(bytes20(output2_ext)); + + // Assert if transferred + assertEq(receiver.balance, DEPOSIT_AMOUNT); + vm.stopPrank(); + + // Assert if receiver can withdraw + vm.startPrank(receiver); + bytes32 bitcoin_address = hex"1234"; // Dummy Bitcoin address + bytes32 withdrawal_root = bridge.getRootWithdrawalTree(); + bridge.withdraw{value: DEPOSIT_AMOUNT}(bitcoin_address); + bytes32 updated_withdrawal_root = bridge.getRootWithdrawalTree(); + + // Assert if tokens are burned from receiver + assertEq(receiver.balance, 0); + + // Assert if withdrawal root is updated + assert(withdrawal_root != updated_withdrawal_root); + bytes32 expected_root = 0x574330cc8e4db82e36b5daf43915ccb2bf785ac361c3882cc4cdd2a13183af99; // Calculate with another implementation of merkle tree + assertEq(updated_withdrawal_root, expected_root); + + vm.stopPrank(); + } + + function testBatchWithdraw() public { + vm.startPrank(user); + vm.deal(address(user), 10 ether); + bytes32[] memory btc_addresses = new bytes32[](10); + for (uint i = 0; i < 10; i++) { + btc_addresses[i] = bytes32(abi.encodePacked(i)); + } + bytes32 withdrawal_root = bridge.getRootWithdrawalTree(); + bridge.batchWithdraw{value: 10 ether}(btc_addresses); + bytes32 updated_withdrawal_root = bridge.getRootWithdrawalTree(); + assert(withdrawal_root != updated_withdrawal_root); + assertEq(user.balance, 0); + } + + function testCannotBatchWithdrawWithWrongValue() public { + vm.startPrank(user); + vm.deal(address(user), 10 ether); + bytes32[] memory btc_addresses = new bytes32[](10); + for (uint i = 0; i < 10; i++) { + btc_addresses[i] = bytes32(abi.encodePacked(i)); + } + vm.expectRevert("Invalid withdraw amount"); + bridge.batchWithdraw{value: 9 ether}(btc_addresses); + } + + function testCannotDoubleDepositWithSameTx() public { + bridge.setOperator(operator); + vm.startPrank(operator); + bridge.deposit(version, vin, vout, locktime, intermediate_nodes, block_header, INITIAL_BLOCK_NUMBER, index); + vm.expectRevert("txId already spent"); + bridge.deposit(version, vin, vout, locktime, intermediate_nodes, block_header, INITIAL_BLOCK_NUMBER, index); + } + + function testCannotDepositWithFalseProof() public { + vin = hex"1234"; + bridge.setOperator(operator); + vm.startPrank(operator); + vm.expectRevert("SPV Verification failed."); + bridge.deposit(version, vin, vout, locktime, intermediate_nodes, block_header, INITIAL_BLOCK_NUMBER, index); + } + + function testCannotDepositWithFalseBlockHash() public { + block_header = hex"1234"; + bridge.setOperator(operator); + vm.startPrank(operator); + vm.expectRevert("Incorrect block hash"); + bridge.deposit(version, vin, vout, locktime, intermediate_nodes, block_header, INITIAL_BLOCK_NUMBER, index); + } + + function testCannotWithdrawWithInvalidAmount() public { + // Operator makes a deposit for the `receiver` address specified in the second output of above Bitcoin txn + bridge.setOperator(operator); + vm.startPrank(operator); + bridge.deposit(version, vin, vout, locktime, intermediate_nodes, block_header, INITIAL_BLOCK_NUMBER, index); + + bytes memory output2 = BTCUtils.extractOutputAtIndex(vout, 1); + bytes memory output2_ext = BTCUtils.extractOpReturnData(output2); + address receiver = address(bytes20(output2_ext)); + + // Assert if transferred + assertEq(receiver.balance, DEPOSIT_AMOUNT); + vm.stopPrank(); + + // Assert if receiver cannot withdraw with invalid amount + vm.startPrank(receiver); + vm.expectRevert("Invalid withdraw amount"); + bridge.withdraw{value: DEPOSIT_AMOUNT - 1}(hex"1234"); + vm.stopPrank(); + } + + function testNonOperatorCannotDeposit() public { + vm.expectRevert("caller is not the operator"); + bridge.deposit(version, vin, vout, locktime, intermediate_nodes, block_header, INITIAL_BLOCK_NUMBER, index); + } + + function testCannotSetOperatorIfNotOwner() public { + vm.startPrank(user); + vm.expectRevert(); + bridge.setOperator(operator); + } + + function testBytesEqual() public { + bytes memory a = hex"1234"; + bytes memory b = hex"1234"; + bytes memory c = hex"1235"; + bytes memory d = hex"c2ddf50500000000225120fc6eb6fa4fd4ed1e8519a7edfa171eddcedfbd0e0be49b5e531ef36e7e66eb05"; + bytes memory e = hex"c2ddf50500000000225120fc6eb6fa4fd4ed1e8519a7edfa171eddcedfbd0e0be49b5e531ef36e7e66eb06"; + bytes memory f = hex"c2ddf50500000000225120fc6eb6fa4fd4ed1e8519a7edfa171eddcedfbd0e0be49b5e531ef36e7e66eb05"; + + assert(bridge.isBytesEqual_(a, b)); + assert(!bridge.isBytesEqual_(a, c)); + assert(!bridge.isBytesEqual_(d, e)); + assert(bridge.isBytesEqual_(d, f)); + + vm.expectRevert(); + bridge.isBytesEqual_(a, d); + + vm.expectRevert(); + bridge.isBytesEqual_(a, hex""); + } + + function testBytesEqualFuzz(bytes memory a, bytes memory b) public { + vm.assume(a.length == b.length); + assertEq(isKeccakEqual(a, b), bridge.isBytesEqual_(a, b)); + } + + function testBytesEqualForEqualInputsFuzz(bytes memory a) public { + assertEq(isKeccakEqual(a, a), bridge.isBytesEqual_(a, a)); + } + + function testSetDepositTxOut0() public { + bytes memory depositTxOut0 = hex"1234"; + bridge.setDepositTxOut0(depositTxOut0); + assert(bridge.isBytesEqual_(depositTxOut0, bridge.DEPOSIT_TXOUT_0())); + } + + function isKeccakEqual(bytes memory a, bytes memory b) public pure returns (bool result) { + result = keccak256(abi.encodePacked(a)) == keccak256(abi.encodePacked(b)); + } + +}