diff --git a/rollup-bridge-contracts/contracts/bridge/l2/L2ETHBridgeVault.sol b/rollup-bridge-contracts/contracts/bridge/l2/L2ETHBridgeVault.sol index 9e54cb754..d60ab6589 100644 --- a/rollup-bridge-contracts/contracts/bridge/l2/L2ETHBridgeVault.sol +++ b/rollup-bridge-contracts/contracts/bridge/l2/L2ETHBridgeVault.sol @@ -192,10 +192,6 @@ contract L2ETHBridgeVault is ethAmountTracker = ethAmountTracker + depositAmount; - /// @notice Encoding the context to process the loan after the price is fetched - /// @dev The context contains the borrower’s details, loan amount, borrow token, and collateral token. - // bytes memory ethTransferCallbackContext = abi.encode("0x"); - /// @notice Send a request to the token contract to get token minted. /// @dev This request is processed with a fee for the transaction, allowing the system to fetch the token price. sendRequest(depositRecipient, depositAmount, Nil.ASYNC_REQUEST_MIN_GAS, "", "", handleETHTransferResponse); diff --git a/rollup-bridge-contracts/hardhat.config.ts b/rollup-bridge-contracts/hardhat.config.ts index f23f7c2de..30b3e69f4 100644 --- a/rollup-bridge-contracts/hardhat.config.ts +++ b/rollup-bridge-contracts/hardhat.config.ts @@ -10,7 +10,7 @@ import * as fs from "fs"; dotenv.config(); function getRemappings() { - const remappingsTxt = fs.readFileSync("remappings.txt", "utf8"); + const remappingsTxt = fs.readFileSync(resolve(__dirname, "remappings.txt"), "utf8"); return remappingsTxt .split("\n") .filter((line) => line.trim() !== "") @@ -97,7 +97,7 @@ const config: HardhatUserConfig = { }, nil: { url: process.env.NIL_RPC_ENDPOINT, - accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], + accounts: process.env.NIL_PRIVATE_KEY ? [process.env.NIL_PRIVATE_KEY] : [], }, }, namedAccounts: { diff --git a/rollup-bridge-contracts/task/nil-smart-account.ts b/rollup-bridge-contracts/task/nil-smart-account.ts index cd3ae56ef..f0d622db9 100644 --- a/rollup-bridge-contracts/task/nil-smart-account.ts +++ b/rollup-bridge-contracts/task/nil-smart-account.ts @@ -7,6 +7,7 @@ import { LocalECDSAKeySigner, PublicClient, SmartAccountV1, + Hex, waitTillCompleted, } from "@nilfoundation/niljs"; import "dotenv/config"; @@ -169,3 +170,113 @@ export async function generateNilSmartAccount(networkName: string): Promise<[Sma return [smartAccount, depositRecipientSmartAccount]; } + +export async function prepareNilSmartAccountsForUnitTest(): Promise<{ + ownerSmartAccount: SmartAccountV1, + depositRecipientSmartAccount: SmartAccountV1, + feeRefundSmartAccount: SmartAccountV1 +}> { + const rpcEndpoint = process.env.NIL_RPC_ENDPOINT as string; + const faucetEndpoint = process.env.FAUCET_ENDPOINT as string; + + const faucetClient = new FaucetClient({ + transport: new HttpTransport({ endpoint: faucetEndpoint }), + }); + + const client = new PublicClient({ + transport: new HttpTransport({ endpoint: rpcEndpoint }), + }); + // Generate a new ECDSA key pair + const owner_wallet = ethers.Wallet.createRandom(); + let owner_privateKey = owner_wallet.privateKey; + console.log(`owner_privateKey is: ${owner_privateKey}`); + const owner_wallet_address = owner_wallet.address; // This is the public address + + console.log("Generated private key:", owner_privateKey); + console.log("Generated address:", owner_wallet_address); + console.log(`preparing owner nil-smart-account`); + + let ownerSmartAccount: SmartAccountV1 | null = null; + + const owner_signer = new LocalECDSAKeySigner({ privateKey: owner_privateKey as `0x${string}` }); + + try { + ownerSmartAccount = new SmartAccountV1({ + signer: owner_signer, + client: client, + salt: BigInt(Math.floor(Math.random() * 10000)), + shardId: 1, + pubkey: owner_signer.getPublicKey(), + }); + + await topupSmartAccount(faucetClient, client, ownerSmartAccount.address); + console.log("πŸ†• owner Smart Account Generated:", ownerSmartAccount.address); + + if ((await ownerSmartAccount.checkDeploymentStatus()) === false) { + await ownerSmartAccount.selfDeploy(true); + } + + console.log("πŸ†• owner Smart Account Generated:", ownerSmartAccount.address); + } catch (err) { + console.error(`failed to self-deploy owner-smart-account: ${err}`); + return; + } + + const deposit_recipient_wallet = ethers.Wallet.createRandom(); + const deposit_recipient_privateKey = deposit_recipient_wallet.privateKey; // This is a 0x... string + const deposit_recipient_wallet_address = deposit_recipient_wallet.address; // This is the public address + + let deposit_recipient_signer = new LocalECDSAKeySigner({ privateKey: deposit_recipient_privateKey as Hex }); + const depositRecipientSmartAccount = new SmartAccountV1({ + signer: deposit_recipient_signer, + client, + salt: BigInt(Math.floor(Math.random() * 10000)), + shardId: 1, + pubkey: deposit_recipient_signer.getPublicKey(), + }); + const depositRecipientSmartAccountAddress = depositRecipientSmartAccount.address; + + await topupSmartAccount(faucetClient, client, depositRecipientSmartAccountAddress); + + if ((await depositRecipientSmartAccount.checkDeploymentStatus()) === false) { + await depositRecipientSmartAccount.selfDeploy(true); + } + + console.log("πŸ†• depositRecipient Smart Account Generated:", depositRecipientSmartAccountAddress); + + const nil_refund_wallet = ethers.Wallet.createRandom(); + const nil_refund_privateKey = nil_refund_wallet.privateKey; // This is a 0x... string + + const nil_refund_signer = new LocalECDSAKeySigner({ privateKey: nil_refund_privateKey as Hex }); + const feeRefundSmartAccount = new SmartAccountV1({ + signer: nil_refund_signer, + client, + salt: BigInt(Math.floor(Math.random() * 10000)), + shardId: 1, + pubkey: nil_refund_signer.getPublicKey(), + }); + const feeRefundSmartAccountAddress = feeRefundSmartAccount.address; + await topupSmartAccount(faucetClient, client, feeRefundSmartAccountAddress); + + if ((await feeRefundSmartAccount.checkDeploymentStatus()) === false) { + await feeRefundSmartAccount.selfDeploy(true); + } + + console.log("πŸ†• feeRefund Smart Account Generated:", feeRefundSmartAccountAddress); + + return { + ownerSmartAccount, + depositRecipientSmartAccount, + feeRefundSmartAccount + }; +} + +export async function topupSmartAccount(faucetClient: FaucetClient, client: PublicClient, smartAccountAddress: String) { + const topUpFaucet = await faucetClient.topUp({ + smartAccountAddress: smartAccountAddress as Hex, + amount: convertEthToWei(0.1), + faucetAddress: process.env.NIL as `0x${string}`, + }); + await waitTillCompleted(client, topUpFaucet); +} + diff --git a/rollup-bridge-contracts/test/hardhat/bridge_eth_test.ts b/rollup-bridge-contracts/test/hardhat/bridge_eth_test.ts new file mode 100644 index 000000000..599805423 --- /dev/null +++ b/rollup-bridge-contracts/test/hardhat/bridge_eth_test.ts @@ -0,0 +1,87 @@ +import { expect } from "chai"; +import "@nomicfoundation/hardhat-ethers"; +import { + ProcessedReceipt, +} from "@nilfoundation/niljs"; +import "dotenv/config"; +import type { Abi } from "abitype"; +import { encodeFunctionData } from "viem"; +import { generateL2RelayMessage } from "./generate-l2-relay-message"; +import { bigIntReplacer } from "../../deploy/config/config-helper"; +import { L2BridgeTestFixtureResult, setupL2BridgeTestFixture } from "./l2-bridge-test-fixture"; + +const l1EthBridgeAddress = '0x0001e0d8f4De4E838a66963f406Fa826cCaCA322'; + +describe("L2BridgeMessenger Contract", () => { + it("Should accept the (ETHDeposit) message relayed by relayer", async () => { + + const l2BridgeTestFixture: L2BridgeTestFixtureResult = await setupL2BridgeTestFixture(); + + // Relay DepositMessage to L2 + const depositorAddressValue = "0xc8d5559BA22d11B0845215a781ff4bF3CCa0EF89"; + const depositAmountValue = "1000000000000"; + const l2DepositRecipientValue = l2BridgeTestFixture.depositRecipientSmartAccount.address; + const l2FeeRefundAddressValue = l2BridgeTestFixture.feeRefundSmartAccount.address; + + try { + const messageSender = l1EthBridgeAddress; + const messageTarget = l2BridgeTestFixture.l2ETHBridgeProxyAddress; + const messageType = "1"; + const messageCreatedAt = Math.floor(Date.now() / 1000); + const messageExpiryTime = Math.floor(Date.now() / 1000) + 10000; + const ethDepositRelayMessage = generateL2RelayMessage(depositorAddressValue, depositAmountValue, l2DepositRecipientValue, l2FeeRefundAddressValue); + const nilGasLimit = "1000000"; + const maxFeePerGas = "27500000"; + const maxPriorityFeePerGas = "1250000"; + const feeCredit = "27500000000000"; + const messageNonce = 0; + + // Dynamically load artifacts + const L2BridgeMessengerJson = await import("../../artifacts/contracts/bridge/l2/L2BridgeMessenger.sol/L2BridgeMessenger.json"); + + if (!L2BridgeMessengerJson || !L2BridgeMessengerJson.default || !L2BridgeMessengerJson.default.abi || !L2BridgeMessengerJson.default.bytecode) { + throw Error(`Invalid L2BridgeMessengerJson ABI`); + } + + const relayMessage = encodeFunctionData({ + abi: L2BridgeMessengerJson.default.abi as Abi, + functionName: "relayMessage", + args: [messageSender, messageTarget, messageType, messageNonce, ethDepositRelayMessage, messageExpiryTime], + }); + + console.log(`generated message to relay on L2BridgeMessenger: ${relayMessage}`); + + const relayEthDepositMessageResponse = await l2BridgeTestFixture.ownerSmartAccount.sendTransaction({ + to: l2BridgeTestFixture.l2BridgeMessengerProxyAddress as `0x${string}`, + data: relayMessage, + feeCredit: BigInt(feeCredit), + maxFeePerGas: BigInt(maxFeePerGas), + maxPriorityFeePerGas: BigInt(maxPriorityFeePerGas) + }); + + console.log(`relayMessage transaction was done and awaiting for ProcessedReceipt`); + + const relayEthDepositMessageTxnReceipts: ProcessedReceipt[] = await relayEthDepositMessageResponse.wait(); + + const relayedEthDepositMessageTxnReceipt: ProcessedReceipt = relayEthDepositMessageTxnReceipts[0] as ProcessedReceipt; + + const outputReceipts: ProcessedReceipt[] = relayedEthDepositMessageTxnReceipt.outputReceipts as ProcessedReceipt[]; + + console.log(`outputReceipt is: ${JSON.stringify(outputReceipts, bigIntReplacer, 2)}`) + + const outputReceipt: ProcessedReceipt = outputReceipts[0] as ProcessedReceipt; + + console.log(`outputReceipt extracted as: ${JSON.stringify(outputReceipt, bigIntReplacer, 2)}`); + + // check the first element in the ProcessedReceipt and verify if it is successful + if (!outputReceipt.success) { + console.error(`Failed to relay message + on the L2BridgeMessenger contract: ${l2BridgeTestFixture.l2BridgeMessengerProxyAddress}`); + } else { + console.log(`successfully relayed EthDepositMessage on to L2BridgeMessenger with transactionReceipt: ${JSON.stringify(relayEthDepositMessageTxnReceipts[0], bigIntReplacer, 2)}`); + } + } catch (err) { + console.error(`Failed to relay ethBridge message on to L2BridgeMessenger: ${JSON.stringify(err)}`); + } + }); +}); diff --git a/rollup-bridge-contracts/test/hardhat/generate-l2-relay-message.ts b/rollup-bridge-contracts/test/hardhat/generate-l2-relay-message.ts new file mode 100644 index 000000000..5b65a1920 --- /dev/null +++ b/rollup-bridge-contracts/test/hardhat/generate-l2-relay-message.ts @@ -0,0 +1,29 @@ +import { ethers } from "ethers"; + +// npx ts-node test/hardhat/generate-l2-relay-message.ts +export const generateL2RelayMessage = (depositorAddress: String, depositAmount: String, l2DepositRecipient: String, l2FeeRefundAddress: String): String => { + const abi = [ + "function finaliseETHDeposit(address depositorAddress, uint256 depositAmount, address l2DepositRecipient, address l2FeeRefundAddress)" + ]; + + const iface = new ethers.Interface(abi); + + console.log(`about to generate depositMessage Data`); + + const depositMessage = iface.encodeFunctionData( + "finaliseETHDeposit", + [depositorAddress, depositAmount, l2DepositRecipient, l2FeeRefundAddress] + ); + + console.log(depositMessage); + + return depositMessage; +} + + +// const depositorAddressValue = "0xc8d5559BA22d11B0845215a781ff4bF3CCa0EF89"; +// const depositAmountValue = "1000000000000"; +// const l2DepositRecipientValue = "0x0001D3A5b915Bc99542a9430423cDe75Bd7F7aC7"; +// const l2FeeRefundAddressValue = "0x000131b12EBeb7A34e1a47d402137df6De7b08Ae"; + +// generateL2RelayMessage(depositorAddressValue, depositAmountValue, l2DepositRecipientValue, l2FeeRefundAddressValue); \ No newline at end of file diff --git a/rollup-bridge-contracts/test/hardhat/l2-bridge-test-fixture.ts b/rollup-bridge-contracts/test/hardhat/l2-bridge-test-fixture.ts new file mode 100644 index 000000000..6b5df2379 --- /dev/null +++ b/rollup-bridge-contracts/test/hardhat/l2-bridge-test-fixture.ts @@ -0,0 +1,547 @@ +import { + convertEthToWei, + FaucetClient, + HttpTransport, + ProcessedReceipt, + PublicClient, + SmartAccountV1, + waitTillCompleted, + getContract +} from "@nilfoundation/niljs"; +import type { Abi } from "abitype"; +import { getCheckSummedAddress } from "../../scripts/utils/validate-config"; +import { encodeFunctionData } from "viem"; +import { prepareNilSmartAccountsForUnitTest } from "../../task/nil-smart-account"; + +export interface L2BridgeTestFixtureResult { + ownerSmartAccount: SmartAccountV1; + depositRecipientSmartAccount: SmartAccountV1; + feeRefundSmartAccount: SmartAccountV1; + l2EthBridgeVaultProxyAddress: string; + l2BridgeMessengerProxyAddress: string; + l2ETHBridgeProxyAddress: string; + l2EnshrinedTokenBridgeProxyAddress: string; + l2BridgeMessengerProxyInstance: any; + l2ETHBridgeVaultProxyInstance: any; + l2ETHBridgeProxyInstance: any; +} + +export async function setupL2BridgeTestFixture(): Promise { + let ownerSmartAccount: SmartAccountV1 | null = null; + let depositRecipientSmartAccount: SmartAccountV1 | null = null; + let feeRefundSmartAccount: SmartAccountV1 | null = null; + + try { + const result = await prepareNilSmartAccountsForUnitTest(); + ownerSmartAccount = result.ownerSmartAccount; + depositRecipientSmartAccount = result.depositRecipientSmartAccount; + feeRefundSmartAccount = result.feeRefundSmartAccount; + } catch (err) { + console.error(`Failed to load NilSmartAccount - 1st catch: ${JSON.stringify(err)}`); + return; + } + + if (!ownerSmartAccount || !depositRecipientSmartAccount || !feeRefundSmartAccount) { + console.error(`Failed to load all required SmartAccounts`); + // Optionally: expect.fail("Failed to load all required SmartAccounts"); + } + + console.log(`loaded smart-account successfully`); + + // // ##### Fund Deployer Wallet ##### + const rpcEndpoint = process.env.NIL_RPC_ENDPOINT as string; + + try { + const client = new PublicClient({ + transport: new HttpTransport({ endpoint: rpcEndpoint }), + }); + const faucetClient = new FaucetClient({ + transport: new HttpTransport({ endpoint: rpcEndpoint }), + }); + + const topUpFaucetTxnHash = await faucetClient.topUp({ + smartAccountAddress: ownerSmartAccount.address, + amount: convertEthToWei(100), + faucetAddress: process.env.NIL as `0x${string}`, + }); + + await waitTillCompleted(client, topUpFaucetTxnHash); + + const balance = await ownerSmartAccount.getBalance(); + + if (!(balance > BigInt(0))) { + throw Error(`Insufficient or Zero balance for smart-account: ${ownerSmartAccount.address}`); + } + } catch (err) { + console.error(`Failed to topup nil-smartAccount: ${ownerSmartAccount.address}`); + } + + // ##### NilMessageTree Deployment ##### + + // Dynamically load artifacts + const NilMessageTreeJson = await import("../../artifacts/contracts/common/NilMessageTree.sol/NilMessageTree.json"); + + if (!NilMessageTreeJson || !NilMessageTreeJson.default || !NilMessageTreeJson.default.abi || !NilMessageTreeJson.default.bytecode) { + throw Error(`Invalid NilMessageTree ABI`); + } + + const { tx: nilMessageTreeDeployTxn, address: nilMessageTreeAddress } = await ownerSmartAccount.deployContract({ + shardId: 1, + bytecode: NilMessageTreeJson.default.bytecode as `0x${string}`, + abi: NilMessageTreeJson.default.abi as Abi, + args: [ownerSmartAccount.address], + salt: BigInt(Math.floor(Math.random() * 10000)), + feeCredit: convertEthToWei(0.001), + }); + + await verifyDeploymentCompletion(nilMessageTreeDeployTxn, ownerSmartAccount.client, "NilMessageTree"); + + if (!nilMessageTreeDeployTxn.hash) { + throw Error(`Invalid transaction output from deployContract call for NilMessageTree Contract`); + } + + if (!nilMessageTreeAddress) { + throw Error(`Invalid address output from deployContract call for NilMessageTree Contract`); + } + + // ##### L2ETHBridgeVault Deployment ##### + + // Dynamically load artifacts + const L2ETHBridgeVaultJson = await import("../../artifacts/contracts/bridge/l2/L2ETHBridgeVault.sol/L2ETHBridgeVault.json"); + const TransparentUpgradeableProxy = await import("../../artifacts/contracts/common/TransparentUpgradeableProxy.sol/MyTransparentUpgradeableProxy.json"); + + if (!L2ETHBridgeVaultJson || !L2ETHBridgeVaultJson.default || !L2ETHBridgeVaultJson.default.abi || !L2ETHBridgeVaultJson.default.bytecode) { + throw Error(`Invalid L2ETHBridgeVault ABI`); + } + + const { tx: l2EthBridgeVaultImplementationDeploymentTx, address: l2EthBridgeVaultImplementationAddress } = await ownerSmartAccount.deployContract({ + shardId: 1, + bytecode: L2ETHBridgeVaultJson.default.bytecode as `0x${string} `, + abi: L2ETHBridgeVaultJson.default.abi as Abi, + args: [], + salt: BigInt(Math.floor(Math.random() * 10000)), + feeCredit: convertEthToWei(0.001) + }); + + await verifyDeploymentCompletion(l2EthBridgeVaultImplementationDeploymentTx, ownerSmartAccount.client, "L2ETHBridgeVault"); + + if (!l2EthBridgeVaultImplementationDeploymentTx || !l2EthBridgeVaultImplementationDeploymentTx.hash) { + throw Error(`Invalid transaction output from deployContract call for L2ETHBridgeVault Contract`); + } + + if (!l2EthBridgeVaultImplementationAddress) { + throw Error(`Invalid address output from deployContract call for L2ETHBridgeVault Contract`); + } + + const l2EthBridgeVaultInitData = encodeFunctionData({ + abi: L2ETHBridgeVaultJson.default.abi, + functionName: "initialize", + args: [ownerSmartAccount.address, ownerSmartAccount.address], + }); + + const { tx: l2EthBridgeVaultProxyDeploymentTx, address: l2EthBridgeVaultProxy } = await ownerSmartAccount.deployContract({ + shardId: 1, + bytecode: TransparentUpgradeableProxy.default.bytecode as `0x${string} `, + abi: TransparentUpgradeableProxy.default.abi as Abi, + args: [l2EthBridgeVaultImplementationAddress, ownerSmartAccount.address, l2EthBridgeVaultInitData], + salt: BigInt(Math.floor(Math.random() * 10000)), + feeCredit: convertEthToWei(0.001), + }); + + await verifyDeploymentCompletion(l2EthBridgeVaultProxyDeploymentTx, ownerSmartAccount.client, "L2ETHBridgeVaultProxy"); + + const faucetClient = new FaucetClient({ + transport: new HttpTransport({ endpoint: rpcEndpoint }), + }); + + const topUpFaucet = await faucetClient.topUp({ + smartAccountAddress: l2EthBridgeVaultProxy as `0x${string} `, + amount: convertEthToWei(200), + faucetAddress: process.env.NIL as `0x${string} `, + }); + + const fundL2ETHBridgeVaultTxnReceipts: ProcessedReceipt[] = await waitTillCompleted(ownerSmartAccount.client, topUpFaucet); + + // check the first element in the ProcessedReceipt and verify if it is successful + if (!fundL2ETHBridgeVaultTxnReceipts[0].success) { + throw Error(`Failed to fund L2ETHBridgeVault: ${l2EthBridgeVaultProxy} `); + } + + const balanceAfterFunding = await ownerSmartAccount.client.getBalance(l2EthBridgeVaultProxy as `0x${string} `); + + const l2EthBridgeVaultProxyAddress = getCheckSummedAddress(l2EthBridgeVaultProxy); + + // ##### L2BridgeMessenger Deployment ##### + + // Dynamically load artifacts + const L2BridgeMessengerJson = await import("../../artifacts/contracts/bridge/l2/L2BridgeMessenger.sol/L2BridgeMessenger.json"); + + if (!L2BridgeMessengerJson || !L2BridgeMessengerJson.default || !L2BridgeMessengerJson.default.abi || !L2BridgeMessengerJson.default.bytecode) { + throw Error(`Invalid L2BridgeMessengerJson ABI`); + } + + const { tx: nilMessengerImplementationDeploymentTx, address: nilMessengerImplementationAddress } = await ownerSmartAccount.deployContract({ + shardId: 1, + bytecode: L2BridgeMessengerJson.default.bytecode as `0x${string} `, + abi: L2BridgeMessengerJson.default.abi as Abi, + args: [], + salt: BigInt(Math.floor(Math.random() * 10000)), + feeCredit: convertEthToWei(0.001), + }); + + await verifyDeploymentCompletion(nilMessengerImplementationDeploymentTx, ownerSmartAccount.client, "L2BridgeMessenger"); + + + if (!nilMessengerImplementationDeploymentTx || !nilMessengerImplementationDeploymentTx.hash) { + throw Error(`Invalid transaction output from deployContract call for L2BridgeMessenger Contract`); + } + + if (!nilMessengerImplementationAddress) { + throw Error(`Invalid address output from deployContract call for L2BridgeMessenger Contract`); + } + + const l2BridgeMessengerImplementationAddress = getCheckSummedAddress(nilMessengerImplementationAddress); + + const l2BridgeMessengerInitData = encodeFunctionData({ + abi: L2BridgeMessengerJson.default.abi, + functionName: "initialize", + args: [ownerSmartAccount.address, + ownerSmartAccount.address, + ownerSmartAccount.address, + nilMessageTreeAddress, + 1000000], + }); + + const { tx: l2BridgeMessengerProxyDeploymentTx, address: l2BridgeMessengerProxy } = await ownerSmartAccount.deployContract({ + shardId: 1, + bytecode: TransparentUpgradeableProxy.default.bytecode as `0x${string} `, + abi: TransparentUpgradeableProxy.default.abi as Abi, + args: [l2BridgeMessengerImplementationAddress, ownerSmartAccount.address, l2BridgeMessengerInitData], + salt: BigInt(Math.floor(Math.random() * 10000)), + feeCredit: convertEthToWei(0.001), + }); + + await verifyDeploymentCompletion(l2BridgeMessengerProxyDeploymentTx, ownerSmartAccount.client, "L2BridgeMessengerProxy"); + + const l2BridgeMessengerProxyAddress = getCheckSummedAddress(l2BridgeMessengerProxy); + + let l2BridgeMessengerProxyInst; + + try { + // verify if the bridges are really authorised + l2BridgeMessengerProxyInst = getContract({ + client: ownerSmartAccount.client, + abi: L2BridgeMessengerJson.default.abi as Abi, + address: l2BridgeMessengerProxyAddress as `0x${string} ` + }); + + } catch (err) { + console.error(`Error caught while loading an instance of L2BridgeMessenger: ${l2BridgeMessengerProxyAddress} `); + } + + // Dynamically load artifacts + const L2ETHBridgeJson = await import("../../artifacts/contracts/bridge/l2/L2ETHBridge.sol/L2ETHBridge.json"); + + if (!L2ETHBridgeJson || !L2ETHBridgeJson.default || !L2ETHBridgeJson.default.abi || !L2ETHBridgeJson.default.bytecode) { + throw Error(`Invalid L2ETHBridge ABI`); + } + + // ##### l2ETHBridge Deployment ##### + + const { tx: l2EthBridgeImplementationDeploymentTx, address: l2EthBridgeImplementation } = await ownerSmartAccount.deployContract({ + shardId: 1, + bytecode: L2ETHBridgeJson.default.bytecode as `0x${string} `, + abi: L2ETHBridgeJson.default.abi as Abi, + args: [], + salt: BigInt(Math.floor(Math.random() * 10000)), + feeCredit: convertEthToWei(0.001), + }); + + await verifyDeploymentCompletion(l2EthBridgeImplementationDeploymentTx, ownerSmartAccount.client, "L2ETHBridge"); + + if (!l2EthBridgeImplementationDeploymentTx.hash) { + throw Error(`Invalid transaction output from deployContract call for L2ETHBridge Contract`); + } + + if (!l2EthBridgeImplementation) { + throw Error(`Invalid address output from deployContract call for L2ETHBridge Contract`); + } + + const l2ETHBridgeImplementationAddress = getCheckSummedAddress(l2EthBridgeImplementation); + + const l2EthBridgeInitData = encodeFunctionData({ + abi: L2ETHBridgeJson.default.abi, + functionName: "initialize", + args: [ownerSmartAccount.address, + ownerSmartAccount.address, + l2BridgeMessengerProxyAddress, + l2EthBridgeVaultProxyAddress], + }); + const { tx: l2EthBridgeProxyDeploymentTx, address: l2EthBridgeProxy } = await ownerSmartAccount.deployContract({ + shardId: 1, + bytecode: TransparentUpgradeableProxy.default.bytecode as `0x${string} `, + abi: TransparentUpgradeableProxy.default.abi as Abi, + args: [l2ETHBridgeImplementationAddress, ownerSmartAccount.address, l2EthBridgeInitData], + salt: BigInt(Math.floor(Math.random() * 10000)), + feeCredit: convertEthToWei(0.001), + }); + + await verifyDeploymentCompletion(l2EthBridgeProxyDeploymentTx, ownerSmartAccount.client, "L2ETHBridgeProxy"); + + const l2ETHBridgeProxyAddress = getCheckSummedAddress(l2EthBridgeProxy); + + // Dynamically load artifacts + const L2EnshrinedTokenBridgeJson = await import("../../artifacts/contracts/bridge/l2/L2EnshrinedTokenBridge.sol/L2EnshrinedTokenBridge.json"); + + if (!L2EnshrinedTokenBridgeJson || !L2EnshrinedTokenBridgeJson.default || !L2EnshrinedTokenBridgeJson.default.abi || !L2EnshrinedTokenBridgeJson.default.bytecode) { + throw Error(`Invalid L2EnshrinedTokenBridge ABI`); + } + + const { tx: l2EnshrinedTokenBridgeImplDepTx, address: l2EnshrinedTokenBridgeImpl } = await ownerSmartAccount.deployContract({ + shardId: 1, + bytecode: L2EnshrinedTokenBridgeJson.default.bytecode as `0x${string} `, + abi: L2EnshrinedTokenBridgeJson.default.abi as Abi, + args: [], + salt: BigInt(Math.floor(Math.random() * 10000)), + feeCredit: convertEthToWei(0.001), + }); + + await verifyDeploymentCompletion(l2EnshrinedTokenBridgeImplDepTx, ownerSmartAccount.client, "L2EnshrinedTokenBridge"); + + if (!l2EnshrinedTokenBridgeImplDepTx || !l2EnshrinedTokenBridgeImplDepTx.hash) { + throw Error(`Invalid transaction output from deployContract call for L2EnshrinedTokenBridge Contract`); + } + + if (!l2EnshrinedTokenBridgeImpl) { + throw Error(`Invalid address output from deployContract call for L2EnshrinedTokenBridge Contract`); + } + + const l2EnshrinedTokenBridgeImplementationAddress = getCheckSummedAddress(l2EnshrinedTokenBridgeImpl); + + const l2EnshrinedTokenBridgeInitData = encodeFunctionData({ + abi: L2EnshrinedTokenBridgeJson.default.abi, + functionName: "initialize", + args: [ownerSmartAccount.address, + ownerSmartAccount.address, + l2BridgeMessengerProxyAddress], + }); + + const { tx: l2EnshrinedTokenBridgeProxyDeploymentTx, address: l2EnshrinedTokenBridgeProxy } = await ownerSmartAccount.deployContract({ + shardId: 1, + bytecode: TransparentUpgradeableProxy.default.bytecode as `0x${string} `, + abi: TransparentUpgradeableProxy.default.abi as Abi, + args: [l2EnshrinedTokenBridgeImplementationAddress, ownerSmartAccount.address, l2EnshrinedTokenBridgeInitData], + salt: BigInt(Math.floor(Math.random() * 10000)), + feeCredit: convertEthToWei(0.001), + }); + + await verifyDeploymentCompletion(l2EnshrinedTokenBridgeProxyDeploymentTx, ownerSmartAccount.client, "L2EnshrinedTokenBridgeProxy"); + + + const l2EnshrinedTokenBridgeProxyAddress = getCheckSummedAddress(l2EnshrinedTokenBridgeProxy); + + const authoriseBridgesData = encodeFunctionData({ + abi: L2BridgeMessengerJson.default.abi as Abi, + functionName: "authoriseBridges", + args: [[l2ETHBridgeProxyAddress, + l2EnshrinedTokenBridgeProxyAddress] + ], + }); + + let authoriseL2BridgesTxnReceipts: ProcessedReceipt[]; + + try { + const authoriseL2BridgesResponse = await ownerSmartAccount.sendTransaction({ + to: l2BridgeMessengerProxyAddress as `0x${string} `, + data: authoriseBridgesData, + feeCredit: convertEthToWei(0.001), + }); + + authoriseL2BridgesTxnReceipts = await authoriseL2BridgesResponse.wait(); + } catch (err) { + console.error(`Error caught when the bridges are being authorised on L2BridgeMessenger: ${l2BridgeMessengerProxyAddress} `, err); + } + + const authoriseL2Bridges_outputProcessedReceipt = authoriseL2BridgesTxnReceipts?.[0]?.outputReceipts?.[0]; + if ( + !authoriseL2Bridges_outputProcessedReceipt?.success || + authoriseL2Bridges_outputProcessedReceipt.status === '' || + (typeof authoriseL2Bridges_outputProcessedReceipt.status === 'string' && authoriseL2Bridges_outputProcessedReceipt.status.toLowerCase().includes('reverted')) + ) { + console.error(`❌ Failed to authorise Bridges: ${[l2ETHBridgeProxyAddress, + l2EnshrinedTokenBridgeProxyAddress]} + on the L2BridgeMessenger contract: ${l2BridgeMessengerProxyAddress}`); + } else { + console.log(`βœ… Successfully authorised Bridges: ${[l2ETHBridgeProxyAddress, + l2EnshrinedTokenBridgeProxyAddress]} + on the L2BridgeMessenger contract: ${l2BridgeMessengerProxyAddress}`); + } + + let l2BridgeMessengerProxyInstance; + + try { + // verify if the bridges are really authorised + l2BridgeMessengerProxyInstance = getContract({ + client: ownerSmartAccount.client, + abi: L2BridgeMessengerJson.default.abi as Abi, + address: l2BridgeMessengerProxyAddress as `0x${string}` + }); + + const isL2EnshrinedTokenBridgeAuthorised = await l2BridgeMessengerProxyInstance.read.isAuthorisedBridge([l2EnshrinedTokenBridgeProxyAddress]); + if (!isL2EnshrinedTokenBridgeAuthorised) { + throw new Error(`❌ L2EnshrinedTokenBridge: ${l2EnshrinedTokenBridgeProxyAddress} is not authorised on L2BridgeMessenger: ${l2BridgeMessengerProxyAddress}`); + } + + const isL2ETHBridgeAuthorised = await l2BridgeMessengerProxyInstance.read.isAuthorisedBridge([l2ETHBridgeProxyAddress]); + if (!isL2ETHBridgeAuthorised) { + throw new Error(`❌ L2ETHBridge: ${l2ETHBridgeProxyAddress} is not authorised on L2BridgeMessenger: ${l2BridgeMessengerProxyAddress}`); + } + + } catch (err) { + throw new Error(`❌ Error caught while getting an instance of L2BridgeMessenger: ${l2BridgeMessengerProxyAddress} , Error: ${err} `); + } + + + const setL2ETHBridgeData = encodeFunctionData({ + abi: L2ETHBridgeVaultJson.default.abi as Abi, + functionName: "setL2ETHBridge", + args: [l2ETHBridgeProxyAddress], + }); + + const setL2ETHBridgeResponse = await ownerSmartAccount.sendTransaction({ + to: l2EthBridgeVaultProxyAddress as `0x${string}`, + data: setL2ETHBridgeData, + feeCredit: convertEthToWei(0.001), + }); + + const setL2ETHBridgeResponseTxnReceipt: ProcessedReceipt[] = await setL2ETHBridgeResponse.wait(); + + + const setL2ETHBridge_outputProcessedReceipt = setL2ETHBridgeResponseTxnReceipt?.[0]?.outputReceipts?.[0]; + if ( + !setL2ETHBridge_outputProcessedReceipt?.success || + setL2ETHBridge_outputProcessedReceipt.status === '' || + (typeof setL2ETHBridge_outputProcessedReceipt.status === 'string' && setL2ETHBridge_outputProcessedReceipt.status.toLowerCase().includes('reverted')) + ) { + console.error(`❌ Failed to wire L2ETHBridge: ${l2ETHBridgeProxyAddress} + as dependency in the ETHBridgeVault contract: ${l2EthBridgeVaultProxyAddress}`); + } else { + console.log(`βœ… Successfully wired L2ETHBridge: ${l2ETHBridgeProxyAddress} + as dependency in the ETHBridgeVault contract: ${l2EthBridgeVaultProxyAddress}`); + } + + // verify if the L2ETHBridge is set + const l2ETHBridgeVaultProxyInstance = getContract({ + client: ownerSmartAccount.client, + abi: L2ETHBridgeVaultJson.default.abi as Abi, + address: l2EthBridgeVaultProxyAddress as `0x${string}` + }); + + const l2ETHBridgeFromVaultContract = await l2ETHBridgeVaultProxyInstance.read.l2ETHBridge([]); + if (!l2ETHBridgeFromVaultContract || l2ETHBridgeFromVaultContract != l2ETHBridgeProxyAddress) { + throw Error(`Invalid L2ETHBridge: ${l2ETHBridgeFromVaultContract} was set in L2ETHBridgeVault. expected L2ETHBridge from Vault: ${l2ETHBridgeProxyAddress}`); + } + + // TODO replace this with dummy contract deployed address + const l1ETHBridgeProxyDummyAddress = l2ETHBridgeProxyAddress; + + const setCounterPartyBridgeData = encodeFunctionData({ + abi: L2ETHBridgeJson.default.abi as Abi, + functionName: "setCounterpartyBridge", + args: [getCheckSummedAddress(l1ETHBridgeProxyDummyAddress)], + }); + + const setCounterPartyBridgeResponse = await ownerSmartAccount.sendTransaction({ + to: l2ETHBridgeProxyAddress as `0x${string}`, + data: setCounterPartyBridgeData, + feeCredit: convertEthToWei(0.001), + }); + + const setCounterPartyETHBridge_Receipt: ProcessedReceipt[] = await setCounterPartyBridgeResponse.wait(); + + const setCounterPartyETHBridge_outputProcessedReceipt = setCounterPartyETHBridge_Receipt?.[0]?.outputReceipts?.[0]; + if ( + !setCounterPartyETHBridge_outputProcessedReceipt?.success || + setCounterPartyETHBridge_outputProcessedReceipt.status === '' || + (typeof setCounterPartyETHBridge_outputProcessedReceipt.status === 'string' && setCounterPartyETHBridge_outputProcessedReceipt.status.toLowerCase().includes('reverted')) + ) { + console.error(`❌ Failed to set counterparty ETHBridge: ${l2ETHBridgeProxyAddress} + as dependency in the L2ETHBridge contract: ${l2ETHBridgeProxyAddress}`); + } else { + console.log(`βœ… Successfully set counterparty ETHBridge: ${l2ETHBridgeProxyAddress} + as dependency in the L2ETHBridge contract: ${l2ETHBridgeProxyAddress}`); + } + + // verify if the CounterpartyBridge is set + const l2ETHBridgeProxyInstance = getContract({ + client: ownerSmartAccount.client, + abi: L2ETHBridgeJson.default.abi as Abi, + address: l2ETHBridgeProxyAddress as `0x${string}` + }); + + const counterpartyBridgeFromL2ETHBridgeContract = await l2ETHBridgeProxyInstance.read.counterpartyBridge([]); + if (!counterpartyBridgeFromL2ETHBridgeContract || counterpartyBridgeFromL2ETHBridgeContract != getCheckSummedAddress(l1ETHBridgeProxyDummyAddress)) { + throw Error(`Invalid counterpartyBridge: ${counterpartyBridgeFromL2ETHBridgeContract} was set in L2ETHBridge. expected counterpartyBridge is: ${getCheckSummedAddress(l1ETHBridgeProxyDummyAddress)}`); + } + + + const grantRelayerRoleTxnData = encodeFunctionData({ + abi: L2BridgeMessengerJson.default.abi as Abi, + functionName: "grantRelayerRole", + args: [getCheckSummedAddress(ownerSmartAccount.address)], + }); + + const grantRelayerRoleResponse = await ownerSmartAccount.sendTransaction({ + to: l2BridgeMessengerProxyAddress as `0x${string}`, + data: grantRelayerRoleTxnData, + feeCredit: convertEthToWei(0.001), + }); + + const grantRelayerRoleResponseTxnReceipt: ProcessedReceipt[] = await grantRelayerRoleResponse.wait(); + + // check the first element in the ProcessedReceipt and verify if it is successful + if (!grantRelayerRoleResponseTxnReceipt[0].success) { + throw Error(`Failed to grant relayerRole for: ${ownerSmartAccount.address} + on the L2EnshrinedTokenBridge contract: ${l2BridgeMessengerProxyAddress}`); + } + + const hasRelayerRole = await l2BridgeMessengerProxyInstance.read.hasRelayerRole([ownerSmartAccount.address]); + if (!hasRelayerRole) { + throw Error(`RELAYER role is not granted for ${ownerSmartAccount.address} on L2BridgeMessenger`); + } + + console.log(`successfully granted RELAYER role for ${ownerSmartAccount.address} on L2BridgeMessenger: ${l2BridgeMessengerProxyAddress}`); + + return { + ownerSmartAccount, + depositRecipientSmartAccount, + feeRefundSmartAccount, + l2EthBridgeVaultProxyAddress, + l2BridgeMessengerProxyAddress, + l2ETHBridgeProxyAddress, + l2EnshrinedTokenBridgeProxyAddress, + l2BridgeMessengerProxyInstance, + l2ETHBridgeVaultProxyInstance, + l2ETHBridgeProxyInstance + }; +} + +async function verifyDeploymentCompletion(deployTxn: any, publicClient: PublicClient, contractName: string): Promise { + + const deployTxnReceipt = await waitTillCompleted(publicClient, deployTxn.hash, { + waitTillMainShard: true + }); + + const output = deployTxnReceipt?.[0]?.outputReceipts?.[0]; + if ( + !output?.success || + output.status === '' || + (typeof output.status === 'string' && output.status.toLowerCase().includes('reverted')) + ) { + console.error(`❌ Failed to deploy ${contractName} contract`); + return false; + } else { + console.log(`βœ… Successfully deployed ${contractName} contract`); + return true; + } +} \ No newline at end of file diff --git a/rollup-bridge-contracts/test/prepare-hardhat-test-env.sh b/rollup-bridge-contracts/test/prepare-hardhat-test-env.sh new file mode 100644 index 000000000..7df4229e0 --- /dev/null +++ b/rollup-bridge-contracts/test/prepare-hardhat-test-env.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -e + +export ANVIL_RPC_ENDPOINT=http://127.0.0.1:8545 +export ANVIL_PRIVATE_KEY= + +export GETH_RPC_ENDPOINT=http://127.0.0.1:8545 +export GETH_PRIVATE_KEY= + +export SEPOLIA_RPC_ENDPOINT= +export SEPOLIA_PRIVATE_KEY= + +export NIL_RPC_ENDPOINT=http://127.0.0.1:8529 +export FAUCET_ENDPOINT=http://127.0.0.1:8527 +export NIL_PRIVATE_KEY="0x4b0a4354dfc246c9a29d56f1c1820f7f6ca8b84f8335660b0a9d23d984c10588" +export DEPOSIT_RECIPIENT_PRIVATE_KEY="0xed1343164c6fa4e2800edc97cc19fc69fa1073555aadcd008fc5bf805361b7cf" +export NIL_FEE_REFUND_PRIVATE_KEY="0xc47edfcb7449d00d4ae1a38bbab6cdb5ba1e036e9358725d90fd8a119a35a36c" +export NIL=0x0001111111111111111111111111111111111110 +export NIL_SMART_ACCOUNT_ADDRESS= diff --git a/rollup-bridge-contracts/test/run_tests.sh b/rollup-bridge-contracts/test/run_tests.sh new file mode 100755 index 000000000..e4643cbac --- /dev/null +++ b/rollup-bridge-contracts/test/run_tests.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +set -e + +trap_with_arg() { + local func="$1" + shift + for sig in "$@"; do + trap "$func $sig" "$sig" + done +} + +stop() { + trap - SIGINT EXIT + printf '\n%s\n' "received $1, killing child processes" + local jobs_list=$(jobs -pr) + if [ -n "$jobs_list" ]; then + kill -s SIGINT $jobs_list + fi +} + +trap_with_arg 'stop' EXIT SIGINT SIGTERM SIGHUP + +export ANVIL_RPC_ENDPOINT=http://127.0.0.1:8545 +export ANVIL_PRIVATE_KEY= + +export GETH_RPC_ENDPOINT=http://127.0.0.1:8545 +export GETH_PRIVATE_KEY= + +export SEPOLIA_RPC_ENDPOINT= +export SEPOLIA_PRIVATE_KEY= + +export NIL_RPC_ENDPOINT=http://127.0.0.1:8529 +export FAUCET_ENDPOINT=http://127.0.0.1:8527 +export NIL_PRIVATE_KEY=0x4d47e8aed46e8b1bb4f4573f68ad43cade273d149b0c2942526ad5141c51b517 +export NIL=0x0001111111111111111111111111111111111110 + +echo "Rpc endpoint: $NIL_RPC_ENDPOINT" +echo "Private key: $NIL_PRIVATE_KEY" + +# Update to reflect the new directory structure +# Move to the directory where the script is located +cd $(dirname "$0") + +set +e +if CI=true npx hardhat test --network nil hardhat/*.ts; then + exit 0 +fi