Skip to content
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

CCTP-support + Remove Replay Protection #18

Merged
merged 23 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from 14 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
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,24 @@ forge install wormhole-foundation/wormhole-solidity-sdk

[HelloWormhole - Simple cross-chain message sending application](https://github.com/wormhole-foundation/hello-wormhole)

[HelloToken - Simple cross-chain token sending application](https://github.com/wormhole-foundation/hello-token)
[HelloToken - Simple cross-chain token sending application](https://github.com/wormhole-foundation/hello-tokens)

[HelloUSDC - Simple cross-chain USDC sending application using CCTP](https://github.com/wormhole-foundation/hello-usdc)

### SDK Summary

- Includes interfaces to interact with contracts in the Wormhole ecosystem ([src/interfaces](https://github.com/wormhole-foundation/wormhole-solidity-sdk/tree/main/src/interfaces))
- Includes the base class ‘Base’ with helpers for common actions that will typically need to be done within ‘receiveWormholeMessages’:
- [`onlyWormholeRelayer()`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L26): Checking that msg.sender is the wormhole relayer contract
- [`replayProtect(bytes32 deliveryHash)`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L31): Checking that the current delivery has not already been processed (via the hash)
- [`onlyWormholeRelayer()`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/Base.sol#L24): Checking that msg.sender is the wormhole relayer contract
Sometimes, Cross-chain applications may be set up such that there is one ‘spoke’ contract on every chain, which sends messages to the ‘hub’ contract. If so, we’d ideally only want to allow messages to be sent from these spoke contracts. Included are helpers for this:
- [`setRegisteredSender(uint16 sourceChain, bytes32 sourceAddress)`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L49): Setting the specified sender for ‘sourceChain’ to be ‘sourceAddress’
- [`isRegisteredSender(uint16 sourceChain, bytes32 sourceAddress)`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L37): Checking that the sender who requested the delivery is the registered address for that chain
Look at [test/Fork.t.sol](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/test/Fork.t.sol#L16) for an example usage of Base
- Included are also the ‘[TokenSender](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L79)’ and ‘[TokenReceiver](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L186)’ base classes with helpers for smart contracts that wish to send and receive tokens using Wormhole’s TokenBridge. See ‘[HelloToken](https://github.com/wormhole-foundation/hello-token)’ for example usage.

- [`setRegisteredSender(uint16 sourceChain, bytes32 sourceAddress)`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/Base.sol#L47): Setting the specified sender for ‘sourceChain’ to be ‘sourceAddress’
- [`isRegisteredSender(uint16 sourceChain, bytes32 sourceAddress)`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/Base.sol#L35): Checking that the sender who requested the delivery is the registered address for that chain

Look at test/Counter.t.sol for an example usage of Base

- Included are also the ‘[TokenSender](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/TokenBase#L36)’ and ‘[TokenReceiver](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/TokenBase.sol#L126)’ base classes with helpers for smart contracts that wish to send and receive tokens using Wormhole’s TokenBridge. See ‘[HelloToken](https://github.com/wormhole-foundation/hello-token)’ for example usage.
- Included are also the ‘[CCTPSender](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/CCTPBase#L70)’ and ‘[CCTPReceiver](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/CCTPBase.sol#L134)’ base classes with helpers for smart contracts that wish to send and receive both tokens using Wormhole’s TokenBridge as well as USDC using CCTP. See ‘[HelloUSDC](https://github.com/wormhole-foundation/hello-usdc)’ for example usage.
- Included are helpers that help set up a local forge testing environment. See ‘[HelloWormhole](https://github.com/wormhole-foundation/hello-wormhole)’ for example usage.
**Note: This code is meant to be used as starter / reference code. Feel free to modify for use in your contracts, and also make sure to audit any code used from here as part of your contracts before deploying to mainnet.**

**Note: This code is meant to be used as starter / reference code. Feel free to modify for use in your contracts, and also make sure to audit any code used from here as part of your contracts before deploying to mainnet.**
1 change: 1 addition & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ solc_version = "0.8.13"
src = "src"
out = "out"
libs = ["lib"]
via_ir = true

# See more config options https://github.com/foundry-rs/foundry/tree/master/config
51 changes: 51 additions & 0 deletions src/Base.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@

pragma solidity ^0.8.13;

import "./interfaces/IWormholeReceiver.sol";
import "./interfaces/IWormholeRelayer.sol";
import "./interfaces/IWormhole.sol";
import "./Utils.sol";

abstract contract Base {
IWormholeRelayer public immutable wormholeRelayer;
IWormhole public immutable wormhole;

mapping(bytes32 => bool) public seenDeliveryVaaHashes;

address registrationOwner;
mapping(uint16 => bytes32) registeredSenders;

constructor(address _wormholeRelayer, address _wormhole) {
wormholeRelayer = IWormholeRelayer(_wormholeRelayer);
wormhole = IWormhole(_wormhole);
registrationOwner = msg.sender;
}

modifier onlyWormholeRelayer() {
require(msg.sender == address(wormholeRelayer), "Msg.sender is not Wormhole Relayer");
_;
}

modifier replayProtect(bytes32 deliveryHash) {
require(!seenDeliveryVaaHashes[deliveryHash], "Message already processed");
seenDeliveryVaaHashes[deliveryHash] = true;
_;
}

modifier isRegisteredSender(uint16 sourceChain, bytes32 sourceAddress) {
require(registeredSenders[sourceChain] == sourceAddress, "Not registered sender");
_;
}

/**
* Sets the registered address for 'sourceChain' to 'sourceAddress'
* So that for messages from 'sourceChain', only ones from 'sourceAddress' are valid
*
* Assumes only one sender per chain is valid
* Sender is the address that called 'send' on the Wormhole Relayer contract on the source chain)
*/
function setRegisteredSender(uint16 sourceChain, bytes32 sourceAddress) public {
require(msg.sender == registrationOwner, "Not allowed to set registered sender");
registeredSenders[sourceChain] = sourceAddress;
}
}
169 changes: 169 additions & 0 deletions src/CCTPBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@

pragma solidity ^0.8.13;

import "./interfaces/IWormholeReceiver.sol";
import "./interfaces/IWormholeRelayer.sol";
import "./interfaces/ITokenBridge.sol";
import {IERC20} from "./interfaces/IERC20.sol";
import "./interfaces/CCTPInterfaces/ITokenMessenger.sol";
import "./interfaces/CCTPInterfaces/IMessageTransmitter.sol";

import "./Utils.sol";
import "./TokenBase.sol";

library CCTPMessageLib {
uint8 constant CCTP_KEY_TYPE = 2;
barnjamin marked this conversation as resolved.
Show resolved Hide resolved

// encoded using abi.encodePacked(domain, nonce)
struct CCTPKey {
uint32 domain;
uint64 nonce;
}

// encoded using abi.encode(message, signature)
struct CCTPMessage {
bytes message;
bytes signature;
}
}

abstract contract CCTPBase is TokenBase {
ITokenMessenger immutable circleTokenMessenger;
IMessageTransmitter immutable circleMessageTransmitter;
address immutable USDC;

constructor(
address _wormholeRelayer,
address _tokenBridge,
address _wormhole,
address _circleMessageTransmitter,
address _circleTokenMessenger,
address _USDC
) TokenBase(_wormholeRelayer, _tokenBridge, _wormhole) {
circleTokenMessenger = ITokenMessenger(_circleTokenMessenger);
circleMessageTransmitter = IMessageTransmitter(_circleMessageTransmitter);
USDC = _USDC;
}

function getCCTPDomain(uint16 chain) internal pure returns (uint32) {
if (chain == 2) {
return 0;
} else if (chain == 6) {
return 1;
} else if (chain == 23) {
return 3;
} else if (chain == 24) {
return 2;
} else {
revert("Wrong CCTP Domain");
}
}

function redeemUSDC(bytes memory cctpMessage) internal returns (uint256 amount) {
(bytes memory message, bytes memory signature) = abi.decode(cctpMessage, (bytes, bytes));
uint256 beforeBalance = IERC20(USDC).balanceOf(address(this));
circleMessageTransmitter.receiveMessage(message, signature);
return IERC20(USDC).balanceOf(address(this)) - beforeBalance;
}
}

abstract contract CCTPSender is CCTPBase {
uint8 internal constant CONSISTENCY_LEVEL_FINALIZED = 15;
barnjamin marked this conversation as resolved.
Show resolved Hide resolved

using CCTPMessageLib for *;

/**
* transferTokens wraps common boilerplate for sending tokens to another chain using IWormholeRelayer
* - approves tokenBridge to spend 'amount' of 'token'
* - emits token transfer VAA
* - returns VAA key for inclusion in WormholeRelayer `additionalVaas` argument
*
* Note: this requires that only the targetAddress can redeem transfers.
*
*/

function transferUSDC(uint256 amount, uint16 targetChain, address targetAddress)
internal
returns (MessageKey memory)
{
IERC20(USDC).approve(address(circleTokenMessenger), amount);
uint64 nonce = circleTokenMessenger.depositForBurnWithCaller(
amount,
getCCTPDomain(targetChain),
addressToBytes32CCTP(targetAddress),
USDC,
addressToBytes32CCTP(targetAddress)
);
return MessageKey(
CCTPMessageLib.CCTP_KEY_TYPE, abi.encodePacked(getCCTPDomain(wormhole.chainId()), nonce)
);
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
}

function sendUSDCWithPayloadToEvm(
uint16 targetChain,
address targetAddress,
bytes memory payload,
uint256 receiverValue,
uint256 gasLimit,
uint256 amount
) internal returns (uint64 sequence) {
MessageKey[] memory messageKeys = new MessageKey[](1);
messageKeys[0] = transferUSDC(amount, targetChain, targetAddress);

bytes memory userPayload = abi.encode(amount, payload);
address defaultDeliveryProvider = wormholeRelayer.getDefaultDeliveryProvider();

(uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit);

sequence = wormholeRelayer.sendToEvm{value: cost}(
targetChain,
targetAddress,
userPayload,
receiverValue,
0,
gasLimit,
targetChain,
address(0x0),
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
defaultDeliveryProvider,
messageKeys,
CONSISTENCY_LEVEL_FINALIZED
);
}

function addressToBytes32CCTP(address addr) private pure returns (bytes32) {
return bytes32(uint256(uint160(addr)));
}
}

abstract contract CCTPReceiver is CCTPBase {
function receiveWormholeMessages(
bytes memory payload,
bytes[] memory additionalMessages,
bytes32 sourceAddress,
uint16 sourceChain,
bytes32 deliveryHash
) external payable {
require(additionalMessages.length <= 1, "CCTP: At most one Message is supported");

uint256 amountUSDCReceived;
if (additionalMessages.length == 1) {
amountUSDCReceived = redeemUSDC(additionalMessages[0]);
}

(uint256 amount, bytes memory userPayload) = abi.decode(payload, (uint256, bytes));

// Check that the correct amount was received
// It is important to verify that the 'USDC' received is
barnjamin marked this conversation as resolved.
Show resolved Hide resolved
require(amount == amountUSDCReceived, "Wrong amount received");

receivePayloadAndUSDC(userPayload, amountUSDCReceived, sourceAddress, sourceChain, deliveryHash);
}

function receivePayloadAndUSDC(
bytes memory payload,
uint256 amountUSDCReceived,
bytes32 sourceAddress,
uint16 sourceChain,
bytes32 deliveryHash
) internal virtual {}
}
Loading