From 4052aadd01b75611be3e9b50970aeb649f2de34b Mon Sep 17 00:00:00 2001 From: shadab-taiko <108871478+shadab-taiko@users.noreply.github.com> Date: Fri, 24 Feb 2023 09:21:22 +0530 Subject: [PATCH] feat(bridge-ui): release token button (#13203) Co-authored-by: jeff <113397187+cyberhorsey@users.noreply.github.com> --- packages/bridge-ui/jest.config.js | 2 +- .../components/MessageStatusTooltip.svelte | 2 +- .../src/components/Transaction.svelte | 129 ++++++++++++++---- .../bridge-ui/src/constants/abi/Bridge.ts | 19 +++ .../bridge-ui/src/constants/abi/HeaderSync.ts | 76 +++++------ .../bridge-ui/src/constants/abi/TokenVault.ts | 64 ++++++++- packages/bridge-ui/src/domain/bridge.ts | 13 +- packages/bridge-ui/src/domain/message.ts | 1 + packages/bridge-ui/src/domain/proof.ts | 13 +- packages/bridge-ui/src/erc20/bridge.spec.ts | 76 ++++++++++- packages/bridge-ui/src/erc20/bridge.ts | 47 +++++++ packages/bridge-ui/src/eth/bridge.spec.ts | 79 ++++++++++- packages/bridge-ui/src/eth/bridge.ts | 49 ++++++- packages/bridge-ui/src/proof/service.spec.ts | 121 ++++++++++++++++ packages/bridge-ui/src/proof/service.ts | 81 ++++++++--- 15 files changed, 671 insertions(+), 101 deletions(-) diff --git a/packages/bridge-ui/jest.config.js b/packages/bridge-ui/jest.config.js index e7adeb038d2..8a0c0adcfea 100644 --- a/packages/bridge-ui/jest.config.js +++ b/packages/bridge-ui/jest.config.js @@ -42,7 +42,7 @@ export default { statements: 95, branches: 72, functions: 89, - lines: 96, + lines: 95, }, }, modulePathIgnorePatterns: ["/public/build/"], diff --git a/packages/bridge-ui/src/components/MessageStatusTooltip.svelte b/packages/bridge-ui/src/components/MessageStatusTooltip.svelte index f24fc143129..d4e6be85d74 100644 --- a/packages/bridge-ui/src/components/MessageStatusTooltip.svelte +++ b/packages/bridge-ui/src/components/MessageStatusTooltip.svelte @@ -31,7 +31,7 @@ message, and you must retry the processing yourself.
  • - Failed: Your bridged asset is unable to be processed + Release: Your bridged asset is unable to be processed and is available to you on the source chain.
  • diff --git a/packages/bridge-ui/src/components/Transaction.svelte b/packages/bridge-ui/src/components/Transaction.svelte index 7a0d6e6590a..79d810dc219 100644 --- a/packages/bridge-ui/src/components/Transaction.svelte +++ b/packages/bridge-ui/src/components/Transaction.svelte @@ -5,7 +5,7 @@ import { ArrowTopRightOnSquare } from "svelte-heros-v2"; import { MessageStatus } from "../domain/message"; import { Contract, ethers } from "ethers"; - import { bridges } from "../store/bridge"; + import { bridges, chainIdToTokenVaultAddress } from "../store/bridge"; import { signer } from "../store/signer"; import { pendingTransactions, @@ -27,37 +27,43 @@ import { fetchSigner, switchNetwork } from "@wagmi/core"; import Bridge from "../constants/abi/Bridge"; import ButtonWithTooltip from "./ButtonWithTooltip.svelte"; + import TokenVault from "../constants/abi/TokenVault"; export let transaction: BridgeTransaction; export let fromChain: Chain; export let toChain: Chain; + let loading: boolean; let processable: boolean = false; onMount(async () => { processable = await isProcessable(); }); - async function claim(bridgeTx: BridgeTransaction) { - if (fromChain.id !== bridgeTx.message.destChainId.toNumber()) { - const chain = chains[bridgeTx.message.destChainId.toNumber()]; - await switchNetwork({ - chainId: chain.id, - }); - const provider = new ethers.providers.Web3Provider(window.ethereum); - await provider.send("eth_requestAccounts", []); + async function switchChainAndSetSigner(chain: Chain) { + await switchNetwork({ + chainId: chain.id, + }); + const provider = new ethers.providers.Web3Provider(window.ethereum); + await provider.send("eth_requestAccounts", []); - fromChainStore.set(chain); - if (chain === CHAIN_MAINNET) { - toChainStore.set(CHAIN_TKO); - } else { - toChainStore.set(CHAIN_MAINNET); - } - const wagmiSigner = await fetchSigner(); - signer.set(wagmiSigner); + fromChainStore.set(chain); + if (chain === CHAIN_MAINNET) { + toChainStore.set(CHAIN_TKO); + } else { + toChainStore.set(CHAIN_MAINNET); } + const wagmiSigner = await fetchSigner(); + signer.set(wagmiSigner); + } + async function claim(bridgeTx: BridgeTransaction) { try { + loading = true; + if (fromChain.id !== bridgeTx.message.destChainId.toNumber()) { + const chain = chains[bridgeTx.message.destChainId.toNumber()]; + await switchChainAndSetSigner(chain) + } const tx = await $bridges .get(bridgeTx.message.data === "0x" ? BridgeType.ETH : BridgeType.ERC20) .Claim({ @@ -79,6 +85,43 @@ } catch (e) { console.log(e); errorToast($_("toast.errorSendingTransaction")); + } finally { + loading = false; + } + } + + async function releaseTokens(bridgeTx: BridgeTransaction) { + try { + loading = true; + if (fromChain.id !== bridgeTx.message.srcChainId.toNumber()) { + const chain = chains[bridgeTx.message.srcChainId.toNumber()]; + await switchChainAndSetSigner(chain) + } + const tx = await $bridges + .get(bridgeTx.message.data === "0x" ? BridgeType.ETH : BridgeType.ERC20) + .ReleaseTokens({ + signer: $signer, + message: bridgeTx.message, + msgHash: bridgeTx.msgHash, + destBridgeAddress: + chains[bridgeTx.message.destChainId.toNumber()].bridgeAddress, + srcBridgeAddress: + chains[bridgeTx.message.srcChainId.toNumber()].bridgeAddress, + destProvider: $providers.get(bridgeTx.message.destChainId.toNumber()), + srcTokenVaultAddress: $chainIdToTokenVaultAddress.get(bridgeTx.message.srcChainId.toNumber()) + }); + + pendingTransactions.update((store) => { + store.push(tx); + return store; + }); + + successToast($_("toast.transactionSent")); + } catch (e) { + console.log(e); + errorToast($_("toast.errorSendingTransaction")); + } finally { + loading = false; } } @@ -109,6 +152,29 @@ ); transaction.status = await contract.getMessageStatus(transaction.msgHash); + if(transaction.status === MessageStatus.Failed) { + if(transaction.message.data !== "0x") { + const srcTokenVaultContract = new ethers.Contract( + $chainIdToTokenVaultAddress.get(transaction.fromChainId), + TokenVault, + $providers.get(chains[transaction.message.srcChainId.toNumber()].id) + ) + const {token, amount} = await srcTokenVaultContract.messageDeposits(transaction.msgHash); + if(token === ethers.constants.AddressZero && amount.eq(0)) { + transaction.status = MessageStatus.FailedReleased; + } + } else { + const srcBridgeContract = new ethers.Contract( + chains[transaction.fromChainId].bridgeAddress, + Bridge, + $providers.get(chains[transaction.message.srcChainId.toNumber()].id) + ) + const isFailedMessageResolved = await srcBridgeContract.isEtherReleased(transaction.msgHash); + if(isFailedMessageResolved) { + transaction.status = MessageStatus.FailedReleased; + } + } + } transaction = transaction; if (transaction.status === MessageStatus.Done) clearInterval(interval); }, 20 * 1000); @@ -135,7 +201,7 @@ {#if !processable} Pending - {:else if !transaction.receipt && transaction.status === MessageStatus.New} + {:else if (!transaction.receipt && transaction.status === MessageStatus.New) || loading}
    {:else if transaction.receipt && transaction.status === MessageStatus.New} - await claim(transaction)} > Claim - + {:else if transaction.status === MessageStatus.Retriable} - await claim(transaction)} - > - Retry - + {:else if transaction.status === MessageStatus.Failed} - Failed + {:else if transaction.status === MessageStatus.Done} Claimed + {:else if transaction.status === MessageStatus.FailedReleased} + Released {/if}
    diff --git a/packages/bridge-ui/src/constants/abi/Bridge.ts b/packages/bridge-ui/src/constants/abi/Bridge.ts index 2e43ccef750..9c2612f484a 100644 --- a/packages/bridge-ui/src/constants/abi/Bridge.ts +++ b/packages/bridge-ui/src/constants/abi/Bridge.ts @@ -414,6 +414,25 @@ export default [ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "msgHash", + "type": "bytes32" + } + ], + "name": "isEtherReleased", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/packages/bridge-ui/src/constants/abi/HeaderSync.ts b/packages/bridge-ui/src/constants/abi/HeaderSync.ts index 0ce81c2ba66..caa811b784f 100644 --- a/packages/bridge-ui/src/constants/abi/HeaderSync.ts +++ b/packages/bridge-ui/src/constants/abi/HeaderSync.ts @@ -1,59 +1,53 @@ export default [ { - anonymous: false, - inputs: [ + "anonymous": false, + "inputs": [ { - indexed: true, - internalType: "uint256", - name: "height", - type: "uint256", + "indexed": true, + "internalType": "uint256", + "name": "srcHeight", + "type": "uint256" }, { - indexed: true, - internalType: "uint256", - name: "srcHeight", - type: "uint256", - }, - { - indexed: false, - internalType: "bytes32", - name: "srcHash", - type: "bytes32", - }, + "indexed": false, + "internalType": "bytes32", + "name": "srcHash", + "type": "bytes32" + } ], - name: "HeaderSynced", - type: "event", + "name": "HeaderSynced", + "type": "event" }, { - inputs: [], - name: "getLatestSyncedHeader", - outputs: [ + "inputs": [], + "name": "getLatestSyncedHeader", + "outputs": [ { - internalType: "bytes32", - name: "", - type: "bytes32", - }, + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } ], - stateMutability: "view", - type: "function", + "stateMutability": "view", + "type": "function" }, { - inputs: [ + "inputs": [ { - internalType: "uint256", - name: "number", - type: "uint256", - }, + "internalType": "uint256", + "name": "number", + "type": "uint256" + } ], - name: "getSyncedHeader", - outputs: [ + "name": "getSyncedHeader", + "outputs": [ { - internalType: "bytes32", - name: "", - type: "bytes32", - }, + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } ], - stateMutability: "view", - type: "function", + "stateMutability": "view", + "type": "function" }, ]; diff --git a/packages/bridge-ui/src/constants/abi/TokenVault.ts b/packages/bridge-ui/src/constants/abi/TokenVault.ts index 18ed573b113..ef547f6a391 100644 --- a/packages/bridge-ui/src/constants/abi/TokenVault.ts +++ b/packages/bridge-ui/src/constants/abi/TokenVault.ts @@ -9,6 +9,56 @@ export default [ "name": "RESOLVER_INVALID_ADDR", "type": "error" }, + { + "inputs": [], + "name": "TOKENVAULT_CANONICAL_TOKEN_NOT_FOUND", + "type": "error" + }, + { + "inputs": [], + "name": "TOKENVAULT_INVALID_AMOUNT", + "type": "error" + }, + { + "inputs": [], + "name": "TOKENVAULT_INVALID_CALL_VALUE", + "type": "error" + }, + { + "inputs": [], + "name": "TOKENVAULT_INVALID_OWNER", + "type": "error" + }, + { + "inputs": [], + "name": "TOKENVAULT_INVALID_SENDER", + "type": "error" + }, + { + "inputs": [], + "name": "TOKENVAULT_INVALID_SRC_CHAIN_ID", + "type": "error" + }, + { + "inputs": [], + "name": "TOKENVAULT_INVALID_TO", + "type": "error" + }, + { + "inputs": [], + "name": "TOKENVAULT_INVALID_TOKEN", + "type": "error" + }, + { + "inputs": [], + "name": "TOKENVAULT_INVALID_VALUE", + "type": "error" + }, + { + "inputs": [], + "name": "TOKENVAULT_MESSAGE_NOT_FAILED", + "type": "error" + }, { "anonymous": false, "inputs": [ @@ -255,7 +305,7 @@ export default [ "inputs": [ { "internalType": "address", - "name": "", + "name": "bridgedAddress", "type": "address" } ], @@ -294,12 +344,12 @@ export default [ "inputs": [ { "internalType": "uint256", - "name": "", + "name": "chainId", "type": "uint256" }, { "internalType": "address", - "name": "", + "name": "canonicalAddress", "type": "address" } ], @@ -307,7 +357,7 @@ export default [ "outputs": [ { "internalType": "address", - "name": "", + "name": "bridgedAddress", "type": "address" } ], @@ -331,7 +381,7 @@ export default [ "inputs": [ { "internalType": "address", - "name": "", + "name": "tokenAddress", "type": "address" } ], @@ -339,7 +389,7 @@ export default [ "outputs": [ { "internalType": "bool", - "name": "", + "name": "isBridged", "type": "bool" } ], @@ -350,7 +400,7 @@ export default [ "inputs": [ { "internalType": "bytes32", - "name": "", + "name": "msgHash", "type": "bytes32" } ], diff --git a/packages/bridge-ui/src/domain/bridge.ts b/packages/bridge-ui/src/domain/bridge.ts index 744ae8e5250..437337a51c5 100644 --- a/packages/bridge-ui/src/domain/bridge.ts +++ b/packages/bridge-ui/src/domain/bridge.ts @@ -36,12 +36,23 @@ type ClaimOpts = { srcBridgeAddress: string; }; +type ReleaseOpts = { + message: Message; + msgHash: string; + signer: ethers.Signer; + destBridgeAddress: string; + srcBridgeAddress: string; + destProvider: ethers.providers.JsonRpcProvider; + srcTokenVaultAddress: string; +}; + interface Bridge { RequiresAllowance(opts: ApproveOpts): Promise; Approve(opts: ApproveOpts): Promise; Bridge(opts: BridgeOpts): Promise; EstimateGas(opts: BridgeOpts): Promise; Claim(opts: ClaimOpts): Promise; + ReleaseTokens(opts: ReleaseOpts): Promise; } -export { ApproveOpts, BridgeOpts, BridgeType, Bridge, ClaimOpts }; +export { ApproveOpts, BridgeOpts, BridgeType, Bridge, ClaimOpts, ReleaseOpts }; diff --git a/packages/bridge-ui/src/domain/message.ts b/packages/bridge-ui/src/domain/message.ts index 0abaa785c7e..1abf1c29bc8 100644 --- a/packages/bridge-ui/src/domain/message.ts +++ b/packages/bridge-ui/src/domain/message.ts @@ -5,6 +5,7 @@ enum MessageStatus { Retriable, Done, Failed, + FailedReleased } type Message = { diff --git a/packages/bridge-ui/src/domain/proof.ts b/packages/bridge-ui/src/domain/proof.ts index 96d4641a8ad..1ee839565ca 100644 --- a/packages/bridge-ui/src/domain/proof.ts +++ b/packages/bridge-ui/src/domain/proof.ts @@ -23,8 +23,19 @@ type GenerateProofOpts = { srcSignalServiceAddress: string; }; +type GenerateReleaseProofOpts = { + msgHash: string; + sender: string; + destBridgeAddress: string; + destChain: number; + destHeaderSyncAddress: string; + srcHeaderSyncAddress: string; + srcChain: number; +}; + interface Prover { GenerateProof(opts: GenerateProofOpts): Promise; + GenerateReleaseProof(opts: GenerateReleaseProofOpts): Promise; } -export { GenerateProofOpts, Prover, StorageEntry, EthGetProofResponse }; +export { GenerateProofOpts, Prover, StorageEntry, EthGetProofResponse, GenerateReleaseProofOpts }; diff --git a/packages/bridge-ui/src/erc20/bridge.spec.ts b/packages/bridge-ui/src/erc20/bridge.spec.ts index af65843a78e..a01aa648a52 100644 --- a/packages/bridge-ui/src/erc20/bridge.spec.ts +++ b/packages/bridge-ui/src/erc20/bridge.spec.ts @@ -1,4 +1,4 @@ -import { BigNumber, Wallet } from "ethers"; +import { BigNumber, ethers, Wallet } from "ethers"; import { CHAIN_ID_MAINNET, CHAIN_ID_TAIKO, @@ -20,10 +20,12 @@ const mockContract = { processMessage: jest.fn(), retryMessage: jest.fn(), getMessageStatus: jest.fn(), + releaseERC20: jest.fn(), }; const mockProver = { GenerateProof: jest.fn(), + GenerateReleaseProof: jest.fn(), }; jest.mock("ethers", () => ({ @@ -395,4 +397,76 @@ describe("bridge tests", () => { expect(mockContract.retryMessage).toHaveBeenCalled(); }); + + it("release tokens throws if message is already in DONE status", async () => { + mockContract.getMessageStatus.mockImplementationOnce(() => { + return MessageStatus.Done; + }); + + mockSigner.getAddress.mockImplementationOnce(() => { + return "0x"; + }); + + const wallet = new Wallet("0x"); + + const bridge: Bridge = new ERC20Bridge(mockProver); + + expect(mockContract.releaseERC20).not.toHaveBeenCalled(); + + expect(mockProver.GenerateReleaseProof).not.toHaveBeenCalled(); + + await expect(bridge.ReleaseTokens({ + message: { + owner: "0x", + srcChainId: BigNumber.from(CHAIN_ID_TAIKO), + destChainId: BigNumber.from(CHAIN_ID_MAINNET), + sender: "0x01", + gasLimit: BigNumber.from(1), + } as unknown as Message, + msgHash: "0x", + srcBridgeAddress: "0x", + destBridgeAddress: "0x", + signer: wallet, + destProvider: new ethers.providers.JsonRpcProvider(), + srcTokenVaultAddress: "0x" + })).rejects.toThrowError("message already processed"); + }); + + it("release tokens", async () => { + mockContract.getMessageStatus.mockImplementationOnce(() => { + return MessageStatus.Failed; + }); + + mockSigner.getAddress.mockImplementationOnce(() => { + return "0x"; + }); + + const wallet = new Wallet("0x"); + + const bridge: Bridge = new ERC20Bridge(mockProver); + + expect(mockContract.releaseERC20).not.toHaveBeenCalled(); + + expect(mockProver.GenerateReleaseProof).not.toHaveBeenCalled(); + + await bridge.ReleaseTokens({ + message: { + owner: "0x", + srcChainId: BigNumber.from(CHAIN_ID_TAIKO), + destChainId: BigNumber.from(CHAIN_ID_MAINNET), + sender: "0x01", + gasLimit: BigNumber.from(1), + } as unknown as Message, + msgHash: "0x", + srcBridgeAddress: "0x", + destBridgeAddress: "0x", + signer: wallet, + destProvider: new ethers.providers.JsonRpcProvider(), + srcTokenVaultAddress: "0x" + }); + + expect(mockProver.GenerateReleaseProof).toHaveBeenCalled(); + + expect(mockContract.releaseERC20).toHaveBeenCalled(); + }); }); diff --git a/packages/bridge-ui/src/erc20/bridge.ts b/packages/bridge-ui/src/erc20/bridge.ts index f59c0361f63..0622f4afdf4 100644 --- a/packages/bridge-ui/src/erc20/bridge.ts +++ b/packages/bridge-ui/src/erc20/bridge.ts @@ -5,6 +5,7 @@ import type { Bridge, BridgeOpts, ClaimOpts, + ReleaseOpts, } from "../domain/bridge"; import TokenVault from "../constants/abi/TokenVault"; import ERC20 from "../constants/abi/ERC20"; @@ -195,6 +196,52 @@ class ERC20Bridge implements Bridge { return await contract.retryMessage(opts.message, false); } } + + async ReleaseTokens(opts: ReleaseOpts): Promise { + const destBridgeContract: Contract = new Contract( + opts.destBridgeAddress, + BridgeABI, + opts.destProvider + ); + + const messageStatus: MessageStatus = await destBridgeContract.getMessageStatus( + opts.msgHash + ); + + if (messageStatus === MessageStatus.Done) { + throw Error("message already processed"); + } + + const signerAddress = await opts.signer.getAddress(); + + if (opts.message.owner.toLowerCase() !== signerAddress.toLowerCase()) { + throw Error("user can not release these tokens, it is not their message"); + } + + if (messageStatus === MessageStatus.Failed) { + const proofOpts = { + srcChain: opts.message.srcChainId.toNumber(), + msgHash: opts.msgHash, + sender: opts.srcBridgeAddress, + destBridgeAddress: opts.destBridgeAddress, + destChain: opts.message.destChainId.toNumber(), + destHeaderSyncAddress: + chains[opts.message.destChainId.toNumber()].headerSyncAddress, + srcHeaderSyncAddress: + chains[opts.message.srcChainId.toNumber()].headerSyncAddress, + }; + + const proof = await this.prover.GenerateReleaseProof(proofOpts); + + const srcTokenVaultContract: Contract = new Contract( + opts.srcTokenVaultAddress, + TokenVault, + opts.signer + ); + + return await srcTokenVaultContract.releaseERC20(opts.message, proof); + } + } } export default ERC20Bridge; diff --git a/packages/bridge-ui/src/eth/bridge.spec.ts b/packages/bridge-ui/src/eth/bridge.spec.ts index 458f9155e4e..470a7ae9e98 100644 --- a/packages/bridge-ui/src/eth/bridge.spec.ts +++ b/packages/bridge-ui/src/eth/bridge.spec.ts @@ -1,4 +1,4 @@ -import { BigNumber, Wallet } from "ethers"; +import { BigNumber, ethers, Wallet } from "ethers"; import { CHAIN_ID_MAINNET, CHAIN_ID_TAIKO, @@ -18,10 +18,12 @@ const mockContract = { getMessageStatus: jest.fn(), processMessage: jest.fn(), retryMessage: jest.fn(), + releaseEther: jest.fn(), }; const mockProver = { GenerateProof: jest.fn(), + GenerateReleaseProof: jest.fn(), }; jest.mock("ethers", () => ({ @@ -36,6 +38,9 @@ jest.mock("ethers", () => ({ Contract: function () { return mockContract; }, + providers: { + JsonRpcProvider: jest.fn() + } })); describe("bridge tests", () => { @@ -250,4 +255,76 @@ describe("bridge tests", () => { expect(mockContract.retryMessage).toHaveBeenCalled(); }); + + it("release tokens throws if message is already in DONE status", async () => { + mockContract.getMessageStatus.mockImplementationOnce(() => { + return MessageStatus.Done; + }); + + mockSigner.getAddress.mockImplementationOnce(() => { + return "0x"; + }); + + const wallet = new Wallet("0x"); + + const bridge: Bridge = new ETHBridge(mockProver); + + expect(mockContract.releaseEther).not.toHaveBeenCalled(); + + expect(mockProver.GenerateReleaseProof).not.toHaveBeenCalled(); + + await expect(bridge.ReleaseTokens({ + message: { + owner: "0x", + srcChainId: BigNumber.from(CHAIN_ID_TAIKO), + destChainId: BigNumber.from(CHAIN_ID_MAINNET), + sender: "0x01", + gasLimit: BigNumber.from(1), + } as unknown as Message, + msgHash: "0x", + srcBridgeAddress: "0x", + destBridgeAddress: "0x", + signer: wallet, + destProvider: new ethers.providers.JsonRpcProvider(), + srcTokenVaultAddress: "0x" + })).rejects.toThrowError("message already processed"); + }); + + it("release tokens", async () => { + mockContract.getMessageStatus.mockImplementationOnce(() => { + return MessageStatus.Failed; + }); + + mockSigner.getAddress.mockImplementationOnce(() => { + return "0x"; + }); + + const wallet = new Wallet("0x"); + + const bridge: Bridge = new ETHBridge(mockProver); + + expect(mockContract.releaseEther).not.toHaveBeenCalled(); + + expect(mockProver.GenerateReleaseProof).not.toHaveBeenCalled(); + + await bridge.ReleaseTokens({ + message: { + owner: "0x", + srcChainId: BigNumber.from(CHAIN_ID_TAIKO), + destChainId: BigNumber.from(CHAIN_ID_MAINNET), + sender: "0x01", + gasLimit: BigNumber.from(1), + } as unknown as Message, + msgHash: "0x", + srcBridgeAddress: "0x", + destBridgeAddress: "0x", + signer: wallet, + destProvider: new ethers.providers.JsonRpcProvider(), + srcTokenVaultAddress: "0x" + }); + + expect(mockProver.GenerateReleaseProof).toHaveBeenCalled(); + + expect(mockContract.releaseEther).toHaveBeenCalled(); + }); }); diff --git a/packages/bridge-ui/src/eth/bridge.ts b/packages/bridge-ui/src/eth/bridge.ts index 3114f92c4b8..e8f3152e8e9 100644 --- a/packages/bridge-ui/src/eth/bridge.ts +++ b/packages/bridge-ui/src/eth/bridge.ts @@ -5,6 +5,7 @@ import type { Bridge as BridgeInterface, BridgeOpts, ClaimOpts, + ReleaseOpts, } from "../domain/bridge"; import TokenVault from "../constants/abi/TokenVault"; import type { Prover } from "../domain/proof"; @@ -138,7 +139,53 @@ class ETHBridge implements BridgeInterface { return await contract.processMessage(opts.message, proof); } else { - return await contract.retryMessage(opts.message); + return await contract.retryMessage(opts.message, true); + } + } + + async ReleaseTokens(opts: ReleaseOpts): Promise { + const destBridgeContract: Contract = new Contract( + opts.destBridgeAddress, + Bridge, + opts.destProvider + ); + + const messageStatus: MessageStatus = await destBridgeContract.getMessageStatus( + opts.msgHash + ); + + if (messageStatus === MessageStatus.Done) { + throw Error("message already processed"); + } + + const signerAddress = await opts.signer.getAddress(); + + if (opts.message.owner.toLowerCase() !== signerAddress.toLowerCase()) { + throw Error("user can not release these tokens, it is not their message"); + } + + if (messageStatus === MessageStatus.Failed) { + const proofOpts = { + srcChain: opts.message.srcChainId.toNumber(), + msgHash: opts.msgHash, + sender: opts.srcBridgeAddress, + destBridgeAddress: opts.destBridgeAddress, + destChain: opts.message.destChainId.toNumber(), + destHeaderSyncAddress: + chains[opts.message.destChainId.toNumber()].headerSyncAddress, + srcHeaderSyncAddress: + chains[opts.message.srcChainId.toNumber()].headerSyncAddress, + }; + + const proof = await this.prover.GenerateReleaseProof(proofOpts); + + const srcBridgeContract: Contract = new Contract( + opts.srcBridgeAddress, + Bridge, + opts.signer + ); + + return await srcBridgeContract.releaseEther(opts.message, proof); } } } diff --git a/packages/bridge-ui/src/proof/service.spec.ts b/packages/bridge-ui/src/proof/service.spec.ts index 0686380ee6e..f9003a2f7f7 100644 --- a/packages/bridge-ui/src/proof/service.spec.ts +++ b/packages/bridge-ui/src/proof/service.spec.ts @@ -73,6 +73,36 @@ const invalidStorageProof: EthGetProofResponse = { ], }; +const storageProof2: EthGetProofResponse = { + balance: "", + nonce: "", + codeHash: "", + storageHash: "", + accountProof: [], + storageProof: [ + { + key: "0x01", + value: "0x3", + proof: [ethers.constants.HashZero], + }, + ], +}; + +const invalidStorageProof2: EthGetProofResponse = { + balance: "", + nonce: "", + codeHash: "", + storageHash: "", + accountProof: [], + storageProof: [ + { + key: "0x01", + value: "0x0", + proof: [ethers.constants.HashZero], + }, + ], +}; + const expectedProof = "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000360a7881266ca0a344c43cb24175d9dbd243b58d45d6ae6ad71310a273a3d1d3afb1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347000000000000000000000000ea674fdde714fd979de3edf0f56aa9716b898ec8c0dcf937b3f6136dd70a1ad11cc57b040fd410f3c49a5146f20c732895a3cc217273ade6b6ed865a9975ac281da23b90b141a8b607d874d2cd95e65e81336f8e74bb61e381e9238a08b169580f3cbf9b8b79d7d5ee708d3e286103eb291dfd0800000000000400000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000020000000000000000000000000000000000000000000000000100000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000007b000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000002e0f5ba25df1e92e89a09e0b32063b81795f631100801158f5fa733f2ba26843bd0000000000000000000000000000000000000000000000000000000000000007b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001265746865726d696e652d75732d7765737431000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000022e1a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; @@ -176,3 +206,94 @@ describe("prover tests", () => { expect(proof).toBe(expectedProofWithBaseFee); }); }); + +describe("generate release proof tests", () => { + beforeEach(() => { + jest.resetAllMocks(); + block.baseFeePerGas = "0"; + }); + + it("throws on invalid proof", async () => { + mockProvider.send.mockImplementation( + (method: string, params: unknown[]) => { + if (method === "eth_getBlockByHash") { + return block; + } + + if (method === "eth_getProof") { + return invalidStorageProof2; + } + } + ); + + const prover: ProofService = new ProofService(map); + + await expect( + prover.GenerateReleaseProof({ + msgHash: ethers.constants.HashZero, + sender: ethers.constants.AddressZero, + destBridgeAddress: ethers.constants.AddressZero, + srcChain: srcChain, + destChain: destChain, + destHeaderSyncAddress: ethers.constants.AddressZero, + srcHeaderSyncAddress: ethers.constants.AddressZero, + }) + ).rejects.toThrowError("invalid proof"); + }); + + it("generates proof", async () => { + mockProvider.send.mockImplementation( + (method: string, params: unknown[]) => { + if (method === "eth_getBlockByHash") { + return block; + } + + if (method === "eth_getProof") { + return storageProof2; + } + } + ); + + const prover: ProofService = new ProofService(map); + + const proof = await prover.GenerateReleaseProof({ + msgHash: ethers.constants.HashZero, + sender: ethers.constants.AddressZero, + destBridgeAddress: ethers.constants.AddressZero, + srcChain: srcChain, + destChain: destChain, + destHeaderSyncAddress: ethers.constants.AddressZero, + srcHeaderSyncAddress: ethers.constants.AddressZero, + }); + expect(proof).toBe(expectedProof); + }); + + it("generates proof with baseFeePerGas set", async () => { + mockProvider.send.mockImplementation( + (method: string, params: unknown[]) => { + if (method === "eth_getBlockByHash") { + return block; + } + + if (method === "eth_getProof") { + return storageProof2; + } + } + ); + + block.baseFeePerGas = "1"; + + const prover: ProofService = new ProofService(map); + + const proof = await prover.GenerateReleaseProof({ + msgHash: ethers.constants.HashZero, + sender: ethers.constants.AddressZero, + destBridgeAddress: ethers.constants.AddressZero, + srcChain: srcChain, + destChain: destChain, + destHeaderSyncAddress: ethers.constants.AddressZero, + srcHeaderSyncAddress: ethers.constants.AddressZero, + }); + expect(proof).toBe(expectedProofWithBaseFee); + }); +}); diff --git a/packages/bridge-ui/src/proof/service.ts b/packages/bridge-ui/src/proof/service.ts index 2280f234a0e..cf635bf8f1f 100644 --- a/packages/bridge-ui/src/proof/service.ts +++ b/packages/bridge-ui/src/proof/service.ts @@ -1,11 +1,13 @@ import { Contract, ethers } from "ethers"; import { RLP } from "ethers/lib/utils.js"; +import Bridge from "../constants/abi/Bridge"; import HeaderSync from "../constants/abi/HeaderSync"; import type { Block, BlockHeader } from "../domain/block"; import type { Prover, GenerateProofOpts, EthGetProofResponse, + GenerateReleaseProofOpts, } from "../domain/proof"; class ProofService implements Prover { @@ -15,7 +17,7 @@ class ProofService implements Prover { this.providerMap = providerMap; } - async GenerateProof(opts: GenerateProofOpts): Promise { + private static getKey(opts: GenerateProofOpts | GenerateReleaseProofOpts) { const key = ethers.utils.keccak256( ethers.utils.solidityPack( ["address", "bytes32"], @@ -23,14 +25,10 @@ class ProofService implements Prover { ) ); - const provider = this.providerMap.get(opts.srcChain); - - const contract = new Contract( - opts.destHeaderSyncAddress, - HeaderSync, - this.providerMap.get(opts.destChain) - ); + return key; + } + private static async getBlockAndBlockHeader(contract: ethers.Contract, provider: ethers.providers.JsonRpcProvider): Promise<{block: Block, blockHeader: BlockHeader}> { const latestSyncedHeader = await contract.getLatestSyncedHeader(); const block: Block = await provider.send("eth_getBlockByHash", [ @@ -59,17 +57,10 @@ class ProofService implements Prover { baseFeePerGas: block.baseFeePerGas ? parseInt(block.baseFeePerGas) : 0, }; - // rpc call to get the merkle proof what value is at key on the bridge contract - const proof: EthGetProofResponse = await provider.send("eth_getProof", [ - opts.srcSignalServiceAddress, - [key], - block.hash, - ]); - - if (proof.storageProof[0].value !== "0x1") { - throw Error("invalid proof"); - } + return { block, blockHeader }; + } + private static getSignalProof(proof: EthGetProofResponse, blockHeader: BlockHeader) { // RLP encode the proof together for LibTrieProof to decode const encodedProof = ethers.utils.defaultAbiCoder.encode( ["bytes", "bytes"], @@ -86,6 +77,60 @@ class ProofService implements Prover { return signalProof; } + + async GenerateProof(opts: GenerateProofOpts): Promise { + const key = ProofService.getKey(opts); + + const provider = this.providerMap.get(opts.srcChain); + + const contract = new Contract( + opts.destHeaderSyncAddress, + HeaderSync, + this.providerMap.get(opts.destChain) + ); + + const { block, blockHeader } = await ProofService.getBlockAndBlockHeader(contract, provider) + + // rpc call to get the merkle proof what value is at key on the SignalService contract + const proof: EthGetProofResponse = await provider.send("eth_getProof", [ + opts.srcSignalServiceAddress, + [key], + block.hash, + ]); + + if (proof.storageProof[0].value !== "0x1") { + throw Error("invalid proof"); + } + + return ProofService.getSignalProof(proof, blockHeader); + } + + async GenerateReleaseProof(opts: GenerateReleaseProofOpts): Promise { + const key = ProofService.getKey(opts); + + const provider = this.providerMap.get(opts.destChain); + + const contract = new Contract( + opts.srcHeaderSyncAddress, + HeaderSync, + this.providerMap.get(opts.srcChain) + ); + + const { block, blockHeader } = await ProofService.getBlockAndBlockHeader(contract, provider) + + // rpc call to get the merkle proof what value is at key on the SignalService contract + const proof: EthGetProofResponse = await provider.send("eth_getProof", [ + opts.destBridgeAddress, + [key], + block.hash, + ]); + + if (proof.storageProof[0].value !== "0x3") { + throw Error("invalid proof"); + } + + return ProofService.getSignalProof(proof, blockHeader); + } } export { ProofService };