CCTP
Forward to IBC Chain

Noble CCTP Receive + IBC Forward

🚨

DISCLAIMER: In the future, Noble plans to introduce a router module that will enable funds received via CCTP to be forwarded to another chain connected to Noble via IBC.

The following is a high level overview of the proposed flow and initial test cases.

Flow

  • User calls smart contract to burn the tokens on the source chain (Ethereum, Avalanche, Optimism, Arbitrum, etc.), including IBC forwarding instructions.
  • CCTP relayer observes finalized burn and requests Attestation from Circle Iris API
  • Cirle Iris API proves burn with metadata and issues attestations
  • CCTP relayer broadcasts the messages and attestations to Noble
  • Noble processes the CCTP BurnMessage and mints the token
  • Noble processes the CCPT IBC forward metadata message and forwards the token to the destination chain

Deposit For Burn

Similar to Circle's original function (opens in a new tab), the wrapper contract includes four new function parameters, which are highlighted below.

function depositForBurn(
    uint64 channel,
    bytes32 destinationBech32Prefix,
    bytes32 destinationRecipient,
    uint256 amount,
    bytes32 mintRecipient,
    address burnToken,
    bytes calldata memo
) external returns (uint64 nonce)

Let's expand on what each of these parameters means:

channel

This parameter defines the IBC Channel (opens in a new tab) to use when forwarding your newly minted $USDC.

You can find a list of trusted channels here (opens in a new tab) (for testnet, here (opens in a new tab)).

💡

In the case of the dYdX Testnet, this would be channel-20 (opens in a new tab), so 20 would be supplied as an argument.

Note that this needs to be supplied as hex (0x14).

destinationBech32Prefix

This parameter defines the Bech32 Prefix (opens in a new tab) used to encode addresses on the chain funds are being forwarded to.

In order to align ourselves with the CCTP standard, users are required to encode this as a padded bytes32 input. Please see the Encoding section to learn how to do this.

💡

In the case of the dYdX Testnet, dydx would be supplied as an argument.

Note that this needs to be supplied as hex (0x0000000000000000000000000000000000000000000000000000000064796478).

destinationRecipient

This parameter defines the address you wish to forward the new minted $USDC to.

In order to align ourselves with the CCTP standard, users are required to encode this as a padded bytes32 input. Please see the Encoding section to learn how to do this.

memo

Recently introduced (opens in a new tab) to the IBC Transfer specification, memo is an optional field within the packet data that allows users to specify arbitrary data to be included with their transfer. This is used to specify programmatic actions after the funds are received on the destination chain, for example with Packet Forward Middleware (opens in a new tab) to forward packets again, or IBC Hooks (opens in a new tab) for performing various actions such as smart contract execution.

Encoding

Below you will find a simple go program that pads & hex encodes both the destination bech32 prefix and recipient.

The highlighted lines indicate inputs you can change to your specific case.

package main
 
import (
	"bytes"
	"fmt"
 
	"github.com/cosmos/cosmos-sdk/types/bech32"
	"github.com/ethereum/go-ethereum/common"
)
 
const (
	prefix  = "dydx"
	address = "dydx1tq944l2tgxugwvu74yke37yt7pa27p84myc2sd"
)
 
func main() {
	rawPrefix := []byte(prefix)
	encodedPrefix := Encode(rawPrefix)
	decodedPrefix := Decode(encodedPrefix)
 
	_, rawAddress, _ := bech32.DecodeAndConvert(address)
	encodedAddress := Encode(rawAddress)
	decodedAddress := Decode(encodedAddress)
	decodedBech32, _ := bech32.ConvertAndEncode(string(decodedPrefix), decodedAddress)
 
	fmt.Println("ENCODED PREFIX: ", encodedPrefix)
	fmt.Println("DECODED PREFIX: ", string(decodedPrefix))
	fmt.Println()
	fmt.Println("ENCODED ADDRESS:", encodedAddress)
	fmt.Println("DECODED ADDRESS:", decodedBech32)
}
 
func Encode(bz []byte) (encoded string) {
	padded := make([]byte, 32)
	copy(padded[32-len(bz):], bz)
 
	return "0x" + common.Bytes2Hex(padded)
}
 
func Decode(encoded string) (bz []byte) {
	padded := common.FromHex(encoded)
	return bytes.TrimLeft(padded, string(byte(0)))
}

The output of the program above is:

ENCODED PREFIX:  0x0000000000000000000000000000000000000000000000000000000064796478
DECODED PREFIX:  dydx

ENCODED ADDRESS: 0x000000000000000000000000580b5afd4b41b887339ea92d98f88bf07aaf04f5
DECODED ADDRESS: dydx1tq944l2tgxugwvu74yke37yt7pa27p84myc2sd

Example

To send 10 USDC from Goerli Ethereum Testnet to dYdX, address dydx1tq944l2tgxugwvu74yke37yt7pa27p84myc2sd, you would call the following on the Goerli Ethereum Testnet

depositForBurn(
	0x14, // channel
	0x0000000000000000000000000000000000000000000000000000000064796478, // "dYdX" padded to bytes32 in hex
	0x000000000000000000000000580b5afd4b41b887339ea92d98f88bf07aaf04f5, // dydx1tq944l2tgxugwvu74yke37yt7pa27p84myc2sd
	0x989680, // = 10000000uusdc = 10 USDC
	0x000000000000000000000000580b5afd4b41b887339ea92d98f88bf07aaf04f5, // noble1tq944l2tgxugwvu74yke37yt7pa27p8467rxg5 for intermediate account on Noble with same private key as dydx.
	0x07865c6E87B9F70255377e024ace6630C1Eaa37F, // USDC on Goerli Ethereum Testnet
	nil // No memo
)

Sample Transactions:

Example E2E test:

This go test runs the CCTP relayer and sends a packet:

https://github.com/strangelove-ventures/noble-cctp-relayer/blob/main/integration/generate_eth_goerli_deposit_for_burn_with_forward_test.go (opens in a new tab)