-
Notifications
You must be signed in to change notification settings - Fork 24
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
base: master
Are you sure you want to change the base?
Wormhole adaptor #94
Changes from 26 commits
d63e58d
70a4daf
6492d92
bf7e7e2
c628223
69c38a5
82027f0
fceeb40
4b7e226
0135754
f15be8e
8244d04
3a05c3a
da5762b
011b755
a6dd68b
cbad532
8a20f74
4696933
83a78be
a57acc4
c52f4b8
3830d3d
65a2ea1
c6dbbf1
0436913
540ec3c
35b8c2a
6ed786e
47bc84c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.27; | ||
|
||
import {IERC7786Attributes} from "../../interfaces/IERC7786Attributes.sol"; | ||
|
||
/// @dev Library of helper to parse/process ERC-7786 attributes | ||
library ERC7786Attributes { | ||
/// @dev Parse the `requestRelay(uint256,uint256,address)` (0x4cbb573a) attribute into its components. | ||
function tryDecodeRequestRelay( | ||
bytes memory attribute | ||
) internal pure returns (bool success, uint256 value, uint256 gasLimit, address refundRecipient) { | ||
success = bytes4(attribute) == IERC7786Attributes.requestRelay.selector && attribute.length >= 0x64; | ||
|
||
assembly ("memory-safe") { | ||
value := mul(success, mload(add(attribute, 0x24))) | ||
gasLimit := mul(success, mload(add(attribute, 0x44))) | ||
refundRecipient := mul(success, mload(add(attribute, 0x64))) | ||
} | ||
} | ||
|
||
/// @dev Calldata variant of {tryDecodeRequestRelay}. | ||
function tryDecodeRequestRelayCalldata( | ||
bytes calldata attribute | ||
) internal pure returns (bool success, uint256 value, uint256 gasLimit, address refundRecipient) { | ||
success = bytes4(attribute) == IERC7786Attributes.requestRelay.selector && attribute.length >= 0x64; | ||
|
||
assembly ("memory-safe") { | ||
value := mul(success, calldataload(add(attribute.offset, 0x04))) | ||
gasLimit := mul(success, calldataload(add(attribute.offset, 0x24))) | ||
refundRecipient := mul(success, calldataload(add(attribute.offset, 0x44))) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,296 @@ | ||
// 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 {VaaKey} from "wormhole-solidity-sdk/interfaces/IWormholeRelayer.sol"; | ||
import {fromUniversalAddress, toUniversalAddress} from "wormhole-solidity-sdk/utils/UniversalAddress.sol"; | ||
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; | ||
import {BitMaps} from "@openzeppelin/contracts/utils/structs/BitMaps.sol"; | ||
import {InteroperableAddress} from "@openzeppelin/contracts/utils/draft-InteroperableAddress.sol"; | ||
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; | ||
import {ERC7786Attributes} from "../utils/ERC7786Attributes.sol"; | ||
import {IERC7786GatewaySource} from "../../interfaces/IERC7786.sol"; | ||
import {IERC7786Receiver} from "../../interfaces/IERC7786.sol"; | ||
import {IERC7786Attributes} from "../../interfaces/IERC7786Attributes.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. | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* | ||
* Note: only EVM chains are currently supported | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this because of address encoding? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see we're using |
||
*/ | ||
// slither-disable-next-line locked-ether | ||
contract WormholeGatewayAdapter is IERC7786GatewaySource, IWormholeReceiver, Ownable { | ||
using BitMaps for BitMaps.BitMap; | ||
using InteroperableAddress for bytes; | ||
|
||
IWormholeRelayer internal immutable _wormholeRelayer; | ||
uint16 internal immutable _wormholeChainId; | ||
uint24 private constant MASK = 1 << 16; | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// 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; | ||
|
||
// Message temporary representation, waiting for gas payment in requestRelay | ||
struct PendingMessage { | ||
bool pending; | ||
address sender; | ||
uint256 value; | ||
bytes recipient; | ||
bytes payload; | ||
} | ||
|
||
uint256 private _lastSendId; | ||
mapping(bytes32 sendId => PendingMessage) private _pending; | ||
mapping(uint256 chainId => BitMaps.BitMap) private _executed; | ||
|
||
/// @dev A message was relayed to Wormhole (part of the post processing of the outbox ids created by {sendMessage}) | ||
event MessageRelayed(bytes32 sendId); | ||
|
||
/// @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 UnauthorizedCaller(address); | ||
error InvalidOriginGateway(uint16 wormholeSourceChain, bytes32 wormholeSourceAddress); | ||
error ReceiverExecutionFailed(); | ||
error UnsupportedChainId(uint256 chainId); | ||
error UnsupportedWormholeChain(uint16 wormholeId); | ||
error ChainEquivalenceAlreadyRegistered(uint256 chainId, uint16 wormhole); | ||
error RemoteGatewayAlreadyRegistered(uint256 chainId); | ||
error InvalidSendId(bytes32 sendId); | ||
error AdditionalMessagesNotSupported(); | ||
error MessageAlreadyExecuted(uint256 chainId, bytes32 outboxId); | ||
|
||
modifier onlyWormholeRelayer() { | ||
require(msg.sender == address(_wormholeRelayer), UnauthorizedCaller(msg.sender)); | ||
_; | ||
} | ||
|
||
/// @dev Initializes the contract with the Wormhole gateway and the initial owner. | ||
constructor(IWormholeRelayer wormholeRelayer, uint16 wormholeChainId, address initialOwner) Ownable(initialOwner) { | ||
_wormholeRelayer = wormholeRelayer; | ||
_wormholeChainId = wormholeChainId; | ||
} | ||
|
||
/// @dev Returns the local Wormhole relayer | ||
function relayer() public view virtual returns (address) { | ||
return address(_wormholeRelayer); | ||
} | ||
|
||
/// @dev Returns whether a binary interoperable chain id is supported. | ||
function supportedChain(bytes memory chain) public view virtual returns (bool) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's clear or intuitive that |
||
(bool success, uint256 chainId, ) = chain.tryParseEvmV1(); | ||
return success && supportedChain(chainId); | ||
} | ||
|
||
/// @dev Returns whether an EVM chain id is supported. | ||
function supportedChain(uint256 chainId) public view virtual returns (bool) { | ||
return _chainIdToWormhole[chainId] & MASK == MASK; | ||
} | ||
|
||
/// @dev Returns the Wormhole chain id that correspond to a given binary interoperable chain id. | ||
function getWormholeChain(bytes memory chain) public view virtual returns (uint16) { | ||
(uint256 chainId, ) = chain.parseEvmV1(); | ||
return getWormholeChain(chainId); | ||
} | ||
|
||
/// @dev Returns the Wormhole chain id that correspond to a given EVM chain id. | ||
function getWormholeChain(uint256 chainId) public view virtual returns (uint16) { | ||
uint24 wormholeId = _chainIdToWormhole[chainId]; | ||
require(wormholeId & MASK == MASK, UnsupportedChainId(chainId)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this check needed? Why? Please add a comment. Makes me ask why the mapping holds There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok I understand now that this is necessary for |
||
return uint16(wormholeId); | ||
} | ||
|
||
/// @dev Returns the EVM chain id for a given Wormhole chain id. | ||
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 binary interoperable chain id. | ||
function getRemoteGateway(bytes memory chain) public view virtual returns (address) { | ||
(uint256 chainId, ) = chain.parseEvmV1(); | ||
return getRemoteGateway(chainId); | ||
} | ||
|
||
/// @dev Returns the address of the remote gateway for a given EVM chain id. | ||
function getRemoteGateway(uint256 chainId) public view virtual returns (address) { | ||
address addr = _remoteGateways[chainId]; | ||
require(addr != address(0), UnsupportedChainId(chainId)); | ||
return addr; | ||
} | ||
|
||
/// @dev Registers a chain equivalence between a binary interoperable chain id and a Wormhole chain id. | ||
function registerChainEquivalence( | ||
bytes calldata chain, | ||
uint16 wormholeId | ||
) public virtual /*onlyOwner in registerChainEquivalence*/ { | ||
(uint256 chainId, ) = chain.parseEvmV1Calldata(); | ||
registerChainEquivalence(chainId, wormholeId); | ||
} | ||
|
||
/// @dev Registers a chain equivalence between an EVM chain id and a Wormhole chain id. | ||
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); | ||
} | ||
|
||
/// @dev Registers the address of a remote gateway (binary interoperable address version). | ||
function registerRemoteGateway(bytes calldata remote) public virtual /*onlyOwner in registerRemoteGateway*/ { | ||
(uint256 chainId, address addr) = remote.parseEvmV1Calldata(); | ||
registerRemoteGateway(chainId, addr); | ||
} | ||
|
||
/// @dev Registers the address of a remote gateway (EVM version). | ||
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); | ||
} | ||
|
||
/// @inheritdoc IERC7786GatewaySource | ||
function supportsAttribute(bytes4 selector) public pure returns (bool) { | ||
return selector == IERC7786Attributes.requestRelay.selector; | ||
} | ||
|
||
/// @inheritdoc IERC7786GatewaySource | ||
function sendMessage( | ||
bytes calldata recipient, // Binary Interoperable Address | ||
bytes calldata payload, | ||
bytes[] calldata attributes | ||
) external payable returns (bytes32 sendId) { | ||
bool relay = false; | ||
uint256 value = msg.value; | ||
uint256 gasLimit = 0; | ||
|
||
for (uint256 i = 0; i < attributes.length; ++i) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This supports duplicate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
(relay, value, gasLimit, ) = ERC7786Attributes.tryDecodeRequestRelayCalldata(attributes[i]); | ||
require(relay, UnsupportedAttribute(attributes[i].length < 0x04 ? bytes4(0) : bytes4(attributes[i]))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There should probably be a utility function to get the selector from a |
||
} | ||
|
||
if (relay) { | ||
// TODO: Do we care about the returned "sequence"? | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
bytes memory encoded = abi.encode( | ||
sendId, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right, we need another way to deduplicate ... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
InteroperableAddress.formatEvmV1(block.chainid, msg.sender), | ||
recipient, | ||
payload | ||
); | ||
_wormholeRelayer.sendPayloadToEvm{value: msg.value}( | ||
getWormholeChain(recipient), | ||
getRemoteGateway(recipient), | ||
encoded, | ||
value, | ||
gasLimit | ||
); | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} else { | ||
sendId = bytes32(++_lastSendId); | ||
|
||
// Note: this reverts with UnsupportedChainId if the recipient is not on a supported chain. | ||
// No real need to check the return value. | ||
getRemoteGateway(recipient); | ||
|
||
_pending[sendId] = PendingMessage(true, msg.sender, value, recipient, payload); | ||
} | ||
|
||
emit MessageSent( | ||
sendId, | ||
InteroperableAddress.formatEvmV1(block.chainid, msg.sender), | ||
recipient, | ||
payload, | ||
value, | ||
attributes | ||
); | ||
} | ||
|
||
/// @dev Returns a quote for the value that must be passed to {requestRelay} | ||
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; | ||
} | ||
|
||
/// @dev Relay a message that was initiated by {sendMessage}. | ||
function requestRelay(bytes32 sendId, uint256 gasLimit, address /*refundRecipient*/) external payable { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. changed |
||
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? | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
delete _pending[sendId]; | ||
|
||
// TODO: Do we care about the returned "sequence"? | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
bytes memory encoded = abi.encode( | ||
sendId, | ||
InteroperableAddress.formatEvmV1(block.chainid, pmsg.sender), | ||
pmsg.recipient, | ||
pmsg.payload | ||
); | ||
_wormholeRelayer.sendPayloadToEvm{value: pmsg.value + msg.value}( | ||
getWormholeChain(pmsg.recipient), | ||
getRemoteGateway(pmsg.recipient), | ||
encoded, | ||
pmsg.value, | ||
gasLimit | ||
); | ||
|
||
emit MessageRelayed(sendId); | ||
} | ||
|
||
/// @inheritdoc IWormholeReceiver | ||
function receiveWormholeMessages( | ||
bytes memory adapterPayload, | ||
bytes[] memory additionalMessages, | ||
bytes32 wormholeSourceAddress, | ||
uint16 wormholeSourceChain, | ||
bytes32 deliveryHash | ||
) public payable virtual onlyWormholeRelayer { | ||
require(additionalMessages.length == 0, AdditionalMessagesNotSupported()); | ||
|
||
(bytes32 sendId, bytes memory sender, bytes memory recipient, bytes memory payload) = abi.decode( | ||
adapterPayload, | ||
(bytes32, bytes, bytes, bytes) | ||
); | ||
|
||
// Wormhole to ERC-7930 translation | ||
uint256 chainId = getChainId(wormholeSourceChain); | ||
address addr = getRemoteGateway(chainId); | ||
|
||
// 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[chainId].get(uint256(sendId)), MessageAlreadyExecuted(chainId, sendId)); | ||
_executed[chainId].set(uint256(sendId)); | ||
|
||
(, address target) = recipient.parseEvmV1(); | ||
bytes4 result = IERC7786Receiver(target).receiveMessage{value: msg.value}(deliveryHash, sender, payload); | ||
require(result == IERC7786Receiver.receiveMessage.selector, ReceiverExecutionFailed()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
// SPDX-License-Identifier: MIT | ||
|
||
pragma solidity ^0.8.4; | ||
|
||
/** | ||
* @dev Standard attributes for ERC-7786. These attributes may be standardized in different ERCs. | ||
*/ | ||
interface IERC7786Attributes { | ||
function requestRelay(uint256 value, uint256 gasLimit, address refundRecipient) external; | ||
} |
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 | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
uint64 seq = _seq++; | ||
IWormholeReceiver(targetAddress).receiveWormholeMessages{value: receiverValue, gas: gasLimit}( | ||
payload, | ||
new bytes[](0), | ||
toUniversalAddress(msg.sender), | ||
targetChain, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be the source chain. Any reasonable way to do that in this mock? I think it should at least not be the target chain. |
||
keccak256(abi.encode(seq)) | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
); | ||
|
||
return seq; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.