diff --git a/packages/bridge-ui/src/App.svelte b/packages/bridge-ui/src/App.svelte
index 5342eb9e00..9aa4c28752 100644
--- a/packages/bridge-ui/src/App.svelte
+++ b/packages/bridge-ui/src/App.svelte
@@ -5,25 +5,22 @@
import Navbar from "./components/Navbar.svelte";
import { SvelteToast } from "@zerodevx/svelte-toast";
- import { onMount } from "svelte";
import Home from "./pages/home/Home.svelte";
import { setupI18n } from "./i18n";
import { BridgeType } from "./domain/bridge";
import ETHBridge from "./eth/bridge";
import { bridges, chainIdToBridgeAddress } from "./store/bridge";
import { CHAIN_MAINNET, CHAIN_TKO } from "./domain/chain";
-
- const { chains, provider } = configureChains(
- [mainnet, taiko],
- [publicProvider()]
- );
+ import ERC20Bridge from "./erc20/bridge";
setupI18n({ withLocale: "en" });
const ethBridge = new ETHBridge();
+ const erc20Bridge = new ERC20Bridge();
bridges.update((store) => {
store.set(BridgeType.ETH, ethBridge);
+ store.set(BridgeType.ERC20, erc20Bridge);
return store;
});
@@ -46,7 +43,6 @@
-
diff --git a/packages/bridge-ui/src/components/buttons/SelectToken.svelte b/packages/bridge-ui/src/components/buttons/SelectToken.svelte
index 889f078ad1..0f7a5bb1b0 100644
--- a/packages/bridge-ui/src/components/buttons/SelectToken.svelte
+++ b/packages/bridge-ui/src/components/buttons/SelectToken.svelte
@@ -1,13 +1,20 @@
diff --git a/packages/bridge-ui/src/components/form/BridgeForm.svelte b/packages/bridge-ui/src/components/form/BridgeForm.svelte
index 8a1411d259..620fbc11d7 100644
--- a/packages/bridge-ui/src/components/form/BridgeForm.svelte
+++ b/packages/bridge-ui/src/components/form/BridgeForm.svelte
@@ -2,21 +2,53 @@
import { _ } from "svelte-i18n";
import { token } from "../../store/token";
import { fromChain, toChain } from "../../store/chain";
- import { activeBridge, chainIdToBridgeAddress } from "../../store/bridge";
+ import {
+ activeBridge,
+ chainIdToBridgeAddress,
+ bridgeType,
+ } from "../../store/bridge";
import { signer } from "../../store/signer";
import { BigNumber, ethers, Signer } from "ethers";
import { toast } from "@zerodevx/svelte-toast";
+ import type { Token } from "../../domain/token";
+ import type { BridgeType } from "../../domain/bridge";
+ import type { Chain } from "../../domain/chain";
let amount: string;
+ let requiresAllowance: boolean = true;
let btnDisabled: boolean = true;
$: isBtnDisabled($signer, amount)
.then((d) => (btnDisabled = d))
.catch((e) => console.log(e));
+ $: checkAllowance(amount, $token, $bridgeType, $fromChain, $signer)
+ .then((a) => (requiresAllowance = a))
+ .catch((e) => console.log(e));
+
+ async function checkAllowance(
+ amt: string,
+ token: Token,
+ bridgeType: BridgeType,
+ fromChain: Chain,
+ signer: Signer
+ ) {
+ if (!signer || !amt || !token || !fromChain) return true;
+
+ return await $activeBridge.RequiresAllowance({
+ amountInWei: amt
+ ? ethers.utils.parseUnits(amt, token.decimals)
+ : BigNumber.from(0),
+ signer: signer,
+ contractAddress: token.address,
+ spenderAddress: $chainIdToBridgeAddress.get(fromChain.id),
+ });
+ }
+
async function isBtnDisabled(signer: Signer, amount: string) {
if (!signer) return true;
if (!amount) return true;
+ if (requiresAllowance) return true;
const balance = await signer.getBalance("latest");
if (balance.lt(ethers.utils.parseUnits(amount, $token.decimals)))
return true;
@@ -24,8 +56,33 @@
return false;
}
+ async function approve() {
+ try {
+ if (!requiresAllowance)
+ throw Error("does not require additional allowance");
+
+ const tx = await $activeBridge.Approve({
+ amountInWei: ethers.utils.parseUnits(amount, $token.decimals),
+ signer: $signer,
+ contractAddress: $token.address,
+ spenderAddress: $chainIdToBridgeAddress.get($fromChain.id),
+ });
+ console.log("approved, waiting for confirmations ", tx);
+ await $signer.provider.waitForTransaction(tx.hash, 3);
+
+ requiresAllowance = false;
+
+ toast.push($_("toast.transactionSent"));
+ } catch (e) {
+ console.log(e);
+ toast.push($_("toast.errorSendingTransaction"));
+ }
+ }
+
async function bridge() {
try {
+ if (requiresAllowance) throw Error("requires additional allowance");
+
const tx = await $activeBridge.Bridge({
amountInWei: ethers.utils.parseUnits(amount, $token.decimals),
signer: $signer,
@@ -76,10 +133,20 @@
-
+{#if !requiresAllowance}
+
+{:else}
+
+{/if}
diff --git a/packages/bridge-ui/src/constants/abi/ERC20.ts b/packages/bridge-ui/src/constants/abi/ERC20.ts
new file mode 100644
index 0000000000..f8bd8a6651
--- /dev/null
+++ b/packages/bridge-ui/src/constants/abi/ERC20.ts
@@ -0,0 +1,222 @@
+export default [
+ {
+ constant: true,
+ inputs: [],
+ name: "name",
+ outputs: [
+ {
+ name: "",
+ type: "string",
+ },
+ ],
+ payable: false,
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ constant: false,
+ inputs: [
+ {
+ name: "_spender",
+ type: "address",
+ },
+ {
+ name: "_value",
+ type: "uint256",
+ },
+ ],
+ name: "approve",
+ outputs: [
+ {
+ name: "",
+ type: "bool",
+ },
+ ],
+ payable: false,
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ constant: true,
+ inputs: [],
+ name: "totalSupply",
+ outputs: [
+ {
+ name: "",
+ type: "uint256",
+ },
+ ],
+ payable: false,
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ constant: false,
+ inputs: [
+ {
+ name: "_from",
+ type: "address",
+ },
+ {
+ name: "_to",
+ type: "address",
+ },
+ {
+ name: "_value",
+ type: "uint256",
+ },
+ ],
+ name: "transferFrom",
+ outputs: [
+ {
+ name: "",
+ type: "bool",
+ },
+ ],
+ payable: false,
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ constant: true,
+ inputs: [],
+ name: "decimals",
+ outputs: [
+ {
+ name: "",
+ type: "uint8",
+ },
+ ],
+ payable: false,
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ constant: true,
+ inputs: [
+ {
+ name: "_owner",
+ type: "address",
+ },
+ ],
+ name: "balanceOf",
+ outputs: [
+ {
+ name: "balance",
+ type: "uint256",
+ },
+ ],
+ payable: false,
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ constant: true,
+ inputs: [],
+ name: "symbol",
+ outputs: [
+ {
+ name: "",
+ type: "string",
+ },
+ ],
+ payable: false,
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ constant: false,
+ inputs: [
+ {
+ name: "_to",
+ type: "address",
+ },
+ {
+ name: "_value",
+ type: "uint256",
+ },
+ ],
+ name: "transfer",
+ outputs: [
+ {
+ name: "",
+ type: "bool",
+ },
+ ],
+ payable: false,
+ stateMutability: "nonpayable",
+ type: "function",
+ },
+ {
+ constant: true,
+ inputs: [
+ {
+ name: "_owner",
+ type: "address",
+ },
+ {
+ name: "_spender",
+ type: "address",
+ },
+ ],
+ name: "allowance",
+ outputs: [
+ {
+ name: "",
+ type: "uint256",
+ },
+ ],
+ payable: false,
+ stateMutability: "view",
+ type: "function",
+ },
+ {
+ payable: true,
+ stateMutability: "payable",
+ type: "fallback",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ name: "owner",
+ type: "address",
+ },
+ {
+ indexed: true,
+ name: "spender",
+ type: "address",
+ },
+ {
+ indexed: false,
+ name: "value",
+ type: "uint256",
+ },
+ ],
+ name: "Approval",
+ type: "event",
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ name: "from",
+ type: "address",
+ },
+ {
+ indexed: true,
+ name: "to",
+ type: "address",
+ },
+ {
+ indexed: false,
+ name: "value",
+ type: "uint256",
+ },
+ ],
+ name: "Transfer",
+ type: "event",
+ },
+];
diff --git a/packages/bridge-ui/src/domain/bridge.ts b/packages/bridge-ui/src/domain/bridge.ts
index 63eeb9caff..9767f95b38 100644
--- a/packages/bridge-ui/src/domain/bridge.ts
+++ b/packages/bridge-ui/src/domain/bridge.ts
@@ -11,6 +11,7 @@ type ApproveOpts = {
amountInWei: BigNumber;
contractAddress: string;
signer: ethers.Signer;
+ spenderAddress: string;
};
type BridgeOpts = {
@@ -26,6 +27,7 @@ type BridgeOpts = {
};
interface Bridge {
+ RequiresAllowance(opts: ApproveOpts): Promise;
Approve(opts: ApproveOpts): Promise;
Bridge(opts: BridgeOpts): Promise;
}
diff --git a/packages/bridge-ui/src/erc20/bridge.spec.ts b/packages/bridge-ui/src/erc20/bridge.spec.ts
new file mode 100644
index 0000000000..8b329410da
--- /dev/null
+++ b/packages/bridge-ui/src/erc20/bridge.spec.ts
@@ -0,0 +1,228 @@
+import { BigNumber, Wallet } from "ethers";
+import { mainnet, taiko } from "../domain/chain";
+import type { ApproveOpts, Bridge, BridgeOpts } from "../domain/bridge";
+import ERC20Bridge from "./bridge";
+
+const mockSigner = {
+ getAddress: jest.fn(),
+};
+
+const mockContract = {
+ sendERC20: jest.fn(),
+ allowance: jest.fn(),
+ approve: jest.fn(),
+};
+
+jest.mock("ethers", () => ({
+ /* eslint-disable-next-line */
+ ...(jest.requireActual("ethers") as object),
+ Wallet: function () {
+ return mockSigner;
+ },
+ Signer: function () {
+ return mockSigner;
+ },
+ Contract: function () {
+ return mockContract;
+ },
+}));
+
+const wallet = new Wallet("0x");
+
+const opts: BridgeOpts = {
+ amountInWei: BigNumber.from(1),
+ signer: wallet,
+ tokenAddress: "0xtoken",
+ fromChainId: mainnet.id,
+ toChainId: taiko.id,
+ bridgeAddress: "0x456",
+ processingFeeInWei: BigNumber.from(2),
+ memo: "memo",
+};
+
+const approveOpts: ApproveOpts = {
+ amountInWei: BigNumber.from(1),
+ signer: wallet,
+ contractAddress: "0x456",
+ spenderAddress: "0x789",
+};
+
+describe("bridge tests", () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it("requires allowance returns true when allowance has not been set", async () => {
+ mockContract.allowance.mockImplementationOnce(() =>
+ opts.amountInWei.sub(1)
+ );
+
+ mockSigner.getAddress.mockImplementationOnce(() => "0xfake");
+
+ const bridge: Bridge = new ERC20Bridge();
+
+ expect(mockContract.allowance).not.toHaveBeenCalled();
+ const requires = await bridge.RequiresAllowance(approveOpts);
+
+ expect(mockSigner.getAddress).toHaveBeenCalled();
+ expect(mockContract.allowance).toHaveBeenCalledWith(
+ "0xfake",
+ approveOpts.spenderAddress
+ );
+ expect(requires).toBe(true);
+ });
+
+ it("requires allowance returns true when allowance is > than amount", async () => {
+ mockContract.allowance.mockImplementationOnce(() =>
+ opts.amountInWei.add(1)
+ );
+ mockSigner.getAddress.mockImplementationOnce(() => "0xfake");
+
+ const bridge: Bridge = new ERC20Bridge();
+
+ expect(mockContract.allowance).not.toHaveBeenCalled();
+ const requires = await bridge.RequiresAllowance(approveOpts);
+
+ expect(mockSigner.getAddress).toHaveBeenCalled();
+ expect(mockContract.allowance).toHaveBeenCalledWith(
+ "0xfake",
+ approveOpts.spenderAddress
+ );
+ expect(requires).toBe(false);
+ });
+
+ it("requires allowance returns true when allowance is === amount", async () => {
+ mockContract.allowance.mockImplementationOnce(() => opts.amountInWei);
+ mockSigner.getAddress.mockImplementationOnce(() => "0xfake");
+
+ const bridge: Bridge = new ERC20Bridge();
+
+ expect(mockContract.allowance).not.toHaveBeenCalled();
+ const requires = await bridge.RequiresAllowance(approveOpts);
+
+ expect(mockSigner.getAddress).toHaveBeenCalled();
+ expect(mockContract.allowance).toHaveBeenCalledWith(
+ "0xfake",
+ approveOpts.spenderAddress
+ );
+ expect(requires).toBe(false);
+ });
+
+ it("approve throws when amount is already greater than whats set", async () => {
+ mockContract.allowance.mockImplementationOnce(() =>
+ opts.amountInWei.add(1)
+ );
+
+ mockSigner.getAddress.mockImplementationOnce(() => "0xfake");
+
+ const bridge: Bridge = new ERC20Bridge();
+
+ expect(mockContract.allowance).not.toHaveBeenCalled();
+ await expect(bridge.Approve(approveOpts)).rejects.toThrowError(
+ "token vault already has required allowance"
+ );
+
+ expect(mockSigner.getAddress).toHaveBeenCalled();
+ expect(mockContract.allowance).toHaveBeenCalledWith(
+ "0xfake",
+ approveOpts.spenderAddress
+ );
+ });
+
+ it("approve succeeds when allowance is less than what is being requested", async () => {
+ mockContract.allowance.mockImplementationOnce(() =>
+ opts.amountInWei.sub(1)
+ );
+
+ mockSigner.getAddress.mockImplementationOnce(() => "0xfake");
+
+ const bridge: Bridge = new ERC20Bridge();
+
+ expect(mockContract.allowance).not.toHaveBeenCalled();
+ await bridge.Approve(approveOpts);
+
+ expect(mockSigner.getAddress).toHaveBeenCalled();
+ expect(mockContract.allowance).toHaveBeenCalledWith(
+ "0xfake",
+ approveOpts.spenderAddress
+ );
+ expect(mockContract.approve).toHaveBeenCalledWith(
+ approveOpts.spenderAddress,
+ approveOpts.amountInWei
+ );
+ });
+
+ it("bridge throws when requires approval", async () => {
+ mockContract.allowance.mockImplementationOnce(() =>
+ opts.amountInWei.sub(1)
+ );
+
+ const bridge: Bridge = new ERC20Bridge();
+
+ expect(mockContract.sendERC20).not.toHaveBeenCalled();
+
+ await expect(bridge.Bridge(opts)).rejects.toThrowError(
+ "token vault does not have required allowance"
+ );
+
+ expect(mockContract.sendERC20).not.toHaveBeenCalled();
+ });
+
+ it("bridge calls senderc20 when doesnt requires approval", async () => {
+ mockContract.allowance.mockImplementationOnce(() =>
+ opts.amountInWei.add(1)
+ );
+ mockSigner.getAddress.mockImplementation(() => "0xfake");
+
+ const bridge: Bridge = new ERC20Bridge();
+
+ expect(mockContract.sendERC20).not.toHaveBeenCalled();
+
+ await bridge.Bridge(opts);
+
+ expect(mockContract.sendERC20).toHaveBeenCalled();
+ expect(mockContract.sendERC20).toHaveBeenCalledWith(
+ opts.toChainId,
+ "0xfake",
+ opts.tokenAddress,
+ opts.amountInWei,
+ BigNumber.from(100000),
+ opts.processingFeeInWei,
+ "0xfake",
+ opts.memo
+ );
+ });
+
+ it("bridge calls senderc20 when doesnt requires approval, with no processing fee and memo", async () => {
+ mockContract.allowance.mockImplementationOnce(() =>
+ opts.amountInWei.add(1)
+ );
+ mockSigner.getAddress.mockImplementation(() => "0xfake");
+
+ const bridge: Bridge = new ERC20Bridge();
+
+ expect(mockContract.sendERC20).not.toHaveBeenCalled();
+
+ const opts: BridgeOpts = {
+ amountInWei: BigNumber.from(1),
+ signer: wallet,
+ tokenAddress: "0xtoken",
+ fromChainId: mainnet.id,
+ toChainId: taiko.id,
+ bridgeAddress: "0x456",
+ };
+
+ await bridge.Bridge(opts);
+
+ expect(mockContract.sendERC20).toHaveBeenCalledWith(
+ opts.toChainId,
+ "0xfake",
+ opts.tokenAddress,
+ opts.amountInWei,
+ BigNumber.from(0),
+ BigNumber.from(0),
+ "0xfake",
+ ""
+ );
+ });
+});
diff --git a/packages/bridge-ui/src/erc20/bridge.ts b/packages/bridge-ui/src/erc20/bridge.ts
new file mode 100644
index 0000000000..ad59c5e002
--- /dev/null
+++ b/packages/bridge-ui/src/erc20/bridge.ts
@@ -0,0 +1,102 @@
+import { BigNumber, Contract, Signer } from "ethers";
+import type { Transaction } from "ethers";
+import type { ApproveOpts, Bridge, BridgeOpts } from "../domain/bridge";
+import TokenVault from "../constants/abi/TokenVault";
+import ERC20 from "../constants/abi/ERC20";
+
+class ERC20Bridge implements Bridge {
+ private async spenderRequiresAllowance(
+ tokenAddress: string,
+ signer: Signer,
+ amount: BigNumber,
+ bridgeAddress: string
+ ): Promise {
+ const contract: Contract = new Contract(tokenAddress, ERC20, signer);
+ const owner = await signer.getAddress();
+ const allowance: BigNumber = await contract.allowance(owner, bridgeAddress);
+
+ return allowance.lt(amount);
+ }
+
+ async RequiresAllowance(opts: ApproveOpts): Promise {
+ return await this.spenderRequiresAllowance(
+ opts.contractAddress,
+ opts.signer,
+ opts.amountInWei,
+ opts.spenderAddress
+ );
+ }
+
+ async Approve(opts: ApproveOpts): Promise {
+ if (
+ !(await this.spenderRequiresAllowance(
+ opts.contractAddress,
+ opts.signer,
+ opts.amountInWei,
+ opts.spenderAddress
+ ))
+ ) {
+ throw Error("token vault already has required allowance");
+ }
+
+ const contract: Contract = new Contract(
+ opts.contractAddress,
+ ERC20,
+ opts.signer
+ );
+
+ const tx = await contract.approve(opts.spenderAddress, opts.amountInWei);
+ return tx;
+ }
+
+ async Bridge(opts: BridgeOpts): Promise {
+ if (
+ await this.spenderRequiresAllowance(
+ opts.tokenAddress,
+ opts.signer,
+ opts.amountInWei,
+ opts.bridgeAddress
+ )
+ ) {
+ throw Error("token vault does not have required allowance");
+ }
+
+ const contract: Contract = new Contract(
+ opts.bridgeAddress,
+ TokenVault,
+ opts.signer
+ );
+
+ const owner = await opts.signer.getAddress();
+ const message = {
+ sender: owner,
+ srcChainId: opts.fromChainId,
+ destChainId: opts.toChainId,
+ owner: owner,
+ to: owner,
+ refundAddress: owner,
+ depositValue: opts.amountInWei,
+ callValue: 0,
+ processingFee: opts.processingFeeInWei ?? BigNumber.from(0),
+ gasLimit: opts.processingFeeInWei
+ ? BigNumber.from(100000)
+ : BigNumber.from(0),
+ memo: opts.memo ?? "",
+ };
+
+ const tx = await contract.sendERC20(
+ message.destChainId,
+ owner,
+ opts.tokenAddress,
+ opts.amountInWei,
+ message.gasLimit,
+ message.processingFee,
+ message.refundAddress,
+ message.memo
+ );
+
+ return tx;
+ }
+}
+
+export default ERC20Bridge;
diff --git a/packages/bridge-ui/src/eth/bridge.spec.ts b/packages/bridge-ui/src/eth/bridge.spec.ts
index 4d247f0303..10e45728a2 100644
--- a/packages/bridge-ui/src/eth/bridge.spec.ts
+++ b/packages/bridge-ui/src/eth/bridge.spec.ts
@@ -30,6 +30,19 @@ describe("bridge tests", () => {
jest.resetAllMocks();
});
+ it("requires allowance returns false", async () => {
+ const bridge: Bridge = new ETHBridge();
+ const wallet = new Wallet("0x");
+
+ const requires = await bridge.RequiresAllowance({
+ amountInWei: BigNumber.from(1),
+ signer: new Wallet("0x"),
+ contractAddress: "0x1234",
+ spenderAddress: "0x",
+ });
+ expect(requires).toBe(false);
+ });
+
it("approve returns empty transaction", async () => {
const bridge: Bridge = new ETHBridge();
@@ -37,6 +50,7 @@ describe("bridge tests", () => {
amountInWei: BigNumber.from(1),
signer: new Wallet("0x"),
contractAddress: "0x1234",
+ spenderAddress: "0x",
});
});
diff --git a/packages/bridge-ui/src/eth/bridge.ts b/packages/bridge-ui/src/eth/bridge.ts
index e56f89bf80..b3809bff0c 100644
--- a/packages/bridge-ui/src/eth/bridge.ts
+++ b/packages/bridge-ui/src/eth/bridge.ts
@@ -4,6 +4,10 @@ import type { ApproveOpts, Bridge, BridgeOpts } from "../domain/bridge";
import TokenVault from "../constants/abi/TokenVault";
class ETHBridge implements Bridge {
+ RequiresAllowance(opts: ApproveOpts): Promise {
+ return Promise.resolve(false);
+ }
+
// ETH does not need to be approved for transacting
Approve(opts: ApproveOpts): Promise {
return new Promise((resolve) => resolve({} as unknown as Transaction));
@@ -18,7 +22,7 @@ class ETHBridge implements Bridge {
const owner = await opts.signer.getAddress();
const message = {
- sender: opts.signer.getAddress(),
+ sender: owner,
srcChainId: opts.fromChainId,
destChainId: opts.toChainId,
owner: owner,
diff --git a/packages/bridge-ui/src/i18n.js b/packages/bridge-ui/src/i18n.js
index 8adccadf3c..f4ac2eb5e2 100644
--- a/packages/bridge-ui/src/i18n.js
+++ b/packages/bridge-ui/src/i18n.js
@@ -9,7 +9,8 @@ function setupI18n({ withLocale: _locale } = { withLocale: "en" }) {
selectToken: "Select Token",
from: "From",
to: "To",
- bridge: "Bridge"
+ bridge: "Bridge",
+ approve: "Approve"
},
nav: {
connect: "Connect Wallet"