From cf457c4aea4c3887ea3a847c564f978d1038864a 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 --- .../adapters/wormhole/WormholeAdapter.sol | 184 +++++++++++++++++ .../adapters/wormhole/interfaces/IAdapter.sol | 45 +++++ contracts/evm/contracts/xcall/CallService.sol | 9 +- .../adapters/wormhole/WormholeAdapter.t.sol | 190 ++++++++++++++++++ contracts/evm/test/xcall/CallService.t.sol | 6 +- 5 files changed, 426 insertions(+), 8 deletions(-) create mode 100644 contracts/evm/contracts/adapters/wormhole/WormholeAdapter.sol create mode 100644 contracts/evm/contracts/adapters/wormhole/interfaces/IAdapter.sol create mode 100644 contracts/evm/test/adapters/wormhole/WormholeAdapter.t.sol diff --git a/contracts/evm/contracts/adapters/wormhole/WormholeAdapter.sol b/contracts/evm/contracts/adapters/wormhole/WormholeAdapter.sol new file mode 100644 index 00000000..6cbc914f --- /dev/null +++ b/contracts/evm/contracts/adapters/wormhole/WormholeAdapter.sol @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +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"; + +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/xcall/CallService.sol b/contracts/evm/contracts/xcall/CallService.sol index 0de37f1f..f54fa351 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/test/adapters/wormhole/WormholeAdapter.t.sol b/contracts/evm/test/adapters/wormhole/WormholeAdapter.t.sol new file mode 100644 index 00000000..eef01a32 --- /dev/null +++ b/contracts/evm/test/adapters/wormhole/WormholeAdapter.t.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +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 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 5767e2ba..73bcc458 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);