Skip to main content

CCTP Receive + IBC Forward

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
  • Circle 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, 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 to use when forwarding your newly minted $USDC.

You can find a list of trusted channels here (for testnet, here).

tip

In the case of the dYdX Testnet, this would be channel-20, 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 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.

tip

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 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 to forward packets again, or IBC Hooks 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