Skip to content

Commit

Permalink
testing utils overhaul (#62)
Browse files Browse the repository at this point in the history
* wormhole override overhaul that stores state on the core bridge

* finish and test WormholeOverride, fix tests

* add and overhaul CCTP utils, add documentation

* Update docs/Testing.md

Co-authored-by: scnale <[email protected]>

---------

Co-authored-by: scnale <[email protected]>
  • Loading branch information
nonergodic and scnale authored Oct 17, 2024
1 parent 5ae8bf3 commit 88c4a6d
Show file tree
Hide file tree
Showing 15 changed files with 1,136 additions and 524 deletions.
55 changes: 55 additions & 0 deletions docs/Testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Testing

As a general point, your testing environment should match your real world environment as much as possible. Therefore, fork testing is generally encouraged over running standalone tests, despite the RPC provider dependency and its associated headaches.

Besides fork testing, forge also offers [mockCall](https://book.getfoundry.sh/cheatcodes/mock-call), [mockCallRevert](https://book.getfoundry.sh/cheatcodes/mock-call-revert), and [mockFunction](https://book.getfoundry.sh/cheatcodes/mock-function) cheat-codes, which should typically be preferred over writing and deploying mock contracts due to their more explicit nature.

## Utils

All Solidity testing utilities can be found in src/testing.

### WormholeOverride

**Purpose**

The `WormholeOverride` library is the main way to fork test integrations. It allows overriding the current guardian set of the core bridge with a newly generated one which can then be used to sign messages and thus create VAAs.

**Default Guardian Set**

By default the new guardian set has the same size as the old one, again to match the forked network's setup as closely as possible and keep message sizes and gas costs accurate. Since this can bloat traces and make them harder to read due to the VAAs' sizes, overriding with a single guardian when debugging tests can be helpful. This can be achieved by setting the environment variable `DEFAULT_TO_DEVNET_GUARDIAN` to true.

**Log Parsing**

Besides signing messages / creating VAAs, `WormholeOverride` also provides convenient forge log parsing capabilities to ensure that the right number of messages with the correct content are emitted by the core bridge. Be sure to call `vm.recordLogs();` beforehand to capture emitted events so that they are available for parsing.

**Message Fee**

Integrators should ensure that their contracts work correctly in case of a non-zero Wormhole message fee. `WormholeOverride` provides `setMessageFee` for this purpose.


### CctpOverride

The `CctpOverride` library, is somewhat similar to `WormholeOverride` in that it allows overriding Circle's attester in their CCTP [MessageTransmitter](https://github.com/circlefin/evm-cctp-contracts/blob/master/src/MessageTransmitter.sol) contract (which is comparable in its functionality to Wormhole's core bridge).

However, `CctpOverride`, rather than providing generic signing and log parsing functionality like `WormholeOverride`, is more specialized and only deals with signing and log-parsing `CctpTokenBurnMessage`s emitted through Circle's [TokenMessenger](https://github.com/circlefin/evm-cctp-contracts/blob/master/src/TokenMessenger.sol) contract (which is roughly comparable to Wormhole's token bridge).


### WormholeCctpSimulator

The `WormholeCctpSimulator` contract can be deployed to simulate a virtual `WormholeCctpTokenMessenger` instance on some made-up foreign chain. It uses `0xDDDDDDDD` as the circle domain of that chain, and also simulates virtual instances of Circle's TokenMessenger and USDC contract, which are correctly registered with the instances on the forked chain. The foreign Wormhole chain id and the address of the foreign sender can be set during construction. Uses `WormholeOverride` and `CctpOverride`.

### UsdcDealer

Forge's `deal` cheat code does not work for USDC. `UsdcDealer` is another override library that implements a `deal` function that allows minting of USDC.

### CctpMessages

Library to parse CCTP messages composed/emitted by Circle's `TokenMessenger` and `MessageTransmitter` contracts. Used in `CctpOverride` and `WormholeCctpSimulator`.

### ERC20Mock

Copy of SolMate's ERC20 Mock token that uses the overrideable `IERC20` interface of this SDK to guarantee compatibility.

### LogUtils

A library to simplify filtering of logs captured in Forge tests. Used by `WormholeOverride`, `CctpOverride`, ...
323 changes: 323 additions & 0 deletions src/WormholeCctpTokenMessenger.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,323 @@
// SPDX-License-Identifier: Apache 2
pragma solidity ^0.8.19;

import {IERC20} from "IERC20/IERC20.sol";

import {IWormhole} from "wormhole-sdk/interfaces/IWormhole.sol";
import {IMessageTransmitter} from "wormhole-sdk/interfaces/cctp/IMessageTransmitter.sol";
import {ITokenMessenger} from "wormhole-sdk/interfaces/cctp/ITokenMessenger.sol";
import {ITokenMinter} from "wormhole-sdk/interfaces/cctp/ITokenMinter.sol";

import {toUniversalAddress} from "wormhole-sdk/Utils.sol";
import {WormholeCctpMessages} from "wormhole-sdk/libraries/WormholeCctpMessages.sol";
import {CONSISTENCY_LEVEL_FINALIZED} from "wormhole-sdk/constants/ConsistencyLevel.sol";

/**
* @notice A way to associate a CCTP token burn message with a Wormhole message.
* @dev To construct the contract, the addresses to the Wormhole Core Bridge and CCTP Token
* Messenger must be provided. Using the CCTP Token Messenger, the Message Transmitter and Token
* Minter are derived.
*
* NOTE: For more information on CCTP message formats, please refer to the following:
* https://developers.circle.com/stablecoins/docs/message-format.
*/
abstract contract WormholeCctpTokenMessenger {
using { toUniversalAddress } for address;

/// @dev Parsing and verifying VAA reverted at the Wormhole Core Bridge contract level.
error InvalidVaa();

/**
* @dev The CCTP message's source domain, destination domain and nonce must match the VAA's.
* NOTE: This nonce is the one acting as the CCTP message sequence (and not the arbitrary one
* specified when publishing Wormhole messages).
*/
error CctpVaaMismatch(uint32, uint32, uint64);

/// @dev The emitter of the VAA must match the expected emitter.
error UnexpectedEmitter(bytes32, bytes32);

/// @dev Wormhole Core Bridge contract address.
IWormhole immutable _wormhole;

/// @dev Wormhole Chain ID. NOTE: This is NOT the EVM chain ID.
uint16 immutable _chainId;

/// @dev CCTP Message Transmitter contract interface.
IMessageTransmitter immutable _messageTransmitter;

/// @dev CCTP Token Messenger contract interface.
ITokenMessenger immutable _tokenMessenger;

/// @dev CCTP Token Minter contract interface.
ITokenMinter immutable _tokenMinter;

/// @dev CCTP domain for this network (configured by the CCTP Message Transmitter).
uint32 immutable _localCctpDomain;

constructor(address wormhole, address cctpTokenMessenger) {
_wormhole = IWormhole(wormhole);
_chainId = _wormhole.chainId();

_tokenMessenger = ITokenMessenger(cctpTokenMessenger);
_messageTransmitter = _tokenMessenger.localMessageTransmitter();
_tokenMinter = _tokenMessenger.localMinter();
_localCctpDomain = _messageTransmitter.localDomain();
}

/**
* @dev A convenience method to set the token spending allowance for the CCTP Token Messenger,
* who will ultimately be burning the tokens.
*/
function setTokenMessengerApproval(address token, uint256 amount) internal {
IERC20(token).approve(address(_tokenMessenger), amount);
}

/**
* @dev Method to burn tokens via CCTP Token Messenger and publish a Wormhole message associated
* with the CCTP Token Burn message. The Wormhole message encodes a `Deposit` (ID == 1), which
* has the same source domain, destination domain and nonce as the CCTP Token Burn message.
*
* NOTE: This method does not protect against re-entrancy here because it relies on the CCTP
* Token Messenger to protect against any possible re-entrancy. We are leaning on the fact that
* the Token Messenger keeps track of its local tokens, which are the only tokens it allows to
* burn (and in turn, mint on another network).
*
* NOTE: The wormhole message fee is not required to be paid by the transaction sender (so an
* integrator can use ETH funds in his contract to pay for this fee if he wants to).
*/
function burnAndPublish(
bytes32 destinationCaller,
uint32 destinationCctpDomain,
address token,
uint256 amount,
bytes32 mintRecipient,
uint32 wormholeNonce,
bytes memory payload,
uint256 wormholeFee
) internal returns (uint64 wormholeSequence, uint64 cctpNonce) {
// Invoke Token Messenger to burn tokens and emit a CCTP token burn message.
cctpNonce = _tokenMessenger.depositForBurnWithCaller(
amount, destinationCctpDomain, mintRecipient, token, destinationCaller
);

// Publish deposit message via Wormhole Core Bridge.
wormholeSequence = _wormhole.publishMessage{value: wormholeFee}(
wormholeNonce,
WormholeCctpMessages.encodeDeposit(
token.toUniversalAddress(),
amount,
_localCctpDomain, // sourceCctpDomain
destinationCctpDomain,
cctpNonce,
msg.sender.toUniversalAddress(), // burnSource
mintRecipient,
payload
),
CONSISTENCY_LEVEL_FINALIZED
);
}

/**
* @dev Method to verify and reconcile CCTP and Wormhole messages in order to mint tokens for
* the encoded mint recipient. This method will revert with custom errors.
* NOTE: This method does not require the caller to be the mint recipient. If your contract
* requires that the mint recipient is the caller, you should add a check after calling this
* method to see if msg.sender.toUniversalAddress() == mintRecipient.
*/
function verifyVaaAndMint(
bytes calldata encodedCctpMessage,
bytes calldata cctpAttestation,
bytes calldata encodedVaa
) internal returns (
IWormhole.VM memory vaa,
bytes32 token,
uint256 amount,
bytes32 burnSource,
bytes32 mintRecipient,
bytes memory payload
) {
// First parse and verify VAA.
vaa = _parseAndVerifyVaa( encodedVaa, true /*revertCustomErrors*/);

// Decode the deposit message so we can match the Wormhole message with the CCTP message.
uint32 sourceCctpDomain;
uint32 destinationCctpDomain;
uint64 cctpNonce;
(
token,
amount,
sourceCctpDomain,
destinationCctpDomain,
cctpNonce,
burnSource,
mintRecipient,
payload
) = WormholeCctpMessages.decodeDeposit(vaa.payload);

// Finally reconcile messages and mint tokens to the mint recipient.
token = _matchMessagesAndMint(
encodedCctpMessage,
cctpAttestation,
sourceCctpDomain,
destinationCctpDomain,
cctpNonce,
token,
true // revertCustomErrors
);
}

/**
* @dev PLEASE USE `verifyVaaAndMint` INSTEAD. Method to verify and reconcile CCTP and Wormhole
* messages in order to mint tokens for the encoded mint recipient. This method will revert with
* Solidity's built-in Error(string).
* NOTE: This method does not require the caller to be the mint recipient. If your contract
* requires that the mint recipient is the caller, you should add a check after calling this
* method to see if msg.sender.toUniversalAddress() == mintRecipient.
*/
function verifyVaaAndMintLegacy(
bytes calldata encodedCctpMessage,
bytes calldata cctpAttestation,
bytes calldata encodedVaa
) internal returns (
IWormhole.VM memory vaa,
bytes32 token,
uint256 amount,
uint32 sourceCctpDomain,
uint32 destinationCctpDomain,
uint64 cctpNonce,
bytes32 burnSource,
bytes32 mintRecipient,
bytes memory payload
) {
// First parse and verify VAA.
vaa = _parseAndVerifyVaa(encodedVaa, false /*revertCustomErrors*/);

// Decode the deposit message so we can match the Wormhole message with the CCTP message.
(
token,
amount,
sourceCctpDomain,
destinationCctpDomain,
cctpNonce,
burnSource,
mintRecipient,
payload
) = WormholeCctpMessages.decodeDeposit(vaa.payload);

// Finally reconcile messages and mint tokens to the mint recipient.
token = _matchMessagesAndMint(
encodedCctpMessage,
cctpAttestation,
sourceCctpDomain,
destinationCctpDomain,
cctpNonce,
token,
false // revertCustomErrors
);
}

/**
* @dev For a given remote domain and token, fetch the corresponding local token, for which the
* CCTP Token Minter has minting authority.
*/
function fetchLocalToken(
uint32 remoteDomain,
bytes32 remoteToken
) internal view returns (bytes32 localToken) {
localToken = _tokenMinter.remoteTokensToLocalTokens(
keccak256(abi.encodePacked(remoteDomain, remoteToken))
).toUniversalAddress();
}

/**
* @dev We encourage an integrator to use this method to make sure the VAA is emitted from one
* that his contract trusts. Usually foreign emitters are stored in a mapping keyed off by
* Wormhole Chain ID (uint16).
*
* NOTE: Reverts with `UnexpectedEmitter(bytes32, bytes32)`.
*/
function requireEmitter(IWormhole.VM memory vaa, bytes32 expectedEmitter) internal pure {
if (expectedEmitter != 0 && vaa.emitterAddress != expectedEmitter)
revert UnexpectedEmitter(vaa.emitterAddress, expectedEmitter);
}

/**
* @dev We encourage an integrator to use this method to make sure the VAA is emitted from one
* that his contract trusts. Usually foreign emitters are stored in a mapping keyed off by
* Wormhole Chain ID (uint16).
*
* NOTE: Reverts with built-in Error(string).
*/
function requireEmitterLegacy(IWormhole.VM memory vaa, bytes32 expectedEmitter) internal pure {
require(expectedEmitter != 0 && vaa.emitterAddress == expectedEmitter, "unknown emitter");
}

// private

function _parseAndVerifyVaa(
bytes calldata encodedVaa,
bool revertCustomErrors
) private view returns (IWormhole.VM memory vaa) {
bool valid;
string memory reason;
(vaa, valid, reason) = _wormhole.parseAndVerifyVM(encodedVaa);

if (!valid) {
if (revertCustomErrors)
revert InvalidVaa();
else
require(false, reason);
}
}

function _matchMessagesAndMint(
bytes calldata encodedCctpMessage,
bytes calldata cctpAttestation,
uint32 vaaSourceCctpDomain,
uint32 vaaDestinationCctpDomain,
uint64 vaaCctpNonce,
bytes32 burnToken,
bool revertCustomErrors
) private returns (bytes32 mintToken) {
// Confirm that the caller passed the correct message pair.
{
uint32 sourceDomain;
uint32 destinationDomain;
uint64 nonce;

assembly ("memory-safe") {
// NOTE: First four bytes is the CCTP message version.
let ptr := calldataload(encodedCctpMessage.offset)

// NOTE: There is no need to mask here because the types defined outside of this
// block will already perform big-endian masking.

// Source domain is bytes 4..8, so shift 24 bytes to the right.
sourceDomain := shr(192, ptr)
// Destination domain is bytes 8..12, so shift 20 bytes to the right.
destinationDomain := shr(160, ptr)
// Nonce is bytes 12..20, so shift 12 bytes to the right.
nonce := shr(96, ptr)
}

if (
vaaSourceCctpDomain != sourceDomain ||
vaaDestinationCctpDomain != destinationDomain||
vaaCctpNonce != nonce
) {
if (revertCustomErrors)
revert CctpVaaMismatch(sourceDomain, destinationDomain, nonce);
else
require(false, "invalid message pair");
}
}

// Call the circle bridge to mint tokens to the recipient.
_messageTransmitter.receiveMessage(encodedCctpMessage, cctpAttestation);

// We should trust that this getter will not return the zero address because the TokenMinter
// will have already minted the valid token for the mint recipient.
mintToken = fetchLocalToken(vaaSourceCctpDomain, burnToken);
}
}
5 changes: 1 addition & 4 deletions src/WormholeRelayer/CCTPAndTokenBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,7 @@ abstract contract CCTPAndTokenSender is CCTPAndTokenBase {
USDC,
targetAddressBytes32
);
return MessageKey(
CCTPMessageLib.CCTP_KEY_TYPE,
abi.encodePacked(getCCTPDomain(wormhole.chainId()), nonce)
);
return MessageKey(CCTP_KEY_TYPE, abi.encodePacked(getCCTPDomain(wormhole.chainId()), nonce));
}

// Publishes a CCTP transfer of 'amount' of USDC
Expand Down
Loading

0 comments on commit 88c4a6d

Please sign in to comment.