Skip to content

Commit

Permalink
feat: implement wormhole adapter (#144)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
redlarva committed Nov 8, 2023
1 parent dabd796 commit 55020dd
Show file tree
Hide file tree
Showing 11 changed files with 448 additions and 24 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
180 changes: 178 additions & 2 deletions contracts/evm/contracts/adapters/wormhole/WormholeAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
}
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;
}
}
45 changes: 45 additions & 0 deletions contracts/evm/contracts/adapters/wormhole/interfaces/IAdapter.sol
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
}
}
Expand Down
9 changes: 4 additions & 5 deletions contracts/evm/contracts/xcall/CallService.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 0 additions & 2 deletions contracts/evm/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
1 change: 1 addition & 0 deletions contracts/evm/lib/wormhole-solidity-sdk
Submodule wormhole-solidity-sdk added at 5b6e2d
5 changes: 5 additions & 0 deletions contracts/evm/library/utils/Types.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,9 @@ library Types {
int code;
}

struct PendingResponse {
bytes msg;
string targetNetwork;
}

}
12 changes: 12 additions & 0 deletions contracts/evm/remappings.txt
Original file line number Diff line number Diff line change
@@ -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/
Loading

0 comments on commit 55020dd

Please sign in to comment.