From c0038b06078d445915133f320e474651c3391e33 Mon Sep 17 00:00:00 2001 From: Alessandro Manfredi Date: Wed, 23 Oct 2024 16:12:22 +0200 Subject: [PATCH 1/4] feat(evm): adds Bouncer and some tests --- packages/evm/contracts/Bouncer.sol | 63 ++++++ .../evm/contracts/interfaces/IBouncer.sol | 15 ++ .../evm/contracts/libraries/HopDecoder.sol | 86 ++++++++ packages/evm/test/06_Bouncer.spec.ts | 185 ++++++++++++++++++ packages/evm/test/utils/Message.ts | 59 +++++- 5 files changed, 405 insertions(+), 3 deletions(-) create mode 100644 packages/evm/contracts/Bouncer.sol create mode 100644 packages/evm/contracts/interfaces/IBouncer.sol create mode 100644 packages/evm/contracts/libraries/HopDecoder.sol create mode 100644 packages/evm/test/06_Bouncer.spec.ts diff --git a/packages/evm/contracts/Bouncer.sol b/packages/evm/contracts/Bouncer.sol new file mode 100644 index 00000000..93a454bf --- /dev/null +++ b/packages/evm/contracts/Bouncer.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.20; + +import { IAdapter } from "./interfaces/IAdapter.sol"; +import { IReporter } from "./interfaces/IReporter.sol"; +import { IYaho } from "./interfaces/IYaho.sol"; +import { IBouncer } from "./interfaces/IBouncer.sol"; +import { HopDecoder } from "./libraries/HopDecoder.sol"; + +contract Bouncer is IBouncer { + address public immutable YAHO; + address public immutable YARU; + + constructor(address yaho, address yaru) { + YAHO = yaho; + YARU = yaru; + } + + function onMessage( + uint256, + uint256 sourceChainId, + address sender, + uint256 threshold, + IAdapter[] calldata adapters, + bytes calldata data + ) external returns (bytes memory) { + if (msg.sender != YARU) revert NotYaru(); + + ( + , + uint8 hopsNonce, + , + , + uint256 nextChainId, + address receiver, + uint256 expectedSourceChainId, + address expectedSender, + uint256 expectedThreshold, + bytes32 expectedAdaptersHash, + uint256 nextThreshold, + IReporter[] memory nextReporters, + IAdapter[] memory nextAdapters, + + ) = HopDecoder.decodeCurrentHop(data); + + if (sourceChainId != expectedSourceChainId) revert InvalidSourceChainId(); + if (sender != expectedSender) revert InvalidSender(); + if (threshold < expectedThreshold) revert InvalidThreshold(); + if (sha256(abi.encode(adapters)) != expectedAdaptersHash) revert InvalidAdapters(); + + bytes memory dataWithUpdatedNonce = abi.encodePacked(data[:4], bytes1(hopsNonce + 1), data[5:]); + IYaho(YAHO).dispatchMessage( + nextChainId, + nextThreshold, + receiver, + dataWithUpdatedNonce, + nextReporters, + nextAdapters + ); + + return abi.encodePacked(true); + } +} diff --git a/packages/evm/contracts/interfaces/IBouncer.sol b/packages/evm/contracts/interfaces/IBouncer.sol new file mode 100644 index 00000000..75607c7b --- /dev/null +++ b/packages/evm/contracts/interfaces/IBouncer.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.0; + +import { IJushin } from "./IJushin.sol"; + +/** + * @title IBouncer + */ +interface IBouncer is IJushin { + error InvalidAdapters(); + error InvalidSender(); + error InvalidSourceChainId(); + error InvalidThreshold(); + error NotYaru(); +} diff --git a/packages/evm/contracts/libraries/HopDecoder.sol b/packages/evm/contracts/libraries/HopDecoder.sol new file mode 100644 index 00000000..d3582312 --- /dev/null +++ b/packages/evm/contracts/libraries/HopDecoder.sol @@ -0,0 +1,86 @@ +pragma solidity ^0.8.20; + +import { IAdapter } from "../interfaces/IAdapter.sol"; +import { IReporter } from "../interfaces/IReporter.sol"; + +library HopDecoder { + function decodeCurrentHop( + bytes calldata data + ) + internal + pure + returns ( + bytes4 magic, + uint8 hopsNonce, + uint8 hopsCount, + bytes8 chainProtocol, + uint256 chainId, + address receiver, + uint256 expectedSourceChainId, + address expectedSender, + uint32 expectedThreshold, + bytes32 expectedAdaptersHash, + uint32 nextThreshold, + IReporter[] memory nextReporters, + IAdapter[] memory nextAdapters, + bytes memory message + ) + { + magic = bytes4(data[:4]); + hopsNonce = uint8(bytes1(data[4:5])); + hopsCount = uint8(bytes1(data[5:6])); + + uint32 hopBytesToSkip = 0; + for (uint256 k = 0; k < hopsNonce; ) { + hopBytesToSkip += uint32(bytes4(data[6 + k * 4:6 + (k * 4) + 4])); + unchecked { + ++k; + } + } + + uint256 hopStart = 6 + hopBytesToSkip + ((hopsNonce + 1) * 4); + uint256 a = hopStart + 8; + uint256 b = hopStart + 24; + uint256 c = hopStart + 56; + uint256 d = hopStart + 72; + uint256 e = hopStart + 104; + uint256 f = hopStart + 108; + uint256 g = hopStart + 140; + uint256 h = hopStart + 144; + uint256 i = hopStart + 148; + + chainProtocol = bytes8(data[hopStart:a]); + chainId = uint128(bytes16(data[a:b])); + receiver = address(uint160(uint256(bytes32(data[b:c])))); + expectedSourceChainId = uint128(bytes16(data[c:d])); + expectedSender = address(uint160(uint256(bytes32(data[d:e])))); + expectedThreshold = uint32(bytes4(data[e:f])); + expectedAdaptersHash = bytes32(data[f:g]); + nextThreshold = uint32(bytes4(data[g:h])); + + uint32 nextReportersLength = uint32(bytes4(data[h:i])); + nextReporters = new IReporter[](nextReportersLength); + for (uint256 k = 0; k < nextReportersLength; ) { + nextReporters[k] = IReporter(address(uint160(uint256(bytes32(data[i + (k * 32):i + (k * 32) + 32]))))); + unchecked { + ++k; + } + } + + uint256 l = i + (nextReportersLength * 32); + uint256 m = l + 4; + uint32 nextAdaptersLength = uint32(bytes4(data[l:m])); + nextAdapters = new IAdapter[](nextAdaptersLength); + for (uint256 k = 0; k < nextAdaptersLength; ) { + nextAdapters[k] = IAdapter(address(uint160(uint256(bytes32(data[m + (k * 32):m + (k * 32) + 32]))))); + unchecked { + ++k; + } + } + + uint256 n = m + (nextAdaptersLength * 32); + uint256 o = n + 3; + uint24 messageLength = uint24(bytes3(data[n:o])); + message = data[o:o + messageLength]; + } +} diff --git a/packages/evm/test/06_Bouncer.spec.ts b/packages/evm/test/06_Bouncer.spec.ts new file mode 100644 index 00000000..9f24b991 --- /dev/null +++ b/packages/evm/test/06_Bouncer.spec.ts @@ -0,0 +1,185 @@ +import { expect } from "chai" +import { Contract } from "ethers" +import { ethers } from "hardhat" + +import Message from "./utils/Message" +import { Chains } from "./utils/constants" +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers" + +let reporter1: Contract, + reporter2: Contract, + reporter3: Contract, + reporter4: Contract, + headerStorage: Contract, + yaho: Contract, + yaru: Contract, + hashi: Contract, + adapter1: Contract, + adapter2: Contract, + adapter3: Contract, + adapter4: Contract, + pingPong: Contract, + bouncer1: Contract, + bouncer2: Contract, + owner: SignerWithAddress + +describe("Bouncer", () => { + beforeEach(async () => { + const signers = await ethers.getSigners() + owner = signers[0] + + const Yaru = await ethers.getContractFactory("Yaru") + const Yaho = await ethers.getContractFactory("Yaho") + const Hashi = await ethers.getContractFactory("Hashi") + const Reporter = await ethers.getContractFactory("MockReporter") + const Adapter = await ethers.getContractFactory("MockAdapter") + const PingPong = await ethers.getContractFactory("PingPong") + const HeaderStorage = await ethers.getContractFactory("HeaderStorage") + const Bouncer = await ethers.getContractFactory("Bouncer") + + hashi = await Hashi.deploy() + yaho = await Yaho.deploy() + headerStorage = await HeaderStorage.deploy() + yaru = await Yaru.deploy(hashi.address, yaho.address, Chains.Hardhat) + reporter1 = await Reporter.deploy(headerStorage.address, yaho.address) + reporter2 = await Reporter.deploy(headerStorage.address, yaho.address) + reporter3 = await Reporter.deploy(headerStorage.address, yaho.address) + reporter4 = await Reporter.deploy(headerStorage.address, yaho.address) + adapter1 = await Adapter.deploy() + adapter2 = await Adapter.deploy() + adapter3 = await Adapter.deploy() + adapter4 = await Adapter.deploy() + pingPong = await PingPong.deploy() + + bouncer1 = await Bouncer.deploy(yaho.address, yaru.address) + bouncer2 = await Bouncer.deploy(yaho.address, yaru.address) + }) + + /*it(`should be able to execute a message after ONE hop`, async () => { + const abiCoder = new ethers.utils.AbiCoder() + const threshold = 1 + const expectedAdaptersHash = ethers.utils.sha256(abiCoder.encode(["address[]"], [[adapter1.address]])) + const header = + "0x04510001" + + "00" + // hops nonce + "02" + // hops count + "000000D8" + // 184 bytes = 1th hop size + "0000000000000001" + // chain protocol + Chains.Hardhat.toString(16).padStart(32, "0") + // chain protocol identifier + pingPong.address.slice(2).padStart(64, "0") + // receiver + Chains.Hardhat.toString(16).padStart(32, "0") + // expected source chain id + owner.address.slice(2).padStart(64, "0") + // expected sender + "00000001" + // expected threshold + expectedAdaptersHash.slice(2) + // expected adapters hash + "00000001" + // threshold + "00000001" + // reporters length + reporter2.address.slice(2).padStart(64, "0") + + "00000001" + // adapters length + adapter2.address.slice(2).padStart(64, "0") + + "000001" + // message length + "01" // raw message + + let tx = await yaho.dispatchMessagesToAdapters( + Chains.Hardhat, + [threshold], + [bouncer1.address], + [header], + [reporter1.address], + [adapter1.address], + ) + const [message1] = Message.fromReceipt(await tx.wait(1)) + const hash1 = await yaho.calculateMessageHash(message1.serialize()) + let adapters = [adapter1] + for (let i = 0; i < threshold; i++) { + await adapters[i].setHashes(Chains.Hardhat, [message1.id], [hash1]) + } + + tx = await yaru.executeMessages([message1]) + const [message2] = Message.fromReceipt(await tx.wait(1)) + const hash2 = await yaho.calculateMessageHash(message2.serialize()) + adapters = [adapter2] + for (let i = 0; i < threshold; i++) { + await adapters[i].setHashes(Chains.Hardhat, [message2.id], [hash2]) + } + await expect(yaru.executeMessages([message2])).to.emit(pingPong, "Pong") + })*/ + + it(`should be able to execute a message after TWO hops`, async () => { + const abiCoder = new ethers.utils.AbiCoder() + const threshold = 1 + const expectedAdaptersHash1 = ethers.utils.sha256(abiCoder.encode(["address[]"], [[adapter1.address]])) + const expectedAdaptersHash2 = ethers.utils.sha256(abiCoder.encode(["address[]"], [[adapter2.address]])) + + console.log("bouncer2.address", bouncer2.address) + console.log("bouncer1.address", bouncer1.address) + console.log("owner.address", owner.address) + + const header = + "0x04510001" + + "00" + // hops nonce + "02" + // hops count + "000000D8" + // 184 bytes = 1th hop size + "0000000000000001" + // chain protocol + Chains.Hardhat.toString(16).padStart(32, "0") + // chain protocol identifier + bouncer2.address.slice(2).padStart(64, "0") + // receiver + Chains.Hardhat.toString(16).padStart(32, "0") + // expected source chain id + owner.address.slice(2).padStart(64, "0") + // expected sender + "00000001" + // expected threshold + expectedAdaptersHash1.slice(2) + // expected adapters hash + "00000001" + // threshold + "00000001" + // reporters length + reporter2.address.slice(2).padStart(64, "0") + + "00000001" + // adapters length + adapter2.address.slice(2).padStart(64, "0") + + "000000D8" + // 184 bytes = 2th hop size + "0000000000000001" + // chain protocol + Chains.Hardhat.toString(16).padStart(32, "0") + // chain protocol identifier + pingPong.address.slice(2).padStart(64, "0") + // receiver + Chains.Hardhat.toString(16).padStart(32, "0") + // expected source chain id + bouncer1.address.slice(2).padStart(64, "0") + // expected sender + "00000001" + // expected threshold + expectedAdaptersHash2.slice(2) + // expected adapters hash + "00000001" + // threshold + "00000002" + // reporters length + reporter3.address.slice(2).padStart(64, "0") + + reporter4.address.slice(2).padStart(64, "0") + + "00000002" + // adapters length + adapter3.address.slice(2).padStart(64, "0") + + adapter4.address.slice(2).padStart(64, "0") + + "000001" + // message length + "01" // raw message + + let tx = await yaho.dispatchMessagesToAdapters( + Chains.Hardhat, + [threshold], + [bouncer1.address], + [header], + [reporter1.address], + [adapter1.address], + ) + const [message1] = Message.fromReceipt(await tx.wait(1)) + const hash1 = await yaho.calculateMessageHash(message1.serialize()) + let adapters = [adapter1, adapter2] + for (let i = 0; i < threshold; i++) { + await adapters[i].setHashes(Chains.Hardhat, [message1.id], [hash1]) + } + + tx = await yaru.executeMessages([message1]) + const [message2] = Message.fromReceipt(await tx.wait(1)) + const hash2 = await yaho.calculateMessageHash(message2.serialize()) + adapters = [adapter2] + for (let i = 0; i < threshold; i++) { + await adapters[i].setHashes(Chains.Hardhat, [message2.id], [hash2]) + } + + tx = await yaru.executeMessages([message2]) + const [message3] = Message.fromReceipt(await tx.wait(1)) + const hash3 = await yaho.calculateMessageHash(message3.serialize()) + adapters = [adapter3, adapter4] + for (let i = 0; i < threshold; i++) { + await adapters[i].setHashes(Chains.Hardhat, [message3.id], [hash3]) + } + + await expect(yaru.executeMessages([message3])).to.emit(pingPong, "Pong") + }) +}) diff --git a/packages/evm/test/utils/Message.ts b/packages/evm/test/utils/Message.ts index 8a16c75e..9fc785dd 100644 --- a/packages/evm/test/utils/Message.ts +++ b/packages/evm/test/utils/Message.ts @@ -1,4 +1,4 @@ -import { ContractReceipt } from "ethers" +import { ContractReceipt, utils } from "ethers" type Configs = { data: string @@ -36,8 +36,61 @@ class Message { } static fromReceipt(_receipt: ContractReceipt) { - const events = _receipt.events.filter(({ event }) => event === "MessageDispatched") - return events.map(({ args: { messageId, message } }) => new Message({ id: messageId, ...message })) + let events = _receipt.events.filter(({ event }) => event === "MessageDispatched") + if (events.length) { + return events.map(({ args: { messageId, message } }) => new Message({ id: messageId, ...message })) + } + + const abiCoder = new utils.AbiCoder() + events = _receipt.events.filter( + ({ topics }) => topics[0] === "0x218247aabc759e65b5bb92ccc074f9d62cd187259f2a0984c3c9cf91f67ff7cf", + ) + return events.map(({ topics, data }) => { + const message = abiCoder.decode( + [ + { + components: [ + { + name: "nonce", + type: "uint256", + }, + { + name: "targetChainId", + type: "uint256", + }, + { + name: "threshold", + type: "uint256", + }, + { + name: "sender", + type: "address", + }, + { + name: "receiver", + type: "address", + }, + { + name: "data", + type: "bytes", + }, + { + name: "reporters", + type: "address[]", + }, + { + name: "adapters", + type: "address[]", + }, + ], + name: "message", + type: "tuple", + }, + ], + data, + ) + return new Message({ id: topics[1], ...message[0] }) + }) } serialize() { From 0678cd117b9ba44d63debedca265f0581366ebe2 Mon Sep 17 00:00:00 2001 From: Alessandro Manfredi Date: Wed, 23 Oct 2024 16:31:14 +0200 Subject: [PATCH 2/4] refactor(evm): rm comments and useless code within 06_Bouncer.spec.ts --- packages/evm/test/06_Bouncer.spec.ts | 40 +++++++++++++--------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/evm/test/06_Bouncer.spec.ts b/packages/evm/test/06_Bouncer.spec.ts index 9f24b991..9c0d33e5 100644 --- a/packages/evm/test/06_Bouncer.spec.ts +++ b/packages/evm/test/06_Bouncer.spec.ts @@ -55,27 +55,27 @@ describe("Bouncer", () => { bouncer2 = await Bouncer.deploy(yaho.address, yaru.address) }) - /*it(`should be able to execute a message after ONE hop`, async () => { + it(`should be able to execute a message after ONE hop`, async () => { const abiCoder = new ethers.utils.AbiCoder() const threshold = 1 const expectedAdaptersHash = ethers.utils.sha256(abiCoder.encode(["address[]"], [[adapter1.address]])) const header = - "0x04510001" + - "00" + // hops nonce - "02" + // hops count - "000000D8" + // 184 bytes = 1th hop size - "0000000000000001" + // chain protocol - Chains.Hardhat.toString(16).padStart(32, "0") + // chain protocol identifier - pingPong.address.slice(2).padStart(64, "0") + // receiver - Chains.Hardhat.toString(16).padStart(32, "0") + // expected source chain id - owner.address.slice(2).padStart(64, "0") + // expected sender - "00000001" + // expected threshold - expectedAdaptersHash.slice(2) + // expected adapters hash - "00000001" + // threshold - "00000001" + // reporters length - reporter2.address.slice(2).padStart(64, "0") + - "00000001" + // adapters length - adapter2.address.slice(2).padStart(64, "0") + + "0x04510001" + + "00" + // hops nonce + "02" + // hops count + "000000D8" + // 184 bytes = 1th hop size + "0000000000000001" + // chain protocol + Chains.Hardhat.toString(16).padStart(32, "0") + // chain protocol identifier + pingPong.address.slice(2).padStart(64, "0") + // receiver + Chains.Hardhat.toString(16).padStart(32, "0") + // expected source chain id + owner.address.slice(2).padStart(64, "0") + // expected sender + "00000001" + // expected threshold + expectedAdaptersHash.slice(2) + // expected adapters hash + "00000001" + // threshold + "00000001" + // reporters length + reporter2.address.slice(2).padStart(64, "0") + + "00000001" + // adapters length + adapter2.address.slice(2).padStart(64, "0") + "000001" + // message length "01" // raw message @@ -102,7 +102,7 @@ describe("Bouncer", () => { await adapters[i].setHashes(Chains.Hardhat, [message2.id], [hash2]) } await expect(yaru.executeMessages([message2])).to.emit(pingPong, "Pong") - })*/ + }) it(`should be able to execute a message after TWO hops`, async () => { const abiCoder = new ethers.utils.AbiCoder() @@ -110,10 +110,6 @@ describe("Bouncer", () => { const expectedAdaptersHash1 = ethers.utils.sha256(abiCoder.encode(["address[]"], [[adapter1.address]])) const expectedAdaptersHash2 = ethers.utils.sha256(abiCoder.encode(["address[]"], [[adapter2.address]])) - console.log("bouncer2.address", bouncer2.address) - console.log("bouncer1.address", bouncer1.address) - console.log("owner.address", owner.address) - const header = "0x04510001" + "00" + // hops nonce From 834dc97e104a1bb85a36dbdecba1c6ddad91c0e5 Mon Sep 17 00:00:00 2001 From: Alessandro Manfredi Date: Thu, 24 Oct 2024 07:51:26 +0200 Subject: [PATCH 3/4] refactor(evm): puts message length & message after magic --- packages/evm/contracts/Bouncer.sol | 8 ++- .../evm/contracts/libraries/HopDecoder.sol | 67 ++++++++++--------- packages/evm/test/06_Bouncer.spec.ts | 12 ++-- 3 files changed, 47 insertions(+), 40 deletions(-) diff --git a/packages/evm/contracts/Bouncer.sol b/packages/evm/contracts/Bouncer.sol index 93a454bf..60cf7450 100644 --- a/packages/evm/contracts/Bouncer.sol +++ b/packages/evm/contracts/Bouncer.sol @@ -40,7 +40,7 @@ contract Bouncer is IBouncer { uint256 nextThreshold, IReporter[] memory nextReporters, IAdapter[] memory nextAdapters, - + bytes memory message ) = HopDecoder.decodeCurrentHop(data); if (sourceChainId != expectedSourceChainId) revert InvalidSourceChainId(); @@ -48,7 +48,11 @@ contract Bouncer is IBouncer { if (threshold < expectedThreshold) revert InvalidThreshold(); if (sha256(abi.encode(adapters)) != expectedAdaptersHash) revert InvalidAdapters(); - bytes memory dataWithUpdatedNonce = abi.encodePacked(data[:4], bytes1(hopsNonce + 1), data[5:]); + bytes memory dataWithUpdatedNonce = abi.encodePacked( + data[:7 + message.length], + bytes1(hopsNonce + 1), + data[7 + 1 + message.length:] + ); IYaho(YAHO).dispatchMessage( nextChainId, nextThreshold, diff --git a/packages/evm/contracts/libraries/HopDecoder.sol b/packages/evm/contracts/libraries/HopDecoder.sol index d3582312..ea402029 100644 --- a/packages/evm/contracts/libraries/HopDecoder.sol +++ b/packages/evm/contracts/libraries/HopDecoder.sol @@ -27,60 +27,63 @@ library HopDecoder { ) { magic = bytes4(data[:4]); - hopsNonce = uint8(bytes1(data[4:5])); - hopsCount = uint8(bytes1(data[5:6])); + + uint24 messageLength = uint24(bytes3(data[4:7])); + message = data[7:7 + messageLength]; + + uint256 a = 7 + messageLength; + uint256 b = a + 1; + uint256 c = b + 1; + + hopsNonce = uint8(bytes1(data[a:b])); + hopsCount = uint8(bytes1(data[b:c])); uint32 hopBytesToSkip = 0; for (uint256 k = 0; k < hopsNonce; ) { - hopBytesToSkip += uint32(bytes4(data[6 + k * 4:6 + (k * 4) + 4])); + hopBytesToSkip += uint32(bytes4(data[c + k * 4:c + (k * 4) + 4])); unchecked { ++k; } } - uint256 hopStart = 6 + hopBytesToSkip + ((hopsNonce + 1) * 4); - uint256 a = hopStart + 8; - uint256 b = hopStart + 24; - uint256 c = hopStart + 56; - uint256 d = hopStart + 72; - uint256 e = hopStart + 104; - uint256 f = hopStart + 108; - uint256 g = hopStart + 140; - uint256 h = hopStart + 144; - uint256 i = hopStart + 148; + uint256 hopStart = c + hopBytesToSkip + ((hopsNonce + 1) * 4); + uint256 d = hopStart + 8; + uint256 e = hopStart + 24; + uint256 f = hopStart + 56; + uint256 g = hopStart + 72; + uint256 h = hopStart + 104; + uint256 i = hopStart + 108; + uint256 l = hopStart + 140; + uint256 m = hopStart + 144; + uint256 n = hopStart + 148; - chainProtocol = bytes8(data[hopStart:a]); - chainId = uint128(bytes16(data[a:b])); - receiver = address(uint160(uint256(bytes32(data[b:c])))); - expectedSourceChainId = uint128(bytes16(data[c:d])); - expectedSender = address(uint160(uint256(bytes32(data[d:e])))); - expectedThreshold = uint32(bytes4(data[e:f])); - expectedAdaptersHash = bytes32(data[f:g]); - nextThreshold = uint32(bytes4(data[g:h])); + chainProtocol = bytes8(data[hopStart:d]); + chainId = uint128(bytes16(data[d:e])); + receiver = address(uint160(uint256(bytes32(data[e:f])))); + expectedSourceChainId = uint128(bytes16(data[f:g])); + expectedSender = address(uint160(uint256(bytes32(data[g:h])))); + expectedThreshold = uint32(bytes4(data[h:i])); + expectedAdaptersHash = bytes32(data[i:l]); + nextThreshold = uint32(bytes4(data[l:m])); - uint32 nextReportersLength = uint32(bytes4(data[h:i])); + uint32 nextReportersLength = uint32(bytes4(data[m:n])); nextReporters = new IReporter[](nextReportersLength); for (uint256 k = 0; k < nextReportersLength; ) { - nextReporters[k] = IReporter(address(uint160(uint256(bytes32(data[i + (k * 32):i + (k * 32) + 32]))))); + nextReporters[k] = IReporter(address(uint160(uint256(bytes32(data[n + (k * 32):n + (k * 32) + 32]))))); unchecked { ++k; } } - uint256 l = i + (nextReportersLength * 32); - uint256 m = l + 4; - uint32 nextAdaptersLength = uint32(bytes4(data[l:m])); + uint256 o = n + (nextReportersLength * 32); + uint256 p = o + 4; + uint32 nextAdaptersLength = uint32(bytes4(data[o:p])); nextAdapters = new IAdapter[](nextAdaptersLength); for (uint256 k = 0; k < nextAdaptersLength; ) { - nextAdapters[k] = IAdapter(address(uint160(uint256(bytes32(data[m + (k * 32):m + (k * 32) + 32]))))); + nextAdapters[k] = IAdapter(address(uint160(uint256(bytes32(data[p + (k * 32):p + (k * 32) + 32]))))); unchecked { ++k; } } - - uint256 n = m + (nextAdaptersLength * 32); - uint256 o = n + 3; - uint24 messageLength = uint24(bytes3(data[n:o])); - message = data[o:o + messageLength]; } } diff --git a/packages/evm/test/06_Bouncer.spec.ts b/packages/evm/test/06_Bouncer.spec.ts index 9c0d33e5..77025a15 100644 --- a/packages/evm/test/06_Bouncer.spec.ts +++ b/packages/evm/test/06_Bouncer.spec.ts @@ -61,6 +61,8 @@ describe("Bouncer", () => { const expectedAdaptersHash = ethers.utils.sha256(abiCoder.encode(["address[]"], [[adapter1.address]])) const header = "0x04510001" + + "000001" + // message length + "01" + // raw message "00" + // hops nonce "02" + // hops count "000000D8" + // 184 bytes = 1th hop size @@ -75,9 +77,7 @@ describe("Bouncer", () => { "00000001" + // reporters length reporter2.address.slice(2).padStart(64, "0") + "00000001" + // adapters length - adapter2.address.slice(2).padStart(64, "0") + - "000001" + // message length - "01" // raw message + adapter2.address.slice(2).padStart(64, "0") let tx = await yaho.dispatchMessagesToAdapters( Chains.Hardhat, @@ -112,6 +112,8 @@ describe("Bouncer", () => { const header = "0x04510001" + + "000001" + // message length + "01" + // raw message "00" + // hops nonce "02" + // hops count "000000D8" + // 184 bytes = 1th hop size @@ -141,9 +143,7 @@ describe("Bouncer", () => { reporter4.address.slice(2).padStart(64, "0") + "00000002" + // adapters length adapter3.address.slice(2).padStart(64, "0") + - adapter4.address.slice(2).padStart(64, "0") + - "000001" + // message length - "01" // raw message + adapter4.address.slice(2).padStart(64, "0") let tx = await yaho.dispatchMessagesToAdapters( Chains.Hardhat, From 6761e2ac1ec5160cfbaa64ec46d5c781e6a262f6 Mon Sep 17 00:00:00 2001 From: Alessandro Manfredi Date: Thu, 24 Oct 2024 09:01:28 +0200 Subject: [PATCH 4/4] refactor(evm): adds HopReceiver to test the last step whit hops and replaces sha256 with keccak256 --- packages/evm/contracts/Bouncer.sol | 2 +- .../evm/contracts/libraries/HopDecoder.sol | 8 ++ packages/evm/contracts/test/HopReceiver.sol | 52 +++++++++++++ packages/evm/test/06_Bouncer.spec.ts | 74 +++++++++++++------ 4 files changed, 113 insertions(+), 23 deletions(-) create mode 100644 packages/evm/contracts/test/HopReceiver.sol diff --git a/packages/evm/contracts/Bouncer.sol b/packages/evm/contracts/Bouncer.sol index 60cf7450..71120bd8 100644 --- a/packages/evm/contracts/Bouncer.sol +++ b/packages/evm/contracts/Bouncer.sol @@ -46,7 +46,7 @@ contract Bouncer is IBouncer { if (sourceChainId != expectedSourceChainId) revert InvalidSourceChainId(); if (sender != expectedSender) revert InvalidSender(); if (threshold < expectedThreshold) revert InvalidThreshold(); - if (sha256(abi.encode(adapters)) != expectedAdaptersHash) revert InvalidAdapters(); + if (keccak256(abi.encodePacked(adapters)) != expectedAdaptersHash) revert InvalidAdapters(); bytes memory dataWithUpdatedNonce = abi.encodePacked( data[:7 + message.length], diff --git a/packages/evm/contracts/libraries/HopDecoder.sol b/packages/evm/contracts/libraries/HopDecoder.sol index ea402029..8be659e6 100644 --- a/packages/evm/contracts/libraries/HopDecoder.sol +++ b/packages/evm/contracts/libraries/HopDecoder.sol @@ -86,4 +86,12 @@ library HopDecoder { } } } + + function decodeHeaderAndMessage( + bytes calldata data + ) internal pure returns (bytes memory header, bytes memory message) { + uint24 messageLength = uint24(bytes3(data[4:7])); + message = data[7:7 + messageLength]; + header = data[7 + messageLength:]; + } } diff --git a/packages/evm/contracts/test/HopReceiver.sol b/packages/evm/contracts/test/HopReceiver.sol new file mode 100644 index 00000000..e61546b9 --- /dev/null +++ b/packages/evm/contracts/test/HopReceiver.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: LGPL-3.0-only +pragma solidity ^0.8.20; + +import { IJushin } from "../interfaces/IJushin.sol"; +import { IAdapter } from "../interfaces/IAdapter.sol"; +import { HopDecoder } from "../libraries/HopDecoder.sol"; + +contract HopReceiver is IJushin { + address public yaru; + uint256 public expectedSourceChainId; + address public expectedSender; + uint256 public expectedThreshold; + bytes32 public expectedAdaptersHash; + bytes32 public expectedHeaderHash; + + event MessageReceived(bytes message); + + function setConfigs( + address yaru_, + uint256 expectedSourceChainId_, + address expectedSender_, + uint256 expectedThreshold_, + bytes32 expectedAdaptersHash_, + bytes32 expectedHeaderHash_ + ) external { + yaru = yaru_; + expectedSourceChainId = expectedSourceChainId_; + expectedSender = expectedSender_; + expectedThreshold = expectedThreshold_; + expectedAdaptersHash = expectedAdaptersHash_; + expectedHeaderHash = expectedHeaderHash_; + } + + function onMessage( + uint256, + uint256 sourceChainId, + address sender, + uint256 threshold, + IAdapter[] calldata adapters, + bytes calldata data + ) external returns (bytes memory) { + require(msg.sender == yaru, "!yaru"); + require(sourceChainId == expectedSourceChainId, "!expectedSourceChainId"); + require(sender == expectedSender, "!expectedSender"); + require(threshold == expectedThreshold, "!expectedThreshold"); + require(keccak256(abi.encodePacked(adapters)) == expectedAdaptersHash, "!expectedAdaptersHash"); + (bytes memory header, bytes memory message) = HopDecoder.decodeHeaderAndMessage(data); + require(keccak256(header) == expectedHeaderHash, "!expectedHeaderHash"); + emit MessageReceived(message); + return abi.encodePacked(true); + } +} diff --git a/packages/evm/test/06_Bouncer.spec.ts b/packages/evm/test/06_Bouncer.spec.ts index 77025a15..5d70b382 100644 --- a/packages/evm/test/06_Bouncer.spec.ts +++ b/packages/evm/test/06_Bouncer.spec.ts @@ -18,9 +18,9 @@ let reporter1: Contract, adapter2: Contract, adapter3: Contract, adapter4: Contract, - pingPong: Contract, bouncer1: Contract, bouncer2: Contract, + hopReceiver: Contract, owner: SignerWithAddress describe("Bouncer", () => { @@ -33,7 +33,7 @@ describe("Bouncer", () => { const Hashi = await ethers.getContractFactory("Hashi") const Reporter = await ethers.getContractFactory("MockReporter") const Adapter = await ethers.getContractFactory("MockAdapter") - const PingPong = await ethers.getContractFactory("PingPong") + const HopReceiver = await ethers.getContractFactory("HopReceiver") const HeaderStorage = await ethers.getContractFactory("HeaderStorage") const Bouncer = await ethers.getContractFactory("Bouncer") @@ -49,41 +49,54 @@ describe("Bouncer", () => { adapter2 = await Adapter.deploy() adapter3 = await Adapter.deploy() adapter4 = await Adapter.deploy() - pingPong = await PingPong.deploy() + hopReceiver = await HopReceiver.deploy() bouncer1 = await Bouncer.deploy(yaho.address, yaru.address) bouncer2 = await Bouncer.deploy(yaho.address, yaru.address) }) it(`should be able to execute a message after ONE hop`, async () => { - const abiCoder = new ethers.utils.AbiCoder() const threshold = 1 - const expectedAdaptersHash = ethers.utils.sha256(abiCoder.encode(["address[]"], [[adapter1.address]])) + const expectedAdaptersHash1 = ethers.utils.solidityKeccak256(["address[]"], [[adapter1.address]]) + const expectedAdaptersHash2 = ethers.utils.solidityKeccak256(["address[]"], [[adapter2.address]]) + const header = - "0x04510001" + - "000001" + // message length - "01" + // raw message "00" + // hops nonce - "02" + // hops count + "01" + // hops count "000000D8" + // 184 bytes = 1th hop size "0000000000000001" + // chain protocol Chains.Hardhat.toString(16).padStart(32, "0") + // chain protocol identifier - pingPong.address.slice(2).padStart(64, "0") + // receiver + hopReceiver.address.slice(2).padStart(64, "0") + // receiver Chains.Hardhat.toString(16).padStart(32, "0") + // expected source chain id owner.address.slice(2).padStart(64, "0") + // expected sender "00000001" + // expected threshold - expectedAdaptersHash.slice(2) + // expected adapters hash + expectedAdaptersHash1.slice(2) + // expected adapters hash "00000001" + // threshold "00000001" + // reporters length reporter2.address.slice(2).padStart(64, "0") + "00000001" + // adapters length adapter2.address.slice(2).padStart(64, "0") + const messageWithHops = + "0x04510001" + + "000001" + // message length + "01" + // raw message + header + + await hopReceiver.setConfigs( + yaru.address, + Chains.Hardhat, + bouncer1.address, + threshold, + expectedAdaptersHash2, + ethers.utils.keccak256("0x" + "01" + header.slice(2)), // NOTE: nonce must equal to count in the last step + ) + let tx = await yaho.dispatchMessagesToAdapters( Chains.Hardhat, [threshold], [bouncer1.address], - [header], + [messageWithHops], [reporter1.address], [adapter1.address], ) @@ -101,19 +114,19 @@ describe("Bouncer", () => { for (let i = 0; i < threshold; i++) { await adapters[i].setHashes(Chains.Hardhat, [message2.id], [hash2]) } - await expect(yaru.executeMessages([message2])).to.emit(pingPong, "Pong") + + await expect(yaru.executeMessages([message2])) + .to.emit(hopReceiver, "MessageReceived") + .withArgs("0x01") }) it(`should be able to execute a message after TWO hops`, async () => { - const abiCoder = new ethers.utils.AbiCoder() const threshold = 1 - const expectedAdaptersHash1 = ethers.utils.sha256(abiCoder.encode(["address[]"], [[adapter1.address]])) - const expectedAdaptersHash2 = ethers.utils.sha256(abiCoder.encode(["address[]"], [[adapter2.address]])) + const expectedAdaptersHash1 = ethers.utils.solidityKeccak256(["address[]"], [[adapter1.address]]) + const expectedAdaptersHash2 = ethers.utils.solidityKeccak256(["address[]"], [[adapter2.address]]) + const expectedAdaptersHash34 = ethers.utils.solidityKeccak256(["address[]"], [[adapter3.address, adapter4.address]]) const header = - "0x04510001" + - "000001" + // message length - "01" + // raw message "00" + // hops nonce "02" + // hops count "000000D8" + // 184 bytes = 1th hop size @@ -132,7 +145,7 @@ describe("Bouncer", () => { "000000D8" + // 184 bytes = 2th hop size "0000000000000001" + // chain protocol Chains.Hardhat.toString(16).padStart(32, "0") + // chain protocol identifier - pingPong.address.slice(2).padStart(64, "0") + // receiver + hopReceiver.address.slice(2).padStart(64, "0") + // receiver Chains.Hardhat.toString(16).padStart(32, "0") + // expected source chain id bouncer1.address.slice(2).padStart(64, "0") + // expected sender "00000001" + // expected threshold @@ -145,11 +158,26 @@ describe("Bouncer", () => { adapter3.address.slice(2).padStart(64, "0") + adapter4.address.slice(2).padStart(64, "0") + const messageWithHops = + "0x04510001" + + "000001" + // message length + "01" + // raw message + header + + await hopReceiver.setConfigs( + yaru.address, + Chains.Hardhat, + bouncer2.address, + threshold, + expectedAdaptersHash34, + ethers.utils.keccak256("0x" + "02" + header.slice(2)), // NOTE: nonce must equal to count in the last step + ) + let tx = await yaho.dispatchMessagesToAdapters( Chains.Hardhat, [threshold], [bouncer1.address], - [header], + [messageWithHops], [reporter1.address], [adapter1.address], ) @@ -176,6 +204,8 @@ describe("Bouncer", () => { await adapters[i].setHashes(Chains.Hardhat, [message3.id], [hash3]) } - await expect(yaru.executeMessages([message3])).to.emit(pingPong, "Pong") + await expect(yaru.executeMessages([message3])) + .to.emit(hopReceiver, "MessageReceived") + .withArgs("0x01") }) })