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 5 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 @@ -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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib/
78 changes: 78 additions & 0 deletions contracts/crosschain/wormhole/WormholeGatewayBase.sol
Original file line number Diff line number Diff line change
@@ -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";

abstract contract WormholeGatewayBase is Ownable {
IWormholeRelayer internal immutable _wormholeRelayer;
uint16 internal immutable _wormholeChainId;

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 wormholeChainId) {
_wormholeRelayer = wormholeRelayer;
_wormholeChainId = wormholeChainId;
}

function relayer() 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);
}
}
58 changes: 58 additions & 0 deletions contracts/crosschain/wormhole/WormholeGatewayDestination.sol
Original file line number Diff line number Diff line change
@@ -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());
}
}
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) {}
}
112 changes: 112 additions & 0 deletions contracts/crosschain/wormhole/WormholeGatewaySource.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

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";
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 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];

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

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

require(pmsg.sequence != 0, CannotRetryMessage(outboxId));

pmsg.sequence = _wormholeRelayer.resendToEvm(
VaaKey(_wormholeChainId, toUniversalAddress(address(this)), pmsg.sequence),
fromCAIP2(pmsg.destinationChain),
0,
gasLimit,
newDeliveryProvider
);
}
}
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;
}
}
1 change: 1 addition & 0 deletions lib/wormhole-solidity-sdk
Submodule wormhole-solidity-sdk added at 575181
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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/*",
Expand Down
1 change: 1 addition & 0 deletions remappings.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Loading
Loading