Skip to content

Commit

Permalink
EVM: Add wormhole transceiver
Browse files Browse the repository at this point in the history
  • Loading branch information
bruce-riley committed Oct 17, 2024
1 parent 457dfce commit fd7698d
Show file tree
Hide file tree
Showing 11 changed files with 962 additions and 39 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
# TODOs

- Add poc-gmp-router as a repo in here (do "forge install").
- Need to access UniversalAddress as a library.
- Need to access ITransceiver as a library.
- Need to access IRouter as a library.

# Multi-Chain Template

## Overview

This repository serves as a template monorepo for developing on multiple blockchains. The goal is to provide the generally recommended starting setups for each runtime along with pre-configured CI.
Implement a transceiver to allow the GMP router to communicate over Wormhole.

## Runtime Support

Expand Down
3 changes: 3 additions & 0 deletions evm/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,6 @@ docs/

# Dotenv file
.env

# Code coverage
lcov.info
14 changes: 0 additions & 14 deletions evm/src/Counter.sol

This file was deleted.

277 changes: 277 additions & 0 deletions evm/src/WormholeTransceiver.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// SPDX-License-Identifier: Apache 2
pragma solidity >=0.8.8 <0.9.0;

import "wormhole-solidity-sdk/libraries/BytesParsing.sol";
import "wormhole-solidity-sdk/interfaces/IWormhole.sol";

import "./interfaces/IRouter.sol";
import "./interfaces/ITransceiver.sol";
import "./interfaces/IWormholeTransceiver.sol";
import "./libraries/TransceiverHelpers.sol";

contract WormholeTransceiver is IWormholeTransceiver, ITransceiver {
using BytesParsing for bytes;

string public constant versionString = "WormholeTransceiver-0.0.1";
address public admin;

// ==================== Immutables ===============================================
uint16 public immutable ourChainId;
uint256 immutable evmChainId;
IRouter public immutable router;
IWormhole public immutable wormhole;
uint8 public immutable consistencyLevel;
uint256 public immutable gasLimit;

// ==================== Constants ================================================

constructor(
uint16 _ourChainId,
uint256 _evmChainId,
address _admin,
address _router,
address wormholeCoreBridge,
uint8 _consistencyLevel,
uint256 _gasLimit
) {
evmChainId = _evmChainId;
ourChainId = _ourChainId;
admin = _admin;
router = IRouter(_router);
wormhole = IWormhole(wormholeCoreBridge);
consistencyLevel = _consistencyLevel;
gasLimit = _gasLimit;
}

// =============== Storage ===============================================

bytes32 private constant WORMHOLE_CONSUMED_VAAS_SLOT = bytes32(uint256(keccak256("whTransceiver.consumedVAAs")) - 1);

bytes32 private constant WORMHOLE_PEERS_SLOT = bytes32(uint256(keccak256("whTransceiver.peers")) - 1);

// =============== Storage Setters/Getters ========================================

function _getWormholePeersStorage() internal pure returns (mapping(uint16 => bytes32) storage $) {
uint256 slot = uint256(WORMHOLE_PEERS_SLOT);
assembly ("memory-safe") {
$.slot := slot
}
}

// =============== Public Getters ======================================================

/// @inheritdoc IWormholeTransceiver
function getWormholePeer(uint16 chainId) public view returns (bytes32) {
return _getWormholePeersStorage()[chainId];
}

// =============== Admin ===============================================================

/// @notice Transfers admin privileges from the current admin to another contract.
/// @dev The msg.sender must be the current admin contract.
/// @param newAdmin The address of the new admin.
function updateAdmin(address newAdmin) external onlyAdmin {
// Update the storage.
admin = newAdmin;
emit AdminUpdated(msg.sender, newAdmin);
}

// /// @notice Starts the two step process of transferring admin privileges from the current admin to another contract.
// /// @dev The msg.sender must be the current admin contract.
// /// @param integrator The address of the integrator contract.
// /// @param newAdmin The address of the new admin.
// function transferAdmin(address newAdmin) external onlyAdmin {
// // Get the storage for this integrator contract
// mapping(address => IntegratorConfig) storage integratorConfigs = _getIntegratorConfigsStorage();

// // Update the storage with this request.
// integratorConfigs[integrator].transfer = newAdmin;
// emit AdminUpdateRequested(integrator, msg.sender, newAdmin);
// }

// /// @notice Starts the two step process of transferring admin privileges from the current admin to another contract.
// /// @dev The msg.sender must be the current admin contract.
// /// @param integrator The address of the integrator contract.
// function claimAdmin(address integrator) external {
// // Get the storage for this integrator contract
// mapping(address => IntegratorConfig) storage integratorConfigs = _getIntegratorConfigsStorage();

// address oldAdmin = integratorConfigs[integrator].admin;
// address newAdmin = integratorConfigs[integrator].transfer;
// if ((msg.sender == oldAdmin) || (msg.sender == newAdmin)) {
// // Update the storage with this request.
// integratorConfigs[integrator].admin = newAdmin;
// integratorConfigs[integrator].transfer = address(0);
// }
// emit AdminUpdated(integrator, oldAdmin, newAdmin);
// }

/// @inheritdoc IWormholeTransceiver
function setWormholePeer(uint16 peerChainId, bytes32 peerContract) external payable onlyAdmin {
if (peerChainId == 0) {
revert InvalidWormholeChainIdZero();
}
if (peerContract == bytes32(0)) {
revert InvalidWormholePeerZeroAddress();
}

bytes32 oldPeerContract = _getWormholePeersStorage()[peerChainId];

// We don't want to allow updating a peer since this adds complexity in the accountant
// If the owner makes a mistake with peer registration they should deploy a new Wormhole
// transceiver and register this new transceiver with the Router
if (oldPeerContract != bytes32(0)) {
revert PeerAlreadySet(peerChainId, oldPeerContract);
}

_getWormholePeersStorage()[peerChainId] = peerContract;
emit SetWormholePeer(peerChainId, peerContract);
}

// =============== Interface ===============================================================

/// @inheritdoc ITransceiver
function getTransceiverType() external pure returns (string memory) {
return versionString;
}

/// @inheritdoc ITransceiver
function quoteDeliveryPrice(uint16 destinationChainId) external view returns (uint256) {
return _quoteDeliveryPrice(destinationChainId);
}

/// @inheritdoc ITransceiver
/// @dev The caller should set the delivery price in msg.value.
function sendMessage(
UniversalAddress sourceAddress,
uint16 recipientChain,
UniversalAddress recipientAddress,
uint64 sequence,
bytes32 payloadHash,
bytes32 refundAddress
) external payable {
_sendMessage(sourceAddress, recipientChain, recipientAddress, sequence, payloadHash, refundAddress);
}

/// @inheritdoc IWormholeTransceiver
function receiveMessage(bytes memory encodedMessage) external {
_receiveMessage(encodedMessage);
}

// ============= Internal ===============================================================

function _quoteDeliveryPrice(uint16 /*destinationChainId*/ ) internal view returns (uint256) {
return wormhole.messageFee();
}

function _sendMessage(
UniversalAddress sourceAddress,
uint16 recipientChain,
UniversalAddress recipientAddress,
uint64 sequence,
bytes32 payloadHash,
bytes32 // refundAddress
) internal {
bytes memory payload = _encodePayload(sourceAddress, sequence, recipientChain, recipientAddress, payloadHash);
wormhole.publishMessage{value: msg.value}(0, payload, consistencyLevel);
emit SendTransceiverMessage(sourceAddress, recipientChain, recipientAddress, sequence, payloadHash);
}

function _receiveMessage(bytes memory encodedMessage) internal {
uint16 sourceChainId;
bytes memory payload;
(sourceChainId, payload) = _verifyMessage(encodedMessage);

(
UniversalAddress sourceAddress,
uint64 sequence,
uint16 recipientChain,
UniversalAddress recipientAddress,
bytes32 payloadHash
) = _decodePayload(payload);

if (recipientChain != ourChainId) {
revert InvalidChainId(recipientChain);
}

router.attestMessage(sourceChainId, sourceAddress, sequence, ourChainId, recipientAddress, payloadHash);
}

function _encodePayload(
UniversalAddress sourceAddress,
uint64 sequence,
uint16 recipientChain,
UniversalAddress recipientAddress,
bytes32 payloadHash
) internal pure returns (bytes memory payload) {
return abi.encodePacked(sourceAddress, sequence, recipientChain, recipientAddress, payloadHash);
}

function _decodePayload(bytes memory payload)
internal
pure
returns (
UniversalAddress sourceAddress,
uint64 sequence,
uint16 recipientChain,
UniversalAddress recipientAddress,
bytes32 payloadHash
)
{
bytes32 b32;
uint256 offset = 0;

(b32, offset) = payload.asBytes32(offset);
sourceAddress = UniversalAddressLibrary.fromBytes32(b32);

(sequence, offset) = payload.asUint64(offset);
(recipientChain, offset) = payload.asUint16(offset);

(b32, offset) = payload.asBytes32(offset);
recipientAddress = UniversalAddressLibrary.fromBytes32(b32);

(payloadHash, offset) = payload.asBytes32(offset);

_checkLength(payload, offset);
}

function _verifyMessage(bytes memory encodedMessage) internal returns (uint16, bytes memory) {
// verify VAA against Wormhole Core Bridge contract
(IWormhole.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(encodedMessage);

// ensure that the VAA is valid
if (!valid) {
revert InvalidVaa(reason);
}

// ensure that the message came from a registered peer contract
if (!_verifyPeer(vm)) {
revert InvalidWormholePeer(vm.emitterChainId, vm.emitterAddress);
}

// emit `ReceivedMessage` event
emit ReceivedMessage(vm.hash, vm.emitterChainId, vm.emitterAddress, vm.sequence);

return (vm.emitterChainId, vm.payload);
}

function _verifyPeer(IWormhole.VM memory vm) internal view returns (bool) {
checkFork(evmChainId);
return getWormholePeer(vm.emitterChainId) == vm.emitterAddress;
}

function _checkLength(bytes memory payload, uint256 expected) private pure {
if (payload.length != expected) {
revert InvalidPayloadLength(payload.length, expected);
}
}

// =============== MODIFIERS ===============================================

modifier onlyAdmin() {
if (admin != msg.sender) {
revert CallerNotAdmin();
}
_;
}
}
52 changes: 52 additions & 0 deletions evm/src/interfaces/IRouter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.13;

import "../libraries/UniversalAddress.sol";

interface IRouter {
/// @notice Send a message to another chain.
/// @param recipientChain The Wormhole chain ID of the recipient.
/// @param recipientAddress The universal address of the peer on the recipient chain.
/// @param refundAddress The source chain refund address passed to the Transceiver.
/// @param payloadHash keccak256 of a message to be sent to the recipient chain.
/// @return uint64 The sequence number of the message.
function sendMessage(
uint16 recipientChain,
UniversalAddress recipientAddress,
address refundAddress,
bytes32 payloadHash
) external payable returns (uint64);

/// @notice Called by a Transceiver contract to attest to a message.
/// @param sourceChainId The Wormhole chain ID of the sender.
/// @param sourceAddress The universal address of the peer on the sending chain.
/// @param sequence The sequence number of the message (per integrator).
/// @param destinationChainId The Wormhole chain ID of the destination.
/// @param destinationAddress The destination address of the message.
/// @param payloadHash The keccak256 of payload from the integrator.
function attestMessage(
uint16 sourceChainId,
UniversalAddress sourceAddress,
uint64 sequence,
uint16 destinationChainId,
UniversalAddress destinationAddress,
bytes32 payloadHash
) external;

/// @notice Called by a integrator contract to receive a message and mark it executed.
/// @param sourceChainId The Wormhole chain ID of the sender.
/// @param sourceAddress The universal address of the peer on the sending chain.
/// @param sequence The sequence number of the message (per integrator).
/// @param destinationChainId The Wormhole chain ID of the destination.
/// @param destinationAddress The destination address of the message.
/// @param payloadHash The keccak256 of payload from the integrator.
/// @return uint128 The bitmap
function receiveMessage(
uint16 sourceChainId,
UniversalAddress sourceAddress,
uint64 sequence,
uint16 destinationChainId,
UniversalAddress destinationAddress,
bytes32 payloadHash
) external payable returns (uint128, uint128);
}
Loading

0 comments on commit fd7698d

Please sign in to comment.