Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/manual-whitelist-bridge-token.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ on:
description: 'Token Symbol (e.g., "USDC")'
required: true
type: string
force_rewhitelist:
description: 'Set to "true" to remove an existing whitelist entry and retry from scratch (use to recover from a failed run)'
required: false
default: 'false'
type: string
confirmation:
description: 'Type "confirm" if whitelisting on sepolia or mainnet'
required: false
Expand Down Expand Up @@ -126,7 +131,7 @@ jobs:
fi

# External L2 gateway URL
L2_GATEWAY_URL="${{ vars.L2_RPC_URL_VALIDATOR }}"
L2_GATEWAY_URL="${{ vars.GATEWAY_URL }}"
echo "L2 Gateway URL: $L2_GATEWAY_URL"
echo "Network env: $TESTNET_TYPE"

Expand All @@ -140,7 +145,8 @@ jobs:
-l2_gateway_url="$L2_GATEWAY_URL" \
-private_key=${{ secrets.ACCOUNT_PK_WORKER }} \
-docker_image=${{ vars.DOCKER_BUILD_TAG_L2_HARDHAT_DEPLOYER }} \
-network_config_addr="${{ steps.parse.outputs.network_config }}"
-network_config_addr="${{ steps.parse.outputs.network_config }}" \
-force_rewhitelist="${{ github.event.inputs.force_rewhitelist }}"

- name: 'Save whitelisting container logs'
if: ${{ always() }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import {HardhatRuntimeEnvironment} from 'hardhat/types';
import {DeployFunction} from 'hardhat-deploy/types';

/*
This script whitelists USDC and USDT tokens on the TenBridge contract
and sets up the WETH address for bridge functionality.
This script sets up the WETH address for bridge functionality.

Note: Token whitelisting (USDC, USDT, etc.) is intentionally not done here.
Whitelisting must be done via the manual-whitelist-bridge-token workflow, which
handles the full e2e process:
1. L1 whitelist
2. Wait for L2 message
3. Finalisation + relaying the message to create the L2 wrapped token.

Whitelisting here without the relay step leaves the bridge in a broken half-state.

Environment variables:
- USDC_ADDRESS: USDC token address (optional)
- USDT_ADDRESS: USDT token address (optional)
- WETH_ADDRESS: WETH token address (optional - defaults to genesis WETH address)

WETH is pre-deployed at genesis on both L1 and L2 at address 0x1000000000000000000000000000000000000042.
Expand All @@ -26,33 +32,9 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const {deployer} = await getNamedAccounts();

// Get environment variables
const usdcAddress = process.env.USDC_ADDRESS;
const usdtAddress = process.env.USDT_ADDRESS;
// Use provided WETH address or fall back to genesis WETH address
const wethAddress = process.env.WETH_ADDRESS || GENESIS_WETH_ADDRESS;

// Whitelist USDC token if address is provided
if (usdcAddress) {
console.log(`Whitelisting USDC: ${usdcAddress}`);
await deployments.execute('TenBridge', {
from: deployer,
log: true
}, 'whitelistToken', usdcAddress, 'USD Coin', 'USDC');
} else {
console.log('Skipping USDC whitelist - USDC_ADDRESS not set');
}

// Whitelist USDT token if address is provided
if (usdtAddress) {
console.log(`Whitelisting USDT: ${usdtAddress}`);
await deployments.execute('TenBridge', {
from: deployer,
log: true
}, 'whitelistToken', usdtAddress, 'Tether USD', 'USDT');
} else {
console.log('Skipping USDT whitelist - USDT_ADDRESS not set');
}

// Set WETH address for WETH unwrapping functionality
// This also grants ERC20_TOKEN_ROLE to WETH so it can be bridged via sendERC20
console.log(`Setting WETH address: ${wethAddress}`);
Expand Down
760 changes: 760 additions & 0 deletions contracts/generated/ConfigurableERC20/ConfigurableERC20.go

Large diffs are not rendered by default.

2,857 changes: 2,857 additions & 0 deletions contracts/generated/Ten/Ten.go

Large diffs are not rendered by default.

25 changes: 23 additions & 2 deletions contracts/generated/TenBridge/TenBridge.go

Large diffs are not rendered by default.

25 changes: 23 additions & 2 deletions contracts/generated/TenBridgeTestnet/TenBridgeTestnet.go

Large diffs are not rendered by default.

28 changes: 23 additions & 5 deletions contracts/scripts/bridge/whitelist_and_register_tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ async function ensureGatewayAccountRegistered(gatewayBaseUrl: string, privateKey
const baseUrl = gatewayBaseUrl.replace(/\/+$/, '');
const wallet = new ethers.Wallet(privateKey);

const joinResponse = await fetch(`${baseUrl}/join`, { method: 'GET' });
const joinResponse = await fetch(`${baseUrl}/v1/join/`, { method: 'GET' });
if (!joinResponse.ok) {
throw new Error(`Gateway join failed with status ${joinResponse.status}`);
}
Expand All @@ -139,7 +139,7 @@ async function ensureGatewayAccountRegistered(gatewayBaseUrl: string, privateKey
throw new Error('Gateway join returned an empty token');
}

const queryUrl = new URL(`${baseUrl}/query/address`);
const queryUrl = new URL(`${baseUrl}/v1/query/`);
queryUrl.searchParams.set('token', token);
queryUrl.searchParams.set('a', wallet.address);
const queryResponse = await fetch(queryUrl.toString(), { method: 'GET' });
Expand All @@ -166,7 +166,7 @@ async function ensureGatewayAccountRegistered(gatewayBaseUrl: string, privateKey
};
const signature = await wallet.signTypedData(domain, types, message);

const authenticateUrl = new URL(`${baseUrl}/authenticate/`);
const authenticateUrl = new URL(`${baseUrl}/v1/authenticate/`);
authenticateUrl.searchParams.set('token', token);
const authResponse = await fetch(authenticateUrl.toString(), {
method: 'POST',
Expand Down Expand Up @@ -397,11 +397,27 @@ async function step2WhitelistToken(
l1MessageBusAddress: string,
tokenAddress: string,
tokenName: string,
tokenSymbol: string
tokenSymbol: string,
forceRewhitelist = false
): Promise<{ whitelistReceipt: any; l1MessageBus: MessageBus }> {
const l1Bridge = await ethers.getContractAt('TenBridge', l1BridgeAddress);
const l1MessageBus = await ethers.getContractAt('MessageBus', l1MessageBusAddress);

const ERC20_TOKEN_ROLE = ethers.keccak256(ethers.toUtf8Bytes('ERC20_TOKEN'));
const alreadyWhitelisted = await l1Bridge.hasRole(ERC20_TOKEN_ROLE, tokenAddress);
if (alreadyWhitelisted) {
if (!forceRewhitelist) {
throw new Error(
`Token ${tokenAddress} is already whitelisted on L1 TenBridge. ` +
`Set FORCE_REWHITELIST=true to remove it and re-whitelist (issues a fresh cross-chain message).`
);
}
console.log(`Token already whitelisted — removing before re-whitelisting (FORCE_REWHITELIST=true)...`);
const removeTx = await l1Bridge.revokeRole(ERC20_TOKEN_ROLE, tokenAddress);
await removeTx.wait();
console.log(`Removed. Re-whitelisting...`);
}

const whitelistTx = await l1Bridge.whitelistToken(tokenAddress, tokenName, tokenSymbol);
console.log(`Transaction hash: ${whitelistTx.hash}`);

Expand Down Expand Up @@ -542,13 +558,15 @@ const whitelistAndRegisterToken = async function (): Promise<void> {
console.log('[1/7] Querying network addresses...');
const { networkConfig, addresses } = await step1QueryNetworkAddresses(config.networkConfigAddr);

const forceRewhitelist = process.env.FORCE_REWHITELIST === 'false';
console.log('[2/7] Whitelisting token on L1 bridge...');
const { whitelistReceipt, l1MessageBus } = await step2WhitelistToken(
addresses.l1BridgeAddress,
addresses.l1MessageBusAddress,
config.tokenAddress,
config.tokenName,
config.tokenSymbol
config.tokenSymbol,
forceRewhitelist
);

console.log('[3/7] Extracting cross-chain message...');
Expand Down
6 changes: 6 additions & 0 deletions contracts/src/reference_bridge/L1/contracts/TenBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ contract TenBridge is
);
}

function removeWhitelistedToken(address asset) external onlyRole(ADMIN_ROLE) {
require(hasRole(ERC20_TOKEN_ROLE, asset), "Token is not whitelisted");
_revokeRole(ERC20_TOKEN_ROLE, asset);
_revokeRole(SUSPENDED_ERC20_ROLE, asset);
}

function pauseToken(address asset) external onlyRole(ADMIN_ROLE) {
require(hasRole(ERC20_TOKEN_ROLE, asset), "Token is not whitelisted");
_grantRole(SUSPENDED_ERC20_ROLE, asset);
Expand Down
4 changes: 4 additions & 0 deletions contracts/src/reference_bridge/L1/interfaces/ITenBridge.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ interface ITenBridge {
string calldata symbol
) external;

// Removes a token from the whitelist entirely. Used to recover from a failed whitelistToken flow
// so the token can be re-whitelisted via a fresh cross-chain message.
function removeWhitelistedToken(address asset) external;

// This will pause deposits for this token on the L1 bridge. Withdrawals are still fine.
function pauseToken(address asset) external;

Expand Down
9 changes: 8 additions & 1 deletion testnet/launcher/bridgetokenwhitelist/cmd/cli_flags.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package main

import "flag"
import (
"flag"
"strings"
)

type CLIConfig struct {
tokenAddress string
Expand All @@ -12,10 +15,12 @@ type CLIConfig struct {
privateKey string
dockerImage string
networkConfigAddr string
forceRewhitelist bool
}

func ParseConfigCLI() *CLIConfig {
cfg := &CLIConfig{}
var forceRewhitelistStr string
flag.StringVar(&cfg.tokenAddress, "token_address", "", "Token contract address (e.g., 0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238)")
flag.StringVar(&cfg.tokenName, "token_name", "", "Token name (e.g., 'USD Coin')")
flag.StringVar(&cfg.tokenSymbol, "token_symbol", "", "Token symbol (e.g., 'USDC')")
Expand All @@ -25,6 +30,8 @@ func ParseConfigCLI() *CLIConfig {
flag.StringVar(&cfg.privateKey, "private_key", "", "Private key for deployment")
flag.StringVar(&cfg.dockerImage, "docker_image", "", "Docker image for hardhat deployer")
flag.StringVar(&cfg.networkConfigAddr, "network_config_addr", "", "NetworkConfig contract address")
flag.StringVar(&forceRewhitelistStr, "force_rewhitelist", "false", "If \"true\", removes an existing ERC20_TOKEN_ROLE from the token before re-whitelisting (use to recover from a failed run)")
flag.Parse()
cfg.forceRewhitelist = strings.EqualFold(forceRewhitelistStr, "true")
return cfg
}
1 change: 1 addition & 0 deletions testnet/launcher/bridgetokenwhitelist/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func main() {
bridgetokenwhitelist.WithPrivateKey(cliConfig.privateKey),
bridgetokenwhitelist.WithDockerImage(cliConfig.dockerImage),
bridgetokenwhitelist.WithNetworkConfigAddress(cliConfig.networkConfigAddr),
bridgetokenwhitelist.WithForceRewhitelist(cliConfig.forceRewhitelist),
),
)
if err != nil {
Expand Down
11 changes: 9 additions & 2 deletions testnet/launcher/bridgetokenwhitelist/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Config struct {
privateKey string
dockerImage string
networkConfigAddr string
forceRewhitelist bool
}

func NewConfig(opts ...ConfigOption) *Config {
Expand Down Expand Up @@ -78,6 +79,12 @@ func WithNetworkConfigAddress(addr string) ConfigOption {
}
}

func WithForceRewhitelist(force bool) ConfigOption {
return func(c *Config) {
c.forceRewhitelist = force
}
}

func (c *Config) Validate() error {
if c.tokenAddress == "" {
return fmt.Errorf("token address is required")
Expand Down Expand Up @@ -110,6 +117,6 @@ func (c *Config) Validate() error {
}

func (c *Config) String() string {
return fmt.Sprintf("Bridge Token Whitelist Config: tokenAddress=%s, tokenName=%s, tokenSymbol=%s, networkEnv=%s, l1HTTPURL=%s, l2GatewayURL=%s, dockerImage=%s, networkConfigAddr=%s",
c.tokenAddress, c.tokenName, c.tokenSymbol, c.networkEnv, c.l1HTTPURL, c.l2GatewayURL, c.dockerImage, c.networkConfigAddr)
return fmt.Sprintf("Bridge Token Whitelist Config: tokenAddress=%s, tokenName=%s, tokenSymbol=%s, networkEnv=%s, l1HTTPURL=%s, l2GatewayURL=%s, dockerImage=%s, networkConfigAddr=%s, forceRewhitelist=%v",
c.tokenAddress, c.tokenName, c.tokenSymbol, c.networkEnv, c.l1HTTPURL, c.l2GatewayURL, c.dockerImage, c.networkConfigAddr, c.forceRewhitelist)
}
1 change: 1 addition & 0 deletions testnet/launcher/bridgetokenwhitelist/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func (w *BridgeTokenWhitelister) Start() error {
"TOKEN_NAME": w.cfg.tokenName,
"TOKEN_SYMBOL": w.cfg.tokenSymbol,
"NETWORK_CONFIG_ADDR": w.cfg.networkConfigAddr,
"FORCE_REWHITELIST": fmt.Sprintf("%v", w.cfg.forceRewhitelist),
}

// Mount scripts and src directories so contracts can be compiled if needed
Expand Down
Loading