diff --git a/web/packages/api/src/index.ts b/web/packages/api/src/index.ts index 96e56bc3f..af8e280e4 100644 --- a/web/packages/api/src/index.ts +++ b/web/packages/api/src/index.ts @@ -18,6 +18,8 @@ import { SnowbridgeL1Adaptor__factory, SnowbridgeL2Adaptor, SnowbridgeL2Adaptor__factory, + Gateway, + Gateway__factory, } from "@snowbridge/contract-types" import { Environment } from "@snowbridge/base-types" @@ -53,6 +55,7 @@ export class Context { #ethChains: EthereumChains #gateway?: IGatewayV1 #gatewayV2?: IGatewayV2 + #gatewayProxy?: Gateway #beefyClient?: BeefyClient #l1Adapter?: SnowbridgeL1Adaptor #l1SwapQuoter?: ISwapQuoter @@ -280,6 +283,17 @@ export class Context { return this.#gatewayV2 } + gatewayProxy(): Gateway { + if (this.#gatewayProxy) { + return this.#gatewayProxy + } + this.#gatewayProxy = Gateway__factory.connect( + this.environment.gatewayContract, + this.ethereum(), + ) + return this.#gatewayProxy + } + beefyClient(): BeefyClient { if (this.#beefyClient) { return this.#beefyClient diff --git a/web/packages/api/src/toEthereumSnowbridgeV2.ts b/web/packages/api/src/toEthereumSnowbridgeV2.ts index c8f54d32b..90a2f6ada 100644 --- a/web/packages/api/src/toEthereumSnowbridgeV2.ts +++ b/web/packages/api/src/toEthereumSnowbridgeV2.ts @@ -35,10 +35,18 @@ import { paraImplementation } from "./parachains" import { Context } from "./index" import { ETHER_TOKEN_ADDRESS, findL2TokenAddress } from "./assets_v2" import { getOperatingStatus } from "./status" -import { AbstractProvider, ethers, Wallet, TransactionReceipt } from "ethers" +import { + AbstractProvider, + ethers, + Wallet, + TransactionReceipt, + AbiCoder, + ContractTransaction, +} from "ethers" import { CreateAgent } from "./registration/agent/createAgent" import { estimateFees } from "./across/api" import { AgentCreation } from "./registration/agent/agentInterface" +import { CommandStruct } from "@snowbridge/contract-types/dist/Gateway" export { ValidationKind, signAndSend } from "./toEthereum_v2" @@ -659,6 +667,8 @@ export const validateTransferFromAssetHub = async ( let sourceDryRunError let assetHubDryRunError let bridgeHubDryRunError + let ethereumDryRunError + let estimatedDryRunGas: bigint | undefined // do the dry run, get the forwarded xcm and dry run that const dryRunResultAssetHub = await dryRunOnSourceParachain( sourceParachain, @@ -673,7 +683,64 @@ export const validateTransferFromAssetHub = async ( registry.assetHubParaId, dryRunResultAssetHub.bridgeHubForwarded[1][0], ) - if (!dryRunResultBridgeHub.success) { + if (dryRunResultBridgeHub.success) { + try { + const ethereumTx = await buildEthereumDryRunCall( + context, + registry.assetHubParaId, + sourceAccountHex, + transfer, + ) + estimatedDryRunGas = await ethereum.estimateGas(ethereumTx) + const forkedProvider = new ethers.JsonRpcProvider( + process.env.FORKED_PROVIDER_URL || + process.env.NEXT_PUBLIC_FORKED_PROVIDER_URL || + "https://virtual.mainnet.eu.rpc.tenderly.co/1d4ab8c5-01fe-45a7-8583-8b4925e5a435", + ) + const txHash = await forkedProvider.send("eth_sendTransaction", [ + { + from: ethereumTx.from, + to: ethereumTx.to, + data: ethereumTx.data, + value: ethereumTx.value, + gas: "0x" + (estimatedDryRunGas! * 2n).toString(16), // multiply by 2 to be safe + }, + ]) + + console.log("Tx hash:", txHash) + + const receipt = await forkedProvider.waitForTransaction(txHash) + console.log("Logs:", receipt?.logs) + const parsedLogs = receipt?.logs + .map((log) => { + try { + return context.gatewayProxy().interface.parseLog(log) + } catch (e) { + return null + } + }) + .filter((log) => log !== null) + const errorLogs = parsedLogs?.filter((log) => { + return log.name === "CommandFailed" + }) + if (errorLogs && errorLogs.length > 0) { + ethereumDryRunError = + "Dry run failed on Ethereum at command index: " + errorLogs[0].args.index + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.DryRunFailed, + message: ethereumDryRunError, + }) + } + } catch (e) { + ethereumDryRunError = "Could not estimate gas on Ethereum." + (e as Error).message + logs.push({ + kind: ValidationKind.Error, + reason: ValidationReason.FeeEstimationError, + message: ethereumDryRunError, + }) + } + } else { logs.push({ kind: ValidationKind.Error, reason: ValidationReason.DryRunFailed, @@ -733,6 +800,7 @@ export const validateTransferFromAssetHub = async ( sourceDryRunError, assetHubDryRunError, bridgeHubDryRunError, + ethereumDryRunError, }, transfer, } @@ -1108,3 +1176,70 @@ export async function sourceAgentAddress( let agentAddress = await gateway.agentOf(agentID) return agentAddress } + +export async function buildEthereumDryRunCall( + context: Context, + parachainId: number, + sourceAccountHex: string, + transfer: Transfer, +): Promise { + let commands: CommandStruct[] = [] + const agentID = await sourceAgentId(context, parachainId, sourceAccountHex) + if (transfer.computed.sourceAssetMetadata.foreignId) { + // PNA + const mintForeignParams = AbiCoder.defaultAbiCoder().encode( + ["bytes32", "address", "uint128"], + [ + transfer.computed.sourceAssetMetadata.foreignId, + transfer.input.beneficiaryAccount, + transfer.input.amount, + ], + ) + const mintCommand: CommandStruct = { + kind: 4, + gas: transfer.computed.tokenErcMetadata.deliveryGas || 200_000n, + payload: mintForeignParams, + } + commands.push(mintCommand) + } else { + // ENA + const unlockNativeParams = AbiCoder.defaultAbiCoder().encode( + ["bytes32", "address", "address", "uint128"], + [ + agentID, + transfer.input.tokenAddress, + transfer.input.beneficiaryAccount, + transfer.input.amount, + ], + ) + const unlockCommand: CommandStruct = { + kind: 2, + gas: transfer.computed.tokenErcMetadata.deliveryGas || 200_000n, + payload: unlockNativeParams, + } + commands.push(unlockCommand) + } + + if (transfer.input.contractCall) { + let callInfo = transfer.input.contractCall + // 2. Transact + const transactParams = AbiCoder.defaultAbiCoder().encode( + ["address", "bytes", "uint256"], + [callInfo.target, callInfo.calldata, callInfo.value || 0n], + ) + const transactCommand: CommandStruct = { + kind: 5, + gas: callInfo.gas, + payload: transactParams, + } + commands.push(transactCommand) + } + let ethereumTx: ContractTransaction = await context + .gatewayProxy() + .getFunction("v2_dispatch") + // nonce is irrelevant in the dry run, can be set to 0 + .populateTransaction(commands, agentID, 0n, { + from: context.environment.gatewayContract, + }) + return ethereumTx +} diff --git a/web/packages/api/src/toEthereum_v2.ts b/web/packages/api/src/toEthereum_v2.ts index d0416154b..8fb407b55 100644 --- a/web/packages/api/src/toEthereum_v2.ts +++ b/web/packages/api/src/toEthereum_v2.ts @@ -453,6 +453,7 @@ export type ValidationResult = { sourceDryRunError: any assetHubDryRunError: any bridgeHubDryRunError?: any + ethereumDryRunError?: any } transfer: Transfer } diff --git a/web/packages/contract-types/package.json b/web/packages/contract-types/package.json index 31ea6eebb..109f34548 100644 --- a/web/packages/contract-types/package.json +++ b/web/packages/contract-types/package.json @@ -12,7 +12,7 @@ "types": "dist/index.d.ts", "scripts": { "build": "rm -rf src && rm -rf dist && cd ../../../contracts && forge build && cd ../web/packages/contract-types && pnpm typechain && tsc --build --force", - "typechain": "typechain --target ethers-v6 '../../../contracts/out/?(IERC20.sol|IERC20Metadata.sol|IGateway.sol|BeefyClient.sol|WETH9.sol|SnowbridgeL1Adaptor.sol|SnowbridgeL2Adaptor.sol|ISwapQuoter.sol|ISwapRouter.sol|ISwapLegacyRouter.sol)/!(*.abi).json' --out-dir src" + "typechain": "typechain --target ethers-v6 '../../../contracts/out/?(IERC20.sol|IERC20Metadata.sol|IGateway.sol|Gateway.sol|BeefyClient.sol|WETH9.sol|SnowbridgeL1Adaptor.sol|SnowbridgeL2Adaptor.sol|ISwapQuoter.sol|ISwapRouter.sol|ISwapLegacyRouter.sol)/!(*.abi).json' --out-dir src" }, "devDependencies": { "@typechain/ethers-v6": "0.5.1",