Skip to content

feat: integrate depositAsset bridge functionality for ETH and ERC20 t…#227

Open
big14way wants to merge 1 commit intoExplore-Beyond-Innovations:mainfrom
big14way:app_main
Open

feat: integrate depositAsset bridge functionality for ETH and ERC20 t…#227
big14way wants to merge 1 commit intoExplore-Beyond-Innovations:mainfrom
big14way:app_main

Conversation

@big14way
Copy link

@big14way big14way commented Aug 24, 2025

…okens

  • Add depositAsset contract integration in Ethereum provider
  • Implement automatic ERC20 token approval handling
  • Support ETH deposits with msg.value parameter
  • Add bridge contract configuration for Sepolia testnet
  • Capture and store commitment hash for L2 operations
  • Update deposit and claim-burn components with new functionality
  • Add comprehensive error handling and user feedback
  • Store transaction details in localStorage for cross-chain tracking

Closes #148

Summary by CodeRabbit

  • New Features
    • Introduced Ethereum-based deposit/bridge flow for ETH and ERC20, including wallet checks, approvals, and transaction handling.
    • Integrated deposit action into Deposit and Claim/Burn screens with clear status messages and disabled states during processing.
    • Persisted deposit identifiers (transaction hash and commitment hash) in local storage for easy reference.
    • Added Sepolia network support for bridging.
  • Chores
    • Exposed deposit functionality through the app’s Ethereum context for use across components.

…okens

- Add depositAsset contract integration in Ethereum provider
- Implement automatic ERC20 token approval handling
- Support ETH deposits with msg.value parameter
- Add bridge contract configuration for Sepolia testnet
- Capture and store commitment hash for L2 operations
- Update deposit and claim-burn components with new functionality
- Add comprehensive error handling and user feedback
- Store transaction details in localStorage for cross-chain tracking

Closes Explore-Beyond-Innovations#148
@vercel
Copy link

vercel bot commented Aug 24, 2025

@big14way is attempting to deploy a commit to the ZeroXBridge Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Aug 24, 2025

Walkthrough

Implements L1->L2 deposit flow via a new depositAsset API in EthereumProvider, wires it into Deposit and Claim/Burn components, and configures Sepolia bridge address. Handles ETH and ERC20 paths, approvals, transaction execution, receipt parsing for commitmentHash, and persists identifiers to localStorage.

Changes

Cohort / File(s) Summary
Ethereum provider: deposit API and on-chain calls
app/components/Ethereum-provider.tsx
Adds DepositResult type and depositAsset(assetType, tokenAddress, amount). Implements ETH and ERC20 flows with allowance/approve, bridge contract call, receipt parsing for commitmentHash, and error handling. Exposes depositAsset via context. Imports BRIDGE_CONTRACTS.
UI: Deposit flow
app/components/deposit.tsx
Integrates useEthereum depositAsset for ETH/ERC20. Adds processing state, validations, tokenAddress resolution, calls depositAsset, handles errors, persists commitmentHash and tx hash, and updates button states/labels.
UI: Claim/Burn flow
app/components/claim-burn.tsx
Re-enables depositAsset during burn path; handles result, throws on failure, and persists commitmentHash and tx hash to localStorage. Maintains existing control flow flags.
Config: Networks and bridge mapping
app/config.ts
Adds sepolia to wagmi config and exports BRIDGE_CONTRACTS mapping to Sepolia bridge address 0x8F25bFe32269632dfd8D223D51FF145414d8107b.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant D as Deposit UI
  participant EP as EthereumProvider
  participant ERC as ERC20 Token (if ERC20)
  participant BR as L1 Bridge Contract
  participant NW as Ethereum Network

  U->>D: Click "Deposit" (amount, token)
  D->>EP: depositAsset(assetType, tokenAddress, amount)
  EP->>NW: getNetwork()
  EP->>BR: Resolve bridge at BRIDGE_CONTRACTS[chainId]

  alt ERC20 path
    EP->>ERC: allowance(user, BR)
    alt Insufficient allowance
      EP->>ERC: approve(BR, amountInWei)
      ERC-->>EP: approval tx receipt
    end
    EP->>BR: depositAsset(assetType, tokenAddress, amountInWei, user)
  else ETH path
    EP->>BR: depositAsset(..., user) with msg.value=amountInWei
  end

  BR-->>EP: tx receipt (logs)
  EP->>EP: parse logs -> commitmentHash
  EP-->>D: {success, transactionHash, commitmentHash}
  D->>D: Persist to localStorage (if present)
  D-->>U: Show success/failure
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Assessment against linked issues

Objective Addressed Explanation
Read assetType, tokenAddress, amount, and user from UI (#148) assetType/tokenAddress/amount read from UI; user appears derived from connected wallet, not passed from UI.
For ERC20, ensure token is approved before calling contract (#148)
Call depositAsset(assetType, tokenAddress, amount, user) with msg.value for ETH (#148)
Capture and store commitmentHash for use on L2 (#148)
Display success/failure in the UI (#148)

Assessment against linked issues: Out-of-scope changes

(no out-of-scope functional code changes identified)

Possibly related PRs

Suggested reviewers

  • Dprof-in-tech

Poem

I thump my paws and bridge the night,
Hops of ETH, ERC20 in flight.
A hash committed, tx aglow,
Sepolia’s burrow, off we go!
Approvals nibbled, logs in stash —
Two chains, one bunny, one swift dash. 🐇✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (5)
app/config.ts (1)

22-26: Externalize bridge addresses per environment and prepare for multi-chain.

Hardcoding the Sepolia bridge in-source is fine for a first pass, but it will become brittle as you add networks or move between staging/prod.

  • Load addresses from env (e.g., NEXT_PUBLIC_BRIDGE_) or a typed registry keyed by chainId.
  • Consider freezing the map with const assertions to prevent accidental mutation.

Proposed minimal refactor:

-// Bridge contract addresses
-export const BRIDGE_CONTRACTS: Record<number, string> = {
-  [sepolia.id]: '0x8F25bFe32269632dfd8D223D51FF145414d8107b',
-  // Add other networks as needed
-}
+// Bridge contract addresses (env-first, typed fallback)
+export const BRIDGE_CONTRACTS: Readonly<Record<number, string>> = {
+  [sepolia.id]: process.env.NEXT_PUBLIC_BRIDGE_11155111
+    ?? '0x8F25bFe32269632dfd8D223D51FF145414d8107b',
+} as const;

Before merging, please verify the Sepolia address against your latest deployment notes/Etherscan for 11155111. If you want, I can generate a small health-check util that reads BRIDGE_CONTRACTS for the connected chain and validates checksum/bytecode.

app/components/claim-burn.tsx (1)

232-236: Guard localStorage usage to avoid SSR edge cases.

If this component ever renders server-side or is imported by a Server Component, direct localStorage access may throw. Add a window guard.

-      if (result.commitmentHash) {
-        localStorage.setItem('latestCommitmentHash', result.commitmentHash);
-        localStorage.setItem('latestDepositTx', result.transactionHash || '');
-      }
+      if (result.commitmentHash && typeof window !== 'undefined') {
+        window.localStorage.setItem('latestCommitmentHash', result.commitmentHash);
+        window.localStorage.setItem('latestDepositTx', result.transactionHash || '');
+      }
app/components/deposit.tsx (1)

96-112: Persisting identifiers is good; consider also persisting a typed envelope for future UX.

Storing both latestCommitmentHash and latestDepositTx is useful. As a small improvement, store a JSON blob keyed by chainId to support multi-chain and richer status.

Example:

-      if (result.commitmentHash) {
-        localStorage.setItem('latestCommitmentHash', result.commitmentHash);
-        localStorage.setItem('latestDepositTx', result.transactionHash || '');
-      }
+      if (result.commitmentHash && typeof window !== 'undefined') {
+        const entry = {
+          chainId: 11155111, // TODO: read from provider
+          commitmentHash: result.commitmentHash,
+          txHash: result.transactionHash,
+          token,
+          amount,
+          ts: Date.now(),
+        };
+        window.localStorage.setItem('latestDeposit', JSON.stringify(entry));
+      }
app/components/Ethereum-provider.tsx (2)

58-67: Graceful early return is good; consider surfacing connect action.

Returning { success: false, error: "Wallet not connected" } is fine. Optionally, export connectWallet in the context and use it in callers to prompt connection flows automatically when this error occurs.


132-137: Return the transaction hash from the response instead of the receipt where possible.

Both are fine, but returning tx.hash before waiting (and keeping receipt.hash after wait) enables optimistic UI if needed. Not a blocker.

-      return {
-        success: true,
-        commitmentHash,
-        transactionHash: receipt.hash,
-      };
+      return {
+        success: true,
+        commitmentHash,
+        transactionHash: receipt.hash, // optionally expose tx.hash too
+      };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b1a2f41 and a0f3213.

📒 Files selected for processing (4)
  • app/components/Ethereum-provider.tsx (4 hunks)
  • app/components/claim-burn.tsx (2 hunks)
  • app/components/deposit.tsx (4 hunks)
  • app/config.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
app/components/Ethereum-provider.tsx (1)
app/config.ts (1)
  • BRIDGE_CONTRACTS (23-26)
app/components/deposit.tsx (2)
app/ThemeContext.tsx (1)
  • useTheme (36-42)
app/components/Ethereum-provider.tsx (1)
  • useEthereum (196-202)
🔇 Additional comments (7)
app/config.ts (1)

8-9: Sepolia chain integration looks correct.

Adding sepolia to wagmi chains and transports is consistent. No functional issues spotted here.

Also applies to: 16-19

app/components/claim-burn.tsx (2)

73-81: Good hook wiring: depositAsset exposed via Ethereum context.

Importing and destructuring depositAsset from useEthereum is the right integration point.


217-226: Error propagation from depositAsset is handled well.

Early-exiting on !result.success with a surfaced message keeps UX predictable.

app/components/deposit.tsx (2)

53-67: Solid preflight validation and in-flight guarding.

The checks for connection, positive amount, and isProcessing gating are correct and keep the flow robust.


197-205: Button state UX is clear.

Dynamic label (“Connect Ethereum Wallet” / “Processing...”) and disabled state logic are spot on.

app/components/Ethereum-provider.tsx (2)

112-131: The script will print the top of Ethereum-provider.tsx, including the bridgeABI declaration, so we can confirm whether event ABI entries (e.g., AssetDeposited) are present and thus determine if filtering by event name and log.address is needed or if the ABI itself must be extended. Once we have that, we can finalize the review.


75-82: Ensure the bridge event ABI is included so parseLog can decode the commitmentHash

The current ABI array only defines the depositAsset function. Without the event signature, bridgeContract.interface.parseLog(...) will never recognize any logs, so commitmentHash will always be undefined.

• File: app/components/Ethereum-provider.tsx
Lines 75–82: the bridgeABI constant must include the event that emits the commitment hash.

Suggested diff:

-      const bridgeABI = [
-        "function depositAsset(uint256 assetType, address tokenAddress, uint256 amount, address user) external payable returns (bytes32)"
-      ];
+      const bridgeABI = [
+        "function depositAsset(uint256 assetType, address tokenAddress, uint256 amount, address user) external payable returns (bytes32)",
+        // TODO: replace with your bridge’s actual event signature
+        "event AssetDeposited(bytes32 indexed commitmentHash, address indexed user, uint256 assetType, address tokenAddress, uint256 amount)"
+      ];

Please confirm the exact event name and parameter order from your deployed bridge contract on Sepolia (chainId 11155111) so we can adapt the ABI and the parseLog call accordingly.

Comment on lines +68 to +87
// Determine asset type and token address
const assetType = token === "ETH" ? 0 : 1;
let tokenAddress;

if (assetType === 0) {
tokenAddress = "0x0000000000000000000000000000000000000000";
} else {
// For ERC20 tokens, you'd need to map token symbols to addresses
// This is a placeholder - in production, you'd have a token registry
const tokenAddresses: Record<string, string> = {
USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
};
tokenAddress = tokenAddresses[token];

if (!tokenAddress) {
setError(`Token ${token} not supported`);
return;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Token address mapping is mainnet-only; deposits on Sepolia will fail for ERC20.

You’re passing mainnet USDC/USDT addresses while the bridge is configured for Sepolia. On Sepolia those contracts either don’t exist or don’t match the expected ERC20s, causing allowance checks and deposits to revert.

Recommended approach:

  • Maintain a chain-aware token registry (addresses, decimals) keyed by chainId (e.g., 11155111 for Sepolia).
  • Pull the connected chainId (either via the Ethereum context or by modifying depositAsset to resolve decimals/addresses internally).
  • Surface “Unsupported token on this network” early if missing.

Minimal inline change (temporary) to avoid accidental mainnet addresses:

-        const tokenAddresses: Record<string, string> = {
-          USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
-          USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
-        };
-        tokenAddress = tokenAddresses[token];
+        // TODO: Replace with a proper per-chain registry.
+        const tokenAddressesByChain: Record<number, Record<string, string>> = {
+          11155111: {
+            // Fill with your Sepolia token deployments
+            // USDC: "<SEPOLIA_USDC_ADDRESS>",
+            // USDT: "<SEPOLIA_USDT_ADDRESS>",
+          },
+        };
+        const chainId = 11155111; // TEMP: read from Ethereum provider/network
+        tokenAddress = tokenAddressesByChain[chainId]?.[token];

If you’d like, I can extract a typed TokenRegistry into app/config and wire it here and in claim-burn for consistency.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Determine asset type and token address
const assetType = token === "ETH" ? 0 : 1;
let tokenAddress;
if (assetType === 0) {
tokenAddress = "0x0000000000000000000000000000000000000000";
} else {
// For ERC20 tokens, you'd need to map token symbols to addresses
// This is a placeholder - in production, you'd have a token registry
const tokenAddresses: Record<string, string> = {
USDC: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
USDT: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
};
tokenAddress = tokenAddresses[token];
if (!tokenAddress) {
setError(`Token ${token} not supported`);
return;
}
}
// Determine asset type and token address
const assetType = token === "ETH" ? 0 : 1;
let tokenAddress;
if (assetType === 0) {
tokenAddress = "0x0000000000000000000000000000000000000000";
} else {
// For ERC20 tokens, you'd need to map token symbols to addresses
// This is a placeholder - in production, you'd have a token registry
// TODO: Replace with a proper per-chain registry.
const tokenAddressesByChain: Record<number, Record<string, string>> = {
11155111: {
// Fill with your Sepolia token deployments
// USDC: "<SEPOLIA_USDC_ADDRESS>",
// USDT: "<SEPOLIA_USDT_ADDRESS>",
},
};
const chainId = 11155111; // TEMP: read from Ethereum provider/network
tokenAddress = tokenAddressesByChain[chainId]?.[token];
if (!tokenAddress) {
setError(`Token ${token} not supported`);
return;
}
}
🤖 Prompt for AI Agents
In app/components/deposit.tsx around lines 68 to 87, the code uses hard-coded
mainnet ERC20 addresses (USDC/USDT) which will fail on Sepolia; change the logic
to be chain-aware by resolving token addresses and decimals from a TokenRegistry
keyed by chainId: obtain the current chainId from the web3/ethers provider or
component props, look up tokenAddresses = tokenRegistry[chainId] and then
tokenAddress = tokenAddresses?.[token]; if the lookup fails, call
setError("Unsupported token on this network") and return; ensure the registry
contains decimals and addresses for each supported chain (e.g., mainnet and
Sepolia) and update any allowance/decimals handling to use the registry values.

Comment on lines +81 to +110
const amountInWei = ethers.parseEther(amount);

let tx;

if (assetType === 0) {
// ETH deposit
tx = await bridgeContract.depositAsset(
assetType,
tokenAddress,
amountInWei,
address,
{ value: amountInWei }
);
} else {
// ERC20 deposit - first check and handle approval
const erc20ABI = [
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)"
];

const tokenContract = new ethers.Contract(tokenAddress, erc20ABI, signer);
const allowance = await tokenContract.allowance(address, bridgeAddress);

if (allowance < amountInWei) {
const approvalTx = await tokenContract.approve(bridgeAddress, amountInWei);
await approvalTx.wait();
}

tx = await bridgeContract.depositAsset(assetType, tokenAddress, amountInWei, address);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

ERC20 amount uses parseEther — breaks for non-18 decimals (USDC/USDT).

For ERC20s, amounts must be in token units (decimals vary). Using parseEther will overshoot by 12 orders for 6-decimal tokens and cause allowance/transfer reverts.

Fix by resolving decimals and using parseUnits; also rename the variable to avoid “wei” implying ETH only:

-      const amountInWei = ethers.parseEther(amount);
+      // Resolve amount units
+      let amountInUnits: bigint;

ETH path:

-      if (assetType === 0) {
+      if (assetType === 0) {
         // ETH deposit
-        tx = await bridgeContract.depositAsset(
+        const amountInWei = ethers.parseEther(amount);
+        tx = await bridgeContract.depositAsset(
           assetType,
           tokenAddress,
-          amountInWei,
+          amountInWei,
           address,
           { value: amountInWei }
         );
       } else {
         // ERC20 deposit - first check and handle approval
         const erc20ABI = [
           "function allowance(address owner, address spender) view returns (uint256)",
-          "function approve(address spender, uint256 amount) returns (bool)"
+          "function approve(address spender, uint256 amount) returns (bool)",
+          "function decimals() view returns (uint8)"
         ];
         
         const tokenContract = new ethers.Contract(tokenAddress, erc20ABI, signer);
+        const decimals: number = await tokenContract.decimals();
+        amountInUnits = ethers.parseUnits(amount, decimals);
-        const allowance = await tokenContract.allowance(address, bridgeAddress);
+        const allowance = await tokenContract.allowance(address, bridgeAddress);
         
-        if (allowance < amountInWei) {
-          const approvalTx = await tokenContract.approve(bridgeAddress, amountInWei);
+        if (allowance < amountInUnits) {
+          const approvalTx = await tokenContract.approve(bridgeAddress, amountInUnits);
           await approvalTx.wait();
         }
         
-        tx = await bridgeContract.depositAsset(assetType, tokenAddress, amountInWei, address);
+        tx = await bridgeContract.depositAsset(assetType, tokenAddress, amountInUnits, address);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const amountInWei = ethers.parseEther(amount);
let tx;
if (assetType === 0) {
// ETH deposit
tx = await bridgeContract.depositAsset(
assetType,
tokenAddress,
amountInWei,
address,
{ value: amountInWei }
);
} else {
// ERC20 deposit - first check and handle approval
const erc20ABI = [
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)"
];
const tokenContract = new ethers.Contract(tokenAddress, erc20ABI, signer);
const allowance = await tokenContract.allowance(address, bridgeAddress);
if (allowance < amountInWei) {
const approvalTx = await tokenContract.approve(bridgeAddress, amountInWei);
await approvalTx.wait();
}
tx = await bridgeContract.depositAsset(assetType, tokenAddress, amountInWei, address);
}
// Resolve amount units
let amountInUnits: bigint;
let tx;
if (assetType === 0) {
// ETH deposit
const amountInWei = ethers.parseEther(amount);
tx = await bridgeContract.depositAsset(
assetType,
tokenAddress,
amountInWei,
address,
{ value: amountInWei }
);
} else {
// ERC20 deposit - first check and handle approval
const erc20ABI = [
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
"function decimals() view returns (uint8)"
];
const tokenContract = new ethers.Contract(tokenAddress, erc20ABI, signer);
const decimals: number = await tokenContract.decimals();
amountInUnits = ethers.parseUnits(amount, decimals);
const allowance = await tokenContract.allowance(address, bridgeAddress);
if (allowance < amountInUnits) {
const approvalTx = await tokenContract.approve(bridgeAddress, amountInUnits);
await approvalTx.wait();
}
tx = await bridgeContract.depositAsset(
assetType,
tokenAddress,
amountInUnits,
address
);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✅ Issue 2: Integrate depositAsset – Bridge ETH or ERC20 tokens

1 participant