From d63e58d8e87b8e24973aa836d5fc7e4a215bfd7b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 21 Mar 2025 17:09:35 +0100 Subject: [PATCH 1/5] wip --- .gitmodules | 3 + .../wormhole/WormholeGatewayBase.sol | 78 ++++++++++++++ .../wormhole/WormholeGatewayDestination.sol | 58 ++++++++++ .../wormhole/WormholeGatewaySource.sol | 101 ++++++++++++++++++ lib/@openzeppelin-contracts | 2 +- lib/@openzeppelin-contracts-upgradeable | 2 +- lib/forge-std | 2 +- lib/wormhole-solidity-sdk | 1 + remappings.txt | 1 + 9 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 contracts/crosschain/wormhole/WormholeGatewayBase.sol create mode 100644 contracts/crosschain/wormhole/WormholeGatewayDestination.sol create mode 100644 contracts/crosschain/wormhole/WormholeGatewaySource.sol create mode 160000 lib/wormhole-solidity-sdk diff --git a/.gitmodules b/.gitmodules index 3773208d..c7f506d6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,3 +9,6 @@ [submodule "lib/forge-std"] path = lib/forge-std url = https://github.com/foundry-rs/forge-std.git +[submodule "lib/wormhole-solidity-sdk"] + path = lib/wormhole-solidity-sdk + url = https://github.com/wormhole-foundation/wormhole-solidity-sdk diff --git a/contracts/crosschain/wormhole/WormholeGatewayBase.sol b/contracts/crosschain/wormhole/WormholeGatewayBase.sol new file mode 100644 index 00000000..1dfaab01 --- /dev/null +++ b/contracts/crosschain/wormhole/WormholeGatewayBase.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {CAIP10} from "@openzeppelin/contracts/utils/CAIP10.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {IWormholeRelayer} from "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; // VaaKey + +abstract contract WormholeGatewayBase is Ownable { + IWormholeRelayer internal immutable _wormholeRelayer; + uint16 internal immutable _currentChain; + + mapping(string caip2 => bytes32 remoteGateway) private _remoteGateways; + mapping(string caip2 => uint24 wormholeId) private _caipToWormholeEquivalence; + mapping(uint16 wormholeId => string caip2) private _wormholeToCaipEquivalence; + + /// @dev A remote gateway has been registered for a chain. + event RegisteredRemoteGateway(string caip2, bytes32 gatewayAddress); + + /// @dev A chain equivalence has been registered. + event RegisteredChainEquivalence(string caip2, uint16 wormholeId); + + /// @dev Error emitted when an unsupported chain is queried. + error UnsupportedChain(string caip2); + error UnsupportedChain2(uint16 wormholeId); + + error ChainEquivalenceAlreadyRegistered(string caip2); + error RemoteGatewayAlreadyRegistered(string caip2); + error UnauthorizedCaller(address); + + modifier onlyWormholeRelayer() { + require(msg.sender == address(_wormholeRelayer), UnauthorizedCaller(msg.sender)); + _; + } + + constructor(IWormholeRelayer wormholeRelayer, uint16 currentChain) { + _wormholeRelayer = wormholeRelayer; + _currentChain = currentChain; + } + + function gateway() public view virtual returns (address) { + return address(_wormholeRelayer); + } + + function supportedChain(string memory caip2) public view virtual returns (bool) { + return _caipToWormholeEquivalence[caip2] & (1 << 16) != 0; + } + + function fromCAIP2(string memory caip2) public view virtual returns (uint16) { + uint24 wormholeId = _caipToWormholeEquivalence[caip2]; + require(wormholeId & (1 << 16) != 0, UnsupportedChain(caip2)); + return uint16(wormholeId); + } + + function toCAIP2(uint16 wormholeId) public view virtual returns (string memory caip2) { + caip2 = _wormholeToCaipEquivalence[wormholeId]; + require(bytes(caip2).length > 0, UnsupportedChain2(wormholeId)); + } + + function getRemoteGateway(string memory caip2) public view virtual returns (bytes32 remoteGateway) { + remoteGateway = _remoteGateways[caip2]; + require(remoteGateway != bytes32(0), UnsupportedChain(caip2)); + } + + function registerChainEquivalence(string calldata caip2, uint16 wormholeId) public virtual onlyOwner { + require(_caipToWormholeEquivalence[caip2] == 0, ChainEquivalenceAlreadyRegistered(caip2)); + _caipToWormholeEquivalence[caip2] = wormholeId | (1 << 16); + _wormholeToCaipEquivalence[wormholeId] = caip2; + emit RegisteredChainEquivalence(caip2, wormholeId); + } + + function registerRemoteGateway(string calldata caip2, bytes32 remoteGateway) public virtual onlyOwner { + require(_remoteGateways[caip2] == bytes32(0), RemoteGatewayAlreadyRegistered(caip2)); + _remoteGateways[caip2] = remoteGateway; + emit RegisteredRemoteGateway(caip2, remoteGateway); + } +} diff --git a/contracts/crosschain/wormhole/WormholeGatewayDestination.sol b/contracts/crosschain/wormhole/WormholeGatewayDestination.sol new file mode 100644 index 00000000..163cedfb --- /dev/null +++ b/contracts/crosschain/wormhole/WormholeGatewayDestination.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IWormholeReceiver} from "wormhole-solidity-sdk/interfaces/IWormholeReceiver.sol"; +import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {IERC7786Receiver} from "../../interfaces/IERC7786.sol"; +import {WormholeGatewayBase} from "./WormholeGatewayBase.sol"; + +abstract contract WormholeGatewayDestination is WormholeGatewayBase, IWormholeReceiver { + using BitMaps for BitMaps.BitMap; + using Strings for *; + + BitMaps.BitMap private _executed; + + error InvalidOriginGateway(string sourceChain, bytes32 wormholeSourceAddress); + error MessageAlreadyExecuted(bytes32 outboxId); + error ReceiverExecutionFailed(); + error AdditionalMessagesNotSupported(); + + function receiveWormholeMessages( + bytes memory adapterPayload, + bytes[] memory additionalMessages, + bytes32 wormholeSourceAddress, + uint16 wormholeSourceChain, + bytes32 deliveryHash + ) public payable virtual onlyWormholeRelayer { + string memory sourceChain = toCAIP2(wormholeSourceChain); + + require(additionalMessages.length == 0, AdditionalMessagesNotSupported()); + require( + getRemoteGateway(sourceChain) == wormholeSourceAddress, + InvalidOriginGateway(sourceChain, wormholeSourceAddress) + ); + + ( + bytes32 outboxId, + string memory sender, + string memory receiver, + bytes memory payload, + bytes[] memory attributes + ) = abi.decode(adapterPayload, (bytes32, string, string, bytes, bytes[])); + + // prevent replay - deliveryHash might not be unique if a message is relayed multiple time + require(!_executed.get(uint256(outboxId)), MessageAlreadyExecuted(outboxId)); + _executed.set(uint256(outboxId)); + + bytes4 result = IERC7786Receiver(receiver.parseAddress()).executeMessage( + uint256(deliveryHash).toHexString(32), + sourceChain, + sender, + payload, + attributes + ); + require(result == IERC7786Receiver.executeMessage.selector, ReceiverExecutionFailed()); + } +} diff --git a/contracts/crosschain/wormhole/WormholeGatewaySource.sol b/contracts/crosschain/wormhole/WormholeGatewaySource.sol new file mode 100644 index 00000000..1df863b0 --- /dev/null +++ b/contracts/crosschain/wormhole/WormholeGatewaySource.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {VaaKey} from "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; // VaaKey +import {toUniversalAddress, fromUniversalAddress} from "wormhole-solidity-sdk/utils/UniversalAddress.sol"; +import {CAIP2} from "@openzeppelin/contracts/utils/CAIP2.sol"; +import {CAIP10} from "@openzeppelin/contracts/utils/CAIP10.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {WormholeGatewayBase} from "./WormholeGatewayBase.sol"; +import {IERC7786GatewaySource} from "../../interfaces/IERC7786.sol"; + +// TODO: allow non-evm destination chains via non-evm-specific finalize/retry variants +abstract contract WormholeGatewaySource is IERC7786GatewaySource, WormholeGatewayBase { + using Strings for *; + + struct PendingMessage { + uint64 sequence; + address sender; + string destinationChain; + string receiver; + bytes payload; + bytes[] attributes; + } + + uint256 private _outboxId; + mapping(bytes32 => PendingMessage) private _pending; + + event MessagePushed(bytes32 outboxId); + error CannotFinalizeMessage(bytes32 outboxId); + error CannotRetryMessage(bytes32 outboxId); + error UnsupportedNativeTransfer(); + + /// @inheritdoc IERC7786GatewaySource + function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) { + return false; + } + + /// @inheritdoc IERC7786GatewaySource + function sendMessage( + string calldata destinationChain, // CAIP-2 chain identifier + string calldata receiver, // CAIP-10 account address (does not include the chain identifier) + bytes calldata payload, + bytes[] calldata attributes + ) external payable returns (bytes32 outboxId) { + require(msg.value == 0, UnsupportedNativeTransfer()); + // Use of `if () revert` syntax to avoid accessing attributes[0] if it's empty + if (attributes.length > 0) + revert UnsupportedAttribute(attributes[0].length < 0x04 ? bytes4(0) : bytes4(attributes[0][0:4])); + + require(supportedChain(destinationChain), UnsupportedChain(destinationChain)); + + outboxId = bytes32(++_outboxId); + _pending[outboxId] = PendingMessage(0, msg.sender, destinationChain, receiver, payload, attributes); + + emit MessagePosted( + outboxId, + CAIP10.format(CAIP2.local(), msg.sender.toChecksumHexString()), + CAIP10.format(destinationChain, receiver), + payload, + attributes + ); + } + + function finalizeEvmMessage(bytes32 outboxId, uint256 gasLimit) external payable { + PendingMessage storage pmsg = _pending[outboxId]; + + require(pmsg.sender != address(0), CannotFinalizeMessage(outboxId)); + + uint16 wormholeDestination = fromCAIP2(pmsg.destinationChain); + bytes32 remoteGateway = getRemoteGateway(pmsg.destinationChain); + string memory sender = pmsg.sender.toChecksumHexString(); + bytes memory adapterPayload = abi.encode(outboxId, sender, pmsg.receiver, pmsg.payload, pmsg.attributes); + + // TODO: potentially delete part/all of the message + + pmsg.sequence = _wormholeRelayer.sendPayloadToEvm{value: msg.value}( + wormholeDestination, + fromUniversalAddress(remoteGateway), + adapterPayload, + 0, + gasLimit + ); + + emit MessagePushed(outboxId); + } + + function retryEvmMessage(bytes32 outboxId, uint256 gasLimit, address newDeliveryProvider) external { + PendingMessage storage pmsg = _pending[outboxId]; + + require(pmsg.sequence != 0, CannotRetryMessage(outboxId)); + + pmsg.sequence = _wormholeRelayer.resendToEvm( + VaaKey(_currentChain, toUniversalAddress(address(this)), pmsg.sequence), + fromCAIP2(pmsg.destinationChain), + 0, + gasLimit, + newDeliveryProvider + ); + } +} diff --git a/lib/@openzeppelin-contracts b/lib/@openzeppelin-contracts index 441dc141..6015d7ef 160000 --- a/lib/@openzeppelin-contracts +++ b/lib/@openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 441dc141ac99622de7e535fa75dfc74af939019c +Subproject commit 6015d7ef79172976b949be6e4b6c495593983c99 diff --git a/lib/@openzeppelin-contracts-upgradeable b/lib/@openzeppelin-contracts-upgradeable index 266b24b1..96dbfd78 160000 --- a/lib/@openzeppelin-contracts-upgradeable +++ b/lib/@openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 266b24b1338f88281040cab1e805f96795d59d3e +Subproject commit 96dbfd7878e54556cbedc5dc638804531a2d9668 diff --git a/lib/forge-std b/lib/forge-std index 3b20d60d..6853b9ec 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 +Subproject commit 6853b9ec7df5dc0c213b05ae67785ad4f4baa0ea diff --git a/lib/wormhole-solidity-sdk b/lib/wormhole-solidity-sdk new file mode 160000 index 00000000..575181b5 --- /dev/null +++ b/lib/wormhole-solidity-sdk @@ -0,0 +1 @@ +Subproject commit 575181b586a315d8f9813eab82e4cb98b45bc381 diff --git a/remappings.txt b/remappings.txt index 27ffccff..16791701 100644 --- a/remappings.txt +++ b/remappings.txt @@ -2,3 +2,4 @@ @openzeppelin/contracts-upgradeable/=lib/@openzeppelin-contracts-upgradeable/contracts/ @openzeppelin/community-contracts/=contracts/ @axelar-network/axelar-gmp-sdk-solidity/=node_modules/@axelar-network/axelar-gmp-sdk-solidity/ +wormhole-solidity-sdk/=lib/wormhole-solidity-sdk/src/ From 70a4daf1d2f58fb64d521358657f417047ec1bea Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 24 Mar 2025 10:37:04 +0100 Subject: [PATCH 2/5] Test wormhole adapter --- .../wormhole/WormholeGatewayBase.sol | 10 +- .../wormhole/WormholeGatewayDuplex.sol | 22 +++++ .../wormhole/WormholeGatewaySource.sol | 4 +- .../wormhole/WormholeRelayerMock.sol | 32 ++++++ .../wormhole/WormholeGateway.test.js | 97 +++++++++++++++++++ test/crosschain/wormhole/WormholeHelper.js | 28 ++++++ 6 files changed, 186 insertions(+), 7 deletions(-) create mode 100644 contracts/crosschain/wormhole/WormholeGatewayDuplex.sol create mode 100644 contracts/mocks/crosschain/wormhole/WormholeRelayerMock.sol create mode 100644 test/crosschain/wormhole/WormholeGateway.test.js create mode 100644 test/crosschain/wormhole/WormholeHelper.js diff --git a/contracts/crosschain/wormhole/WormholeGatewayBase.sol b/contracts/crosschain/wormhole/WormholeGatewayBase.sol index 1dfaab01..bf97f2ad 100644 --- a/contracts/crosschain/wormhole/WormholeGatewayBase.sol +++ b/contracts/crosschain/wormhole/WormholeGatewayBase.sol @@ -5,11 +5,11 @@ pragma solidity ^0.8.27; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {CAIP10} from "@openzeppelin/contracts/utils/CAIP10.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; -import {IWormholeRelayer} from "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; // VaaKey +import {IWormholeRelayer} from "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; abstract contract WormholeGatewayBase is Ownable { IWormholeRelayer internal immutable _wormholeRelayer; - uint16 internal immutable _currentChain; + uint16 internal immutable _wormholeChainId; mapping(string caip2 => bytes32 remoteGateway) private _remoteGateways; mapping(string caip2 => uint24 wormholeId) private _caipToWormholeEquivalence; @@ -34,12 +34,12 @@ abstract contract WormholeGatewayBase is Ownable { _; } - constructor(IWormholeRelayer wormholeRelayer, uint16 currentChain) { + constructor(IWormholeRelayer wormholeRelayer, uint16 wormholeChainId) { _wormholeRelayer = wormholeRelayer; - _currentChain = currentChain; + _wormholeChainId = wormholeChainId; } - function gateway() public view virtual returns (address) { + function relayer() public view virtual returns (address) { return address(_wormholeRelayer); } diff --git a/contracts/crosschain/wormhole/WormholeGatewayDuplex.sol b/contracts/crosschain/wormhole/WormholeGatewayDuplex.sol new file mode 100644 index 00000000..b0bfd652 --- /dev/null +++ b/contracts/crosschain/wormhole/WormholeGatewayDuplex.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {WormholeGatewayBase, IWormholeRelayer} from "./WormholeGatewayBase.sol"; +import {WormholeGatewayDestination} from "./WormholeGatewayDestination.sol"; +import {WormholeGatewaySource} from "./WormholeGatewaySource.sol"; + +/** + * @dev A contract that combines the functionality of both the source and destination gateway + * adapters for the Wormhole Network. Allowing to either send or receive messages across chains. + */ +// slither-disable-next-line locked-ether +contract WormholeGatewayDuplex is WormholeGatewaySource, WormholeGatewayDestination { + /// @dev Initializes the contract with the Wormhole gateway and the initial owner. + constructor( + IWormholeRelayer gateway, + uint16 wormholeChainId, + address initialOwner + ) Ownable(initialOwner) WormholeGatewayBase(gateway, wormholeChainId) {} +} diff --git a/contracts/crosschain/wormhole/WormholeGatewaySource.sol b/contracts/crosschain/wormhole/WormholeGatewaySource.sol index 1df863b0..566b47d0 100644 --- a/contracts/crosschain/wormhole/WormholeGatewaySource.sol +++ b/contracts/crosschain/wormhole/WormholeGatewaySource.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.27; -import {VaaKey} from "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; // VaaKey +import {VaaKey} from "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; import {toUniversalAddress, fromUniversalAddress} from "wormhole-solidity-sdk/utils/UniversalAddress.sol"; import {CAIP2} from "@openzeppelin/contracts/utils/CAIP2.sol"; import {CAIP10} from "@openzeppelin/contracts/utils/CAIP10.sol"; @@ -91,7 +91,7 @@ abstract contract WormholeGatewaySource is IERC7786GatewaySource, WormholeGatewa require(pmsg.sequence != 0, CannotRetryMessage(outboxId)); pmsg.sequence = _wormholeRelayer.resendToEvm( - VaaKey(_currentChain, toUniversalAddress(address(this)), pmsg.sequence), + VaaKey(_wormholeChainId, toUniversalAddress(address(this)), pmsg.sequence), fromCAIP2(pmsg.destinationChain), 0, gasLimit, diff --git a/contracts/mocks/crosschain/wormhole/WormholeRelayerMock.sol b/contracts/mocks/crosschain/wormhole/WormholeRelayerMock.sol new file mode 100644 index 00000000..4ef946bb --- /dev/null +++ b/contracts/mocks/crosschain/wormhole/WormholeRelayerMock.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {IWormholeRelayer} from "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; +import {IWormholeReceiver} from "wormhole-solidity-sdk/interfaces/IWormholeReceiver.sol"; +import {toUniversalAddress} from "wormhole-solidity-sdk/utils/UniversalAddress.sol"; + +contract WormholeRelayerMock { + uint64 private _seq; + + function sendPayloadToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit + ) external payable returns (uint64) { + // TODO: check that destination chain is local + + uint64 seq = _seq++; + IWormholeReceiver(targetAddress).receiveWormholeMessages{value: receiverValue, gas: gasLimit}( + payload, + new bytes[](0), + toUniversalAddress(msg.sender), + targetChain, + keccak256(abi.encode(seq)) + ); + + return seq; + } +} diff --git a/test/crosschain/wormhole/WormholeGateway.test.js b/test/crosschain/wormhole/WormholeGateway.test.js new file mode 100644 index 00000000..587ec21a --- /dev/null +++ b/test/crosschain/wormhole/WormholeGateway.test.js @@ -0,0 +1,97 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { anyValue } = require('@nomicfoundation/hardhat-chai-matchers/withArgs'); + +const WormholeHelper = require('./WormholeHelper'); + +const getAddress = account => ethers.getAddress(account.target ?? account.address ?? account); + +async function fixture() { + const [owner, sender, ...accounts] = await ethers.getSigners(); + + const { CAIP2, wormholeChainId, wormhole, gatewayA, gatewayB } = await WormholeHelper.deploy(owner); + + const receiver = await ethers.deployContract('$ERC7786ReceiverMock', [gatewayB]); + const invalidReceiver = await ethers.deployContract('$ERC7786ReceiverInvalidMock'); + + const asCAIP10 = account => `${CAIP2}:${getAddress(account)}`; + + return { + owner, + sender, + accounts, + CAIP2, + asCAIP10, + wormholeChainId, + wormhole, + gatewayA, + gatewayB, + receiver, + invalidReceiver, + }; +} + +describe('WormholeGateway', function () { + const outboxId = '0x0000000000000000000000000000000000000000000000000000000000000001'; + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + it('initial setup', async function () { + await expect(this.gatewayA.relayer()).to.eventually.equal(this.wormhole); + await expect(this.gatewayA.fromCAIP2(this.CAIP2)).to.eventually.equal(this.wormholeChainId); + await expect(this.gatewayA.toCAIP2(this.wormholeChainId)).to.eventually.equal(this.CAIP2); + await expect(this.gatewayA.getRemoteGateway(this.CAIP2)).to.eventually.equal( + WormholeHelper.toUniversalAddress(this.gatewayB), + ); + + await expect(this.gatewayB.relayer()).to.eventually.equal(this.wormhole); + await expect(this.gatewayB.fromCAIP2(this.CAIP2)).to.eventually.equal(this.wormholeChainId); + await expect(this.gatewayB.toCAIP2(this.wormholeChainId)).to.eventually.equal(this.CAIP2); + await expect(this.gatewayB.getRemoteGateway(this.CAIP2)).to.eventually.equal( + WormholeHelper.toUniversalAddress(this.gatewayA), + ); + }); + + it('workflow', async function () { + const srcCAIP10 = this.asCAIP10(this.sender); + const dstCAIP10 = this.asCAIP10(this.receiver); + const payload = ethers.randomBytes(128); + const attributes = []; + // const encoded = ethers.AbiCoder.defaultAbiCoder().encode( + // ['bytes32', 'string', 'string', 'bytes', 'bytes[]'], + // [outboxId, getAddress(this.sender), getAddress(this.receiver), payload, attributes], + // ); + + await expect( + this.gatewayA.connect(this.sender).sendMessage(this.CAIP2, getAddress(this.receiver), payload, attributes), + ) + .to.emit(this.gatewayA, 'MessagePosted') + .withArgs(outboxId, srcCAIP10, dstCAIP10, payload, attributes); + + await expect(this.gatewayA.finalizeEvmMessage(outboxId, 100_000n)) + .to.emit(this.receiver, 'MessageReceived') + .withArgs(this.gatewayB, anyValue, this.CAIP2, getAddress(this.sender), payload, attributes); + }); + + it('invalid receiver - bad return value', async function () { + await this.gatewayA + .connect(this.sender) + .sendMessage(this.CAIP2, getAddress(this.invalidReceiver), ethers.randomBytes(128), []); + + await expect(this.gatewayA.finalizeEvmMessage(outboxId, 100_000n)).to.be.revertedWithCustomError( + this.gatewayB, + 'ReceiverExecutionFailed', + ); + }); + + it('invalid receiver - EOA', async function () { + await this.gatewayA + .connect(this.sender) + .sendMessage(this.CAIP2, getAddress(this.accounts[0]), ethers.randomBytes(128), []); + + await expect(this.gatewayA.finalizeEvmMessage(outboxId, 100_000n)).to.be.revertedWithoutReason(); + }); +}); diff --git a/test/crosschain/wormhole/WormholeHelper.js b/test/crosschain/wormhole/WormholeHelper.js new file mode 100644 index 00000000..1b029350 --- /dev/null +++ b/test/crosschain/wormhole/WormholeHelper.js @@ -0,0 +1,28 @@ +const { ethers } = require('hardhat'); + +const toUniversalAddress = addr => ethers.zeroPadValue(addr.target ?? addr.address ?? addr, 32); +const fromUniversalAddress = addr => ethers.getAddress(ethers.hexlify(ethers.getBytes(addr).slice(-20))); + +async function deploy(owner, CAIP2 = undefined, wormholeChainId = 23600) { + CAIP2 ??= await ethers.provider.getNetwork().then(({ chainId }) => `eip155:${chainId}`); + + const wormhole = await ethers.deployContract('WormholeRelayerMock'); + + const gatewayA = await ethers.deployContract('WormholeGatewayDuplex', [wormhole, wormholeChainId, owner]); + const gatewayB = await ethers.deployContract('WormholeGatewayDuplex', [wormhole, wormholeChainId, owner]); + + await Promise.all([ + gatewayA.connect(owner).registerChainEquivalence(CAIP2, wormholeChainId), + gatewayB.connect(owner).registerChainEquivalence(CAIP2, wormholeChainId), + gatewayA.connect(owner).registerRemoteGateway(CAIP2, toUniversalAddress(gatewayB)), + gatewayB.connect(owner).registerRemoteGateway(CAIP2, toUniversalAddress(gatewayA)), + ]); + + return { CAIP2, wormholeChainId, wormhole, gatewayA, gatewayB }; +} + +module.exports = { + deploy, + toUniversalAddress, + fromUniversalAddress, +}; From 6492d925ba5d11c457f3b9e96de08606f415bf23 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 24 Mar 2025 10:49:56 +0100 Subject: [PATCH 3/5] minimize changes --- lib/@openzeppelin-contracts | 2 +- lib/@openzeppelin-contracts-upgradeable | 2 +- lib/forge-std | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/@openzeppelin-contracts b/lib/@openzeppelin-contracts index 6015d7ef..441dc141 160000 --- a/lib/@openzeppelin-contracts +++ b/lib/@openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 6015d7ef79172976b949be6e4b6c495593983c99 +Subproject commit 441dc141ac99622de7e535fa75dfc74af939019c diff --git a/lib/@openzeppelin-contracts-upgradeable b/lib/@openzeppelin-contracts-upgradeable index 96dbfd78..266b24b1 160000 --- a/lib/@openzeppelin-contracts-upgradeable +++ b/lib/@openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 96dbfd7878e54556cbedc5dc638804531a2d9668 +Subproject commit 266b24b1338f88281040cab1e805f96795d59d3e diff --git a/lib/forge-std b/lib/forge-std index 6853b9ec..3b20d60d 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 6853b9ec7df5dc0c213b05ae67785ad4f4baa0ea +Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981 From bf7e7e24f3f6bfc8a4137c3741b48a919b0eb1f8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Fri, 28 Mar 2025 21:45:32 +0100 Subject: [PATCH 4/5] add quote function --- .../crosschain/wormhole/WormholeGatewayDuplex.sol | 4 ++-- .../crosschain/wormhole/WormholeGatewaySource.sol | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/contracts/crosschain/wormhole/WormholeGatewayDuplex.sol b/contracts/crosschain/wormhole/WormholeGatewayDuplex.sol index b0bfd652..a3ee4242 100644 --- a/contracts/crosschain/wormhole/WormholeGatewayDuplex.sol +++ b/contracts/crosschain/wormhole/WormholeGatewayDuplex.sol @@ -15,8 +15,8 @@ import {WormholeGatewaySource} from "./WormholeGatewaySource.sol"; contract WormholeGatewayDuplex is WormholeGatewaySource, WormholeGatewayDestination { /// @dev Initializes the contract with the Wormhole gateway and the initial owner. constructor( - IWormholeRelayer gateway, + IWormholeRelayer wormholeRelayer, uint16 wormholeChainId, address initialOwner - ) Ownable(initialOwner) WormholeGatewayBase(gateway, wormholeChainId) {} + ) Ownable(initialOwner) WormholeGatewayBase(wormholeRelayer, wormholeChainId) {} } diff --git a/contracts/crosschain/wormhole/WormholeGatewaySource.sol b/contracts/crosschain/wormhole/WormholeGatewaySource.sol index 566b47d0..d1d22861 100644 --- a/contracts/crosschain/wormhole/WormholeGatewaySource.sol +++ b/contracts/crosschain/wormhole/WormholeGatewaySource.sol @@ -62,6 +62,15 @@ abstract contract WormholeGatewaySource is IERC7786GatewaySource, WormholeGatewa ); } + function quoteEvmMessage(string memory destinationChain, uint256 gasLimit) public view returns (uint256) { + (uint256 cost, ) = _wormholeRelayer.quoteEVMDeliveryPrice(fromCAIP2(destinationChain), 0, gasLimit); + return cost; + } + + function quoteEvmMessage(bytes32 outboxId, uint256 gasLimit) external view returns (uint256) { + return quoteEvmMessage(_pending[outboxId].destinationChain, gasLimit); + } + function finalizeEvmMessage(bytes32 outboxId, uint256 gasLimit) external payable { PendingMessage storage pmsg = _pending[outboxId]; @@ -85,6 +94,8 @@ abstract contract WormholeGatewaySource is IERC7786GatewaySource, WormholeGatewa emit MessagePushed(outboxId); } + // Is this necessary ? How does that work since we are not providing any additional payment ? + // Is re-calling finalizeEvmMessage an alternative ? function retryEvmMessage(bytes32 outboxId, uint256 gasLimit, address newDeliveryProvider) external { PendingMessage storage pmsg = _pending[outboxId]; From c6282239a4aea37c4aa172f54d6ba644bead53ec Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 3 Apr 2025 13:21:22 +0200 Subject: [PATCH 5/5] cvhange prettier rules --- .prettierignore | 1 + package.json | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..c3af8579 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +lib/ diff --git a/package.json b/package.json index 46535f8b..5cc692af 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,10 @@ "prepare-docs": "scripts/prepare-docs.sh", "lint": "npm run lint:js && npm run lint:sol", "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", - "lint:js": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --check && eslint .", - "lint:js:fix": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --write && eslint . --fix", - "lint:sol": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'", - "lint:sol:fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write", + "lint:js": "prettier --log-level warn '**/*.{js,ts}' --check && eslint .", + "lint:js:fix": "prettier --log-level warn '**/*.{js,ts}' --write && eslint . --fix", + "lint:sol": "prettier --log-level warn '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'", + "lint:sol:fix": "prettier --log-level warn '{contracts,test}/**/*.sol' --write", "coverage": "scripts/checks/coverage.sh", "test": "hardhat test", "test:inheritance": "scripts/checks/inheritance-ordering.js artifacts/build-info/*",