Skip to main content

Testnet Guide

How to fund your Stable Testnet wallet

Stable uses gasUSDT(unwrapped USDT0) as the gas token, so you’ll need gasUSDT in your wallet to interact with the chain. First, you need to fund gasUSDT to your account using the faucet.

  1. Visit https://demo.testnet.chain0.dev/faucet, and connect your MetaMask wallet.
  2. Click ‘Get gasUSDT’ button and 1 gasUSDT will be dropped to your wallet.

If you want more gasUSDT or USDT0, you can bridge Test USDT from Ethereum Sepolia to Stable testnet.

  1. Visit https://sepolia.etherscan.io/token/0x7169d38820dfd117c3fa1f22a697dba58d90ba06#writeContract, and mint desired amount of Test Tether USD to your account by calling the _mint function.

  2. Send the following transaction to the LayerZero bridge contract on Ethereum Sepolia to bridge Test USDT to the Stable testnet:

    export function addrTo32Bytes(addr: string): Buffer {
    const hex20 = ethers.utils.getAddress(addr).slice(2);
    const padded = hex20.padStart(64, "0"); // 32 bytes ⇒ 64 hex
    return Buffer.from(padded, "hex"); // length === 32
    }

    async function main() {
    const [owner] = await ethers.getSigners();

    const SEPOLIA_USDT0 = "0xc4DCC311c028e341fd8602D8eB89c5de94625927";
    const SEPOLIA_USDT0_OAPP = "0xc099cD946d5efCC35A99D64E808c1430cEf08126"
    const RECEIVER_EID = 40374;

    const usdt0 = await ethers.getContractAt("ERC20", SEPOLIA_USDT0);
    await usdt0.approve(SEPOLIA_USDT0_OAPP, ethers.utils.parseEther("1"));

    const options = Options.newOptions().addExecutorLzReceiveOption(0, 0).toBytes();
    const amount = ethers.utils.parseEther("1"); // Change this to your desired amount
    const OFTAdapter = await ethers.getContractAt("OFTAdapter", SEPOLIA_USDT0_OAPP);

    const sendParams = {
    dstEid: RECEIVER_EID,
    to: addrTo32Bytes(owner.address),
    amountLD: amount,
    minAmountLD: amount,
    extraOptions: options,
    composeMsg: Buffer.from(""),
    oftCmd: Buffer.from(""),
    };
    const fee = await OFTAdapter.quoteSend(sendParams, false);
    await OFTAdapter.send(
    sendParams,
    fee,
    owner.address,
    {
    value: fee.nativeFee,
    }
    )
    }
  3. Unwrap testnet USDT0 to gasUSDT by calling depositTo function in OStableWrapper contract

How to add your account to the gasUSDT transfer whitelist

By default, Stable restricts EOAs from transferring gasUSDT. This safeguard prevents regular users from mistakenly sending gasUSDT to centralized exchanges that only support USDT0, which could otherwise result in the permanent loss of their funds.

However, developers can add their EOA accounts to a whitelist, which grants those accounts permission to transfer gasUSDT.

Use the sample code below to add your EOA account to the whitelist:

import { Wallet } from "ethers";
import { JsonRpcProvider } from "@ethersproject/providers";
import { sendTransactionAndWait } from "../utils";

// Configuration from environment variables or command line arguments
const RPC_URL = process.env.RPC_URL || process.argv[2] || "https://stable-jsonrpc.testnet.chain0.dev";
const CONTRACT = process.env.CONTRACT || process.argv[3] || "0x0000000000000000000000000000000000001006";
const CHAIN_ID = parseInt(process.env.CHAIN_ID || process.argv[4] || "2201");
const PRIVATE_KEY = process.env.PRIVATE_KEY || process.argv[5];
const DATA = "0xd56c1573"; //

if (!PRIVATE_KEY) {
console.error("Error: PRIVATE_KEY is required");
console.error("Usage: ts-node add_eoa.ts [RPC_URL] [CONTRACT] [CHAIN_ID] <PRIVATE_KEY>");
console.error("Or set PRIVATE_KEY environment variable");
process.exit(1);
}

const provider = new JsonRpcProvider(RPC_URL);
const wallet = new Wallet(PRIVATE_KEY, provider);

const main = async () => {
console.log("Configuration:");
console.log(" RPC URL:", RPC_URL);
console.log(" Contract:", CONTRACT);
console.log(" Chain ID:", CHAIN_ID);
console.log(" Wallet Address:", wallet.address);
console.log("");

const nonce = await provider.getTransactionCount(wallet.address);

const tx = {
to: CONTRACT,
data: DATA,
nonce,
gasPrice: 2_000_000_000n, // 2 gwei
gasLimit: 3000000n,
chainId: CHAIN_ID,
type: 0, // legacy
value: 0n
};

const signedTx = await wallet.signTransaction(tx);
console.log("Signed Transaction:", signedTx);

// Send the raw transaction and wait for receipt
await sendTransactionAndWait(provider, signedTx);
};

main().catch(console.error);

Testing $STABLE Token Functionality

Since the $STABLE token contract is implemented as a precompile, its token details are not visible on the Stable testnet explorer. The contract source code and ABI are provided below:

[Source code]

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "../common/CosmosTypes.sol";

address constant IBANK_PRECOMPILE_ADDRESS = 0x0000000000000000000000000000000000001003;
IBank constant IBANK_CONTRACT = IBank(IBANK_PRECOMPILE_ADDRESS);

interface IBank {
function mint(address,uint256) external returns (bool);
function burn(address,uint256) external returns (bool);
function transfer(address,uint256) external returns (bool);
function transferFrom(address,address,uint256) external returns (bool);
function multiTransfer(address[] calldata, uint256[] calldata) external returns (bool);
function approve(address,uint256) external returns (bool);
function revoke(address) external returns (bool); // custom method

function balanceOf(address) external view returns (uint256);
function totalSupply() external view returns (uint256);
function allowance(address,address) external view returns (uint256);

// Events
event CoinReceived(address indexed receiver, Coin[] amount);
event Coinbase(address indexed minter, Coin[] amount);
event CoinSpent(address indexed spender, Coin[] amount);
event Burn(address indexed burner, Coin[] amount);
event PrecompiledBankMint(address indexed from, address indexed to, uint256 value);
event PrecompiledBankBurn(address indexed from, address indexed to, uint256 value);
event PrecompiledBankTransfer(address indexed from, address indexed to, uint256 value);
event PrecompiledBankApproval(address indexed owner, address indexed spender, uint256 value);
event PrecompiledBankRevoke(address indexed owner, address indexed spender, uint256 value);
}

[ABI]

[
{
"type": "function",
"name": "allowance",
"inputs": [
{ "name": "", "type": "address", "internalType": "address" },
{ "name": "", "type": "address", "internalType": "address" }
],
"outputs": [
{ "name": "", "type": "uint256", "internalType": "uint256" }
],
"stateMutability": "view"
},
{
"type": "function",
"name": "approve",
"inputs": [
{ "name": "", "type": "address", "internalType": "address" },
{ "name": "", "type": "uint256", "internalType": "uint256" }
],
"outputs": [
{ "name": "", "type": "bool", "internalType": "bool" }
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "balanceOf",
"inputs": [
{ "name": "", "type": "address", "internalType": "address" }
],
"outputs": [
{ "name": "", "type": "uint256", "internalType": "uint256" }
],
"stateMutability": "view"
},
{
"type": "function",
"name": "burn",
"inputs": [
{ "name": "", "type": "address", "internalType": "address" },
{ "name": "", "type": "uint256", "internalType": "uint256" }
],
"outputs": [
{ "name": "", "type": "bool", "internalType": "bool" }
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "mint",
"inputs": [
{ "name": "", "type": "address", "internalType": "address" },
{ "name": "", "type": "uint256", "internalType": "uint256" }
],
"outputs": [
{ "name": "", "type": "bool", "internalType": "bool" }
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "multiTransfer",
"inputs": [
{ "name": "", "type": "address[]", "internalType": "address[]" },
{ "name": "", "type": "uint256[]", "internalType": "uint256[]" }
],
"outputs": [
{ "name": "", "type": "bool", "internalType": "bool" }
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "revoke",
"inputs": [
{ "name": "", "type": "address", "internalType": "address" }
],
"outputs": [
{ "name": "", "type": "bool", "internalType": "bool" }
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "totalSupply",
"inputs": [],
"outputs": [
{ "name": "", "type": "uint256", "internalType": "uint256" }
],
"stateMutability": "view"
},
{
"type": "function",
"name": "transfer",
"inputs": [
{ "name": "", "type": "address", "internalType": "address" },
{ "name": "", "type": "uint256", "internalType": "uint256" }
],
"outputs": [
{ "name": "", "type": "bool", "internalType": "bool" }
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "transferFrom",
"inputs": [
{ "name": "", "type": "address", "internalType": "address" },
{ "name": "", "type": "address", "internalType": "address" },
{ "name": "", "type": "uint256", "internalType": "uint256" }
],
"outputs": [
{ "name": "", "type": "bool", "internalType": "bool" }
],
"stateMutability": "nonpayable"
},
{
"type": "event",
"name": "Burn",
"inputs": [
{ "name": "burner", "type": "address", "indexed": true, "internalType": "address" },
{
"name": "amount",
"type": "tuple[]",
"indexed": false,
"internalType": "structCoin[]",
"components": [
{ "name": "denom", "type": "string", "internalType": "string" },
{ "name": "amount", "type": "uint256", "internalType": "uint256" }
]
}
],
"anonymous": false
},
{
"type": "event",
"name": "CoinReceived",
"inputs": [
{ "name": "receiver", "type": "address", "indexed": true, "internalType": "address" },
{
"name": "amount",
"type": "tuple[]",
"indexed": false,
"internalType": "structCoin[]",
"components": [
{ "name": "denom", "type": "string", "internalType": "string" },
{ "name": "amount", "type": "uint256", "internalType": "uint256" }
]
}
],
"anonymous": false
},
{
"type": "event",
"name": "CoinSpent",
"inputs": [
{ "name": "spender", "type": "address", "indexed": true, "internalType": "address" },
{
"name": "amount",
"type": "tuple[]",
"indexed": false,
"internalType": "structCoin[]",
"components": [
{ "name": "denom", "type": "string", "internalType": "string" },
{ "name": "amount", "type": "uint256", "internalType": "uint256" }
]
}
],
"anonymous": false
},
{
"type": "event",
"name": "Coinbase",
"inputs": [
{ "name": "minter", "type": "address", "indexed": true, "internalType": "address" },
{
"name": "amount",
"type": "tuple[]",
"indexed": false,
"internalType": "structCoin[]",
"components": [
{ "name": "denom", "type": "string", "internalType": "string" },
{ "name": "amount", "type": "uint256", "internalType": "uint256" }
]
}
],
"anonymous": false
},
{
"type": "event",
"name": "PrecompiledBankApproval",
"inputs": [
{ "name": "owner", "type": "address", "indexed": true, "internalType": "address" },
{ "name": "spender", "type": "address", "indexed": true, "internalType": "address" },
{ "name": "value", "type": "uint256", "indexed": false, "internalType": "uint256" }
],
"anonymous": false
},
{
"type": "event",
"name": "PrecompiledBankBurn",
"inputs": [
{ "name": "from", "type": "address", "indexed": true, "internalType": "address" },
{ "name": "to", "type": "address", "indexed": true, "internalType": "address" },
{ "name": "value", "type": "uint256", "indexed": false, "internalType": "uint256" }
],
"anonymous": false
},
{
"type": "event",
"name": "PrecompiledBankMint",
"inputs": [
{ "name": "from", "type": "address", "indexed": true, "internalType": "address" },
{ "name": "to", "type": "address", "indexed": true, "internalType": "address" },
{ "name": "value", "type": "uint256", "indexed": false, "internalType": "uint256" }
],
"anonymous": false
},
{
"type": "event",
"name": "PrecompiledBankRevoke",
"inputs": [
{ "name": "owner", "type": "address", "indexed": true, "internalType": "address" },
{ "name": "spender", "type": "address", "indexed": true, "internalType": "address" },
{ "name": "value", "type": "uint256", "indexed": false, "internalType": "uint256" }
],
"anonymous": false
},
{
"type": "event",
"name": "PrecompiledBankTransfer",
"inputs": [
{ "name": "from", "type": "address", "indexed": true, "internalType": "address" },
{ "name": "to", "type": "address", "indexed": true, "internalType": "address" },
{ "name": "value", "type": "uint256", "indexed": false, "internalType": "uint256" }
],
"anonymous": false
}
]