Feat/122 Add TokenBridge e2e tests (#210)

* fix: add mutex in account manager to avoid nonce issue

* fix: optimize global setup

* Limiting number of concurrent traces API requests for the local stack to avoid occasional OOM-s

* Limiting number of verticles for Traces API node

* Add E2E TokenBridge tests

* fixing test and adding concurency

* fixing test and adding concurency

* fixing test and adding concurency

* fixing test and adding concurency

* fixing nonce management

* deploying l2token for the L2 -> L1 test

* adjusting accounts for L2->L1 test

* adjusting l2TestContractAddress

* use nonce management for L1->L2 test

* adjusting the TestERC20 contract and tests

* rebasing with fix/133-improve-e2e-tests-performance

* fix: update jest config to exit even if there are open handles

* Trying out Besu untuned and raising limit per endpoint to 2 for traces

* Trying out Besu untuned and raising limit per endpoint to 2 for traces and Shomei node

* Using besu untuned for arithmetization as well

* Compile once and parallelise setRemoteTokenBridge

* feat: deploy smart contracts from artifacts + change e2e tests setup

* fix: update pnpm

* fix: remove compile contracts gradle task

* fix: remove compileContracts gradle task

* fix: refactor genesis generator dockerfile + downgrade l1-el-node besu version

* fix: move abi from e2e folder to contract folder + refactor contracts deployments scripts

* feat: add deployment script from artifacts for LineaRollupV6

* update pnpm version in get-started.md

* fix: update console log in deployment scripts

* fix: update besu version + fix deployment scripts

* correct addresses

* fix import

* use abi and bytecode for deployments

* use upgradable beacon for BridgedToken ABI deploys

* use saved abi and bytecode for TestERC20 deploy

* correct deployBridgedTokenAndTokenBridge casing

* optimize token bridge e2e calls

* use explicit message event data

* use precomputed nonces for e2e stack

---------

Co-authored-by: VGau <victorien.gauch@consensys.net>
Co-authored-by: Victorien Gauch <85494462+VGau@users.noreply.github.com>
Co-authored-by: Roman <4833306+Filter94@users.noreply.github.com>
Co-authored-by: thedarkjester <grant.southey@consensys.net>
Co-authored-by: The Dark Jester <thedarkjester@users.noreply.github.com>
This commit is contained in:
Andrei A.
2024-10-31 19:03:53 +02:00
committed by GitHub
parent bf2a760824
commit 390d6ff860
26 changed files with 3237 additions and 18 deletions

View File

@@ -0,0 +1,211 @@
import { ethers } from "ethers";
import { describe, expect, it } from "@jest/globals";
import { config } from "./config/tests-config";
import { waitForEvents, etherToWei } from "./common/utils";
import { MESSAGE_SENT_EVENT_SIGNATURE } from "./common/constants";
const l1AccountManager = config.getL1AccountManager();
const l2AccountManager = config.getL2AccountManager();
const bridgeAmount = ethers.parseEther("100");
const messageSentEventMessageNumberIndex = 4;
const messageSentEventMessageHashIndex = 6;
describe("Bridge ERC20 Tokens L1 -> L2 and L2 -> L1", () => {
it.concurrent("Bridge a token from L1 to L2", async () => {
const [l1Account, l2Account] = await Promise.all([
l1AccountManager.generateAccount(),
l2AccountManager.generateAccount(),
]);
const lineaRollup = config.getLineaRollupContract();
const l2MessageService = config.getL2MessageServiceContract();
const l1TokenBridge = config.getL1TokenBridgeContract();
const l2TokenBridge = config.getL2TokenBridgeContract();
const l1Token = config.getL1TokenContract();
const l1Provider = config.getL1Provider();
console.log("Minting ERC20 tokens to L1 Account");
let { maxPriorityFeePerGas: l1MaxPriorityFeePerGas, maxFeePerGas: l1MaxFeePerGas } = await l1Provider.getFeeData();
let nonce = await l1Provider.getTransactionCount(l1Account.address, "pending");
console.log("Minting and approving tokens to L1 TokenBridge");
await Promise.all([
(
await l1Token.connect(l1Account).mint(l1Account.address, bridgeAmount, {
nonce: nonce,
maxPriorityFeePerGas: l1MaxPriorityFeePerGas,
maxFeePerGas: l1MaxFeePerGas,
})
).wait(),
(
await l1Token.connect(l1Account).approve(l1TokenBridge.getAddress(), bridgeAmount, {
maxPriorityFeePerGas: l1MaxPriorityFeePerGas,
maxFeePerGas: l1MaxFeePerGas,
nonce: nonce + 1,
})
).wait(),
]);
const l1TokenBridgeAddress = await l1TokenBridge.getAddress();
const l1TokenAddress = await l1Token.getAddress();
const allowanceL1Account = await l1Token.allowance(l1Account.address, l1TokenBridgeAddress);
console.log("Current allowance of L1 account to L1 TokenBridge is :", allowanceL1Account.toString());
console.log("Calling the bridgeToken function on the L1 TokenBridge contract");
({ maxPriorityFeePerGas: l1MaxPriorityFeePerGas, maxFeePerGas: l1MaxFeePerGas } = await l1Provider.getFeeData());
nonce = await l1Provider.getTransactionCount(l1Account.address, "pending");
const bridgeTokenTx = await l1TokenBridge
.connect(l1Account)
.bridgeToken(l1TokenAddress, bridgeAmount, l2Account.address, {
value: etherToWei("0.01"),
maxPriorityFeePerGas: l1MaxPriorityFeePerGas,
maxFeePerGas: l1MaxFeePerGas,
nonce: nonce,
});
const bridgedTxReceipt = await bridgeTokenTx.wait();
const sentEventLog = bridgedTxReceipt?.logs.find((log) => log.topics[0] == MESSAGE_SENT_EVENT_SIGNATURE);
const messageSentEvent = lineaRollup.interface.decodeEventLog(
"MessageSent",
sentEventLog!.data,
sentEventLog!.topics,
);
const l1TokenBalance = await l1Token.balanceOf(l1Account.address);
console.log("Token balance of L1 account :", l1TokenBalance.toString());
expect(l1TokenBalance).toEqual(0n);
console.log("Waiting for MessageSent event on L1.");
const messageNumber = messageSentEvent[messageSentEventMessageNumberIndex];
const messageHash = messageSentEvent[messageSentEventMessageHashIndex];
console.log(`Message sent on L1 : messageHash=${messageHash}`);
console.log("Waiting for anchoring...");
const [rollingHashUpdatedEvent] = await waitForEvents(
l2MessageService,
l2MessageService.filters.RollingHashUpdated(),
1_000,
0,
"latest",
async (events) => events.filter((event) => event.args.messageNumber >= messageNumber),
);
expect(rollingHashUpdatedEvent).not.toBeNull();
const anchoredStatus = await l2MessageService.inboxL1L2MessageStatus(messageHash);
expect(anchoredStatus).toBeGreaterThan(0);
console.log(`Message anchored : ${JSON.stringify(rollingHashUpdatedEvent)}`);
console.log("Waiting for MessageClaimed event on L2...");
const [claimedEvent] = await waitForEvents(l2MessageService, l2MessageService.filters.MessageClaimed(messageHash));
expect(claimedEvent).not.toBeNull();
const [newTokenDeployed] = await waitForEvents(l2TokenBridge, l2TokenBridge.filters.NewTokenDeployed());
expect(newTokenDeployed).not.toBeNull();
console.log(`Message claimed on L2 : ${JSON.stringify(claimedEvent)}.`);
const l2Token = config.getL2BridgedTokenContract(newTokenDeployed.args.bridgedToken);
console.log("Verify the token balance on L2");
const l2TokenBalance = await l2Token.balanceOf(l2Account.address);
console.log("Token balance of L2 account :", l2TokenBalance.toString());
expect(l2TokenBalance).toEqual(bridgeAmount);
});
it.concurrent("Bridge a token from L2 to L1", async () => {
const [l1Account, l2Account] = await Promise.all([
l1AccountManager.generateAccount(),
l2AccountManager.generateAccount(),
]);
const lineaRollup = config.getLineaRollupContract();
const l2MessageService = config.getL2MessageServiceContract();
const l1TokenBridge = config.getL1TokenBridgeContract();
const l2TokenBridge = config.getL2TokenBridgeContract();
const l2Token = config.getL2TokenContract();
const l2Provider = config.getL2Provider();
const { maxPriorityFeePerGas: l2MaxPriorityFeePerGas, maxFeePerGas: l2MaxFeePerGas } =
await l2Provider.getFeeData();
let nonce = await l2Provider.getTransactionCount(l2Account.address, "pending");
await Promise.all([
(
await l2Token.connect(l2Account).mint(l2Account.address, bridgeAmount, {
nonce: nonce,
maxPriorityFeePerGas: l2MaxPriorityFeePerGas,
maxFeePerGas: l2MaxFeePerGas,
})
).wait(),
(
await l2Token.connect(l2Account).approve(l2TokenBridge.getAddress(), ethers.parseEther("100"), {
maxPriorityFeePerGas: l2MaxPriorityFeePerGas,
maxFeePerGas: l2MaxFeePerGas,
nonce: nonce + 1,
})
).wait(),
]);
const allowanceL2Account = await l2Token.allowance(l2Account.address, l2TokenBridge.getAddress());
console.log("Current allowance of L2 account to L2 TokenBridge is :", allowanceL2Account.toString());
console.log("Current balance of L2 account is :", await l2Token.balanceOf(l2Account));
console.log("Calling the bridgeToken function on the L2 TokenBridge contract");
nonce = await l2Provider.getTransactionCount(l2Account.address, "pending");
const bridgeTokenTx = await l2TokenBridge
.connect(l2Account)
.bridgeToken(await l2Token.getAddress(), bridgeAmount, l1Account.address, {
value: etherToWei("0.01"),
maxPriorityFeePerGas: l2MaxPriorityFeePerGas,
maxFeePerGas: l2MaxFeePerGas,
nonce: nonce,
});
const receipt = await bridgeTokenTx.wait();
const sentEventLog = receipt?.logs.find((log) => log.topics[0] == MESSAGE_SENT_EVENT_SIGNATURE);
const messageSentEvent = l2MessageService.interface.decodeEventLog(
"MessageSent",
sentEventLog!.data,
sentEventLog!.topics,
);
const messageHash = messageSentEvent[messageSentEventMessageHashIndex];
console.log("Waiting for L1 MessageClaimed event.");
const [claimedEvent] = await waitForEvents(lineaRollup, lineaRollup.filters.MessageClaimed(messageHash));
expect(claimedEvent).not.toBeNull();
console.log(`Message claimed on L1 : ${JSON.stringify(claimedEvent)}`);
const [newTokenDeployed] = await waitForEvents(l1TokenBridge, l1TokenBridge.filters.NewTokenDeployed());
expect(newTokenDeployed).not.toBeNull();
const l1BridgedToken = config.getL1BridgedTokenContract(newTokenDeployed.args.bridgedToken);
console.log("Verify the token balance on L1");
const l1BridgedTokenBalance = await l1BridgedToken.balanceOf(l1Account.address);
console.log("Token balance of L1 account :", l1BridgedTokenBalance.toString());
expect(l1BridgedTokenBalance).toEqual(bridgeAmount);
});
});

View File

@@ -3,7 +3,7 @@ import assert from "assert";
import { AbstractSigner, BaseContract, BlockTag, TransactionReceipt, TransactionRequest, Wallet, ethers } from "ethers";
import path from "path";
import { exec } from "child_process";
import { L2MessageService, LineaRollupV5 } from "../typechain";
import { L2MessageService, TokenBridge, LineaRollupV5 } from "../typechain";
import { PayableOverrides, TypedContractEvent, TypedDeferredTopicFilter, TypedEventLog } from "../typechain/common";
import { MessageEvent, SendMessageArgs } from "./types";
@@ -147,7 +147,10 @@ export async function getBlockByNumberOrBlockTag(rpcUrl: URL, blockTag: BlockTag
}
}
export async function getEvents<TContract extends LineaRollupV5 | L2MessageService, TEvent extends TypedContractEvent>(
export async function getEvents<
TContract extends LineaRollupV5 | L2MessageService | TokenBridge,
TEvent extends TypedContractEvent,
>(
contract: TContract,
eventFilter: TypedDeferredTopicFilter<TEvent>,
fromBlock?: BlockTag,
@@ -168,7 +171,7 @@ export async function getEvents<TContract extends LineaRollupV5 | L2MessageServi
}
export async function waitForEvents<
TContract extends LineaRollupV5 | L2MessageService,
TContract extends LineaRollupV5 | L2MessageService | TokenBridge,
TEvent extends TypedContractEvent,
>(
contract: TContract,

View File

@@ -9,10 +9,18 @@ declare global {
}
export default async (): Promise<void> => {
const l1JsonRpcProvider = config.getL1Provider();
const l1AccountManager = config.getL1AccountManager();
const l2AccountManager = config.getL2AccountManager();
const account = config.getL1AccountManager().whaleAccount(0);
const l2Account = config.getL2AccountManager().whaleAccount(0);
const lineaRollup = config.getLineaRollupContract(account);
const l1JsonRpcProvider = config.getL1Provider();
const l1TokenBridge = config.getL1TokenBridgeContract();
const l2TokenBridge = config.getL2TokenBridgeContract();
const l1SecurityCouncil = l1AccountManager.whaleAccount(3);
const l2SecurityCouncil = l2AccountManager.whaleAccount(3);
const [l1AccountNonce, l2AccountNonce, { maxPriorityFeePerGas, maxFeePerGas }] = await Promise.all([
account.getNonce(),
@@ -37,6 +45,8 @@ export default async (): Promise<void> => {
nonce: l1AccountNonce + 1,
})
).wait(),
(await l1TokenBridge.connect(l1SecurityCouncil).setRemoteTokenBridge(await l2TokenBridge.getAddress())).wait(),
(await l2TokenBridge.connect(l2SecurityCouncil).setRemoteTokenBridge(await l1TokenBridge.getAddress())).wait(),
]);
console.log(`L1 Dummy contract deployed at address: ${await dummyContract.getAddress()}`);

View File

@@ -26,6 +26,8 @@ const config: Config = {
rpcUrl: L1_RPC_URL,
chainId: L1_CHAIN_ID,
lineaRollupAddress: "0x2A5CDCfc38856e2590E9Bd32F54Fa348e5De5f48",
tokenBridgeAddress: "",
l1TokenAddress: "",
accountManager: new EnvironmentBasedAccountManager(
new ethers.JsonRpcProvider(L1_RPC_URL.toString()),
L1_WHALE_ACCOUNTS,
@@ -37,6 +39,8 @@ const config: Config = {
rpcUrl: L2_RPC_URL,
chainId: L2_CHAIN_ID,
l2MessageServiceAddress: "0x33bf916373159A8c1b54b025202517BfDbB7863D",
tokenBridgeAddress: "",
l2TokenAddress: "",
l2TestContractAddress: "",
accountManager: new EnvironmentBasedAccountManager(
new ethers.JsonRpcProvider(L2_RPC_URL.toString()),

View File

@@ -16,7 +16,9 @@ const config: Config = {
rpcUrl: L1_RPC_URL,
chainId: 31648428,
lineaRollupAddress: "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9",
dummyContractAddress: "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9",
dummyContractAddress: "0x610178dA211FEF7D417bC0e6FeD39F05609AD788",
tokenBridgeAddress: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6",
l1TokenAddress: "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318",
accountManager: new GenesisBasedAccountManager(
new ethers.JsonRpcProvider(L1_RPC_URL.toString()),
path.resolve(
@@ -30,8 +32,10 @@ const config: Config = {
besuNodeRpcUrl: L2_BESU_NODE_RPC_URL,
chainId: 1337,
l2MessageServiceAddress: "0xe537D669CA013d86EBeF1D64e40fC74CADC91987",
l2TestContractAddress: "0xeB0b0a14F92e3BA35aEF3a2B6A24D7ED1D11631B",
dummyContractAddress: "0x2f6dAaF8A81AB675fbD37Ca6Ed5b72cf86237453",
l2TestContractAddress: "0x997FC3aF1F193Cbdc013060076c67A13e218980e",
dummyContractAddress: "0xE4392c8ecC46b304C83cDB5edaf742899b1bda93",
tokenBridgeAddress: "0x5C95Bcd50E6D1B4E3CDC478484C9030Ff0a7D493",
l2TokenAddress: "0xCC1B08B17301e090cbb4c1F5598Cbaa096d591FB",
accountManager: new GenesisBasedAccountManager(
new ethers.JsonRpcProvider(L2_RPC_URL.toString()),
path.resolve(

View File

@@ -25,6 +25,8 @@ const config: Config = {
rpcUrl: L1_RPC_URL,
chainId: L1_CHAIN_ID,
lineaRollupAddress: "0xB218f8A4Bc926cF1cA7b3423c154a0D627Bdb7E5",
tokenBridgeAddress: "0x5A0a48389BB0f12E5e017116c1105da97E129142",
l1TokenAddress: "",
accountManager: new EnvironmentBasedAccountManager(
new ethers.JsonRpcProvider(L1_RPC_URL.toString()),
L1_WHALE_ACCOUNTS,
@@ -36,6 +38,8 @@ const config: Config = {
rpcUrl: L2_RPC_URL,
chainId: L2_CHAIN_ID,
l2MessageServiceAddress: "0x971e727e956690b9957be6d51Ec16E73AcAC83A7",
tokenBridgeAddress: "0x93DcAdf238932e6e6a85852caC89cBd71798F463",
l2TokenAddress: "",
l2TestContractAddress: "",
accountManager: new EnvironmentBasedAccountManager(
new ethers.JsonRpcProvider(L2_RPC_URL.toString()),

View File

@@ -1,6 +1,8 @@
import { AbstractSigner, JsonRpcProvider, Wallet } from "ethers";
import { Config, L2Config, LocalL2Config } from "./types";
import {
BridgedToken,
BridgedToken__factory,
DummyContract,
DummyContract__factory,
L2MessageService,
@@ -9,6 +11,10 @@ import {
LineaRollupV5__factory,
TestContract,
TestContract__factory,
TestERC20,
TestERC20__factory,
TokenBridge,
TokenBridge__factory,
} from "../../typechain";
import { AccountManager } from "./accounts/account-manager";
@@ -99,6 +105,71 @@ export default class TestSetup {
return l2MessageService;
}
public getL1TokenBridgeContract(signer?: Wallet): TokenBridge {
const l1TokenBridge: TokenBridge = TokenBridge__factory.connect(
this.config.L1.tokenBridgeAddress,
this.getL1Provider(),
);
if (signer) {
return l1TokenBridge.connect(signer);
}
return l1TokenBridge;
}
public getL2TokenBridgeContract(signer?: Wallet): TokenBridge {
const l2TokenBridge: TokenBridge = TokenBridge__factory.connect(
this.config.L2.tokenBridgeAddress,
this.getL2Provider(),
);
if (signer) {
return l2TokenBridge.connect(signer);
}
return l2TokenBridge;
}
public getL1TokenContract(signer?: Wallet): TestERC20 {
const l1Token: TestERC20 = TestERC20__factory.connect(this.config.L1.l1TokenAddress, this.getL1Provider());
if (signer) {
return l1Token.connect(signer);
}
return l1Token;
}
public getL2TokenContract(signer?: Wallet): TestERC20 {
const l2Token: TestERC20 = TestERC20__factory.connect(this.config.L2.l2TokenAddress, this.getL2Provider());
if (signer) {
return l2Token.connect(signer);
}
return l2Token;
}
public getL1BridgedTokenContract(bridgedTokenAddress: string, signer?: Wallet): BridgedToken {
const l1BridgedToken: BridgedToken = BridgedToken__factory.connect(bridgedTokenAddress, this.getL1Provider());
if (signer) {
return l1BridgedToken.connect(signer);
}
return l1BridgedToken;
}
public getL2BridgedTokenContract(bridgedTokenAddress: string, signer?: Wallet): BridgedToken {
const l2BridgedToken: BridgedToken = BridgedToken__factory.connect(bridgedTokenAddress, this.getL2Provider());
if (signer) {
return l2BridgedToken.connect(signer);
}
return l2BridgedToken;
}
public getL1DummyContract(signer?: Wallet): DummyContract {
const dummyContract = DummyContract__factory.connect(this.config.L1.dummyContractAddress, this.getL1Provider());

View File

@@ -9,11 +9,20 @@ export type BaseConfig = {
export type L1Config = BaseConfig & {
lineaRollupAddress: string;
tokenBridgeAddress: string;
l1TokenAddress: string;
};
export type BaseL2Config = BaseConfig & {
l2MessageServiceAddress: string;
l2TestContractAddress: string;
l2TestContractAddress?: string;
besuNodeRpcUrl?: URL;
tokenBridgeAddress: string;
l2TokenAddress: string;
shomeiEndpoint?: URL;
shomeiFrontendEndpoint?: URL;
sequencerEndpoint?: URL;
transactionExclusionEndpoint?: URL;
};
export type LocalL2Config = BaseL2Config & {

View File

@@ -7,6 +7,8 @@ import { TRANSACTION_CALLDATA_LIMIT } from "./common/constants";
const l2AccountManager = config.getL2AccountManager();
describe("Layer 2 test suite", () => {
const l2Provider = config.getL2Provider();
it.concurrent("Should revert if transaction data size is above the limit", async () => {
const account = await l2AccountManager.generateAccount();
const dummyContract = config.getL2DummyContract(account);
@@ -19,8 +21,14 @@ describe("Layer 2 test suite", () => {
it.concurrent("Should succeed if transaction data size is below the limit", async () => {
const account = await l2AccountManager.generateAccount();
const dummyContract = config.getL2DummyContract(account);
const nonce = await l2Provider.getTransactionCount(account.address, "pending");
const { maxPriorityFeePerGas, maxFeePerGas } = await l2Provider.getFeeData();
const tx = await dummyContract.connect(account).setPayload(ethers.randomBytes(1000));
const tx = await dummyContract.connect(account).setPayload(ethers.randomBytes(1000), {
nonce: nonce,
maxPriorityFeePerGas: maxPriorityFeePerGas,
maxFeePerGas: maxFeePerGas,
});
const receipt = await tx.wait();
expect(receipt?.status).toEqual(1);