Skip to content

Wormhole adaptor #94

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@
path = lib/zk-email-verify
branch = v6.3.2
url = https://github.com/zkemail/zk-email-verify
[submodule "lib/wormhole-solidity-sdk"]
path = lib/wormhole-solidity-sdk
url = https://github.com/wormhole-foundation/wormhole-solidity-sdk
118 changes: 118 additions & 0 deletions contracts/crosschain/wormhole/WormholeGatewayBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {IWormholeRelayer} from "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol";

/// Note: only EVM chains are currently supported
abstract contract WormholeGatewayBase is Ownable {
using InteroperableAddress for bytes;

IWormholeRelayer internal immutable _wormholeRelayer;
uint16 internal immutable _wormholeChainId;
uint24 private constant MASK = 1 << 16;

// Remote gateway.
mapping(uint256 chainId => address) private _remoteGateways;

// chain equivalence ChainId <> Wormhole
mapping(uint256 chainId => uint24 wormholeId) private _chainIdToWormhole;
mapping(uint16 wormholeId => uint256 chainId) private _wormholeToChainId;

/// @dev A remote gateway has been registered for a chain.
event RegisteredRemoteGateway(uint256 chainId, address remote);

/// @dev A chain equivalence has been registered.
event RegisteredChainEquivalence(uint256 chainId, uint16 wormholeId);

error UnsupportedChainId(uint256 chainId);
error UnsupportedWormholeChain(uint16 wormholeId);
error ChainEquivalenceAlreadyRegistered(uint256 chainId, uint16 wormhole);
error RemoteGatewayAlreadyRegistered(uint256 chainId);
error UnauthorizedCaller(address);

modifier onlyWormholeRelayer() {
require(msg.sender == address(_wormholeRelayer), UnauthorizedCaller(msg.sender));
_;
}

constructor(IWormholeRelayer wormholeRelayer, uint16 wormholeChainId) {
_wormholeRelayer = wormholeRelayer;
_wormholeChainId = wormholeChainId;
}

function relayer() public view virtual returns (address) {
return address(_wormholeRelayer);
}

function supportedChain(bytes memory chain) public view virtual returns (bool) {
(bool success, uint256 chainId, ) = chain.tryParseEvmV1();
return success && supportedChain(chainId);
}

function supportedChain(uint256 chainId) public view virtual returns (bool) {
return _chainIdToWormhole[chainId] & MASK == MASK;
}

function getWormholeChain(bytes memory chain) public view virtual returns (uint16) {
(uint256 chainId, ) = chain.parseEvmV1();
return getWormholeChain(chainId);
}

function getWormholeChain(uint256 chainId) public view virtual returns (uint16) {
uint24 wormholeId = _chainIdToWormhole[chainId];
require(wormholeId & MASK == MASK, UnsupportedChainId(chainId));
return uint16(wormholeId);
}

function getChainId(uint16 wormholeId) public view virtual returns (uint256) {
uint256 chainId = _wormholeToChainId[wormholeId];
require(chainId != 0, UnsupportedWormholeChain(wormholeId));
return chainId;
}

/// @dev Returns the address of the remote gateway for a given chainType and chainReference.
function getRemoteGateway(bytes memory chain) public view virtual returns (address) {
(uint256 chainId, ) = chain.parseEvmV1();
return getRemoteGateway(chainId);
}

function getRemoteGateway(uint256 chainId) public view virtual returns (address) {
address addr = _remoteGateways[chainId];
require(addr != address(0), UnsupportedChainId(chainId));
return addr;
}

function registerChainEquivalence(
bytes calldata chain,
uint16 wormholeId
) public virtual /*onlyOwner in registerChainEquivalence*/ {
(uint256 chainId, ) = chain.parseEvmV1Calldata();
registerChainEquivalence(chainId, wormholeId);
}

function registerChainEquivalence(uint256 chainId, uint16 wormholeId) public virtual onlyOwner {
require(
_chainIdToWormhole[chainId] == 0 && _wormholeToChainId[wormholeId] == 0,
ChainEquivalenceAlreadyRegistered(chainId, wormholeId)
);

_chainIdToWormhole[chainId] = wormholeId | MASK;
_wormholeToChainId[wormholeId] = chainId;
emit RegisteredChainEquivalence(chainId, wormholeId);
}

function registerRemoteGateway(bytes calldata remote) public virtual /*onlyOwner in registerRemoteGateway*/ {
(uint256 chainId, address addr) = remote.parseEvmV1Calldata();
registerRemoteGateway(chainId, addr);
}

function registerRemoteGateway(uint256 chainId, address addr) public virtual onlyOwner {
require(supportedChain(chainId), UnsupportedChainId(chainId));
require(_remoteGateways[chainId] == address(0), RemoteGatewayAlreadyRegistered(chainId));
_remoteGateways[chainId] = addr;
emit RegisteredRemoteGateway(chainId, addr);
}
}
55 changes: 55 additions & 0 deletions contracts/crosschain/wormhole/WormholeGatewayDestination.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {IWormholeReceiver} from "wormhole-solidity-sdk/interfaces/IWormholeReceiver.sol";
import {fromUniversalAddress} from "wormhole-solidity-sdk/utils/UniversalAddress.sol";
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol";
import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol";
import {IERC7786Receiver} from "../../interfaces/IERC7786.sol";
import {WormholeGatewayBase} from "./WormholeGatewayBase.sol";

abstract contract WormholeGatewayDestination is WormholeGatewayBase, IWormholeReceiver {
using BitMaps for BitMaps.BitMap;
using InteroperableAddress for bytes;

BitMaps.BitMap private _executed;

error InvalidOriginGateway(uint16 wormholeSourceChain, 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 {
require(additionalMessages.length == 0, AdditionalMessagesNotSupported());

(bytes32 outboxId, bytes memory sender, bytes memory recipient, bytes memory payload) = abi.decode(
adapterPayload,
(bytes32, bytes, bytes, bytes)
);

// Wormhole to ERC-7930 translation
address addr = getRemoteGateway(getChainId(wormholeSourceChain));

// check message validity
// - `wormholeSourceAddress` is the remote gateway on the origin chain.
require(
addr == fromUniversalAddress(wormholeSourceAddress),
InvalidOriginGateway(wormholeSourceChain, wormholeSourceAddress)
);

// 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));

(, address target) = recipient.parseEvmV1();
bytes4 result = IERC7786Receiver(target).receiveMessage(deliveryHash, sender, payload);
require(result == IERC7786Receiver.receiveMessage.selector, ReceiverExecutionFailed());
}
}
22 changes: 22 additions & 0 deletions contracts/crosschain/wormhole/WormholeGatewayDuplex.sol
Original file line number Diff line number Diff line change
@@ -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 wormholeRelayer,
uint16 wormholeChainId,
address initialOwner
) Ownable(initialOwner) WormholeGatewayBase(wormholeRelayer, wormholeChainId) {}
}
100 changes: 100 additions & 0 deletions contracts/crosschain/wormhole/WormholeGatewaySource.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {VaaKey} from "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol";
import {toUniversalAddress} from "wormhole-solidity-sdk/utils/UniversalAddress.sol";
import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.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 InteroperableAddress for bytes;
// using Strings for *;

struct PendingMessage {
bool pending;
address sender;
uint256 value;
bytes recipient;
bytes payload;
}

uint256 private _sendId;
mapping(bytes32 => PendingMessage) private _pending;

event MessageRelayed(bytes32 sendId);
error InvalidSendId(bytes32 sendId);

/// @inheritdoc IERC7786GatewaySource
function supportsAttribute(bytes4 /*selector*/) public pure returns (bool) {
return false;
}

/// @inheritdoc IERC7786GatewaySource
function sendMessage(
bytes calldata recipient, // Binary Interoperable Address
bytes calldata payload,
bytes[] calldata attributes
) external payable returns (bytes32 sendId) {
// 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]));

// Note: this reverts with UnsupportedChainId if the recipient is not on a supported chain.
// No real need to check the return value.
getRemoteGateway(recipient);

sendId = bytes32(++_sendId);
_pending[sendId] = PendingMessage(true, msg.sender, msg.value, recipient, payload);

emit MessageSent(
sendId,
InteroperableAddress.formatEvmV1(block.chainid, msg.sender),
recipient,
payload,
0,
attributes
);
}

function quoteRelay(
bytes calldata recipient, // Binary Interoperable Address
bytes calldata /*payload*/,
bytes[] calldata /*attributes*/,
uint256 value,
uint256 gasLimit,
address /*refundRecipient*/
) external view returns (uint256) {
(uint256 cost, ) = _wormholeRelayer.quoteEVMDeliveryPrice(getWormholeChain(recipient), value, gasLimit);
return cost - value;
}

function requestRelay(bytes32 sendId, uint256 gasLimit, address /*refundRecipient*/) external payable {
// TODO: revert if refundRecipient is not address(0)?

PendingMessage memory pmsg = _pending[sendId];
require(pmsg.pending, InvalidSendId(sendId));

// Do we want to do that to get a gas refund? Would it be valuable to keep that information stored?
delete _pending[sendId];

// TODO: Do we care about the returned "sequence"?
_wormholeRelayer.sendPayloadToEvm{value: pmsg.value + msg.value}(
getWormholeChain(pmsg.recipient),
getRemoteGateway(pmsg.recipient),
abi.encode(
sendId,
InteroperableAddress.formatEvmV1(block.chainid, pmsg.sender),
pmsg.recipient,
pmsg.payload
),
pmsg.value,
gasLimit
);

emit MessageRelayed(sendId);
}
}
32 changes: 32 additions & 0 deletions contracts/mocks/crosschain/wormhole/WormholeRelayerMock.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion lib/@openzeppelin-contracts
2 changes: 1 addition & 1 deletion lib/@openzeppelin-contracts-upgradeable
1 change: 1 addition & 0 deletions lib/wormhole-solidity-sdk
Submodule wormhole-solidity-sdk added at 575181
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
@axelar-network/axelar-gmp-sdk-solidity/=lib/axelar-gmp-sdk-solidity/
@zk-email/email-tx-builder/=lib/email-tx-builder/packages/contracts/
@zk-email/contracts/=lib/zk-email-verify/packages/contracts/
wormhole-solidity-sdk/=lib/wormhole-solidity-sdk/src/
Loading