Skip to content

UCS01 Relay Solidity Integration

This example demonstrates how to create a Solidity contract that calls Union’s UCS01 Relay to transfer assets.

UCS01_RELAY=0xD0081080Ae8493cf7340458Eaf4412030df5FEEb

Source code for this example can be found her: example-ucs01-solidity For this example we will be using foundry.

Install foundry

curl -L https://foundry.paradigm.xyz | bash
  • Directorysrc
    • Transfer.sol
  • Directoryscript
    • Transfer.s.sol
  • remappings.txt
  • foundry.toml

Transfer USDC on Sepolia

Let’s write a dead simple contract that transfers USDC from one address to another.

Contract Source

// src/Transfer.sol
pragma solidity ^0.8.27;

import {IERC20} from "forge-std/interfaces/IERC20.sol";

struct LocalToken {
  address denom;
  uint128 amount;
}

struct Height {
  uint64 revisionNumber;
  uint64 revisionHeight;
}

interface IRelay {
  function send(
    string calldata sourceChannel,
    bytes calldata receiver,
    LocalToken[] calldata tokens,
    string calldata extension,
    Height calldata timeoutHeight,
    uint64 timeoutTimestamp
  ) external;
}

contract Transfer {
  // https://github.com/unionlabs/union/blob/main/evm/README.md#sepolia
  address public constant relay = 0xD0081080Ae8493cf7340458Eaf4412030df5FEEb;

  // https://github.com/unionlabs/union/blob/main/evm/contracts/apps/ucs/01-relay/Relay.sol#L54-L61
  function transferAsset() public {
    LocalToken[] memory tokens = new LocalToken[](1);
    tokens[0].denom = 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238;
    tokens[0].amount = 1000000;

    IERC20(tokens[0].denom).approve(relay, 1000000);

    IRelay(relay).send(
      "channel-90",
      hex"a833B03D8ED1228C4791cBfAb22b3ED57954429F",
      tokens,
      "",
      Height({revisionNumber: 100, revisionHeight: 1000000}),
      0
    );
  }
}

Contract Running Script

// script/Transfer.s.sol
pragma solidity ^0.8.27;

import {Script, console} from "forge-std/Script.sol";
import {Transfer} from "../src/Transfer.sol";
import {IERC20} from "forge-std/interfaces/IERC20.sol";

contract TransferScript is Script {
  address public constant USDC = 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238;

  function run() public {
    uint256 privateKey = vm.envUint("PRIVATE_KEY");
    vm.startBroadcast(privateKey);
    Transfer transfer = new Transfer();

    IERC20(USDC).transfer(address(transfer), 1500000);

    console.log("transferring");
    transfer.transferAsset();
    console.log("complete");
    vm.stopBroadcast();
  }
}

You will be using the same RPC url across examples, so good to export it to an environment variable:

RPC_URL=https://eth-sepolia.g.alchemy.com/v2/<YOUR_API_KEY>

Funding the Wallet

ETH Sepolia faucets:

USDC faucet: faucet.circle.com

or pick a different token from faucet list here app.union.build/faucet

Executing the Contract

We will transfer 1 USDC from Sepolia to a Union wallet.

The first transfer we will do against a forked network using anvil.

Forked Network Example

  1. Grab a sepolia RPC url from chainlist.org. Recommended to use a premium RPC provider.

  2. Start a local anvil fork of Sepolia

    anvil --fork-url $RPC_URL
    
  3. Run the script with forge

    forge script script/Transfer.s.sol:TransferScript \
        --fork-url $RPC_URL \
        --broadcast
    
  4. we should see something like this:

    [⠊] Compiling...
    No files changed, compilation skipped
    Script ran successfully.
    
    ##### sepolia
    ✅  [Success]Hash: 0x38b0fc68c482f75c1ca89e069cf18fea712131fd44c930bee03274804e9fc6b7
    Block: 6801346
    Paid: 0.042409917351145045 ETH (183391 gas * 231.254081995 gwei)
    

Live Sepolia Example

  1. Grab a sepolia RPC url from chainlist.org. Recommended to use a premium RPC provider.
  2. Run the script with forge, this will also deploy the contract
    forge script script/Transfer.s.sol:TransferScript \
        --rpc-url $RPC_URL \
        --private-key $PRIVATE_KEY \
        --broadcast
    
    [⠊] Compiling...
    No files changed, compilation skipped
    Script ran successfully.
    
    ##### sepolia
    ✅  [Success]Hash: 0x462a91caf0a55bbb708fbae48e930902316a8e53d2bd1a4dbcbf0e33e8d04898
    Contract Address: 0xA8cE7c5a7b367dF6ef6c9E33Ec56145816b9931A
    Block: 6803704
    Paid: 0.042853127668735725 ETH (258225 gas * 165.952667901 gwei)
    
    ##### sepolia
    ✅  [Success]Hash: 0x26bc6d93323fd57315141c370ca346c10757b2eb989bffd0e9f9a91d8b2864f6
    Block: 6803704
    Paid: 0.03045895266654954 ETH (183540 gas * 165.952667901 gwei)
    
    ✅ Sequence #1 on sepolia | Total Paid: 0.083627532219343524 ETH (503924 gas * avg 165.952667901 gwei)
    ==========================
    ONCHAIN EXECUTION COMPLETE & SUCCESSFUL.
    
    deployment example transactiondeployed contract
  3. Optional - verify the contract
    forge verify-contract 0xA8cE7c5a7b367dF6ef6c9E33Ec56145816b9931A \
        src/Transfer.sol:Transfer
        --chain-id 11155111 \
        --verifier sourcify \
        --watch
    
    we verified this contract on Sourcify
  4. Query Union’s GraphQL API for the deployed contract transfers: playground link