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 here: 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:

export 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