From 8a6304d575360271233255196707f91c157bc07e Mon Sep 17 00:00:00 2001 From: redlarva <91685111+redlarva@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:12:55 +0545 Subject: [PATCH] feat: implement wormhole adapter (#144) * feat: implement wormhole adapter * fix: removed event listener for request submitted * tests: added rollback test for wormhole * tests: added admin test * fix: xcall test * feat: add method to update gas limit --- .gitmodules | 3 + .../adapters/wormhole/WormholeAdapter.sol | 180 ++++++++++++++++- .../adapters/wormhole/interfaces/IAdapter.sol | 45 +++++ .../MultiProtocolSampleDapp.sol | 20 +- contracts/evm/contracts/xcall/CallService.sol | 9 +- contracts/evm/foundry.toml | 2 - contracts/evm/lib/wormhole-solidity-sdk | 1 + contracts/evm/library/utils/Types.sol | 5 + contracts/evm/remappings.txt | 12 ++ .../adapters/wormhole/WormholeAdapter.t.sol | 189 +++++++++++++++++- contracts/evm/test/xcall/CallService.t.sol | 6 +- 11 files changed, 448 insertions(+), 24 deletions(-) create mode 100644 contracts/evm/contracts/adapters/wormhole/interfaces/IAdapter.sol create mode 160000 contracts/evm/lib/wormhole-solidity-sdk create mode 100644 contracts/evm/remappings.txt diff --git a/.gitmodules b/.gitmodules index bc8680e2..8567d878 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "contracts/evm/xcall/lib/openzeppelin-contracts"] path = contracts/evm/lib/openzeppelin-contracts url = https://github.com/Openzeppelin/openzeppelin-contracts +[submodule "contracts/evm/lib/wormhole-solidity-sdk"] + path = contracts/evm/lib/wormhole-solidity-sdk + url = https://github.com/wormhole-foundation/wormhole-solidity-sdk diff --git a/contracts/evm/contracts/adapters/wormhole/WormholeAdapter.sol b/contracts/evm/contracts/adapters/wormhole/WormholeAdapter.sol index 7da87763..6cbc914f 100644 --- a/contracts/evm/contracts/adapters/wormhole/WormholeAdapter.sol +++ b/contracts/evm/contracts/adapters/wormhole/WormholeAdapter.sol @@ -3,6 +3,182 @@ pragma solidity >=0.8.0; pragma abicoder v2; import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; +import "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; +import "wormhole-solidity-sdk/interfaces/IWormholeReceiver.sol"; +import "wormhole-solidity-sdk/Utils.sol"; -contract WormholeAdapter is Initializable { -} \ No newline at end of file +import "./interfaces/IAdapter.sol"; + +import "@xcall/utils/Types.sol"; +import "@iconfoundation/btp2-solidity-library/interfaces/ICallService.sol"; +import "@iconfoundation/btp2-solidity-library/interfaces/IConnection.sol"; + +/** + * @title WormholeAdapter + * @dev This contract serves as a cross-chain xcall adapter, enabling communication between xcall on different blockchain networks via Wormhole. + */ +contract WormholeAdapter is IAdapter, Initializable, IWormholeReceiver, IConnection { + mapping(uint256 => Types.PendingResponse) private pendingResponses; + mapping(string => uint16) private chainIds; + mapping(uint16 => string) private networkIds; + mapping(string => uint256) private gasLimits; + mapping(string => bytes32) private remoteEndpoint; + mapping(bytes32 => bool) public seenDeliveryVaaHashes; + address private wormholeRelayer; + address private xCall; + address private owner; + address private adminAddress; + + modifier onlyOwner() { + require(msg.sender == owner, "OnlyOwner"); + _; + } + + modifier onlyAdmin() { + require(msg.sender == this.admin(), "OnlyAdmin"); + _; + } + + function initialize(address _wormholeRelayer, address _xCall) public initializer { + owner = msg.sender; + adminAddress = msg.sender; + wormholeRelayer = _wormholeRelayer; + xCall = _xCall; + } + + /** + * @notice Configure connection settings for a destination chain. + * @param networkId The network ID of the destination chain. + * @param chainId The chain ID of the destination chain. + * @param endpoint The endpoint or address of the destination chain. + * @param gasLimit The gas limit for transactions on the destination chain. + */ + function configureConnection( + string calldata networkId, + uint16 chainId, + bytes32 endpoint, + uint256 gasLimit + ) external override onlyAdmin { + require(bytes(networkIds[chainId]).length == 0, "Connection already configured"); + networkIds[chainId] = networkId; + chainIds[networkId] = chainId; + remoteEndpoint[networkId] = endpoint; + gasLimits[networkId] = gasLimit; + } + + /** +* @notice set or update gas limit for a destination chain. + * @param networkId The network ID of the destination chain. + * @param gasLimit The gas limit for transactions on the destination chain. + */ + function setGasLimit( + string calldata networkId, + uint256 gasLimit + ) external override onlyAdmin { + gasLimits[networkId] = gasLimit; + } + + /** + * @notice Get the gas fee required to send a message to a specified destination network. + * @param _to The network ID of the target chain. + * @param _response Indicates whether the response fee is included (true) or not (false). + * @return _fee The fee for sending a message to the given destination network. + */ + function getFee(string memory _to, bool _response) external view override returns (uint256 _fee) { + uint256 gasLimit = gasLimits[_to]; + (_fee,) = IWormholeRelayer(wormholeRelayer).quoteEVMDeliveryPrice(chainIds[_to], 0, gasLimit); + } + + /** + * @notice Send a message to a specified destination network. + * @param _to The network ID of the destination network. + * @param _svc The name of the service. + * @param _sn The serial number of the message. + * @param _msg The serialized bytes of the service message. + */ + function sendMessage( + string calldata _to, + string calldata _svc, + int256 _sn, + bytes calldata _msg + ) external override payable { + require(msg.sender == xCall, "Only xCall can send messages"); + uint256 fee = msg.value; + + if (_sn < 0) { + fee = this.getFee(_to, false); + if (address(this).balance < fee) { + uint256 sn = uint256(- _sn); + pendingResponses[sn] = Types.PendingResponse(_msg, _to); + emit ResponseOnHold(sn); + return; + } + } + + IWormholeRelayer(wormholeRelayer).sendPayloadToEvm{value: fee}( + chainIds[_to], + fromWormholeFormat(remoteEndpoint[_to]), + abi.encodePacked(_msg), + 0, + gasLimits[_to] + ); + } + + /** + * @notice Endpoint that the Wormhole Relayer contract calls to deliver the payload. + */ + function receiveWormholeMessages( + bytes memory payload, + bytes[] memory, // additionalVaas + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) public payable override { + require(msg.sender == wormholeRelayer, "Only relayer allowed"); + require(!seenDeliveryVaaHashes[deliveryHash], "Message already processed"); + seenDeliveryVaaHashes[deliveryHash] = true; + string memory nid = networkIds[sourceChain]; + require(keccak256(abi.encodePacked(sourceAddress)) == keccak256(abi.encodePacked(remoteEndpoint[nid])), "source address mismatched"); + ICallService(xCall).handleMessage(nid, payload); + } + + /** + * @notice Pay and trigger the execution of a stored response to be sent back. + * @param _sn The serial number of the message for which the response is being triggered. + */ + function triggerResponse(uint256 _sn) external override payable { + int256 sn = int256(_sn); + Types.PendingResponse memory resp = pendingResponses[_sn]; + delete pendingResponses[_sn]; + uint256 fee = msg.value; + IWormholeRelayer(wormholeRelayer).sendPayloadToEvm{value: fee}( + chainIds[resp.targetNetwork], + fromWormholeFormat(remoteEndpoint[resp.targetNetwork]), + abi.encodePacked(resp.msg), + 0, + gasLimits[resp.targetNetwork] + ); + } + + /** + * @notice Set the address of the admin. + * @param _address The address of the admin. + */ + function setAdmin(address _address) external onlyAdmin { + adminAddress = _address; + } + + /** + @notice Gets the address of admin + @return (Address) the address of admin + */ + function admin( + ) external view returns ( + address + ) { + if (adminAddress == address(0)) { + return owner; + } + return adminAddress; + } +} diff --git a/contracts/evm/contracts/adapters/wormhole/interfaces/IAdapter.sol b/contracts/evm/contracts/adapters/wormhole/interfaces/IAdapter.sol new file mode 100644 index 00000000..b64d5b45 --- /dev/null +++ b/contracts/evm/contracts/adapters/wormhole/interfaces/IAdapter.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +/** + * @title IAdapter - Interface for Wormhole-xCall Adapter + * @dev This interface defines the functions and events for a Wormhole-xCall adapter, + * allowing communication and message transfer between xCall on different blockchain networks. + */ +interface IAdapter { + /** + * @notice Emitted when a response is put on hold. + * @param _sn The serial number of the response. + */ + event ResponseOnHold(uint256 indexed _sn); + + /** + * @notice Configure connection settings for a destination chain. + * @param networkId The network ID of the destination chain. + * @param chainId The chain ID of the destination chain. + * @param endpoint The endpoint or address of the destination chain. + * @param gasLimit The gas limit for transactions on the destination chain. + */ + function configureConnection( + string calldata networkId, + uint16 chainId, + bytes32 endpoint, + uint256 gasLimit + ) external; + + /** + * @notice set or update gas limit for a destination chain. + * @param networkId The network ID of the destination chain. + * @param gasLimit The gas limit for transactions on the destination chain. + */ + function setGasLimit ( + string calldata networkId, + uint256 gasLimit + ) external; + + /** + * @notice Pay and trigger the execution of a stored response to be sent back. + * @param _sn The serial number of the message for which the response is being triggered. + */ + function triggerResponse(uint256 _sn) external payable; +} diff --git a/contracts/evm/contracts/mocks/multi-protocol-dapp/MultiProtocolSampleDapp.sol b/contracts/evm/contracts/mocks/multi-protocol-dapp/MultiProtocolSampleDapp.sol index 1d014fb8..daba7af6 100644 --- a/contracts/evm/contracts/mocks/multi-protocol-dapp/MultiProtocolSampleDapp.sol +++ b/contracts/evm/contracts/mocks/multi-protocol-dapp/MultiProtocolSampleDapp.sol @@ -1,14 +1,16 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; + import "@iconfoundation/btp2-solidity-library/utils/NetworkAddress.sol"; import "@iconfoundation/btp2-solidity-library/utils/Integers.sol"; import "@iconfoundation/btp2-solidity-library/utils/ParseAddress.sol"; import "@iconfoundation/btp2-solidity-library/utils/Strings.sol"; import "@iconfoundation/btp2-solidity-library/interfaces/ICallService.sol"; +import "@iconfoundation/btp2-solidity-library/interfaces/ICallServiceReceiver.sol"; import "openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; -contract MultiProtocolSampleDapp is Initializable { +contract MultiProtocolSampleDapp is Initializable, ICallServiceReceiver { using Strings for string; using Integers for uint; using ParseAddress for address; @@ -18,10 +20,10 @@ contract MultiProtocolSampleDapp is Initializable { address private callSvc; mapping(string => string[]) private sources; mapping(string => string[]) private destinations; - + event MessageReceived(string indexed from, bytes data); - function initialize(address _callService) public { + function initialize(address _callService) public initializer { callSvc = _callService; } @@ -53,20 +55,20 @@ contract MultiProtocolSampleDapp is Initializable { bytes memory data, bytes memory rollback ) private { - (string memory net,) = to.parseNetworkAddress(); - ICallService(callSvc).sendCallMessage{value:value}(to, data, rollback, getSources(net), getDestinations(net)); + (string memory net,) = to.parseNetworkAddress(); + ICallService(callSvc).sendCallMessage{value: value}(to, data, rollback, getSources(net), getDestinations(net)); } - function handleCallMessage(string memory from, bytes memory data, string[] memory protocols) onlyCallService external { - - (string memory netFrom,) = from.parseNetworkAddress(); + + function handleCallMessage(string memory from, bytes memory data, string[] memory protocols) external onlyCallService { + (string memory netFrom,) = from.parseNetworkAddress(); string memory rollbackAddress = ICallService(callSvc).getNetworkAddress(); if (from.compareTo(rollbackAddress)) { return; } else { require(protocolsEqual(protocols, getSources(netFrom)), "invalid protocols"); - require(keccak256(data) != keccak256(abi.encodePacked(from)), "failed"); + require(keccak256(data) != keccak256(abi.encodePacked("rollback")), "rollback"); emit MessageReceived(from, data); } } diff --git a/contracts/evm/contracts/xcall/CallService.sol b/contracts/evm/contracts/xcall/CallService.sol index afddbaa2..0e5b25a2 100644 --- a/contracts/evm/contracts/xcall/CallService.sol +++ b/contracts/evm/contracts/xcall/CallService.sol @@ -153,11 +153,10 @@ contract CallService is IBSH, ICallService, IFeeManage, Initializable { (string memory netTo, string memory dstAccount) = _to.parseNetworkAddress(); string memory from = nid.networkAddress(msg.sender.toString()); uint256 sn = getNextSn(); - int256 msgSn = 0; - if (needResponse) { - requests[sn] = Types.CallRequest(msg.sender, netTo, sources, _rollback, false); - msgSn = int256(sn); - } + int256 msgSn = int256(sn); + if (needResponse) { + requests[sn] = Types.CallRequest(msg.sender, netTo, sources, _rollback, false); + } Types.CSMessageRequest memory reqMsg = Types.CSMessageRequest( from, dstAccount, sn, needResponse, _data, destinations); bytes memory _msg = reqMsg.encodeCSMessageRequest(); diff --git a/contracts/evm/foundry.toml b/contracts/evm/foundry.toml index 38a89b8c..5d5ced27 100644 --- a/contracts/evm/foundry.toml +++ b/contracts/evm/foundry.toml @@ -4,8 +4,6 @@ out = "out" libs = ["lib"] test = "test" -remappings = ["@xcall/contracts=./contracts", "@iconfoundation/btp2-solidity-library=./library/btp2", "@xcall/utils=./library/utils"] - [rpc_endpoints] binance_testnet = "${BINANCE_TESTNET_RPC_URL}" diff --git a/contracts/evm/lib/wormhole-solidity-sdk b/contracts/evm/lib/wormhole-solidity-sdk new file mode 160000 index 00000000..5b6e2ddb --- /dev/null +++ b/contracts/evm/lib/wormhole-solidity-sdk @@ -0,0 +1 @@ +Subproject commit 5b6e2ddb3d1f66638566b33c9835965106a681ff diff --git a/contracts/evm/library/utils/Types.sol b/contracts/evm/library/utils/Types.sol index 2e7f9a71..cb9a9b9b 100644 --- a/contracts/evm/library/utils/Types.sol +++ b/contracts/evm/library/utils/Types.sol @@ -51,4 +51,9 @@ library Types { int code; } + struct PendingResponse { + bytes msg; + string targetNetwork; + } + } diff --git a/contracts/evm/remappings.txt b/contracts/evm/remappings.txt new file mode 100644 index 00000000..4e5027f4 --- /dev/null +++ b/contracts/evm/remappings.txt @@ -0,0 +1,12 @@ +@xcall/contracts/=./contracts/ +@iconfoundation/btp2-solidity-library/=./library/btp2/ +@xcall/utils/=./library/utils/ +ds-test/=lib/forge-std/lib/ds-test/src/ +erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/ +forge-std/=lib/forge-std/src/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/ +openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ +openzeppelin/=lib/openzeppelin-contracts-upgradeable/contracts/ +wormhole-solidity-sdk/=lib/wormhole-solidity-sdk/src/ diff --git a/contracts/evm/test/adapters/wormhole/WormholeAdapter.t.sol b/contracts/evm/test/adapters/wormhole/WormholeAdapter.t.sol index 6ae82c80..eef01a32 100644 --- a/contracts/evm/test/adapters/wormhole/WormholeAdapter.t.sol +++ b/contracts/evm/test/adapters/wormhole/WormholeAdapter.t.sol @@ -1,7 +1,190 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; -import "forge-std/Test.sol"; +import "wormhole-solidity-sdk/testing/WormholeRelayerTest.sol"; +import "@xcall/contracts/adapters/wormhole/WormholeAdapter.sol"; +import "@xcall/contracts/xcall/CallService.sol"; +import "@xcall/contracts/mocks/multi-protocol-dapp/MultiProtocolSampleDapp.sol"; -contract WormholeAdapterTest is Test { -} \ No newline at end of file +contract WormholeAdapterTest is WormholeRelayerBasicTest { + + event CallExecuted( + uint256 indexed _reqId, + int _code, + string _msg + ); + + event RollbackExecuted( + uint256 indexed _sn + ); + + event ResponseOnHold(uint256 indexed _sn); + + MultiProtocolSampleDapp dappSource; + MultiProtocolSampleDapp dappTarget; + + CallService xCallSource; + CallService xCallTarget; + + WormholeAdapter adapterSource; + WormholeAdapter adapterTarget; + + string public nidSource = "nid.source"; + string public nidTarget = "nid.target"; + + + address public admin = address(0x1111); + address public user = address(0x1234); + + + function setUpSource() public override { + console2.log("------>setting up source<-------"); + xCallSource = new CallService(); + xCallSource.initialize(nidSource); + + dappSource = new MultiProtocolSampleDapp(); + dappSource.initialize(address(xCallSource)); + + adapterSource = new WormholeAdapter(); + adapterSource.initialize(address(relayerSource), address(xCallSource)); + + xCallSource.setDefaultConnection(nidTarget, address(adapterSource)); + } + + function setUpTarget() public override { + console2.log("------>setting up target<-------"); + + xCallTarget = new CallService(); + xCallTarget.initialize(nidTarget); + + dappTarget = new MultiProtocolSampleDapp(); + dappTarget.initialize(address(xCallTarget)); + + adapterTarget = new WormholeAdapter(); + adapterTarget.initialize(address(relayerTarget), address(xCallTarget)); + + xCallTarget.setDefaultConnection(nidSource, address(adapterTarget)); + toWormholeFormat(address(xCallTarget)); + + } + + function setUpGeneral() public override { + console2.log("------>setting up connections<-------"); + + string memory adapterSourceAdr = ParseAddress.toString( + address(adapterSource) + ); + string memory adapterTargetAdr = ParseAddress.toString( + address(adapterTarget) + ); + + + dappSource.addConnection(nidTarget, adapterSourceAdr, adapterTargetAdr); + + adapterSource.configureConnection( + nidTarget, + targetChain, + toWormholeFormat(address(adapterTarget)), + 5_000_000 + ); + vm.selectFork(targetFork); + dappTarget.addConnection(nidSource, adapterTargetAdr, adapterSourceAdr); + + adapterTarget.configureConnection( + nidSource, + sourceChain, + toWormholeFormat(address(adapterSource)), + 5_000_000 + ); + } + + function testSetAdmin() public { + adapterSource.setAdmin(admin); + assertEq(adapterSource.admin(), admin); + } + + function testSetAdminUnauthorized() public { + vm.prank(user); + vm.expectRevert("OnlyAdmin"); + adapterSource.setAdmin(user); + } + + function testConnection() public { + vm.selectFork(sourceFork); + adapterSource.setAdmin(admin); + vm.prank(user); + vm.expectRevert("OnlyAdmin"); + adapterSource.configureConnection( + nidTarget, + targetChain, + toWormholeFormat(address(adapterTarget)), + 5_000_000 + ); + + vm.prank(admin); + vm.expectRevert("Connection already configured"); + + adapterSource.configureConnection( + nidTarget, + targetChain, + toWormholeFormat(address(adapterTarget)), + 5_000_000 + ); + + } + + + function testSendMessage() public { + vm.recordLogs(); + vm.selectFork(sourceFork); + + string memory to = NetworkAddress.networkAddress(nidTarget, ParseAddress.toString(address(dappTarget))); + + uint256 cost = adapterSource.getFee(nidTarget, false); + + bytes memory data = bytes("test"); + bytes memory rollback = bytes(""); + dappSource.sendMessage{value: cost}(to, data, rollback); + + performDelivery(); + + vm.selectFork(targetFork); + vm.expectEmit(); + emit CallExecuted(1, 1, ""); + xCallTarget.executeCall(1, data); + } + + function testRollback() public { + vm.recordLogs(); + vm.selectFork(sourceFork); + + string memory to = NetworkAddress.networkAddress(nidTarget, ParseAddress.toString(address(dappTarget))); + + uint256 cost = adapterSource.getFee(nidTarget, false); + + bytes memory data = bytes("rollback"); + bytes memory rollback = bytes("rollback-data"); + dappSource.sendMessage{value: cost}(to, data, rollback); + + performDelivery(); + + vm.selectFork(targetFork); + vm.expectEmit(); + emit CallExecuted(1, 0, "rollback"); + + emit ResponseOnHold(1); + xCallTarget.executeCall(1, data); + + // trigger response + adapterTarget.triggerResponse{value: cost}(1); + performDelivery(); + + //execute rollback + vm.selectFork(sourceFork); + vm.expectEmit(); + emit RollbackExecuted(1); + xCallSource.executeRollback(1); + } + + +} diff --git a/contracts/evm/test/xcall/CallService.t.sol b/contracts/evm/test/xcall/CallService.t.sol index 44044aa9..d5469efc 100644 --- a/contracts/evm/test/xcall/CallService.t.sol +++ b/contracts/evm/test/xcall/CallService.t.sol @@ -160,7 +160,7 @@ contract CallServiceTest is Test { Types.CSMessageRequest memory request = Types.CSMessageRequest(ethDappAddress, dstAccount, 1, false, data, _baseDestination); Types.CSMessage memory message = Types.CSMessage(Types.CS_REQUEST, request.encodeCSMessageRequest()); - vm.expectCall(address(baseConnection), abi.encodeCall(baseConnection.sendMessage, (iconNid, Types.NAME, 0, message.encodeCSMessage()))); + vm.expectCall(address(baseConnection), abi.encodeCall(baseConnection.sendMessage, (iconNid, Types.NAME, 1, message.encodeCSMessage()))); uint256 sn = callService.sendCallMessage{value: 0 ether}(iconDapp, data, rollbackData, _baseSource, _baseDestination); assertEq(sn, 1); @@ -191,8 +191,8 @@ contract CallServiceTest is Test { Types.CSMessageRequest memory request = Types.CSMessageRequest(ethDappAddress, dstAccount, 1, false, data, destinations); Types.CSMessage memory message = Types.CSMessage(Types.CS_REQUEST,request.encodeCSMessageRequest()); - vm.expectCall(address(connection1), abi.encodeCall(connection1.sendMessage, (iconNid, Types.NAME, 0, message.encodeCSMessage()))); - vm.expectCall(address(connection2), abi.encodeCall(connection2.sendMessage, (iconNid, Types.NAME, 0, message.encodeCSMessage()))); + vm.expectCall(address(connection1), abi.encodeCall(connection1.sendMessage, (iconNid, Types.NAME, 1, message.encodeCSMessage()))); + vm.expectCall(address(connection2), abi.encodeCall(connection2.sendMessage, (iconNid, Types.NAME, 1, message.encodeCSMessage()))); vm.prank(address(dapp)); uint256 sn = callService.sendCallMessage{value: 0 ether}(iconDapp, data, rollbackData, sources, destinations);