From f36b5384fa3cf963ff5d242779f4cd5cc654cfc7 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Fri, 7 Jun 2024 19:05:49 +0200 Subject: [PATCH 01/21] feat: add wip, scaffold FuelMessagePortalV4 (FTI) --- .../v4/FuelMessagePortalV3.sol | 54 ++++++++ .../test/FuelMessagePortalV4.test.ts | 40 ++++++ .../behaviors/AccessControl.behavior.test.ts | 117 ++++++++++++++++++ .../test/behaviors/index.ts | 1 + 4 files changed, 212 insertions(+) create mode 100644 packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV3.sol create mode 100644 packages/solidity-contracts/test/FuelMessagePortalV4.test.ts create mode 100644 packages/solidity-contracts/test/behaviors/AccessControl.behavior.test.ts diff --git a/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV3.sol b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV3.sol new file mode 100644 index 00000000..df734d75 --- /dev/null +++ b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV3.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity 0.8.9; + +import "../v3/FuelMessagePortalV3.sol"; + +/// @custom:oz-upgrades-unsafe-allow constructor state-variable-immutable +contract FuelMessagePortalV4 is FuelMessagePortalV3 { + event Transaction(uint256 indexed nonce, uint64 max_gas, bytes canonically_serialized_tx); + + error GasLimit(); + + uint64 public immutable GAS_LIMIT; + + uint192 internal lastSeenBlock; + uint64 internal usedGas; + + constructor(uint256 _depositLimitGlobal, uint64 _gasLimit) FuelMessagePortalV3(_depositLimitGlobal) { + GAS_LIMIT = _gasLimit; + } + + function sendTransaction(uint64 gas, bytes calldata /*serializedTx*/) external payable virtual { + uint64 _usedGas = usedGas; + + if (lastSeenBlock == block.number) { + _usedGas += gas; + } else { + _usedGas = gas; + } + + if (_usedGas > GAS_LIMIT) { + revert GasLimit(); + } + + lastSeenBlock = uint192(block.number); + usedGas = _usedGas; + } + + function getLastSeenBlock() public virtual returns (uint256) { + return uint256(lastSeenBlock); + } + + function getUsedGas() external virtual returns (uint64) { + if (getLastSeenBlock() == block.number) return usedGas; + + return 0; + } + + /** + * @dev This empty reserved space is put in place to allow future versions to add new + * variables without shifting down storage in the inheritance chain. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} diff --git a/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts b/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts new file mode 100644 index 00000000..f5d6005f --- /dev/null +++ b/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts @@ -0,0 +1,40 @@ +import { MaxUint256 } from 'ethers'; +import hre from 'hardhat'; + +import { behavesLikeAccessControl } from './behaviors'; + +const DEPOSIT_LIMIT = MaxUint256; +const GAS_LIMIT = 1_000; + +describe.only('FuelMessagePortalV4', () => { + const fixture = hre.deployments.createFixture( + async ({ ethers, upgrades }) => { + const signers = await ethers.getSigners(); + const FuelMessagePortal = await ethers.getContractFactory( + 'FuelMessagePortalV4' + ); + const FuelChainState = await ethers.getContractFactory('FuelChainState'); + const fuelChainState = await upgrades.deployProxy(FuelChainState, { + initializer: 'initialize', + // constructorArgs: [ + // TIME_TO_FINALIZE, + // BLOCKS_PER_COMMIT_INTERVAL, + // TIME_TO_FINALIZE, + // ], + }); + + const fuelMessagePortal = await upgrades.deployProxy( + FuelMessagePortal, + [await fuelChainState.getAddress()], + { + initializer: 'initialize', + constructorArgs: [DEPOSIT_LIMIT, GAS_LIMIT], + } + ); + + return { signers, fuelMessagePortal }; + } + ); + + behavesLikeAccessControl(fixture, 'fuelMessagePortal'); +}); diff --git a/packages/solidity-contracts/test/behaviors/AccessControl.behavior.test.ts b/packages/solidity-contracts/test/behaviors/AccessControl.behavior.test.ts new file mode 100644 index 00000000..572dc98b --- /dev/null +++ b/packages/solidity-contracts/test/behaviors/AccessControl.behavior.test.ts @@ -0,0 +1,117 @@ +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { keccak256, toUtf8Bytes } from 'ethers'; + +import type { AccessControlUpgradeable } from '../../typechain'; + +export type AccessControlFixture = { + signers: HardhatEthersSigner[]; + [key: string]: any; +}; + +export function behavesLikeAccessControl( + fixture: () => Promise, + name: string = 'fuelMessagePortal' +) { + let fixt: AccessControlFixture; + describe('Includes access control features', () => { + const defaultAdminRole = + '0x0000000000000000000000000000000000000000000000000000000000000000'; + const pauserRole = keccak256(toUtf8Bytes('PAUSER_ROLE')); + let signer0: string; + let signer1: string; + let signer2: string; + let contract: AccessControlUpgradeable; + + before('instantiate fixture', async () => { + fixt = await fixture(); + [signer0, signer1, signer2] = fixt.signers.map( + (signer) => signer.address + ); + contract = fixt[name]; + }); + + it('Should be able to grant admin role', async () => { + expect(await contract.hasRole(defaultAdminRole, signer1)).to.equal(false); + + // Grant admin role + await expect(contract.grantRole(defaultAdminRole, signer1)).to.not.be + .reverted; + expect(await contract.hasRole(defaultAdminRole, signer1)).to.equal(true); + }); + + it('Should be able to renounce admin role', async () => { + expect(await contract.hasRole(defaultAdminRole, signer0)).to.equal(true); + + // Revoke admin role + await expect(contract.renounceRole(defaultAdminRole, signer0)).to.not.be + .reverted; + expect(await contract.hasRole(defaultAdminRole, signer0)).to.equal(false); + }); + + it('Should not be able to grant admin role as non-admin', async () => { + expect(await contract.hasRole(defaultAdminRole, signer0)).to.equal(false); + + // Attempt grant admin role + await expect( + contract.grantRole(defaultAdminRole, signer0) + ).to.be.revertedWith( + `AccessControl: account ${signer0.toLowerCase()} is missing role ${defaultAdminRole}` + ); + expect(await contract.hasRole(defaultAdminRole, signer0)).to.equal(false); + }); + + it('Should be able to grant then revoke admin role', async () => { + expect(await contract.hasRole(defaultAdminRole, signer0)).to.equal(false); + expect(await contract.hasRole(defaultAdminRole, signer1)).to.equal(true); + + // Grant admin role + await expect( + contract.connect(fixt.signers[1]).grantRole(defaultAdminRole, signer0) + ).to.not.be.reverted; + expect(await contract.hasRole(defaultAdminRole, signer0)).to.equal(true); + + // Revoke previous admin + await expect(contract.revokeRole(defaultAdminRole, signer1)).to.not.be + .reverted; + expect(await contract.hasRole(defaultAdminRole, signer1)).to.equal(false); + }); + + it('Should be able to grant pauser role', async () => { + expect(await contract.hasRole(pauserRole, signer1)).to.equal(false); + + // Grant pauser role + await expect(contract.grantRole(pauserRole, signer1)).to.not.be.reverted; + expect(await contract.hasRole(pauserRole, signer1)).to.equal(true); + }); + + it('Should not be able to grant permission as pauser', async () => { + expect(await contract.hasRole(defaultAdminRole, signer2)).to.equal(false); + expect(await contract.hasRole(pauserRole, signer2)).to.equal(false); + + // Attempt grant admin role + await expect( + contract.connect(fixt.signers[1]).grantRole(defaultAdminRole, signer2) + ).to.be.revertedWith( + `AccessControl: account ${signer1.toLowerCase()} is missing role ${defaultAdminRole}` + ); + expect(await contract.hasRole(defaultAdminRole, signer2)).to.equal(false); + + // Attempt grant pauser role + await expect( + contract.connect(fixt.signers[1]).grantRole(pauserRole, signer2) + ).to.be.revertedWith( + `AccessControl: account ${signer1.toLowerCase()} is missing role ${defaultAdminRole}` + ); + expect(await contract.hasRole(pauserRole, signer2)).to.equal(false); + }); + + it('Should be able to revoke pauser role', async () => { + expect(await contract.hasRole(pauserRole, signer1)).to.equal(true); + + // Grant pauser role + await expect(contract.revokeRole(pauserRole, signer1)).to.not.be.reverted; + expect(await contract.hasRole(pauserRole, signer1)).to.equal(false); + }); + }); +} diff --git a/packages/solidity-contracts/test/behaviors/index.ts b/packages/solidity-contracts/test/behaviors/index.ts index 6a8fd980..64325616 100644 --- a/packages/solidity-contracts/test/behaviors/index.ts +++ b/packages/solidity-contracts/test/behaviors/index.ts @@ -1,3 +1,4 @@ export * from './erc20GatewayV2.behavior.test'; export * from './erc20GatewayV3.behavior.test'; export * from './erc20GatewayV4.behavior.test'; +export * from './AccessControl.behavior.test'; From db9d42a98556e82f22c2ad77d9f79a62ac633955 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Mon, 10 Jun 2024 15:14:24 +0200 Subject: [PATCH 02/21] feat: add Transaction event to sendTransaction in FuelMessagePortalV4 --- ...gePortalV3.sol => FuelMessagePortalV4.sol} | 11 ++++- .../test/FuelMessagePortalV4.test.ts | 12 +++-- .../FuelMessagePortalV4.behavior.test.ts | 49 +++++++++++++++++++ .../test/behaviors/index.ts | 1 + 4 files changed, 69 insertions(+), 4 deletions(-) rename packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/{FuelMessagePortalV3.sol => FuelMessagePortalV4.sol} (80%) create mode 100644 packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts diff --git a/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV3.sol b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol similarity index 80% rename from packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV3.sol rename to packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol index df734d75..02a4eff3 100644 --- a/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV3.sol +++ b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol @@ -14,11 +14,13 @@ contract FuelMessagePortalV4 is FuelMessagePortalV3 { uint192 internal lastSeenBlock; uint64 internal usedGas; + uint256 internal transactionNonce; + constructor(uint256 _depositLimitGlobal, uint64 _gasLimit) FuelMessagePortalV3(_depositLimitGlobal) { GAS_LIMIT = _gasLimit; } - function sendTransaction(uint64 gas, bytes calldata /*serializedTx*/) external payable virtual { + function sendTransaction(uint64 gas, bytes calldata serializedTx) external payable virtual { uint64 _usedGas = usedGas; if (lastSeenBlock == block.number) { @@ -33,6 +35,9 @@ contract FuelMessagePortalV4 is FuelMessagePortalV3 { lastSeenBlock = uint192(block.number); usedGas = _usedGas; + unchecked { + emit Transaction(transactionNonce++, gas, serializedTx); + } } function getLastSeenBlock() public virtual returns (uint256) { @@ -45,6 +50,10 @@ contract FuelMessagePortalV4 is FuelMessagePortalV3 { return 0; } + function getCurrentTransactionNonce() external virtual returns (uint256) { + return transactionNonce; + } + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. diff --git a/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts b/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts index f5d6005f..724269fd 100644 --- a/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts +++ b/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts @@ -1,7 +1,12 @@ import { MaxUint256 } from 'ethers'; import hre from 'hardhat'; -import { behavesLikeAccessControl } from './behaviors'; +import type { FuelMessagePortalV4 } from '../typechain'; + +import { + behavesLikeAccessControl, + behavesLikeFuelMessagePortalV4, +} from './behaviors'; const DEPOSIT_LIMIT = MaxUint256; const GAS_LIMIT = 1_000; @@ -23,18 +28,19 @@ describe.only('FuelMessagePortalV4', () => { // ], }); - const fuelMessagePortal = await upgrades.deployProxy( + const fuelMessagePortal = (await upgrades.deployProxy( FuelMessagePortal, [await fuelChainState.getAddress()], { initializer: 'initialize', constructorArgs: [DEPOSIT_LIMIT, GAS_LIMIT], } - ); + )) as unknown as FuelMessagePortalV4; return { signers, fuelMessagePortal }; } ); behavesLikeAccessControl(fixture, 'fuelMessagePortal'); + behavesLikeFuelMessagePortalV4(fixture); }); diff --git a/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts new file mode 100644 index 00000000..39a14aac --- /dev/null +++ b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts @@ -0,0 +1,49 @@ +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { randomInt } from 'crypto'; +import { randomBytes } from 'ethers'; + +import type { FuelMessagePortalV4 } from '../../typechain'; + +export type FuelMessagePortalV4Fixture = { + signers: HardhatEthersSigner[]; + fuelMessagePortal: FuelMessagePortalV4; + [key: string]: any; +}; + +export function behavesLikeFuelMessagePortalV4( + fixture: () => Promise +) { + describe('Includes access control features', () => { + describe('sendTransaction()', () => { + it('emits a Transaction event', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const gas = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + const tx = fuelMessagePortal.sendTransaction(gas, serializedTx); + + await expect(tx) + .to.emit(fuelMessagePortal, 'Transaction') + .withArgs(0, gas, serializedTx); + }); + + it('increments nonces', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const gas = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + await fuelMessagePortal.sendTransaction(gas, serializedTx); + const tx = fuelMessagePortal.sendTransaction(gas, serializedTx); + + await expect(tx) + .to.emit(fuelMessagePortal, 'Transaction') + .withArgs(1, gas, serializedTx); + }); + }); + }); +} diff --git a/packages/solidity-contracts/test/behaviors/index.ts b/packages/solidity-contracts/test/behaviors/index.ts index 64325616..46c37802 100644 --- a/packages/solidity-contracts/test/behaviors/index.ts +++ b/packages/solidity-contracts/test/behaviors/index.ts @@ -2,3 +2,4 @@ export * from './erc20GatewayV2.behavior.test'; export * from './erc20GatewayV3.behavior.test'; export * from './erc20GatewayV4.behavior.test'; export * from './AccessControl.behavior.test'; +export * from './FuelMessagePortalV4.behavior.test'; From c70923d6bf9866517c2acefbb8867a53aadf2699 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Mon, 10 Jun 2024 15:17:28 +0200 Subject: [PATCH 03/21] feat: update local deployments with FuelMessagePortalV4 --- ...essage_portal_v3.ts => 002.fuel_message_portal_v4.ts} | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) rename packages/solidity-contracts/deploy/hardhat/{002.fuel_message_portal_v3.ts => 002.fuel_message_portal_v4.ts} (81%) diff --git a/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v3.ts b/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts similarity index 81% rename from packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v3.ts rename to packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts index 0e1fb987..2be24cbe 100644 --- a/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v3.ts +++ b/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts @@ -2,7 +2,10 @@ import { MaxUint256 } from 'ethers'; import type { HardhatRuntimeEnvironment } from 'hardhat/types'; import type { DeployFunction } from 'hardhat-deploy/dist/types'; -import { FuelMessagePortalV3__factory as FuelMessagePortal } from '../../typechain'; +import { FuelMessagePortalV4__factory as FuelMessagePortal } from '../../typechain'; + +const ETH_DEPOSIT_LIMIT = MaxUint256; +const FTI_GAS_LIMIT = (2n ^ 64n) - 1n; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { @@ -19,7 +22,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { [fuelChainState], { initializer: 'initialize', - constructorArgs: [MaxUint256], + constructorArgs: [ETH_DEPOSIT_LIMIT, FTI_GAS_LIMIT], } ); await contract.waitForDeployment(); @@ -30,7 +33,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { console.log('Deployed FuelMessagePortal at', address); await save('FuelMessagePortal', { address, - abi: [], + abi: [...FuelMessagePortal.abi], implementation, }); From bc7fcb31128d4213ac51132e5742c578e6094fbd Mon Sep 17 00:00:00 2001 From: DefiCake Date: Mon, 10 Jun 2024 18:32:40 +0200 Subject: [PATCH 04/21] test: scaffold integration test for fti --- packages/integration-tests/tests/fti.ts | 122 ++++++++++++++++++ .../hardhat/002.fuel_message_portal_v4.ts | 8 +- packages/test-utils/src/utils/setup.ts | 8 +- 3 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 packages/integration-tests/tests/fti.ts diff --git a/packages/integration-tests/tests/fti.ts b/packages/integration-tests/tests/fti.ts new file mode 100644 index 00000000..5c4d4152 --- /dev/null +++ b/packages/integration-tests/tests/fti.ts @@ -0,0 +1,122 @@ +import type { TestEnvironment } from '@fuel-bridge/test-utils'; +import { + setupEnvironment, + fuels_parseEther, + createRelayMessageParams, + getMessageOutReceipt, + waitForMessage, + waitForBlockCommit, + waitForBlockFinalization, + getBlock, + FUEL_CALL_TX_PARAMS, +} from '@fuel-bridge/test-utils'; +import chai from 'chai'; +import type { Signer } from 'ethers'; +import { keccak256, parseEther, toUtf8Bytes } from 'ethers'; +import { Address, bn, padFirst12BytesOfEvmAddress } from 'fuels'; +import type { + AbstractAddress, + WalletUnlocked as FuelWallet, + MessageProof, + BN, +} from 'fuels'; + +const { expect } = chai; + +describe.only('Forced Transaction Inclusion', async function () { + // Timeout 6 minutes + const DEFAULT_TIMEOUT_MS: number = 400_000; + const FUEL_MESSAGE_TIMEOUT_MS: number = 30_000; + let BASE_ASSET_ID: string; + + let env: TestEnvironment; + + // override the default test timeout of 2000ms + this.timeout(DEFAULT_TIMEOUT_MS); + + before(async () => { + env = await setupEnvironment({}); + BASE_ASSET_ID = env.fuel.provider.getBaseAssetId(); + }); + + describe('Send a transaction through Ethereum', async () => { + const NUM_ETH = '0.1'; + let ethSender: any; + let fuelSender: FuelWallet; + let fuelReceiver: FuelWallet; + let fuelSignerBalance: BN; + + before(async () => { + ethSender; + fuelSender = env.fuel.deployer; + fuelReceiver = env.fuel.signers[1]; + fuelSignerBalance = await fuelSender.getBalance(BASE_ASSET_ID); + }); + + it('Send ETH via OutputMessage', async () => { + console.log('balance', fuelSignerBalance); + + const transferRequest = await fuelSender.createTransfer( + fuelReceiver.address, + fuelSignerBalance.div(10), + BASE_ASSET_ID + ); + const maxGas = transferRequest.calculateMaxGas( + (await env.fuel.provider.fetchChainAndNodeInfo()).chain, + bn(1) + ); + console.log('maxGas', maxGas.toString()); + const serializedTx = transferRequest.toTransactionBytes(); + + console.log('uhm', keccak256(toUtf8Bytes('GasLimit()'))); + + console.log(await env.eth.fuelMessagePortal.GAS_LIMIT()); + + await env.eth.fuelMessagePortal.sendTransaction( + maxGas.toString(), + serializedTx + ); + + // // withdraw ETH back to the base chain + // const fWithdrawTx = await fuelSigner.withdrawToBaseLayer( + // Address.fromString( + // padFirst12BytesOfEvmAddress(ethereumETHReceiverAddress) + // ), + // fuels_parseEther(NUM_ETH), + // FUEL_CALL_TX_PARAMS + // ); + // const fWithdrawTxResult = await fWithdrawTx.waitForResult(); + // expect(fWithdrawTxResult.status).to.equal('success'); + + // // Wait for the commited block + // const withdrawBlock = await getBlock( + // env.fuel.provider.url, + // fWithdrawTxResult.blockId + // ); + // const commitHashAtL1 = await waitForBlockCommit( + // env, + // withdrawBlock.header.height + // ); + + // // get message proof + // const messageOutReceipt = getMessageOutReceipt( + // fWithdrawTxResult.receipts + // ); + // withdrawMessageProof = await fuelSigner.provider.getMessageProof( + // fWithdrawTx.id, + // messageOutReceipt.nonce, + // commitHashAtL1 + // ); + + // // check that the sender balance has decreased by the expected amount + // const newSenderBalance = await fuelSigner.getBalance(BASE_ASSET_ID); + + // // Get just the first 3 digits of the balance to compare to the expected balance + // // this is required because the payment of gas fees is not deterministic + // const diffOnSenderBalance = newSenderBalance + // .sub(fuelSignerBalance) + // .formatUnits(); + // expect(diffOnSenderBalance.startsWith(NUM_ETH)).to.be.true; + }); + }); +}); diff --git a/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts b/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts index 2be24cbe..908930af 100644 --- a/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts +++ b/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts @@ -5,7 +5,7 @@ import type { DeployFunction } from 'hardhat-deploy/dist/types'; import { FuelMessagePortalV4__factory as FuelMessagePortal } from '../../typechain'; const ETH_DEPOSIT_LIMIT = MaxUint256; -const FTI_GAS_LIMIT = (2n ^ 64n) - 1n; +const FTI_GAS_LIMIT = 2n ** 64n - 1n; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { @@ -17,6 +17,9 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { address: fuelChainState } = await get('FuelChainState'); + console.log('holaaaa'); + console.log(FTI_GAS_LIMIT); + const contract = await deployProxy( new FuelMessagePortal(deployer), [fuelChainState], @@ -27,6 +30,9 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { ); await contract.waitForDeployment(); + console.log('waaaat'); + await contract.GAS_LIMIT().then(console.log); + const address = await contract.getAddress(); const implementation = await erc1967.getImplementationAddress(address); diff --git a/packages/test-utils/src/utils/setup.ts b/packages/test-utils/src/utils/setup.ts index 1212fda8..471a819a 100644 --- a/packages/test-utils/src/utils/setup.ts +++ b/packages/test-utils/src/utils/setup.ts @@ -2,13 +2,13 @@ /// A set of useful helper methods for setting up the integration test environment. import type { FuelChainState, - FuelMessagePortal, + FuelMessagePortalV4, FuelERC20GatewayV4 as FuelERC20Gateway, FuelERC721Gateway, } from '@fuel-bridge/solidity-contracts/typechain'; import { FuelChainState__factory, - FuelMessagePortal__factory, + FuelMessagePortalV4__factory as FuelMessagePortal__factory, FuelERC20GatewayV4__factory as FuelERC20Gateway__factory, FuelERC721Gateway__factory, } from '@fuel-bridge/solidity-contracts/typechain'; @@ -78,7 +78,7 @@ export interface TestEnvironment { provider: EthProvider; jsonRPC: string; fuelChainState: FuelChainState; - fuelMessagePortal: FuelMessagePortal; + fuelMessagePortal: FuelMessagePortalV4; fuelERC20Gateway: FuelERC20Gateway; fuelERC721Gateway: FuelERC721Gateway; deployer: EthSigner; @@ -275,7 +275,7 @@ export async function setupEnvironment( eth_fuelChainStateAddress, eth_deployer ); - const eth_fuelMessagePortal: FuelMessagePortal = + const eth_fuelMessagePortal: FuelMessagePortalV4 = FuelMessagePortal__factory.connect( eth_fuelMessagePortalAddress, eth_deployer From 4d39381c0242317016b8ddce896d1a62b5684dba Mon Sep 17 00:00:00 2001 From: DefiCake Date: Thu, 13 Jun 2024 12:35:41 +0200 Subject: [PATCH 05/21] chore: update fuel-core to 0.28 with testnet parameters --- docker/fuel-core/Dockerfile | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/docker/fuel-core/Dockerfile b/docker/fuel-core/Dockerfile index ace3eb38..4a04ac83 100644 --- a/docker/fuel-core/Dockerfile +++ b/docker/fuel-core/Dockerfile @@ -1,4 +1,9 @@ -FROM ghcr.io/fuellabs/fuel-core:v0.27.0 +# IMPORTANT! +# Make sure to check: +# https://github.com/FuelLabs/chain-configuration/tree/master/upgradelog/ignition +# and apply the latest state_transition_function and consensus_parameter +# when upgrading fuel-core +FROM ghcr.io/fuellabs/fuel-core:v0.28.0 ARG FUEL_IP=0.0.0.0 ARG FUEL_PORT=4001 @@ -13,12 +18,28 @@ WORKDIR /fuel COPY ./genesis_coins.json . -RUN git clone https://github.com/FuelLabs/chain-configuration.git /chain-configuration +RUN git clone \ + https://github.com/FuelLabs/chain-configuration.git \ + /chain-configuration +# Copy the base local configuration RUN cp -R /chain-configuration/local/* ./ -# merge genesis_coins.json into state_config.json -RUN jq '.coins = input' state_config.json genesis_coins.json > state_config_tmp.json && mv state_config_tmp.json state_config.json +# Copy the testnet consensus parameters and state transition bytecode +RUN cp /chain-configuration/upgradelog/ignition/consensus_parameters/3.json \ + ./latest_consensus_parameters.json +RUN cp /chain-configuration/upgradelog/ignition/state_transition_function/2.wasm \ + ./state_transition_bytecode.wasm + +# update local state_config with custom genesis coins config +RUN jq '.coins = input' \ + state_config.json genesis_coins.json > tmp.json \ + && mv tmp.json state_config.json + +# update local state_config with testnet consensus parameters +RUN jq '.consensus_parameters = input' \ + state_config.json latest_consensus_parameters.json > tmp.json \ + && mv tmp.json state_config.json # expose fuel node port ENV FUEL_IP="${FUEL_IP}" From 41f66731bac35736464f96a3b930284cba58de02 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Tue, 18 Jun 2024 21:52:42 +0200 Subject: [PATCH 06/21] test: scaffold integration test for fti --- docker/fuel-core/Dockerfile | 2 +- packages/integration-tests/tests/fti.ts | 153 +++++++++--------- .../test/FuelMessagePortalV4.test.ts | 11 +- packages/test-utils/src/utils/fuels/index.ts | 1 + .../src/utils/fuels/waitForTransaction.ts | 68 ++++++++ packages/test-utils/src/utils/setup.ts | 48 +++--- 6 files changed, 177 insertions(+), 106 deletions(-) create mode 100644 packages/test-utils/src/utils/fuels/waitForTransaction.ts diff --git a/docker/fuel-core/Dockerfile b/docker/fuel-core/Dockerfile index 4a04ac83..d330bb67 100644 --- a/docker/fuel-core/Dockerfile +++ b/docker/fuel-core/Dockerfile @@ -3,7 +3,7 @@ # https://github.com/FuelLabs/chain-configuration/tree/master/upgradelog/ignition # and apply the latest state_transition_function and consensus_parameter # when upgrading fuel-core -FROM ghcr.io/fuellabs/fuel-core:v0.28.0 +FROM ghcr.io/fuellabs/fuel-core:sha-c6112b0 ARG FUEL_IP=0.0.0.0 ARG FUEL_PORT=4001 diff --git a/packages/integration-tests/tests/fti.ts b/packages/integration-tests/tests/fti.ts index 5c4d4152..769844e7 100644 --- a/packages/integration-tests/tests/fti.ts +++ b/packages/integration-tests/tests/fti.ts @@ -1,33 +1,19 @@ import type { TestEnvironment } from '@fuel-bridge/test-utils'; -import { - setupEnvironment, - fuels_parseEther, - createRelayMessageParams, - getMessageOutReceipt, - waitForMessage, - waitForBlockCommit, - waitForBlockFinalization, - getBlock, - FUEL_CALL_TX_PARAMS, -} from '@fuel-bridge/test-utils'; +import { setupEnvironment, waitForTransaction } from '@fuel-bridge/test-utils'; import chai from 'chai'; -import type { Signer } from 'ethers'; -import { keccak256, parseEther, toUtf8Bytes } from 'ethers'; -import { Address, bn, padFirst12BytesOfEvmAddress } from 'fuels'; -import type { - AbstractAddress, - WalletUnlocked as FuelWallet, - MessageProof, - BN, -} from 'fuels'; +import { hexlify, solidityPacked } from 'ethers'; +import { sha256, transactionRequestify } from 'fuels'; +import type { WalletUnlocked as FuelWallet, BN } from 'fuels'; const { expect } = chai; +const MAX_GAS = 10000000; + describe.only('Forced Transaction Inclusion', async function () { // Timeout 6 minutes const DEFAULT_TIMEOUT_MS: number = 400_000; - const FUEL_MESSAGE_TIMEOUT_MS: number = 30_000; let BASE_ASSET_ID: string; + let CHAIN_ID: number; let env: TestEnvironment; @@ -37,6 +23,7 @@ describe.only('Forced Transaction Inclusion', async function () { before(async () => { env = await setupEnvironment({}); BASE_ASSET_ID = env.fuel.provider.getBaseAssetId(); + CHAIN_ID = env.fuel.provider.getChainId(); }); describe('Send a transaction through Ethereum', async () => { @@ -44,79 +31,93 @@ describe.only('Forced Transaction Inclusion', async function () { let ethSender: any; let fuelSender: FuelWallet; let fuelReceiver: FuelWallet; - let fuelSignerBalance: BN; + let fuelSenderBalance: BN; + let fuelReceiverBalance: BN; before(async () => { ethSender; fuelSender = env.fuel.deployer; fuelReceiver = env.fuel.signers[1]; - fuelSignerBalance = await fuelSender.getBalance(BASE_ASSET_ID); + fuelSenderBalance = await fuelSender.getBalance(BASE_ASSET_ID); + fuelReceiverBalance = await fuelReceiver.getBalance(BASE_ASSET_ID); }); - it('Send ETH via OutputMessage', async () => { - console.log('balance', fuelSignerBalance); + // it.skip('deposit to wallet address', async () => { + // await env.eth.fuelMessagePortal.depositETH(fuelSender.address.toB256(), { + // value: parseEther('1'), + // }); + + // let bal = await fuelSender.getBalance(BASE_ASSET_ID); + + // while (bal.toString() === '0') { + // bal = await fuelSender.getBalance(BASE_ASSET_ID); + // } + + // fuelSenderBalance = await fuelSender.getBalance(BASE_ASSET_ID); + // }); + it('allows to send transactions', async () => { const transferRequest = await fuelSender.createTransfer( fuelReceiver.address, - fuelSignerBalance.div(10), + fuelSenderBalance.div(10), BASE_ASSET_ID ); - const maxGas = transferRequest.calculateMaxGas( - (await env.fuel.provider.fetchChainAndNodeInfo()).chain, - bn(1) + + const transactionRequest = transactionRequestify(transferRequest); + await env.fuel.provider.estimateTxDependencies(transactionRequest); + + const signature = await fuelSender.signTransaction(transactionRequest); + transactionRequest.updateWitnessByOwner(fuelSender.address, signature); + + const fuelSerializedTx = hexlify(transactionRequest.toTransactionBytes()); + + const ethTx = await env.eth.fuelMessagePortal.sendTransaction( + MAX_GAS, + fuelSerializedTx ); - console.log('maxGas', maxGas.toString()); - const serializedTx = transferRequest.toTransactionBytes(); - console.log('uhm', keccak256(toUtf8Bytes('GasLimit()'))); + const { blockNumber } = await ethTx.wait(); - console.log(await env.eth.fuelMessagePortal.GAS_LIMIT()); + const [event] = await env.eth.fuelMessagePortal.queryFilter( + env.eth.fuelMessagePortal.filters.Transaction, + blockNumber, + blockNumber + ); + + expect(event.args.canonically_serialized_tx).to.be.equal( + fuelSerializedTx + ); - await env.eth.fuelMessagePortal.sendTransaction( - maxGas.toString(), - serializedTx + const payload = solidityPacked( + ['uint256', 'uint64', 'bytes'], + [ + event.args.nonce, + event.args.max_gas, + event.args.canonically_serialized_tx, + ] + ); + const relayedTxId = sha256(payload); + const fuelTxId = transactionRequest.getTransactionId(CHAIN_ID); + + const { response, error } = await waitForTransaction( + fuelTxId, + env.fuel.provider, + { + relayedTxId, + } ); - // // withdraw ETH back to the base chain - // const fWithdrawTx = await fuelSigner.withdrawToBaseLayer( - // Address.fromString( - // padFirst12BytesOfEvmAddress(ethereumETHReceiverAddress) - // ), - // fuels_parseEther(NUM_ETH), - // FUEL_CALL_TX_PARAMS - // ); - // const fWithdrawTxResult = await fWithdrawTx.waitForResult(); - // expect(fWithdrawTxResult.status).to.equal('success'); - - // // Wait for the commited block - // const withdrawBlock = await getBlock( - // env.fuel.provider.url, - // fWithdrawTxResult.blockId - // ); - // const commitHashAtL1 = await waitForBlockCommit( - // env, - // withdrawBlock.header.height - // ); - - // // get message proof - // const messageOutReceipt = getMessageOutReceipt( - // fWithdrawTxResult.receipts - // ); - // withdrawMessageProof = await fuelSigner.provider.getMessageProof( - // fWithdrawTx.id, - // messageOutReceipt.nonce, - // commitHashAtL1 - // ); - - // // check that the sender balance has decreased by the expected amount - // const newSenderBalance = await fuelSigner.getBalance(BASE_ASSET_ID); - - // // Get just the first 3 digits of the balance to compare to the expected balance - // // this is required because the payment of gas fees is not deterministic - // const diffOnSenderBalance = newSenderBalance - // .sub(fuelSignerBalance) - // .formatUnits(); - // expect(diffOnSenderBalance.startsWith(NUM_ETH)).to.be.true; + if (error) { + throw new Error(error); + } + + const txResult = await response.waitForResult(); + + expect(txResult.status).to.equal('success'); }); + + it('rejects transactions without signatures'); + + it('rejects transactions without gas'); }); }); diff --git a/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts b/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts index 724269fd..89acd34d 100644 --- a/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts +++ b/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts @@ -7,6 +7,7 @@ import { behavesLikeAccessControl, behavesLikeFuelMessagePortalV4, } from './behaviors'; +import { BLOCKS_PER_COMMIT_INTERVAL, TIME_TO_FINALIZE } from './utils'; const DEPOSIT_LIMIT = MaxUint256; const GAS_LIMIT = 1_000; @@ -21,11 +22,11 @@ describe.only('FuelMessagePortalV4', () => { const FuelChainState = await ethers.getContractFactory('FuelChainState'); const fuelChainState = await upgrades.deployProxy(FuelChainState, { initializer: 'initialize', - // constructorArgs: [ - // TIME_TO_FINALIZE, - // BLOCKS_PER_COMMIT_INTERVAL, - // TIME_TO_FINALIZE, - // ], + constructorArgs: [ + TIME_TO_FINALIZE, + BLOCKS_PER_COMMIT_INTERVAL, + TIME_TO_FINALIZE, + ], }); const fuelMessagePortal = (await upgrades.deployProxy( diff --git a/packages/test-utils/src/utils/fuels/index.ts b/packages/test-utils/src/utils/fuels/index.ts index f250f15a..623f3da8 100644 --- a/packages/test-utils/src/utils/fuels/index.ts +++ b/packages/test-utils/src/utils/fuels/index.ts @@ -5,3 +5,4 @@ export * from './getTokenId'; export * from './relayCommonMessage'; export * from './transaction'; export * from './waitForMessage'; +export * from './waitForTransaction'; diff --git a/packages/test-utils/src/utils/fuels/waitForTransaction.ts b/packages/test-utils/src/utils/fuels/waitForTransaction.ts new file mode 100644 index 00000000..b8313148 --- /dev/null +++ b/packages/test-utils/src/utils/fuels/waitForTransaction.ts @@ -0,0 +1,68 @@ +/// @dev The Fuel testing utils. +/// A set of useful helper methods for the integration test environment. +import type { + Provider as FuelProvider, + Message, + TransactionResponse, +} from 'fuels'; + +import { FUEL_MESSAGE_POLL_MS } from '../constants'; +import { delay } from '../delay'; +import { debug } from '../logs'; + +type Opts = { + relayedTxId?: string; + timeout?: number; +}; + +type Result = { + response: TransactionResponse | null; + error: string | null; +}; + +/** + * @description waits until a transaction has been included. Used mainly in FTI + * @param provider + * @param recipient + * @param nonce + * @param timeout + * @returns + */ +export async function waitForTransaction( + transactionId: string, + provider: FuelProvider, + opts: Opts, + timePassed = 0 +): Promise { + debug(`Waiting for transaction ${transactionId}`); + const startTime = new Date().getTime(); + + if (opts.relayedTxId) { + const relayedTxError = await provider.getRelayedTransactionStatus( + opts.relayedTxId + ); + + if (relayedTxError) { + return { response: null, error: relayedTxError.failure }; + } + } + + const tx = await provider.getTransaction(transactionId); + + if (!tx) { + if (opts?.timeout && timePassed > opts?.timeout) { + throw new Error(`Waiting for ${transactionId} timed out`); + } + + await delay(FUEL_MESSAGE_POLL_MS); + + timePassed += new Date().getTime() - startTime; + + return waitForTransaction(transactionId, provider, opts, timePassed); + } + + return { + response: await provider.getTransactionResponse(transactionId), + error: null, + }; +} diff --git a/packages/test-utils/src/utils/setup.ts b/packages/test-utils/src/utils/setup.ts index 471a819a..0e35fcf4 100644 --- a/packages/test-utils/src/utils/setup.ts +++ b/packages/test-utils/src/utils/setup.ts @@ -140,32 +140,32 @@ export async function setupEnvironment( ); } const fuel_deployer = Wallet.fromPrivateKey(pk_fuel_deployer, fuel_provider); - const fuel_deployerBalance = await fuel_deployer.getBalance(); - if (fuel_deployerBalance.lt(fuels_parseEther('5')) && skip_deployer_balance) { - throw new Error( - 'Fuel deployer balance is very low (' + - fuels_formatEther(fuel_deployerBalance) + - 'ETH)' - ); - } + // const fuel_deployerBalance = await fuel_deployer.getBalance(); + // if (fuel_deployerBalance.lt(fuels_parseEther('5')) && skip_deployer_balance) { + // throw new Error( + // 'Fuel deployer balance is very low (' + + // fuels_formatEther(fuel_deployerBalance) + + // 'ETH)' + // ); + // } const fuel_signer1 = Wallet.fromPrivateKey(pk_fuel_signer1, fuel_provider); - const fuel_signer1Balance = await fuel_signer1.getBalance(); - if (fuel_signer1Balance.lt(fuels_parseEther('1')) && skip_deployer_balance) { - const tx = await fuel_deployer.transfer( - fuel_signer1.address, - fuels_parseEther('1').toHex() - ); - await tx.wait(); - } + // const fuel_signer1Balance = await fuel_signer1.getBalance(); + // if (fuel_signer1Balance.lt(fuels_parseEther('1')) && skip_deployer_balance) { + // const tx = await fuel_deployer.transfer( + // fuel_signer1.address, + // fuels_parseEther('1').toHex() + // ); + // await tx.wait(); + // } const fuel_signer2 = Wallet.fromPrivateKey(pk_fuel_signer2, fuel_provider); - const fuel_signer2Balance = await fuel_signer2.getBalance(); - if (fuel_signer2Balance.lt(fuels_parseEther('1')) && skip_deployer_balance) { - const tx = await fuel_deployer.transfer( - fuel_signer2.address, - fuels_parseEther('1').toHex() - ); - await tx.wait(); - } + // const fuel_signer2Balance = await fuel_signer2.getBalance(); + // if (fuel_signer2Balance.lt(fuels_parseEther('1')) && skip_deployer_balance) { + // const tx = await fuel_deployer.transfer( + // fuel_signer2.address, + // fuels_parseEther('1').toHex() + // ); + // await tx.wait(); + // } // Create provider and signers from http_ethereum_client const eth_provider = new JsonRpcProvider(http_ethereum_client); From 76764ade5688ab742fe0ac456506af5d0c3af7c5 Mon Sep 17 00:00:00 2001 From: Mad <46090742+DefiCake@users.noreply.github.com> Date: Fri, 21 Jun 2024 21:53:46 +0200 Subject: [PATCH 07/21] (ci): fix pnpm audit alert and pnpm version for actions (#217) Closes #216 --- .changeset/happy-worms-breathe.md | 6 ++ .github/workflows/pr-release.yaml | 2 + .github/workflows/pr.yml | 6 ++ .github/workflows/release.yaml | 2 + package.json | 3 +- packages/integration-tests/package.json | 2 +- packages/solidity-contracts/package.json | 2 +- packages/test-utils/package.json | 2 +- pnpm-lock.yaml | 92 ++++++++++-------------- 9 files changed, 59 insertions(+), 58 deletions(-) create mode 100644 .changeset/happy-worms-breathe.md diff --git a/.changeset/happy-worms-breathe.md b/.changeset/happy-worms-breathe.md new file mode 100644 index 00000000..294c9395 --- /dev/null +++ b/.changeset/happy-worms-breathe.md @@ -0,0 +1,6 @@ +--- +'@fuel-bridge/solidity-contracts': patch +'@fuel-bridge/test-utils': patch +--- + +Fix CI on pnpm audit diff --git a/.github/workflows/pr-release.yaml b/.github/workflows/pr-release.yaml index 1a4dc06b..53dc392f 100644 --- a/.github/workflows/pr-release.yaml +++ b/.github/workflows/pr-release.yaml @@ -19,6 +19,8 @@ jobs: - uses: ./.github/actions/setup-rust - uses: FuelLabs/github-actions/setups/node@master + with: + pnpm-version: 9.0.6 - uses: FuelLabs/github-actions/setups/npm@master with: npm-token: ${{ secrets.NPM_TOKEN }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index a9558dc5..38e4c1f1 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -19,6 +19,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: FuelLabs/github-actions/setups/node@master + with: + pnpm-version: 9.0.6 - run: pnpm audit --prod --audit-level high check-packages-changed: @@ -47,6 +49,8 @@ jobs: # need this to get full git-history/clone in order to build changelogs and check changesets fetch-depth: 0 - uses: FuelLabs/github-actions/setups/node@master + with: + pnpm-version: 9.0.6 - run: pnpm changeset:check validate: @@ -55,6 +59,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: FuelLabs/github-actions/setups/node@master + with: + pnpm-version: 9.0.6 - uses: FuelLabs/github-actions/setups/docker@master with: username: ${{ github.repository_owner }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 055edbd8..50811c88 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -19,6 +19,8 @@ jobs: # need this to get full git-history/clone in order to build changelogs and check changesets fetch-depth: 0 - uses: FuelLabs/github-actions/setups/node@master + with: + pnpm-version: 9.0.6 - uses: FuelLabs/github-actions/setups/npm@master with: npm-token: ${{ secrets.NPM_TOKEN }} diff --git a/package.json b/package.json index 979ecdcd..8d289d05 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ }, "pnpm": { "overrides": { - "braces": ">=3.0.3" + "braces": ">=3.0.3", + "ws@<7.5.10": "7.5.10" } } } diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index 50365814..c40bcd59 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -23,7 +23,7 @@ "@types/mocha": "^9.1.1", "chai": "^4.3.6", "dotenv": "^16.0.3", - "ethers": "6.11.0", + "ethers": "6.13.1", "fuels": "0.89.1", "mocha": "^10.0.0", "ts-node": "^10.9.1", diff --git a/packages/solidity-contracts/package.json b/packages/solidity-contracts/package.json index ae506630..1d4c9b2b 100644 --- a/packages/solidity-contracts/package.json +++ b/packages/solidity-contracts/package.json @@ -58,7 +58,7 @@ "chai": "^4.3.7", "cors": "2.8.5", "dotenv": "^16.0.3", - "ethers": "6.11.0", + "ethers": "6.13.1", "express": "^4.18.2", "hardhat": "^2.20.1", "hardhat-deploy": "^0.11.44", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 425984e2..adeef9af 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -17,7 +17,7 @@ }, "peerDependencies": { "fuels": "0.89.1", - "ethers": "6.11.0" + "ethers": "6.13.1" }, "devDependencies": { "@fuel-bridge/fungible-token": "workspace:*", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2775920b..dec8d1c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,7 @@ settings: overrides: braces: '>=3.0.3' + ws@<7.5.10: 7.5.10 importers: @@ -88,8 +89,8 @@ importers: specifier: ^16.0.3 version: 16.4.5 ethers: - specifier: 6.11.0 - version: 6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7) + specifier: 6.13.1 + version: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) fuels: specifier: 0.89.1 version: 0.89.1 @@ -119,10 +120,10 @@ importers: version: 0.21.2 '@nomicfoundation/hardhat-chai-matchers': specifier: ^2.0.4 - version: 2.0.6(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(chai@4.4.1)(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + version: 2.0.6(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(chai@4.4.1)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) '@nomicfoundation/hardhat-ethers': specifier: ^3.0.5 - version: 3.0.5(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + version: 3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) '@nomicfoundation/hardhat-network-helpers': specifier: ^1.0.10 version: 1.0.10(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) @@ -137,13 +138,13 @@ importers: version: 4.9.6 '@openzeppelin/hardhat-upgrades': specifier: ^3.0.4 - version: 3.0.5(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(@nomicfoundation/hardhat-verify@1.1.1(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(bufferutil@4.0.5)(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(utf-8-validate@5.0.7) + version: 3.0.5(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(@nomicfoundation/hardhat-verify@1.1.1(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(bufferutil@4.0.5)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(utf-8-validate@5.0.7) '@typechain/ethers-v6': specifier: ^0.5.1 - version: 0.5.1(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5) + version: 0.5.1(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5) '@typechain/hardhat': specifier: ^9.1.0 - version: 9.1.0(@typechain/ethers-v6@0.5.1(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5))(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5)) + version: 9.1.0(@typechain/ethers-v6@0.5.1(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5))(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5)) '@types/chai': specifier: ^4.3.4 version: 4.3.14 @@ -178,8 +179,8 @@ importers: specifier: ^16.0.3 version: 16.4.5 ethers: - specifier: 6.11.0 - version: 6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7) + specifier: 6.13.1 + version: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) express: specifier: ^4.18.2 version: 4.19.2 @@ -235,8 +236,8 @@ importers: packages/test-utils: dependencies: ethers: - specifier: 6.11.0 - version: 6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7) + specifier: 6.13.1 + version: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) fuels: specifier: 0.89.1 version: 0.89.1 @@ -2538,8 +2539,8 @@ packages: ethers@5.7.2: resolution: {integrity: sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==} - ethers@6.11.0: - resolution: {integrity: sha512-kPHNTnhVWiWU6AVo6CAeTjXEK24SpCXyZvwG9ROFjT0Vlux0EOhWKBAeC+45iDj80QNJTYaT1SDEmeunT0vDNw==} + ethers@6.13.1: + resolution: {integrity: sha512-hdJ2HOxg/xx97Lm9HdCWk949BfYqYWpyw4//78SiwOLgASyfrNszfMUNB2joKjvGUdwhHfaiMMFFwacVVoLR9A==} engines: {node: '>=14.0.0'} ethjs-unit@0.1.6: @@ -4820,8 +4821,8 @@ packages: resolution: {integrity: sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==} engines: {node: '>=4'} - ws@7.4.6: - resolution: {integrity: sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==} + ws@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} peerDependencies: bufferutil: ^4.0.1 @@ -4832,24 +4833,12 @@ packages: utf-8-validate: optional: true - ws@7.5.9: - resolution: {integrity: sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.5.0: - resolution: {integrity: sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==} + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: '>=5.0.2' peerDependenciesMeta: bufferutil: optional: true @@ -5448,7 +5437,7 @@ snapshots: '@ethersproject/transactions': 5.7.0 '@ethersproject/web': 5.7.1 bech32: 1.1.4 - ws: 7.4.6(bufferutil@4.0.5)(utf-8-validate@5.0.7) + ws: 7.5.10(bufferutil@4.0.5)(utf-8-validate@5.0.7) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -5930,21 +5919,21 @@ snapshots: '@nomicfoundation/ethereumjs-rlp': 5.0.4 ethereum-cryptography: 0.1.3 - '@nomicfoundation/hardhat-chai-matchers@2.0.6(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(chai@4.4.1)(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': + '@nomicfoundation/hardhat-chai-matchers@2.0.6(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(chai@4.4.1)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': dependencies: - '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) '@types/chai-as-promised': 7.1.8 chai: 4.4.1 chai-as-promised: 7.1.1(chai@4.4.1) deep-eql: 4.1.3 - ethers: 6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7) + ethers: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) hardhat: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) ordinal: 1.0.3 - '@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': + '@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': dependencies: debug: 4.3.4(supports-color@8.1.1) - ethers: 6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7) + ethers: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) hardhat: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) lodash.isequal: 4.5.0 transitivePeerDependencies: @@ -6066,9 +6055,9 @@ snapshots: - debug - encoding - '@openzeppelin/hardhat-upgrades@3.0.5(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(@nomicfoundation/hardhat-verify@1.1.1(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(bufferutil@4.0.5)(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(utf-8-validate@5.0.7)': + '@openzeppelin/hardhat-upgrades@3.0.5(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(@nomicfoundation/hardhat-verify@1.1.1(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(bufferutil@4.0.5)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(utf-8-validate@5.0.7)': dependencies: - '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) '@openzeppelin/defender-admin-client': 1.54.1(bufferutil@4.0.5)(debug@4.3.4)(utf-8-validate@5.0.7) '@openzeppelin/defender-base-client': 1.54.1(debug@4.3.4) '@openzeppelin/defender-sdk-base-client': 1.12.0 @@ -6078,7 +6067,7 @@ snapshots: chalk: 4.1.2 debug: 4.3.4(supports-color@8.1.1) ethereumjs-util: 7.1.5 - ethers: 6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7) + ethers: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) hardhat: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) proper-lockfile: 4.1.2 undici: 6.13.0 @@ -6249,18 +6238,18 @@ snapshots: '@tsconfig/node16@1.0.4': {} - '@typechain/ethers-v6@0.5.1(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5)': + '@typechain/ethers-v6@0.5.1(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5)': dependencies: - ethers: 6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7) + ethers: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) lodash: 4.17.21 ts-essentials: 7.0.3(typescript@4.9.5) typechain: 8.3.2(typescript@4.9.5) typescript: 4.9.5 - '@typechain/hardhat@9.1.0(@typechain/ethers-v6@0.5.1(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5))(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))': + '@typechain/hardhat@9.1.0(@typechain/ethers-v6@0.5.1(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5))(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))': dependencies: - '@typechain/ethers-v6': 0.5.1(ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5) - ethers: 6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7) + '@typechain/ethers-v6': 0.5.1(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5) + ethers: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) fs-extra: 9.1.0 hardhat: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) typechain: 8.3.2(typescript@4.9.5) @@ -7909,7 +7898,7 @@ snapshots: - bufferutil - utf-8-validate - ethers@6.11.0(bufferutil@4.0.5)(utf-8-validate@5.0.7): + ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7): dependencies: '@adraffy/ens-normalize': 1.10.1 '@noble/curves': 1.2.0 @@ -7917,7 +7906,7 @@ snapshots: '@types/node': 18.15.13 aes-js: 4.0.0-beta.5 tslib: 2.4.0 - ws: 8.5.0(bufferutil@4.0.5)(utf-8-validate@5.0.7) + ws: 8.17.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -8460,7 +8449,7 @@ snapshots: tsort: 0.0.1 undici: 5.28.4 uuid: 8.3.2 - ws: 7.5.9(bufferutil@4.0.5)(utf-8-validate@5.0.7) + ws: 7.5.10(bufferutil@4.0.5)(utf-8-validate@5.0.7) optionalDependencies: ts-node: 10.9.2(@types/node@18.19.31)(typescript@4.9.5) typescript: 4.9.5 @@ -10577,17 +10566,12 @@ snapshots: dependencies: mkdirp: 0.5.6 - ws@7.4.6(bufferutil@4.0.5)(utf-8-validate@5.0.7): - optionalDependencies: - bufferutil: 4.0.5 - utf-8-validate: 5.0.7 - - ws@7.5.9(bufferutil@4.0.5)(utf-8-validate@5.0.7): + ws@7.5.10(bufferutil@4.0.5)(utf-8-validate@5.0.7): optionalDependencies: bufferutil: 4.0.5 utf-8-validate: 5.0.7 - ws@8.5.0(bufferutil@4.0.5)(utf-8-validate@5.0.7): + ws@8.17.1(bufferutil@4.0.5)(utf-8-validate@5.0.7): optionalDependencies: bufferutil: 4.0.5 utf-8-validate: 5.0.7 From c8c9b382da957829b2ee6c318629e61a463f4e70 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Tue, 18 Jun 2024 21:56:14 +0200 Subject: [PATCH 08/21] chore: add changeset --- .changeset/spicy-ghosts-call.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/spicy-ghosts-call.md diff --git a/.changeset/spicy-ghosts-call.md b/.changeset/spicy-ghosts-call.md new file mode 100644 index 00000000..63d11855 --- /dev/null +++ b/.changeset/spicy-ghosts-call.md @@ -0,0 +1,6 @@ +--- +'@fuel-bridge/solidity-contracts': minor +'@fuel-bridge/test-utils': minor +--- + +Add Forced Transaction Inclusion (FTI) to FuelMessagePortal From 2c94bd70dcb35ea319a4bf0f2fda3ccd6f27c041 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Tue, 25 Jun 2024 14:49:47 +0200 Subject: [PATCH 09/21] feat: add gas price computation --- .../v4/FuelMessagePortalV4.sol | 75 +++++- packages/solidity-contracts/hardhat.config.ts | 1 + .../test/FuelMessagePortalV4.test.ts | 11 +- .../FuelMessagePortalV4.behavior.test.ts | 234 +++++++++++++++++- .../test/utils/blockProduction.ts | 14 ++ .../solidity-contracts/test/utils/index.ts | 1 + pnpm-lock.yaml | 147 +++++------ 7 files changed, 384 insertions(+), 99 deletions(-) create mode 100644 packages/solidity-contracts/test/utils/blockProduction.ts diff --git a/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol index 02a4eff3..993ca781 100644 --- a/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol +++ b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol @@ -8,52 +8,103 @@ contract FuelMessagePortalV4 is FuelMessagePortalV3 { event Transaction(uint256 indexed nonce, uint64 max_gas, bytes canonically_serialized_tx); error GasLimit(); + error MinGas(); uint64 public immutable GAS_LIMIT; + uint64 public immutable GAS_TARGET; + uint64 public immutable MIN_GAS_PER_TX; + uint256 public immutable MIN_GAS_PRICE; uint192 internal lastSeenBlock; uint64 internal usedGas; - + uint256 internal gasPrice; uint256 internal transactionNonce; - constructor(uint256 _depositLimitGlobal, uint64 _gasLimit) FuelMessagePortalV3(_depositLimitGlobal) { - GAS_LIMIT = _gasLimit; + constructor( + uint256 depositLimitGlobal, + uint64 gasLimit, + uint64 minGasPerTx, + uint256 minGasPrice + ) FuelMessagePortalV3(depositLimitGlobal) { + GAS_LIMIT = gasLimit; + GAS_TARGET = gasLimit / 2; + MIN_GAS_PER_TX = minGasPerTx; + MIN_GAS_PRICE = minGasPrice; } function sendTransaction(uint64 gas, bytes calldata serializedTx) external payable virtual { + if (gas < MIN_GAS_PER_TX) { + revert MinGas(); + } + uint64 _usedGas = usedGas; + uint192 _lastSeenBlock = lastSeenBlock; + uint256 _gasPrice = gasPrice; + + if (_lastSeenBlock < block.number) { + uint256 distance; + unchecked { + distance = block.number - uint256(_lastSeenBlock); + } + + if (distance == 1) { + if (_usedGas > GAS_TARGET) { + // Max increment: x2 + _gasPrice = (_gasPrice * PRECISION * _usedGas) / GAS_TARGET; + } else { + // Max decrement: x0.5 + _gasPrice = (_gasPrice * (PRECISION + (_usedGas * PRECISION) / GAS_TARGET)) / 2; + } + + _gasPrice /= PRECISION; + } else { + _gasPrice /= distance; + } - if (lastSeenBlock == block.number) { - _usedGas += gas; - } else { _usedGas = gas; + } else { + _usedGas += gas; } if (_usedGas > GAS_LIMIT) { revert GasLimit(); } + if (_gasPrice < MIN_GAS_PRICE) { + _gasPrice = MIN_GAS_PRICE; + } + lastSeenBlock = uint192(block.number); usedGas = _usedGas; + gasPrice = _gasPrice; + unchecked { emit Transaction(transactionNonce++, gas, serializedTx); } } - function getLastSeenBlock() public virtual returns (uint256) { + function getLastSeenBlock() public view virtual returns (uint256) { return uint256(lastSeenBlock); } - function getUsedGas() external virtual returns (uint64) { - if (getLastSeenBlock() == block.number) return usedGas; - - return 0; + function getUsedGas() external view returns (uint64) { + return usedGas; } - function getCurrentTransactionNonce() external virtual returns (uint256) { + function getTransactionNonce() external view virtual returns (uint256) { return transactionNonce; } + function getGasPrice() external view virtual returns (uint256) { + return gasPrice; + } + + function getCurrentUsedGas() external view virtual returns (uint64) { + if (getLastSeenBlock() == block.number) return usedGas; + + return 0; + } + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. diff --git a/packages/solidity-contracts/hardhat.config.ts b/packages/solidity-contracts/hardhat.config.ts index 29d6e9f0..78a719a3 100644 --- a/packages/solidity-contracts/hardhat.config.ts +++ b/packages/solidity-contracts/hardhat.config.ts @@ -40,6 +40,7 @@ const config: HardhatUserConfig = { count: 128, }, deploy: ['deploy/hardhat'], + gas: 'auto', // https://github.com/NomicFoundation/hardhat/issues/4090 }, localhost: { url: 'http://127.0.0.1:8545/', diff --git a/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts b/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts index 89acd34d..557c33bb 100644 --- a/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts +++ b/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts @@ -1,4 +1,4 @@ -import { MaxUint256 } from 'ethers'; +import { MaxUint256, parseUnits } from 'ethers'; import hre from 'hardhat'; import type { FuelMessagePortalV4 } from '../typechain'; @@ -11,6 +11,8 @@ import { BLOCKS_PER_COMMIT_INTERVAL, TIME_TO_FINALIZE } from './utils'; const DEPOSIT_LIMIT = MaxUint256; const GAS_LIMIT = 1_000; +const MIN_GAS_PRICE = parseUnits('1', 'gwei'); +const MIN_GAS_PER_TX = 1; describe.only('FuelMessagePortalV4', () => { const fixture = hre.deployments.createFixture( @@ -34,7 +36,12 @@ describe.only('FuelMessagePortalV4', () => { [await fuelChainState.getAddress()], { initializer: 'initialize', - constructorArgs: [DEPOSIT_LIMIT, GAS_LIMIT], + constructorArgs: [ + DEPOSIT_LIMIT, + GAS_LIMIT, + MIN_GAS_PER_TX, + MIN_GAS_PRICE, + ], } )) as unknown as FuelMessagePortalV4; diff --git a/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts index 39a14aac..286ce32e 100644 --- a/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts +++ b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts @@ -1,9 +1,12 @@ +import hre from 'hardhat'; import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; +import { mine } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; import { randomInt } from 'crypto'; -import { randomBytes } from 'ethers'; +import { parseUnits, randomBytes } from 'ethers'; import type { FuelMessagePortalV4 } from '../../typechain'; +import { haltBlockProduction, resumeInstantBlockProduction } from '../utils'; export type FuelMessagePortalV4Fixture = { signers: HardhatEthersSigner[]; @@ -11,11 +14,26 @@ export type FuelMessagePortalV4Fixture = { [key: string]: any; }; +const SCALED_UNIT = 10n ** 18n; +const ONE_AND_A_HALF = SCALED_UNIT + SCALED_UNIT / 2n; + export function behavesLikeFuelMessagePortalV4( fixture: () => Promise ) { - describe('Includes access control features', () => { + let GAS_LIMIT: bigint; + let GAS_TARGET: bigint; + + describe('Behaves like FuelMessagePortalV4', () => { + before('cache gas limit', async () => { + const { fuelMessagePortal } = await fixture(); + GAS_LIMIT = await fuelMessagePortal.GAS_LIMIT(); + GAS_TARGET = await fuelMessagePortal.GAS_TARGET(); + }); describe('sendTransaction()', () => { + afterEach('restore block production', async () => { + await resumeInstantBlockProduction(hre); + }); + it('emits a Transaction event', async () => { const { fuelMessagePortal } = await fixture(); @@ -44,6 +62,218 @@ export function behavesLikeFuelMessagePortalV4( .to.emit(fuelMessagePortal, 'Transaction') .withArgs(1, gas, serializedTx); }); + + it('increments used gas', async () => { + const { + fuelMessagePortal, + signers: [signer], + } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const gas = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + await haltBlockProduction(hre); + + const nonce = await signer.getNonce(); + + const tx1 = await fuelMessagePortal + .connect(signer) + .sendTransaction(gas, serializedTx, { + nonce, + }); + const tx2 = await fuelMessagePortal + .connect(signer) + .sendTransaction(gas, serializedTx, { + nonce: nonce + 1, + }); + + await mine(); + + expect((await tx1.wait()).blockNumber).to.be.equal( + (await tx2.wait()).blockNumber + ); + + expect(await fuelMessagePortal.getUsedGas()).to.be.equal(gas * 2); + }); + + it('initializes gasPrice to MIN_GAS_PRICE', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const gas = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + await fuelMessagePortal.sendTransaction(gas, serializedTx); + + expect(await fuelMessagePortal.getGasPrice()).to.be.equal( + await fuelMessagePortal.MIN_GAS_PRICE() + ); + }); + + it('rejects when block is full', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + const tx = fuelMessagePortal.sendTransaction( + GAS_LIMIT + 1n, + serializedTx + ); + await expect(tx).to.be.revertedWithCustomError( + fuelMessagePortal, + 'GasLimit' + ); + }); + + describe('with increasing congestion (used gas above target)', () => { + it('duplicates gas price for full blocks gasPrice', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + await fuelMessagePortal.sendTransaction(1, serializedTx); // Initialize to 1 gwei + const initialGasPrice = await fuelMessagePortal.getGasPrice(); + expect(initialGasPrice).to.equal( + await fuelMessagePortal.MIN_GAS_PRICE() + ); + + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx); // Fills a block + await fuelMessagePortal.sendTransaction(1, serializedTx); // Update gas price + + expect(await fuelMessagePortal.getGasPrice()).to.equal( + initialGasPrice * 2n + ); + }); + + it('multiplies gas price by 1.5 for blocks 1.5 times above gas target', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const gas = GAS_TARGET + (GAS_LIMIT - GAS_TARGET) / 2n; + const serializedTx = randomBytes(payloadLength); + + await fuelMessagePortal.sendTransaction(1, serializedTx); // Initialize to 1 gwei + const initialGasPrice = await fuelMessagePortal.getGasPrice(); + + await fuelMessagePortal.sendTransaction(gas, serializedTx); + + await fuelMessagePortal.sendTransaction(1, serializedTx); // Update gas price + + expect(await fuelMessagePortal.getGasPrice()).to.equal( + (initialGasPrice * ONE_AND_A_HALF) / SCALED_UNIT + ); + }); + }); + + describe('with decreasing congestion (used gas below target)', () => { + describe('when there are multiple transactions in a row of blocks', () => { + it('multiplies gas price by 0.75 for blocks with GAS_TARGET / 2', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const gas = GAS_TARGET / 2n; + const serializedTx = randomBytes(payloadLength); + + await Promise.all([ + fuelMessagePortal.sendTransaction(1, serializedTx), // Initialize to 1 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 2 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 4 gwei + ]); + + await fuelMessagePortal.sendTransaction(gas, serializedTx); + + const initialGasPrice = await fuelMessagePortal.getGasPrice(); + expect(initialGasPrice).to.equal(parseUnits('4', 'gwei')); + + await fuelMessagePortal.sendTransaction(1, serializedTx); // Update gas price + + expect(await fuelMessagePortal.getGasPrice()).to.equal( + (initialGasPrice * 3n) / 4n + ); + }); + + it('multiplies gas price by ~0.5 for blocks with ~0 gas', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const gas = 1; + const serializedTx = randomBytes(payloadLength); + + await Promise.all([ + fuelMessagePortal.sendTransaction(1, serializedTx), // Initialize to 1 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 2 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 4 gwei + ]); + + await fuelMessagePortal.sendTransaction(gas, serializedTx); + + const initialGasPrice = await fuelMessagePortal.getGasPrice(); + expect(initialGasPrice).to.equal(parseUnits('4', 'gwei')); + + await fuelMessagePortal.sendTransaction(1, serializedTx); // Update gas price + + expect(await fuelMessagePortal.getGasPrice()).to.be.within( + initialGasPrice / 2n, + (initialGasPrice * 101n) / 200n + ); + }); + + it('maintains gas price if block hits gas target', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const gas = GAS_TARGET; + const serializedTx = randomBytes(payloadLength); + + await Promise.all([ + fuelMessagePortal.sendTransaction(1, serializedTx), // Initialize to 1 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 2 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 4 gwei + ]); + + await fuelMessagePortal.sendTransaction(gas, serializedTx); + + const initialGasPrice = await fuelMessagePortal.getGasPrice(); + expect(initialGasPrice).to.equal(parseUnits('4', 'gwei')); + + await fuelMessagePortal.sendTransaction(1, serializedTx); // Update gas price + + expect(await fuelMessagePortal.getGasPrice()).to.equal( + initialGasPrice + ); + }); + }); + + describe('when transactions are spaced out in a row of block', () => { + it('divides gasPrice by the distance', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const gas = GAS_LIMIT; + const serializedTx = randomBytes(payloadLength); + + await Promise.all([ + fuelMessagePortal.sendTransaction(gas, serializedTx), + fuelMessagePortal.sendTransaction(gas, serializedTx), + fuelMessagePortal.sendTransaction(gas, serializedTx), + fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx), + ]); + + const initialGasPrice = await fuelMessagePortal.getGasPrice(); + + const distance = 3; + await mine(distance - 1); // The last block will be mined by the tx + await fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx); + + expect(await fuelMessagePortal.getGasPrice()).to.equal( + initialGasPrice / BigInt(distance) + ); + }); + }); + }); }); }); } diff --git a/packages/solidity-contracts/test/utils/blockProduction.ts b/packages/solidity-contracts/test/utils/blockProduction.ts new file mode 100644 index 00000000..d4d3a04c --- /dev/null +++ b/packages/solidity-contracts/test/utils/blockProduction.ts @@ -0,0 +1,14 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +export async function haltBlockProduction(hre: HardhatRuntimeEnvironment) { + await hre.network.provider.send('evm_setAutomine', [false]); + await hre.network.provider.send('evm_setIntervalMining', [0]); +} + +export async function resumeInstantBlockProduction( + hre: HardhatRuntimeEnvironment, + interval = 0 +) { + await hre.network.provider.send('evm_setAutomine', [true]); + await hre.network.provider.send('evm_setIntervalMining', [interval]); +} diff --git a/packages/solidity-contracts/test/utils/index.ts b/packages/solidity-contracts/test/utils/index.ts index 8e4af959..e2592fed 100644 --- a/packages/solidity-contracts/test/utils/index.ts +++ b/packages/solidity-contracts/test/utils/index.ts @@ -5,3 +5,4 @@ export * from './impersonateAccount'; export * from './merkle'; export * from './deployProxy'; export * from './createRandomWalletWithFunds'; +export * from './blockProduction'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dec8d1c7..b6184da0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,16 +120,16 @@ importers: version: 0.21.2 '@nomicfoundation/hardhat-chai-matchers': specifier: ^2.0.4 - version: 2.0.6(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(chai@4.4.1)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + version: 2.0.6(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(chai@4.4.1)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) '@nomicfoundation/hardhat-ethers': specifier: ^3.0.5 - version: 3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + version: 3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) '@nomicfoundation/hardhat-network-helpers': specifier: ^1.0.10 - version: 1.0.10(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + version: 1.0.10(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) '@nomicfoundation/hardhat-verify': specifier: 1.1.1 - version: 1.1.1(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + version: 1.1.1(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) '@openzeppelin/contracts': specifier: ^4.8.3 version: 4.9.6 @@ -138,13 +138,13 @@ importers: version: 4.9.6 '@openzeppelin/hardhat-upgrades': specifier: ^3.0.4 - version: 3.0.5(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(@nomicfoundation/hardhat-verify@1.1.1(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(bufferutil@4.0.5)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(utf-8-validate@5.0.7) + version: 3.0.5(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(@nomicfoundation/hardhat-verify@1.1.1(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(bufferutil@4.0.5)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(utf-8-validate@5.0.7) '@typechain/ethers-v6': specifier: ^0.5.1 version: 0.5.1(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5) '@typechain/hardhat': specifier: ^9.1.0 - version: 9.1.0(@typechain/ethers-v6@0.5.1(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5))(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5)) + version: 9.1.0(@typechain/ethers-v6@0.5.1(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5))(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5)) '@types/chai': specifier: ^4.3.4 version: 4.3.14 @@ -186,7 +186,7 @@ importers: version: 4.19.2 hardhat: specifier: ^2.20.1 - version: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) + version: 2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) hardhat-deploy: specifier: ^0.11.44 version: 0.11.45(bufferutil@4.0.5)(utf-8-validate@5.0.7) @@ -216,7 +216,7 @@ importers: version: 3.3.7 solidity-coverage: specifier: ^0.8.5 - version: 0.8.12(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + version: 0.8.12(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) ts-generator: specifier: ^0.1.1 version: 0.1.1 @@ -939,50 +939,36 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nomicfoundation/edr-darwin-arm64@0.3.5': - resolution: {integrity: sha512-gIXUIiPMUy6roLHpNlxf15DumU7/YhffUf7XIB+WUjMecaySfTGyZsTGnCMJZqrDyiYqWPyPKwCV/2u/jqFAUg==} + '@nomicfoundation/edr-darwin-arm64@0.4.0': + resolution: {integrity: sha512-7+rraFk9tCqvfemv9Ita5vTlSBAeO/S5aDKOgGRgYt0JEKZlrX161nDW6UfzMPxWl9GOLEDUzCEaYuNmXseUlg==} engines: {node: '>= 18'} - cpu: [arm64] - os: [darwin] - '@nomicfoundation/edr-darwin-x64@0.3.5': - resolution: {integrity: sha512-0MrpOCXUK8gmplpYZ2Cy0holHEylvWoNeecFcrP2WJ5DLQzrB23U5JU2MvUzOJ7aL76Za1VXNBWi/UeTWdHM+w==} + '@nomicfoundation/edr-darwin-x64@0.4.0': + resolution: {integrity: sha512-+Hrc0mP9L6vhICJSfyGo/2taOToy1AIzVZawO3lU8Lf7oDQXfhQ4UkZnkWAs9SVu1eUwHUGGGE0qB8644piYgg==} engines: {node: '>= 18'} - cpu: [x64] - os: [darwin] - '@nomicfoundation/edr-linux-arm64-gnu@0.3.5': - resolution: {integrity: sha512-aw9f7AZMiY1dZFNePJGKho2k+nEgFgzUAyyukiKfSqUIMXoFXMf1U3Ujv848czrSq9c5XGcdDa2xnEf3daU3xg==} + '@nomicfoundation/edr-linux-arm64-gnu@0.4.0': + resolution: {integrity: sha512-4HUDMchNClQrVRfVTqBeSX92hM/3khCgpZkXP52qrnJPqgbdCxosOehlQYZ65wu0b/kaaZSyvACgvCLSQ5oSzQ==} engines: {node: '>= 18'} - cpu: [arm64] - os: [linux] - '@nomicfoundation/edr-linux-arm64-musl@0.3.5': - resolution: {integrity: sha512-cVFRQjyABBlsbDj+XTczYBfrCHprZ6YNzN8gGGSqAh+UGIJkAIRomK6ar27GyJLNx3HkgbuDoi/9kA0zOo/95w==} + '@nomicfoundation/edr-linux-arm64-musl@0.4.0': + resolution: {integrity: sha512-D4J935ZRL8xfnP3zIFlCI9jXInJ0loDUkCTLeCEbOf2uuDumWDghKNQlF1itUS+EHaR1pFVBbuwqq8hVK0dASg==} engines: {node: '>= 18'} - cpu: [arm64] - os: [linux] - '@nomicfoundation/edr-linux-x64-gnu@0.3.5': - resolution: {integrity: sha512-CjOg85DfR1Vt0fQWn5U0qi26DATK9tVzo3YOZEyI0JBsnqvk43fUTPv3uUAWBrPIRg5O5kOc9xG13hSpCBBxBg==} + '@nomicfoundation/edr-linux-x64-gnu@0.4.0': + resolution: {integrity: sha512-6x7HPy+uN5Cb9N77e2XMmT6+QSJ+7mRbHnhkGJ8jm4cZvWuj2Io7npOaeHQ3YHK+TiQpTnlbkjoOIpEwpY3XZA==} engines: {node: '>= 18'} - cpu: [x64] - os: [linux] - '@nomicfoundation/edr-linux-x64-musl@0.3.5': - resolution: {integrity: sha512-hvX8bBGpBydAVevzK8jsu2FlqVZK1RrCyTX6wGHnltgMuBaoGLHYtNHiFpteOaJw2byYMiORc2bvj+98LhJ0Ew==} + '@nomicfoundation/edr-linux-x64-musl@0.4.0': + resolution: {integrity: sha512-3HFIJSXgyubOiaN4MWGXx2xhTnhwlJk0PiSYNf9+L/fjBtcRkb2nM910ZJHTvqCb6OT98cUnaKuAYdXIW2amgw==} engines: {node: '>= 18'} - cpu: [x64] - os: [linux] - '@nomicfoundation/edr-win32-x64-msvc@0.3.5': - resolution: {integrity: sha512-IJXjW13DY5UPsx/eG5DGfXtJ7Ydwrvw/BTZ2Y93lRLHzszVpSmeVmlxjZP5IW2afTSgMLaAAsqNw4NhppRGN8A==} + '@nomicfoundation/edr-win32-x64-msvc@0.4.0': + resolution: {integrity: sha512-CP4GsllEfXEz+lidcGYxKe5rDJ60TM5/blB5z/04ELVvw6/CK9eLcYeku7HV0jvV7VE6dADYKSdQyUkvd0El+A==} engines: {node: '>= 18'} - cpu: [x64] - os: [win32] - '@nomicfoundation/edr@0.3.5': - resolution: {integrity: sha512-dPSM9DuI1sr71gqWUMgLo8MjHQWO4+WNDm3iWaT6P4vUFJReZX5qwA5X+3UwIPBry8GvNY084u7yWUvB3/8rqA==} + '@nomicfoundation/edr@0.4.0': + resolution: {integrity: sha512-T96DMSogO8TCdbKKctvxfsDljbhFOUKWc9fHJhSeUh71EEho2qR4951LKQF7t7UWEzguVYh/idQr5L/E3QeaMw==} engines: {node: '>= 18'} '@nomicfoundation/ethereumjs-common@4.0.4': @@ -2790,6 +2776,7 @@ packages: glob@7.2.0: resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + deprecated: Glob versions prior to v9 are no longer supported glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} @@ -2869,8 +2856,8 @@ packages: hardhat-deploy@0.11.45: resolution: {integrity: sha512-aC8UNaq3JcORnEUIwV945iJuvBwi65tjHVDU3v6mOcqik7WAzHVCJ7cwmkkipsHrWysrB5YvGF1q9S1vIph83w==} - hardhat@2.22.3: - resolution: {integrity: sha512-k8JV2ECWNchD6ahkg2BR5wKVxY0OiKot7fuxiIpRK0frRqyOljcR2vKwgWSLw6YIeDcNNA4xybj7Og7NSxr2hA==} + hardhat@2.22.5: + resolution: {integrity: sha512-9Zq+HonbXCSy6/a13GY1cgHglQRfh4qkzmj1tpPlhxJDwNVnhxlReV6K7hCWFKlOrV13EQwsdcD0rjcaQKWRZw==} hasBin: true peerDependencies: ts-node: '*' @@ -4051,6 +4038,7 @@ packages: rimraf@2.7.1: resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@3.0.2: @@ -5868,36 +5856,29 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - '@nomicfoundation/edr-darwin-arm64@0.3.5': - optional: true + '@nomicfoundation/edr-darwin-arm64@0.4.0': {} - '@nomicfoundation/edr-darwin-x64@0.3.5': - optional: true + '@nomicfoundation/edr-darwin-x64@0.4.0': {} - '@nomicfoundation/edr-linux-arm64-gnu@0.3.5': - optional: true + '@nomicfoundation/edr-linux-arm64-gnu@0.4.0': {} - '@nomicfoundation/edr-linux-arm64-musl@0.3.5': - optional: true + '@nomicfoundation/edr-linux-arm64-musl@0.4.0': {} - '@nomicfoundation/edr-linux-x64-gnu@0.3.5': - optional: true + '@nomicfoundation/edr-linux-x64-gnu@0.4.0': {} - '@nomicfoundation/edr-linux-x64-musl@0.3.5': - optional: true + '@nomicfoundation/edr-linux-x64-musl@0.4.0': {} - '@nomicfoundation/edr-win32-x64-msvc@0.3.5': - optional: true + '@nomicfoundation/edr-win32-x64-msvc@0.4.0': {} - '@nomicfoundation/edr@0.3.5': - optionalDependencies: - '@nomicfoundation/edr-darwin-arm64': 0.3.5 - '@nomicfoundation/edr-darwin-x64': 0.3.5 - '@nomicfoundation/edr-linux-arm64-gnu': 0.3.5 - '@nomicfoundation/edr-linux-arm64-musl': 0.3.5 - '@nomicfoundation/edr-linux-x64-gnu': 0.3.5 - '@nomicfoundation/edr-linux-x64-musl': 0.3.5 - '@nomicfoundation/edr-win32-x64-msvc': 0.3.5 + '@nomicfoundation/edr@0.4.0': + dependencies: + '@nomicfoundation/edr-darwin-arm64': 0.4.0 + '@nomicfoundation/edr-darwin-x64': 0.4.0 + '@nomicfoundation/edr-linux-arm64-gnu': 0.4.0 + '@nomicfoundation/edr-linux-arm64-musl': 0.4.0 + '@nomicfoundation/edr-linux-x64-gnu': 0.4.0 + '@nomicfoundation/edr-linux-x64-musl': 0.4.0 + '@nomicfoundation/edr-win32-x64-msvc': 0.4.0 '@nomicfoundation/ethereumjs-common@4.0.4': dependencies: @@ -5919,39 +5900,39 @@ snapshots: '@nomicfoundation/ethereumjs-rlp': 5.0.4 ethereum-cryptography: 0.1.3 - '@nomicfoundation/hardhat-chai-matchers@2.0.6(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(chai@4.4.1)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': + '@nomicfoundation/hardhat-chai-matchers@2.0.6(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(chai@4.4.1)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': dependencies: - '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) '@types/chai-as-promised': 7.1.8 chai: 4.4.1 chai-as-promised: 7.1.1(chai@4.4.1) deep-eql: 4.1.3 ethers: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) - hardhat: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) + hardhat: 2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) ordinal: 1.0.3 - '@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': + '@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': dependencies: debug: 4.3.4(supports-color@8.1.1) ethers: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) - hardhat: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) + hardhat: 2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) lodash.isequal: 4.5.0 transitivePeerDependencies: - supports-color - '@nomicfoundation/hardhat-network-helpers@1.0.10(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': + '@nomicfoundation/hardhat-network-helpers@1.0.10(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': dependencies: ethereumjs-util: 7.1.5 - hardhat: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) + hardhat: 2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) - '@nomicfoundation/hardhat-verify@1.1.1(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': + '@nomicfoundation/hardhat-verify@1.1.1(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))': dependencies: '@ethersproject/abi': 5.7.0 '@ethersproject/address': 5.7.0 cbor: 8.1.0 chalk: 2.4.2 debug: 4.3.4(supports-color@8.1.1) - hardhat: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) + hardhat: 2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) lodash.clonedeep: 4.5.0 semver: 6.3.1 table: 6.8.2 @@ -6055,9 +6036,9 @@ snapshots: - debug - encoding - '@openzeppelin/hardhat-upgrades@3.0.5(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(@nomicfoundation/hardhat-verify@1.1.1(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(bufferutil@4.0.5)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(utf-8-validate@5.0.7)': + '@openzeppelin/hardhat-upgrades@3.0.5(@nomicfoundation/hardhat-ethers@3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(@nomicfoundation/hardhat-verify@1.1.1(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)))(bufferutil@4.0.5)(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(utf-8-validate@5.0.7)': dependencies: - '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + '@nomicfoundation/hardhat-ethers': 3.0.5(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) '@openzeppelin/defender-admin-client': 1.54.1(bufferutil@4.0.5)(debug@4.3.4)(utf-8-validate@5.0.7) '@openzeppelin/defender-base-client': 1.54.1(debug@4.3.4) '@openzeppelin/defender-sdk-base-client': 1.12.0 @@ -6068,11 +6049,11 @@ snapshots: debug: 4.3.4(supports-color@8.1.1) ethereumjs-util: 7.1.5 ethers: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) - hardhat: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) + hardhat: 2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) proper-lockfile: 4.1.2 undici: 6.13.0 optionalDependencies: - '@nomicfoundation/hardhat-verify': 1.1.1(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) + '@nomicfoundation/hardhat-verify': 1.1.1(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)) transitivePeerDependencies: - bufferutil - encoding @@ -6246,12 +6227,12 @@ snapshots: typechain: 8.3.2(typescript@4.9.5) typescript: 4.9.5 - '@typechain/hardhat@9.1.0(@typechain/ethers-v6@0.5.1(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5))(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))': + '@typechain/hardhat@9.1.0(@typechain/ethers-v6@0.5.1(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5))(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))': dependencies: '@typechain/ethers-v6': 0.5.1(ethers@6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7))(typechain@8.3.2(typescript@4.9.5))(typescript@4.9.5) ethers: 6.13.1(bufferutil@4.0.5)(utf-8-validate@5.0.7) fs-extra: 9.1.0 - hardhat: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) + hardhat: 2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) typechain: 8.3.2(typescript@4.9.5) '@types/bn.js@4.11.6': @@ -8405,11 +8386,11 @@ snapshots: - supports-color - utf-8-validate - hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7): + hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7): dependencies: '@ethersproject/abi': 5.7.0 '@metamask/eth-sig-util': 4.0.1 - '@nomicfoundation/edr': 0.3.5 + '@nomicfoundation/edr': 0.4.0 '@nomicfoundation/ethereumjs-common': 4.0.4 '@nomicfoundation/ethereumjs-tx': 5.0.4 '@nomicfoundation/ethereumjs-util': 9.0.4 @@ -9582,7 +9563,7 @@ snapshots: rimraf@2.7.1: dependencies: - glob: 7.2.0 + glob: 7.2.3 rimraf@3.0.2: dependencies: @@ -9851,7 +9832,7 @@ snapshots: solidity-comments-extractor@0.0.8: {} - solidity-coverage@0.8.12(hardhat@2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)): + solidity-coverage@0.8.12(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7)): dependencies: '@ethersproject/abi': 5.7.0 '@solidity-parser/parser': 0.18.0 @@ -9862,7 +9843,7 @@ snapshots: ghost-testrpc: 0.0.2 global-modules: 2.0.0 globby: 10.0.2 - hardhat: 2.22.3(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) + hardhat: 2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) jsonschema: 1.4.1 lodash: 4.17.21 mocha: 10.4.0 From f0d0fc0ec9ccee30a2d4d8541e14c456c7e032ab Mon Sep 17 00:00:00 2001 From: DefiCake Date: Tue, 25 Jun 2024 15:10:23 +0200 Subject: [PATCH 10/21] test: add fti min gas price test and fix flakiness --- .../FuelMessagePortalV4.behavior.test.ts | 47 +++++++++++++++---- .../test/utils/blockProduction.ts | 5 +- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts index 286ce32e..a56f11bc 100644 --- a/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts +++ b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts @@ -22,13 +22,20 @@ export function behavesLikeFuelMessagePortalV4( ) { let GAS_LIMIT: bigint; let GAS_TARGET: bigint; + let MIN_GAS_PER_TX: number; + let MIN_GAS_PRICE: bigint; describe('Behaves like FuelMessagePortalV4', () => { before('cache gas limit', async () => { const { fuelMessagePortal } = await fixture(); GAS_LIMIT = await fuelMessagePortal.GAS_LIMIT(); GAS_TARGET = await fuelMessagePortal.GAS_TARGET(); + MIN_GAS_PRICE = await fuelMessagePortal.MIN_GAS_PRICE(); + MIN_GAS_PER_TX = Number( + (await fuelMessagePortal.MIN_GAS_PER_TX()).toString() + ); }); + describe('sendTransaction()', () => { afterEach('restore block production', async () => { await resumeInstantBlockProduction(hre); @@ -38,7 +45,7 @@ export function behavesLikeFuelMessagePortalV4( const { fuelMessagePortal } = await fixture(); const payloadLength = Math.abs(randomInt(256)); - const gas = Math.abs(randomInt(256)); + const gas = Math.abs(randomInt(MIN_GAS_PER_TX, 256)); const serializedTx = randomBytes(payloadLength); const tx = fuelMessagePortal.sendTransaction(gas, serializedTx); @@ -52,7 +59,7 @@ export function behavesLikeFuelMessagePortalV4( const { fuelMessagePortal } = await fixture(); const payloadLength = Math.abs(randomInt(256)); - const gas = Math.abs(randomInt(256)); + const gas = Math.abs(randomInt(MIN_GAS_PER_TX, 256)); const serializedTx = randomBytes(payloadLength); await fuelMessagePortal.sendTransaction(gas, serializedTx); @@ -70,7 +77,7 @@ export function behavesLikeFuelMessagePortalV4( } = await fixture(); const payloadLength = Math.abs(randomInt(256)); - const gas = Math.abs(randomInt(256)); + const gas = Math.abs(randomInt(MIN_GAS_PER_TX, 256)); const serializedTx = randomBytes(payloadLength); await haltBlockProduction(hre); @@ -101,7 +108,7 @@ export function behavesLikeFuelMessagePortalV4( const { fuelMessagePortal } = await fixture(); const payloadLength = Math.abs(randomInt(256)); - const gas = Math.abs(randomInt(256)); + const gas = Math.abs(randomInt(MIN_GAS_PER_TX, 256)); const serializedTx = randomBytes(payloadLength); await fuelMessagePortal.sendTransaction(gas, serializedTx); @@ -178,7 +185,7 @@ export function behavesLikeFuelMessagePortalV4( const serializedTx = randomBytes(payloadLength); await Promise.all([ - fuelMessagePortal.sendTransaction(1, serializedTx), // Initialize to 1 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Initialize to 1 gwei fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 2 gwei fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 4 gwei ]); @@ -186,7 +193,7 @@ export function behavesLikeFuelMessagePortalV4( await fuelMessagePortal.sendTransaction(gas, serializedTx); const initialGasPrice = await fuelMessagePortal.getGasPrice(); - expect(initialGasPrice).to.equal(parseUnits('4', 'gwei')); + expect(initialGasPrice).to.equal(parseUnits('8', 'gwei')); await fuelMessagePortal.sendTransaction(1, serializedTx); // Update gas price @@ -252,13 +259,12 @@ export function behavesLikeFuelMessagePortalV4( const { fuelMessagePortal } = await fixture(); const payloadLength = Math.abs(randomInt(256)); - const gas = GAS_LIMIT; const serializedTx = randomBytes(payloadLength); await Promise.all([ - fuelMessagePortal.sendTransaction(gas, serializedTx), - fuelMessagePortal.sendTransaction(gas, serializedTx), - fuelMessagePortal.sendTransaction(gas, serializedTx), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx), ]); @@ -272,6 +278,27 @@ export function behavesLikeFuelMessagePortalV4( initialGasPrice / BigInt(distance) ); }); + + it('bottoms gas price at MIN_GAS_PRICE', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + await Promise.all([ + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), + ]); + + await mine(200); + await fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx); + + expect(await fuelMessagePortal.getGasPrice()).to.equal( + MIN_GAS_PRICE + ); + }); }); }); }); diff --git a/packages/solidity-contracts/test/utils/blockProduction.ts b/packages/solidity-contracts/test/utils/blockProduction.ts index d4d3a04c..4cb1928a 100644 --- a/packages/solidity-contracts/test/utils/blockProduction.ts +++ b/packages/solidity-contracts/test/utils/blockProduction.ts @@ -2,13 +2,10 @@ import { HardhatRuntimeEnvironment } from 'hardhat/types'; export async function haltBlockProduction(hre: HardhatRuntimeEnvironment) { await hre.network.provider.send('evm_setAutomine', [false]); - await hre.network.provider.send('evm_setIntervalMining', [0]); } export async function resumeInstantBlockProduction( - hre: HardhatRuntimeEnvironment, - interval = 0 + hre: HardhatRuntimeEnvironment ) { await hre.network.provider.send('evm_setAutomine', [true]); - await hre.network.provider.send('evm_setIntervalMining', [interval]); } From 6a3ef35d88a022e748ad0343820fa8d77e1d8dd2 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Tue, 25 Jun 2024 15:53:56 +0200 Subject: [PATCH 11/21] feat: add fee collection --- .../v4/FuelMessagePortalV4.sol | 33 +++ .../FuelMessagePortalV4.behavior.test.ts | 246 +++++++++++++++--- 2 files changed, 243 insertions(+), 36 deletions(-) diff --git a/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol index 993ca781..2b169ff3 100644 --- a/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol +++ b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol @@ -9,6 +9,10 @@ contract FuelMessagePortalV4 is FuelMessagePortalV3 { error GasLimit(); error MinGas(); + error InsufficientFee(); + error RecipientRejectedETH(); + + bytes32 public constant FEE_COLLECTOR_ROLE = keccak256("FEE_COLLECTOR_ROLE"); uint64 public immutable GAS_LIMIT; uint64 public immutable GAS_TARGET; @@ -81,6 +85,30 @@ contract FuelMessagePortalV4 is FuelMessagePortalV3 { unchecked { emit Transaction(transactionNonce++, gas, serializedTx); } + + uint256 fee = _gasPrice * gas; + + if (msg.value != fee) { + if (msg.value < fee) { + revert InsufficientFee(); + } + + unchecked { + (bool success, bytes memory result) = _msgSender().call{value: msg.value - fee}(""); + if (!success) { + // Look for revert reason and bubble it up if present + if (result.length > 0) { + // The easiest way to bubble the revert reason is using memory via assembly + /// @solidity memory-safe-assembly + assembly { + let returndata_size := mload(result) + revert(add(32, result), returndata_size) + } + } + revert RecipientRejectedETH(); + } + } + } } function getLastSeenBlock() public view virtual returns (uint256) { @@ -105,6 +133,11 @@ contract FuelMessagePortalV4 is FuelMessagePortalV3 { return 0; } + function collectFees() external onlyRole(FEE_COLLECTOR_ROLE) { + (bool success, ) = _msgSender().call{value: address(this).balance}(""); + if (!success) revert RecipientRejectedETH(); + } + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. diff --git a/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts index a56f11bc..9e838a53 100644 --- a/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts +++ b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts @@ -3,7 +3,7 @@ import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signer import { mine } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; import { randomInt } from 'crypto'; -import { parseUnits, randomBytes } from 'ethers'; +import { parseEther, parseUnits, randomBytes } from 'ethers'; import type { FuelMessagePortalV4 } from '../../typechain'; import { haltBlockProduction, resumeInstantBlockProduction } from '../utils'; @@ -24,6 +24,7 @@ export function behavesLikeFuelMessagePortalV4( let GAS_TARGET: bigint; let MIN_GAS_PER_TX: number; let MIN_GAS_PRICE: bigint; + let FEE_COLLECTOR_ROLE: string; describe('Behaves like FuelMessagePortalV4', () => { before('cache gas limit', async () => { @@ -34,6 +35,7 @@ export function behavesLikeFuelMessagePortalV4( MIN_GAS_PER_TX = Number( (await fuelMessagePortal.MIN_GAS_PER_TX()).toString() ); + FEE_COLLECTOR_ROLE = await fuelMessagePortal.FEE_COLLECTOR_ROLE(); }); describe('sendTransaction()', () => { @@ -48,7 +50,9 @@ export function behavesLikeFuelMessagePortalV4( const gas = Math.abs(randomInt(MIN_GAS_PER_TX, 256)); const serializedTx = randomBytes(payloadLength); - const tx = fuelMessagePortal.sendTransaction(gas, serializedTx); + const tx = fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); await expect(tx) .to.emit(fuelMessagePortal, 'Transaction') @@ -62,8 +66,12 @@ export function behavesLikeFuelMessagePortalV4( const gas = Math.abs(randomInt(MIN_GAS_PER_TX, 256)); const serializedTx = randomBytes(payloadLength); - await fuelMessagePortal.sendTransaction(gas, serializedTx); - const tx = fuelMessagePortal.sendTransaction(gas, serializedTx); + await fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); + const tx = fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); await expect(tx) .to.emit(fuelMessagePortal, 'Transaction') @@ -88,11 +96,13 @@ export function behavesLikeFuelMessagePortalV4( .connect(signer) .sendTransaction(gas, serializedTx, { nonce, + value: parseEther('1'), }); const tx2 = await fuelMessagePortal .connect(signer) .sendTransaction(gas, serializedTx, { nonce: nonce + 1, + value: parseEther('1'), }); await mine(); @@ -111,7 +121,9 @@ export function behavesLikeFuelMessagePortalV4( const gas = Math.abs(randomInt(MIN_GAS_PER_TX, 256)); const serializedTx = randomBytes(payloadLength); - await fuelMessagePortal.sendTransaction(gas, serializedTx); + await fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); expect(await fuelMessagePortal.getGasPrice()).to.be.equal( await fuelMessagePortal.MIN_GAS_PRICE() @@ -134,6 +146,64 @@ export function behavesLikeFuelMessagePortalV4( ); }); + it('collects fees', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + const expectedFee = MIN_GAS_PRICE * GAS_LIMIT; + + const tx = fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: expectedFee, + }); + + await expect(tx).to.changeEtherBalance(fuelMessagePortal, expectedFee); + }); + + it('returns excess fees', async () => { + const { + fuelMessagePortal, + signers: [signer], + } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + const expectedFee = MIN_GAS_PRICE * GAS_LIMIT; + const excessFee = parseEther('1'); + const tx = fuelMessagePortal + .connect(signer) + .sendTransaction(GAS_LIMIT, serializedTx, { + value: expectedFee + excessFee, + }); + + await expect(tx).to.changeEtherBalance(fuelMessagePortal, expectedFee); + await expect(tx).to.changeEtherBalance(signer, -expectedFee); + }); + + it('reject underfunded transactions', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + const expectedFee = MIN_GAS_PRICE * GAS_LIMIT; + + const tx = fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: expectedFee - 1n, + }); + + await expect(tx).to.be.revertedWithCustomError( + fuelMessagePortal, + 'InsufficientFee' + ); + + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: expectedFee, + }); + }); + describe('with increasing congestion (used gas above target)', () => { it('duplicates gas price for full blocks gasPrice', async () => { const { fuelMessagePortal } = await fixture(); @@ -141,14 +211,20 @@ export function behavesLikeFuelMessagePortalV4( const payloadLength = Math.abs(randomInt(256)); const serializedTx = randomBytes(payloadLength); - await fuelMessagePortal.sendTransaction(1, serializedTx); // Initialize to 1 gwei + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // Initialize to 1 gwei const initialGasPrice = await fuelMessagePortal.getGasPrice(); expect(initialGasPrice).to.equal( await fuelMessagePortal.MIN_GAS_PRICE() ); - await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx); // Fills a block - await fuelMessagePortal.sendTransaction(1, serializedTx); // Update gas price + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); // Fills a block + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // Update gas price expect(await fuelMessagePortal.getGasPrice()).to.equal( initialGasPrice * 2n @@ -162,12 +238,18 @@ export function behavesLikeFuelMessagePortalV4( const gas = GAS_TARGET + (GAS_LIMIT - GAS_TARGET) / 2n; const serializedTx = randomBytes(payloadLength); - await fuelMessagePortal.sendTransaction(1, serializedTx); // Initialize to 1 gwei + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // Initialize to 1 gwei const initialGasPrice = await fuelMessagePortal.getGasPrice(); - await fuelMessagePortal.sendTransaction(gas, serializedTx); + await fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); - await fuelMessagePortal.sendTransaction(1, serializedTx); // Update gas price + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // Update gas price expect(await fuelMessagePortal.getGasPrice()).to.equal( (initialGasPrice * ONE_AND_A_HALF) / SCALED_UNIT @@ -185,17 +267,27 @@ export function behavesLikeFuelMessagePortalV4( const serializedTx = randomBytes(payloadLength); await Promise.all([ - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Initialize to 1 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 2 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 4 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), // Initialize to 1 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), // Bump to 2 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), // Bump to 4 gwei ]); - await fuelMessagePortal.sendTransaction(gas, serializedTx); + await fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); const initialGasPrice = await fuelMessagePortal.getGasPrice(); expect(initialGasPrice).to.equal(parseUnits('8', 'gwei')); - await fuelMessagePortal.sendTransaction(1, serializedTx); // Update gas price + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // Update gas price expect(await fuelMessagePortal.getGasPrice()).to.equal( (initialGasPrice * 3n) / 4n @@ -210,17 +302,27 @@ export function behavesLikeFuelMessagePortalV4( const serializedTx = randomBytes(payloadLength); await Promise.all([ - fuelMessagePortal.sendTransaction(1, serializedTx), // Initialize to 1 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 2 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 4 gwei + fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }), // Initialize to 1 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), // Bump to 2 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), // Bump to 4 gwei ]); - await fuelMessagePortal.sendTransaction(gas, serializedTx); + await fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); const initialGasPrice = await fuelMessagePortal.getGasPrice(); expect(initialGasPrice).to.equal(parseUnits('4', 'gwei')); - await fuelMessagePortal.sendTransaction(1, serializedTx); // Update gas price + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // Update gas price expect(await fuelMessagePortal.getGasPrice()).to.be.within( initialGasPrice / 2n, @@ -236,17 +338,27 @@ export function behavesLikeFuelMessagePortalV4( const serializedTx = randomBytes(payloadLength); await Promise.all([ - fuelMessagePortal.sendTransaction(1, serializedTx), // Initialize to 1 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 2 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), // Bump to 4 gwei + fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }), // Initialize to 1 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), // Bump to 2 gwei + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), // Bump to 4 gwei ]); - await fuelMessagePortal.sendTransaction(gas, serializedTx); + await fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); const initialGasPrice = await fuelMessagePortal.getGasPrice(); expect(initialGasPrice).to.equal(parseUnits('4', 'gwei')); - await fuelMessagePortal.sendTransaction(1, serializedTx); // Update gas price + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // Update gas price expect(await fuelMessagePortal.getGasPrice()).to.equal( initialGasPrice @@ -262,17 +374,27 @@ export function behavesLikeFuelMessagePortalV4( const serializedTx = randomBytes(payloadLength); await Promise.all([ - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), - fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), + fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx, { + value: parseEther('1'), + }), ]); const initialGasPrice = await fuelMessagePortal.getGasPrice(); const distance = 3; await mine(distance - 1); // The last block will be mined by the tx - await fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx); + await fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx, { + value: parseEther('1'), + }); expect(await fuelMessagePortal.getGasPrice()).to.equal( initialGasPrice / BigInt(distance) @@ -286,14 +408,24 @@ export function behavesLikeFuelMessagePortalV4( const serializedTx = randomBytes(payloadLength); await Promise.all([ - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), + fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }), ]); await mine(200); - await fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx); + await fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx, { + value: parseEther('1'), + }); expect(await fuelMessagePortal.getGasPrice()).to.equal( MIN_GAS_PRICE @@ -302,5 +434,47 @@ export function behavesLikeFuelMessagePortalV4( }); }); }); + + describe('collectFees()', async () => { + it('rejects unauthorized calls', async () => { + const { + fuelMessagePortal, + signers: [deployer, mallory], + } = await fixture(); + + const rogueTx = fuelMessagePortal.connect(mallory).collectFees(); + const expectedError = `AccessControl: account ${mallory.address.toLowerCase()} is missing role ${FEE_COLLECTOR_ROLE}`; + await expect(rogueTx).to.be.revertedWith(expectedError); + + await fuelMessagePortal + .connect(deployer) + .grantRole(FEE_COLLECTOR_ROLE, mallory); + await fuelMessagePortal.connect(mallory).collectFees(); + }); + + it('transfers fees to caller', async () => { + const { + fuelMessagePortal, + signers: [deployer, collector], + } = await fixture(); + + await fuelMessagePortal + .connect(deployer) + .grantRole(FEE_COLLECTOR_ROLE, collector); + + const expectedFee = MIN_GAS_PRICE * GAS_LIMIT; + await fuelMessagePortal.sendTransaction(GAS_LIMIT, '0x', { + value: expectedFee, + }); + + const tx = fuelMessagePortal.connect(collector).collectFees(); + await tx; + + await expect(tx).to.changeEtherBalances( + [fuelMessagePortal, collector], + [-expectedFee, expectedFee] + ); + }); + }); }); } From 80139a8b9291118c94aced102c76351d2dc64e35 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Tue, 25 Jun 2024 16:52:01 +0200 Subject: [PATCH 12/21] test: update fti integration tests --- packages/integration-tests/tests/fti.ts | 8 ++++++-- .../hardhat/002.fuel_message_portal_v4.ts | 17 +++++++++-------- .../src/utils/fuels/waitForTransaction.ts | 3 ++- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/integration-tests/tests/fti.ts b/packages/integration-tests/tests/fti.ts index 769844e7..a3d3c337 100644 --- a/packages/integration-tests/tests/fti.ts +++ b/packages/integration-tests/tests/fti.ts @@ -7,7 +7,7 @@ import type { WalletUnlocked as FuelWallet, BN } from 'fuels'; const { expect } = chai; -const MAX_GAS = 10000000; +const MAX_GAS = 10000000n; describe.only('Forced Transaction Inclusion', async function () { // Timeout 6 minutes @@ -71,9 +71,13 @@ describe.only('Forced Transaction Inclusion', async function () { const fuelSerializedTx = hexlify(transactionRequest.toTransactionBytes()); + const gasPrice = await env.eth.fuelMessagePortal.MIN_GAS_PRICE(); + const expectedFee = gasPrice * MAX_GAS; + const ethTx = await env.eth.fuelMessagePortal.sendTransaction( MAX_GAS, - fuelSerializedTx + fuelSerializedTx, + { value: expectedFee } ); const { blockNumber } = await ethTx.wait(); diff --git a/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts b/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts index 908930af..3665ffeb 100644 --- a/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts +++ b/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts @@ -1,4 +1,4 @@ -import { MaxUint256 } from 'ethers'; +import { MaxUint256, parseUnits } from 'ethers'; import type { HardhatRuntimeEnvironment } from 'hardhat/types'; import type { DeployFunction } from 'hardhat-deploy/dist/types'; @@ -6,6 +6,8 @@ import { FuelMessagePortalV4__factory as FuelMessagePortal } from '../../typecha const ETH_DEPOSIT_LIMIT = MaxUint256; const FTI_GAS_LIMIT = 2n ** 64n - 1n; +const FTI_MIN_GAS_PRICE = parseUnits('1', 'gwei'); +const FTI_MIN_GAS_PER_TX = 1; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { @@ -17,22 +19,21 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { address: fuelChainState } = await get('FuelChainState'); - console.log('holaaaa'); - console.log(FTI_GAS_LIMIT); - const contract = await deployProxy( new FuelMessagePortal(deployer), [fuelChainState], { initializer: 'initialize', - constructorArgs: [ETH_DEPOSIT_LIMIT, FTI_GAS_LIMIT], + constructorArgs: [ + ETH_DEPOSIT_LIMIT, + FTI_GAS_LIMIT, + FTI_MIN_GAS_PER_TX, + FTI_MIN_GAS_PRICE, + ], } ); await contract.waitForDeployment(); - console.log('waaaat'); - await contract.GAS_LIMIT().then(console.log); - const address = await contract.getAddress(); const implementation = await erc1967.getImplementationAddress(address); diff --git a/packages/test-utils/src/utils/fuels/waitForTransaction.ts b/packages/test-utils/src/utils/fuels/waitForTransaction.ts index b8313148..acfd8b91 100644 --- a/packages/test-utils/src/utils/fuels/waitForTransaction.ts +++ b/packages/test-utils/src/utils/fuels/waitForTransaction.ts @@ -11,7 +11,7 @@ import { delay } from '../delay'; import { debug } from '../logs'; type Opts = { - relayedTxId?: string; + relayedTxId?: string; // This ID will only appear if the tx fails timeout?: number; }; @@ -38,6 +38,7 @@ export async function waitForTransaction( const startTime = new Date().getTime(); if (opts.relayedTxId) { + // Note: getRelayedTransactionStatus will only return if the transaction failed const relayedTxError = await provider.getRelayedTransactionStatus( opts.relayedTxId ); From a200e69bb0da032713849586d27dfa228849d7d1 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Tue, 25 Jun 2024 18:34:37 +0200 Subject: [PATCH 13/21] test: complete fti unit test coverage --- .../contracts/test/EthReceiver.sol | 19 ++ .../FuelMessagePortalV4.behavior.test.ts | 273 +++++++++++++----- 2 files changed, 214 insertions(+), 78 deletions(-) create mode 100644 packages/solidity-contracts/contracts/test/EthReceiver.sol diff --git a/packages/solidity-contracts/contracts/test/EthReceiver.sol b/packages/solidity-contracts/contracts/test/EthReceiver.sol new file mode 100644 index 00000000..fe258c48 --- /dev/null +++ b/packages/solidity-contracts/contracts/test/EthReceiver.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.9; + +contract EthReceiver { + bool reject = false; + string reason; + + receive() external payable { + if (reject) { + if (bytes(reason).length > 0) require(false, reason); + revert(); + } + } + + function setupRevert(bool value, string calldata _reason) external { + reject = value; + reason = _reason; + } +} diff --git a/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts index 9e838a53..5f787ff7 100644 --- a/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts +++ b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts @@ -6,7 +6,11 @@ import { randomInt } from 'crypto'; import { parseEther, parseUnits, randomBytes } from 'ethers'; import type { FuelMessagePortalV4 } from '../../typechain'; -import { haltBlockProduction, resumeInstantBlockProduction } from '../utils'; +import { + haltBlockProduction, + impersonateAccount, + resumeInstantBlockProduction, +} from '../utils'; export type FuelMessagePortalV4Fixture = { signers: HardhatEthersSigner[]; @@ -76,6 +80,8 @@ export function behavesLikeFuelMessagePortalV4( await expect(tx) .to.emit(fuelMessagePortal, 'Transaction') .withArgs(1, gas, serializedTx); + + expect(await fuelMessagePortal.getTransactionNonce()).to.be.equal(2); }); it('increments used gas', async () => { @@ -88,6 +94,9 @@ export function behavesLikeFuelMessagePortalV4( const gas = Math.abs(randomInt(MIN_GAS_PER_TX, 256)); const serializedTx = randomBytes(payloadLength); + expect(await fuelMessagePortal.getCurrentUsedGas()).to.equal(0); + + // This is needed to allow more than one transaction in a single block await haltBlockProduction(hre); const nonce = await signer.getNonce(); @@ -112,37 +121,39 @@ export function behavesLikeFuelMessagePortalV4( ); expect(await fuelMessagePortal.getUsedGas()).to.be.equal(gas * 2); + expect(await fuelMessagePortal.getCurrentUsedGas()).to.equal(gas * 2); }); - it('initializes gasPrice to MIN_GAS_PRICE', async () => { + it('updates last seen block', async () => { const { fuelMessagePortal } = await fixture(); - const payloadLength = Math.abs(randomInt(256)); const gas = Math.abs(randomInt(MIN_GAS_PER_TX, 256)); const serializedTx = randomBytes(payloadLength); - await fuelMessagePortal.sendTransaction(gas, serializedTx, { - value: parseEther('1'), - }); + const { blockNumber } = await fuelMessagePortal + .sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }) + .then((tx) => tx.wait()); - expect(await fuelMessagePortal.getGasPrice()).to.be.equal( - await fuelMessagePortal.MIN_GAS_PRICE() + expect(await fuelMessagePortal.getLastSeenBlock()).to.equal( + blockNumber ); }); - it('rejects when block is full', async () => { + it('initializes gasPrice to MIN_GAS_PRICE', async () => { const { fuelMessagePortal } = await fixture(); const payloadLength = Math.abs(randomInt(256)); + const gas = Math.abs(randomInt(MIN_GAS_PER_TX, 256)); const serializedTx = randomBytes(payloadLength); - const tx = fuelMessagePortal.sendTransaction( - GAS_LIMIT + 1n, - serializedTx - ); - await expect(tx).to.be.revertedWithCustomError( - fuelMessagePortal, - 'GasLimit' + await fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); + + expect(await fuelMessagePortal.getGasPrice()).to.be.equal( + await fuelMessagePortal.MIN_GAS_PRICE() ); }); @@ -182,7 +193,36 @@ export function behavesLikeFuelMessagePortalV4( await expect(tx).to.changeEtherBalance(signer, -expectedFee); }); - it('reject underfunded transactions', async () => { + it('rejects when block is full', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + const tx = fuelMessagePortal.sendTransaction( + GAS_LIMIT + 1n, + serializedTx + ); + await expect(tx).to.be.revertedWithCustomError( + fuelMessagePortal, + 'GasLimit' + ); + }); + + it('rejects transactions with not enough gas', async () => { + const { fuelMessagePortal } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + const tx = fuelMessagePortal.sendTransaction(0, serializedTx); + await expect(tx).to.be.revertedWithCustomError( + fuelMessagePortal, + 'MinGas' + ); + }); + + it('rejects underfunded transactions', async () => { const { fuelMessagePortal } = await fixture(); const payloadLength = Math.abs(randomInt(256)); @@ -204,6 +244,71 @@ export function behavesLikeFuelMessagePortalV4( }); }); + it('rejects if the excess fee cannot be forwarded', async () => { + const { + fuelMessagePortal, + signers: [signer], + } = await fixture(); + + const receiverContract = await hre.ethers + .getContractFactory('EthReceiver') + .then((f) => f.deploy()); + const receiver = await impersonateAccount(receiverContract, hre); + await signer.sendTransaction({ + to: receiverContract, + value: parseEther('100'), + }); + + await receiverContract.setupRevert(true, ''); + + const payloadLength = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + const expectedFee = MIN_GAS_PRICE * GAS_LIMIT; + const excessFee = parseEther('1'); + const tx = fuelMessagePortal + .connect(receiver) + .sendTransaction(GAS_LIMIT, serializedTx, { + value: expectedFee + excessFee, + }); + + await expect(tx).to.be.revertedWithCustomError( + fuelMessagePortal, + 'RecipientRejectedETH' + ); + }); + + it('bubbles up revert reasons of ETH receiver', async () => { + const { + fuelMessagePortal, + signers: [signer], + } = await fixture(); + + const receiverContract = await hre.ethers + .getContractFactory('EthReceiver') + .then((f) => f.deploy()); + const receiver = await impersonateAccount(receiverContract, hre); + await signer.sendTransaction({ + to: receiverContract, + value: parseEther('100'), + }); + const revertReason = 'revertReason'; + await receiverContract.setupRevert(true, revertReason); + + const payloadLength = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + const expectedFee = MIN_GAS_PRICE * GAS_LIMIT; + const excessFee = parseEther('1'); + const tx = fuelMessagePortal + .connect(receiver) + .sendTransaction(GAS_LIMIT, serializedTx, { + value: expectedFee + excessFee, + }); + + await expect(tx).to.be.revertedWith(revertReason); + }); + describe('with increasing congestion (used gas above target)', () => { it('duplicates gas price for full blocks gasPrice', async () => { const { fuelMessagePortal } = await fixture(); @@ -266,17 +371,15 @@ export function behavesLikeFuelMessagePortalV4( const gas = GAS_TARGET / 2n; const serializedTx = randomBytes(payloadLength); - await Promise.all([ - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), // Initialize to 1 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), // Bump to 2 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), // Bump to 4 gwei - ]); + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); // Initialize to 1 gwei + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); // Bump to 2 gwei + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); // Bump to 4 gwei await fuelMessagePortal.sendTransaction(gas, serializedTx, { value: parseEther('1'), @@ -301,17 +404,15 @@ export function behavesLikeFuelMessagePortalV4( const gas = 1; const serializedTx = randomBytes(payloadLength); - await Promise.all([ - fuelMessagePortal.sendTransaction(1, serializedTx, { - value: parseEther('1'), - }), // Initialize to 1 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), // Bump to 2 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), // Bump to 4 gwei - ]); + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // Initialize to 1 gwei + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); // Bump to 2 gwei + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); // Bump to 4 gwei await fuelMessagePortal.sendTransaction(gas, serializedTx, { value: parseEther('1'), @@ -337,17 +438,15 @@ export function behavesLikeFuelMessagePortalV4( const gas = GAS_TARGET; const serializedTx = randomBytes(payloadLength); - await Promise.all([ - fuelMessagePortal.sendTransaction(1, serializedTx, { - value: parseEther('1'), - }), // Initialize to 1 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), // Bump to 2 gwei - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), // Bump to 4 gwei - ]); + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // Initialize to 1 gwei + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); // Bump to 2 gwei + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); // Bump to 4 gwei await fuelMessagePortal.sendTransaction(gas, serializedTx, { value: parseEther('1'), @@ -373,20 +472,18 @@ export function behavesLikeFuelMessagePortalV4( const payloadLength = Math.abs(randomInt(256)); const serializedTx = randomBytes(payloadLength); - await Promise.all([ - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), - fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx, { - value: parseEther('1'), - }), - ]); + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); + await fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx, { + value: parseEther('1'), + }); const initialGasPrice = await fuelMessagePortal.getGasPrice(); @@ -407,20 +504,18 @@ export function behavesLikeFuelMessagePortalV4( const payloadLength = Math.abs(randomInt(256)); const serializedTx = randomBytes(payloadLength); - await Promise.all([ - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), - fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { - value: parseEther('1'), - }), - ]); + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); + await fuelMessagePortal.sendTransaction(GAS_LIMIT, serializedTx, { + value: parseEther('1'), + }); await mine(200); await fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx, { @@ -452,6 +547,28 @@ export function behavesLikeFuelMessagePortalV4( await fuelMessagePortal.connect(mallory).collectFees(); }); + it('reverts if caller cannot receive ETH', async () => { + const { + fuelMessagePortal, + signers: [deployer], + } = await fixture(); + + const receiverContract = await hre.ethers + .getContractFactory('EthReceiver') + .then((f) => f.deploy()); + const receiver = await impersonateAccount(receiverContract, hre); + await receiverContract.setupRevert(true, ''); + + await fuelMessagePortal + .connect(deployer) + .grantRole(FEE_COLLECTOR_ROLE, receiver); + const tx = fuelMessagePortal.connect(receiver).collectFees(); + await expect(tx).to.be.revertedWithCustomError( + fuelMessagePortal, + 'RecipientRejectedETH' + ); + }); + it('transfers fees to caller', async () => { const { fuelMessagePortal, From 4701e1b9f20ad228f57246592b3aa01189b9590e Mon Sep 17 00:00:00 2001 From: DefiCake Date: Tue, 25 Jun 2024 19:08:29 +0200 Subject: [PATCH 14/21] test: complete fti integration tests --- packages/integration-tests/tests/fti.ts | 132 +++++++++++++++++++++--- 1 file changed, 115 insertions(+), 17 deletions(-) diff --git a/packages/integration-tests/tests/fti.ts b/packages/integration-tests/tests/fti.ts index a3d3c337..ea58d130 100644 --- a/packages/integration-tests/tests/fti.ts +++ b/packages/integration-tests/tests/fti.ts @@ -9,7 +9,7 @@ const { expect } = chai; const MAX_GAS = 10000000n; -describe.only('Forced Transaction Inclusion', async function () { +describe('Forced Transaction Inclusion', async function () { // Timeout 6 minutes const DEFAULT_TIMEOUT_MS: number = 400_000; let BASE_ASSET_ID: string; @@ -42,20 +42,6 @@ describe.only('Forced Transaction Inclusion', async function () { fuelReceiverBalance = await fuelReceiver.getBalance(BASE_ASSET_ID); }); - // it.skip('deposit to wallet address', async () => { - // await env.eth.fuelMessagePortal.depositETH(fuelSender.address.toB256(), { - // value: parseEther('1'), - // }); - - // let bal = await fuelSender.getBalance(BASE_ASSET_ID); - - // while (bal.toString() === '0') { - // bal = await fuelSender.getBalance(BASE_ASSET_ID); - // } - - // fuelSenderBalance = await fuelSender.getBalance(BASE_ASSET_ID); - // }); - it('allows to send transactions', async () => { const transferRequest = await fuelSender.createTransfer( fuelReceiver.address, @@ -120,8 +106,120 @@ describe.only('Forced Transaction Inclusion', async function () { expect(txResult.status).to.equal('success'); }); - it('rejects transactions without signatures'); + it('rejects transactions without signatures', async () => { + const transferRequest = await fuelSender.createTransfer( + fuelReceiver.address, + fuelSenderBalance.div(10), + BASE_ASSET_ID + ); + + const transactionRequest = transactionRequestify(transferRequest); + await env.fuel.provider.estimateTxDependencies(transactionRequest); + + const fuelSerializedTx = hexlify(transactionRequest.toTransactionBytes()); + + const gasPrice = await env.eth.fuelMessagePortal.MIN_GAS_PRICE(); + const expectedFee = gasPrice * MAX_GAS; + + const ethTx = await env.eth.fuelMessagePortal.sendTransaction( + MAX_GAS, + fuelSerializedTx, + { value: expectedFee } + ); + + const { blockNumber } = await ethTx.wait(); - it('rejects transactions without gas'); + const [event] = await env.eth.fuelMessagePortal.queryFilter( + env.eth.fuelMessagePortal.filters.Transaction, + blockNumber, + blockNumber + ); + + expect(event.args.canonically_serialized_tx).to.be.equal( + fuelSerializedTx + ); + + const payload = solidityPacked( + ['uint256', 'uint64', 'bytes'], + [ + event.args.nonce, + event.args.max_gas, + event.args.canonically_serialized_tx, + ] + ); + const relayedTxId = sha256(payload); + const fuelTxId = transactionRequest.getTransactionId(CHAIN_ID); + + const { response, error } = await waitForTransaction( + fuelTxId, + env.fuel.provider, + { + relayedTxId, + } + ); + + expect(response).to.be.null; + expect(error).to.contain('InvalidSignature'); + }); + + it('rejects transactions without enough gas', async () => { + const transferRequest = await fuelSender.createTransfer( + fuelReceiver.address, + fuelSenderBalance.div(10), + BASE_ASSET_ID + ); + + const transactionRequest = transactionRequestify(transferRequest); + await env.fuel.provider.estimateTxDependencies(transactionRequest); + + const signature = await fuelSender.signTransaction(transactionRequest); + transactionRequest.updateWitnessByOwner(fuelSender.address, signature); + + const fuelSerializedTx = hexlify(transactionRequest.toTransactionBytes()); + + const gasPrice = await env.eth.fuelMessagePortal.MIN_GAS_PRICE(); + const minGasPerTx = await env.eth.fuelMessagePortal.MIN_GAS_PER_TX(); + const expectedFee = gasPrice * minGasPerTx; + + const ethTx = await env.eth.fuelMessagePortal.sendTransaction( + minGasPerTx, + fuelSerializedTx, + { value: expectedFee } + ); + + const { blockNumber } = await ethTx.wait(); + + const [event] = await env.eth.fuelMessagePortal.queryFilter( + env.eth.fuelMessagePortal.filters.Transaction, + blockNumber, + blockNumber + ); + + expect(event.args.canonically_serialized_tx).to.be.equal( + fuelSerializedTx + ); + + const payload = solidityPacked( + ['uint256', 'uint64', 'bytes'], + [ + event.args.nonce, + event.args.max_gas, + event.args.canonically_serialized_tx, + ] + ); + const relayedTxId = sha256(payload); + const fuelTxId = transactionRequest.getTransactionId(CHAIN_ID); + + const { response, error } = await waitForTransaction( + fuelTxId, + env.fuel.provider, + { + relayedTxId, + } + ); + + expect(response).to.be.null; + expect(error).to.contain('Insufficient'); + }); }); }); From a6aa26d42cfb38efae6914fe90b68e676f677a4e Mon Sep 17 00:00:00 2001 From: DefiCake Date: Thu, 27 Jun 2024 10:42:23 +0200 Subject: [PATCH 15/21] test: pack V3 tests in a behaviour file and include it in V4 tests --- .../test/FuelMessagePortalV4.test.ts | 64 +- ...uelMessagePortalV3.L2toL1.behavior.test.ts | 1031 +++++++++++++++++ 2 files changed, 1089 insertions(+), 6 deletions(-) create mode 100644 packages/solidity-contracts/test/behaviors/FuelMessagePortalV3.L2toL1.behavior.test.ts diff --git a/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts b/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts index 557c33bb..d7691c42 100644 --- a/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts +++ b/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts @@ -1,13 +1,19 @@ import { MaxUint256, parseUnits } from 'ethers'; import hre from 'hardhat'; -import type { FuelMessagePortalV4 } from '../typechain'; +import type { + FuelChainState, + FuelMessagePortalV3, + FuelMessagePortalV4, +} from '../typechain'; import { behavesLikeAccessControl, behavesLikeFuelMessagePortalV4, } from './behaviors'; import { BLOCKS_PER_COMMIT_INTERVAL, TIME_TO_FINALIZE } from './utils'; +import { behavesLikeFuelMessagePortalV3 } from './behaviors/FuelMessagePortalV3.L2toL1.behavior.test'; +import { expect } from 'chai'; const DEPOSIT_LIMIT = MaxUint256; const GAS_LIMIT = 1_000; @@ -18,21 +24,21 @@ describe.only('FuelMessagePortalV4', () => { const fixture = hre.deployments.createFixture( async ({ ethers, upgrades }) => { const signers = await ethers.getSigners(); - const FuelMessagePortal = await ethers.getContractFactory( + const FuelMessagePortalV4 = await ethers.getContractFactory( 'FuelMessagePortalV4' ); const FuelChainState = await ethers.getContractFactory('FuelChainState'); - const fuelChainState = await upgrades.deployProxy(FuelChainState, { + const fuelChainState = (await upgrades.deployProxy(FuelChainState, { initializer: 'initialize', constructorArgs: [ TIME_TO_FINALIZE, BLOCKS_PER_COMMIT_INTERVAL, TIME_TO_FINALIZE, ], - }); + })) as unknown as FuelChainState; const fuelMessagePortal = (await upgrades.deployProxy( - FuelMessagePortal, + FuelMessagePortalV4, [await fuelChainState.getAddress()], { initializer: 'initialize', @@ -45,10 +51,56 @@ describe.only('FuelMessagePortalV4', () => { } )) as unknown as FuelMessagePortalV4; - return { signers, fuelMessagePortal }; + const messageTester = await ethers + .getContractFactory('MessageTester', signers[0]) + .then(async (f) => f.deploy(fuelMessagePortal)); + + return { signers, fuelMessagePortal, messageTester, fuelChainState }; } ); + it('can upgrade from V3 to V4', async () => { + const V3 = await hre.ethers.getContractFactory('FuelMessagePortalV3'); + const V4 = await hre.ethers.getContractFactory('FuelMessagePortalV4'); + + const fuelChainState = await hre.ethers + .getContractFactory('FuelChainState') + .then((f) => + f.deploy(TIME_TO_FINALIZE, BLOCKS_PER_COMMIT_INTERVAL, TIME_TO_FINALIZE) + ); + + const proxy = await hre.upgrades.deployProxy( + V3, + [await fuelChainState.getAddress()], + { + initializer: 'initialize', + constructorArgs: [0], + } + ); + + const contract = V4.attach(proxy) as unknown as FuelMessagePortalV4; + + // Check a function of V3 + await contract.withdrawalsPaused(); + + // Check a function of V4 reverts + await expect(contract.getLastSeenBlock()).to.be.reverted; + + // Upgrade + await hre.upgrades.upgradeProxy(contract, V4, { + constructorArgs: [ + DEPOSIT_LIMIT, + GAS_LIMIT, + MIN_GAS_PER_TX, + MIN_GAS_PRICE, + ], + }); + + // Check a function of V4 no longer reverts + await expect(contract.getLastSeenBlock()).not.to.be.reverted; + }); + behavesLikeAccessControl(fixture, 'fuelMessagePortal'); + behavesLikeFuelMessagePortalV3(fixture); behavesLikeFuelMessagePortalV4(fixture); }); diff --git a/packages/solidity-contracts/test/behaviors/FuelMessagePortalV3.L2toL1.behavior.test.ts b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV3.L2toL1.behavior.test.ts new file mode 100644 index 00000000..948453f4 --- /dev/null +++ b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV3.L2toL1.behavior.test.ts @@ -0,0 +1,1031 @@ +import hre from 'hardhat'; +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +import { MaxUint256, parseEther, toBeHex } from 'ethers'; + +import type { + FuelChainState, + FuelMessagePortal, + FuelMessagePortalV3, + MessageTester, +} from '../../typechain'; +import { + BLOCKS_PER_COMMIT_INTERVAL, + TIME_TO_FINALIZE, + TreeNode, + addressToB256, + b256ToAddress, + createBlock, + createRandomWalletWithFunds, + generateProof, + getLeafIndexKey, +} from '../utils'; +import Message, { computeMessageId } from '../../protocol/message'; +import { randomBytes32, tai64Time } from '../../protocol/utils'; +import { constructTree, calcRoot, getProof } from '@fuel-ts/merkle'; +import BlockHeader, { + BlockHeaderLite, + computeBlockId, + generateBlockHeaderLite, +} from '../../protocol/blockHeader'; +import { HardhatEthersProvider } from '@nomicfoundation/hardhat-ethers/internal/hardhat-ethers-provider'; + +export type FuelMessagePortalFixture = { + signers: HardhatEthersSigner[]; + fuelMessagePortal: FuelMessagePortalV3; + fuelChainState: FuelChainState; + messageTester: MessageTester; + [key: string]: any; +}; + +const ETH_DECIMALS = 18n; +const FUEL_BASE_ASSET_DECIMALS = 9n; +const BASE_ASSET_CONVERSION = 10n ** (ETH_DECIMALS - FUEL_BASE_ASSET_DECIMALS); + +export function behavesLikeFuelMessagePortalV3( + fixture: () => Promise +) { + let provider = hre.ethers.provider; + let fuelMessagePortal: FuelMessagePortalV3; + let fuelChainState: FuelChainState; + let messageTester: MessageTester; + let addresses: string[]; + let signers: HardhatEthersSigner[]; + + // Message data + const messageTestData1 = randomBytes32(); + const messageTestData2 = randomBytes32(); + const messageTestData3 = randomBytes32(); + let messageNodes: TreeNode[]; + let trustedSenderAddress: string; + + // Testing contracts + let messageTesterAddress: string; + let b256_fuelMessagePortalAddress: string; + + // Messages + let message1: Message; + let message2: Message; + let messageWithAmount: Message; + let messageBadSender: Message; + let messageBadRecipient: Message; + let messageBadData: Message; + let messageEOA: Message; + let messageEOANoAmount: Message; + + // Arrays of committed block headers and their IDs + let blockHeaders: BlockHeader[]; + let blockIds: string[]; + let endOfCommitIntervalHeader: BlockHeader; + let endOfCommitIntervalHeaderLite: BlockHeaderLite; + let unfinalizedBlock: BlockHeader; + let prevBlockNodes: TreeNode[]; + + async function setupMessages( + provider: HardhatEthersProvider, + portalAddr: string, + messageTester: MessageTester, + fuelChainState: FuelChainState, + addresses: string[] + ) { + blockIds = []; + blockHeaders = []; + // get data for building messages + messageTesterAddress = addressToB256(await messageTester.getAddress()); + b256_fuelMessagePortalAddress = addressToB256(portalAddr); + + trustedSenderAddress = await messageTester.getTrustedSender(); + + // message from trusted sender + message1 = new Message( + trustedSenderAddress, + messageTesterAddress, + BigInt(0), + randomBytes32(), + messageTester.interface.encodeFunctionData('receiveMessage', [ + messageTestData1, + messageTestData2, + ]) + ); + message2 = new Message( + trustedSenderAddress, + messageTesterAddress, + BigInt(0), + randomBytes32(), + messageTester.interface.encodeFunctionData('receiveMessage', [ + messageTestData2, + messageTestData1, + ]) + ); + // message from trusted sender with amount + messageWithAmount = new Message( + trustedSenderAddress, + messageTesterAddress, + parseEther('0.1') / BASE_ASSET_CONVERSION, + randomBytes32(), + messageTester.interface.encodeFunctionData('receiveMessage', [ + messageTestData2, + messageTestData3, + ]) + ); + // message from untrusted sender + messageBadSender = new Message( + randomBytes32(), + messageTesterAddress, + BigInt(0), + randomBytes32(), + messageTester.interface.encodeFunctionData('receiveMessage', [ + messageTestData3, + messageTestData1, + ]) + ); + // message to bad recipient + messageBadRecipient = new Message( + trustedSenderAddress, + addressToB256(portalAddr), + BigInt(0), + randomBytes32(), + messageTester.interface.encodeFunctionData('receiveMessage', [ + messageTestData2, + messageTestData2, + ]) + ); + // message with bad data + messageBadData = new Message( + trustedSenderAddress, + messageTesterAddress, + BigInt(0), + randomBytes32(), + randomBytes32() + ); + // message to EOA + messageEOA = new Message( + randomBytes32(), + addressToB256(addresses[2]), + parseEther('0.1') / BASE_ASSET_CONVERSION, + randomBytes32(), + '0x' + ); + // message to EOA no amount + messageEOANoAmount = new Message( + randomBytes32(), + addressToB256(addresses[3]), + BigInt(0), + randomBytes32(), + '0x' + ); + + // compile all message IDs + const messageIds: string[] = []; + messageIds.push(computeMessageId(message1)); + messageIds.push(computeMessageId(message2)); + messageIds.push(computeMessageId(messageWithAmount)); + messageIds.push(computeMessageId(messageBadSender)); + messageIds.push(computeMessageId(messageBadRecipient)); + messageIds.push(computeMessageId(messageBadData)); + messageIds.push(computeMessageId(messageEOA)); + messageIds.push(computeMessageId(messageEOANoAmount)); + messageNodes = constructTree(messageIds); + + // create blocks + const messageCount = messageIds.length.toString(); + const messagesRoot = calcRoot(messageIds); + for (let i = 0; i < BLOCKS_PER_COMMIT_INTERVAL - 1; i++) { + const blockHeader = createBlock('', i, '', messageCount, messagesRoot); + const blockId = computeBlockId(blockHeader); + + // append block header and Id to arrays + blockHeaders.push(blockHeader); + blockIds.push(blockId); + } + + // create end of commit interval block + const timestamp = tai64Time(new Date().getTime()); + endOfCommitIntervalHeader = createBlock( + calcRoot(blockIds), + blockIds.length, + timestamp, + messageCount, + messagesRoot + ); + endOfCommitIntervalHeaderLite = generateBlockHeaderLite( + endOfCommitIntervalHeader + ); + prevBlockNodes = constructTree(blockIds); + blockIds.push(computeBlockId(endOfCommitIntervalHeader)); + + // finalize blocks in the state contract + await fuelChainState.commit(computeBlockId(endOfCommitIntervalHeader), 0); + provider.send('evm_increaseTime', [TIME_TO_FINALIZE]); + + // create an unfinalized block + unfinalizedBlock = createBlock( + calcRoot(blockIds), + BLOCKS_PER_COMMIT_INTERVAL * 11 - 1, + timestamp, + messageCount, + messagesRoot + ); + + await fuelChainState.commit(computeBlockId(unfinalizedBlock), 10); + } + + before('setup invariants', async () => { + const fixt = await fixture(); + + ({ fuelMessagePortal, fuelChainState, messageTester, signers } = fixt); + + addresses = fixt.signers.map((signer) => signer.address); + + await setupMessages( + hre.ethers.provider, + await fuelMessagePortal.getAddress(), + messageTester, + fuelChainState, + addresses + ); + }); + + describe('Behaves like V3 - Blacklisting', () => { + beforeEach('fixture', async () => { + await fixture(); + await setupMessages( + hre.ethers.provider, + await fuelMessagePortal.getAddress(), + messageTester, + fuelChainState, + addresses + ); + }); + + describe('pauseWithdrawals', () => { + it('pauses all withdrawals', async () => { + const [, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + messageEOA, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + + await fuelMessagePortal.pauseWithdrawals(); + expect(await fuelMessagePortal.withdrawalsPaused()).to.be.true; + + const relayTx = fuelMessagePortal.relayMessage( + messageEOA, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ); + + await expect(relayTx).to.be.revertedWithCustomError( + fuelMessagePortal, + 'WithdrawalsPaused' + ); + }); + }); + + describe('unpauseWithdrawals', () => { + it('unpauses withdrawals', async () => { + const withdrawnAmount = messageEOA.amount * BASE_ASSET_CONVERSION; + const depositedAmount = withdrawnAmount * 2n; + await fuelMessagePortal.depositETH(messageEOA.recipient, { + value: depositedAmount, + }); + + await fuelMessagePortal.pauseWithdrawals(); + const [, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + messageEOA, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + + await fuelMessagePortal.unpauseWithdrawals(); + expect(await fuelMessagePortal.withdrawalsPaused()).to.be.false; + + const relayTx = fuelMessagePortal.relayMessage( + messageEOA, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ); + + await expect(relayTx).not.to.be.reverted; + }); + }); + + describe('addMessageToBlacklist', () => { + it('can only be called by pauser role', async () => { + const mallory = await createRandomWalletWithFunds(); + + const [msgID] = generateProof( + messageEOA, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + + const PAUSER_ROLE = await fuelMessagePortal.PAUSER_ROLE(); + + const tx = fuelMessagePortal + .connect(mallory) + .addMessageToBlacklist(msgID); + + const expectedErrorMsg = + `AccessControl: account ${mallory.address.toLowerCase()} ` + + `is missing role ${PAUSER_ROLE}`; + + await expect(tx).to.be.revertedWith(expectedErrorMsg); + }); + + it('prevents withdrawals', async () => { + // Blacklisted message + { + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = + generateProof( + messageEOA, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + + await fuelMessagePortal.addMessageToBlacklist(msgID); + + const relayTx = fuelMessagePortal.relayMessage( + messageEOA, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ); + + await expect(relayTx).to.be.revertedWithCustomError( + fuelMessagePortal, + 'MessageBlacklisted' + ); + } + + // Non blacklisted message + { + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = + generateProof( + messageEOANoAmount, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + const relayTx = fuelMessagePortal.relayMessage( + messageEOANoAmount, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ); + await expect(relayTx).to.not.be.reverted; + } + }); + }); + + describe('removeMessageFromBlacklist', () => { + it('can only be called by admin role', async () => { + const mallory = await createRandomWalletWithFunds(); + const [msgID] = generateProof( + messageEOA, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + + const ADMIN_ROLE = await fuelMessagePortal.DEFAULT_ADMIN_ROLE(); + const tx = fuelMessagePortal + .connect(mallory) + .removeMessageFromBlacklist(msgID); + + const expectedErrorMsg = + `AccessControl: account ${mallory.address.toLowerCase()} ` + + `is missing role ${ADMIN_ROLE}`; + expect(tx).to.be.revertedWith(expectedErrorMsg); + }); + it('restores ability to withdraw', async () => { + // Whitelist back the blacklisted message + { + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = + generateProof( + messageEOA, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + + const withdrawnAmount = messageEOA.amount * BASE_ASSET_CONVERSION; + const depositedAmount = withdrawnAmount * 2n; + await fuelMessagePortal.depositETH(messageEOA.recipient, { + value: depositedAmount, + }); + await fuelMessagePortal.removeMessageFromBlacklist(msgID); + + const relayTx = fuelMessagePortal.relayMessage( + messageEOA, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ); + + await expect(relayTx).not.to.be.reverted; + } + }); + }); + }); + + describe('Behaves like FuelMessagePortalV3 - Relay both valid and invalid messages', () => { + before(async () => { + await fixture(); + await setupMessages( + hre.ethers.provider, + await fuelMessagePortal.getAddress(), + messageTester, + fuelChainState, + addresses + ); + }); + + it('Should not get a valid message sender outside of relaying', async () => { + await expect( + fuelMessagePortal.messageSender() + ).to.be.revertedWithCustomError( + fuelMessagePortal, + 'CurrentMessageSenderNotSet' + ); + }); + + it('Should not be able to relay message with bad root block', async () => { + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + message1, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + await expect( + fuelMessagePortal.relayMessage( + message1, + generateBlockHeaderLite( + createBlock('', BLOCKS_PER_COMMIT_INTERVAL * 20 - 1) + ), + msgBlockHeader, + blockInRoot, + msgInBlock + ) + ).to.be.revertedWithCustomError(fuelChainState, 'UnknownBlock'); + + await expect( + fuelMessagePortal.relayMessage( + message1, + generateBlockHeaderLite(unfinalizedBlock), + msgBlockHeader, + blockInRoot, + msgInBlock + ) + ).to.be.revertedWithCustomError(fuelMessagePortal, 'UnfinalizedBlock'); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + }); + + it('Should not be able to relay message with bad proof in root block', async () => { + const portalBalance = await provider.getBalance(fuelMessagePortal); + const messageTesterBalance = await provider.getBalance(messageTester); + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + message1, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + blockInRoot.key = blockInRoot.key + 1; + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + await expect( + fuelMessagePortal.relayMessage( + message1, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ) + ).to.be.revertedWithCustomError( + fuelMessagePortal, + 'InvalidBlockInHistoryProof' + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + expect(await provider.getBalance(fuelMessagePortal)).to.be.equal( + portalBalance + ); + expect(await provider.getBalance(messageTester)).to.be.equal( + messageTesterBalance + ); + }); + + it('Should be able to relay valid message', async () => { + const portalBalance = await provider.getBalance(fuelMessagePortal); + const messageTesterBalance = await provider.getBalance(messageTester); + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + message1, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + await expect( + fuelMessagePortal.relayMessage( + message1, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ) + ).to.not.be.reverted; + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(true); + expect(await messageTester.data1()).to.be.equal(messageTestData1); + expect(await messageTester.data2()).to.be.equal(messageTestData2); + expect(await provider.getBalance(fuelMessagePortal)).to.be.equal( + portalBalance + ); + expect(await provider.getBalance(messageTester)).to.be.equal( + messageTesterBalance + ); + }); + + it('Should not be able to relay already relayed message', async () => { + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + message1, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(true); + await expect( + fuelMessagePortal.relayMessage( + message1, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ) + ).to.be.revertedWithCustomError(fuelMessagePortal, 'AlreadyRelayed'); + }); + + it('Should not be able to relay message with low gas', async () => { + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + messageWithAmount, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + const options = { + gasLimit: 140000, + }; + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + await expect( + fuelMessagePortal.relayMessage( + messageWithAmount, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock, + options + ) + ).to.be.reverted; + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + }); + + it('Should be able to relay message with amount', async () => { + const expectedWithdrawnAmount = + messageWithAmount.amount * BASE_ASSET_CONVERSION; + + await fuelMessagePortal.depositETH(messageWithAmount.sender, { + value: expectedWithdrawnAmount, + }); + + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + messageWithAmount, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + + const relayTx = fuelMessagePortal.relayMessage( + messageWithAmount, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ); + + await expect(relayTx).to.not.be.reverted; + await expect(relayTx).to.changeEtherBalances( + [fuelMessagePortal, messageTester], + [expectedWithdrawnAmount * -1n, expectedWithdrawnAmount] + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(true); + + expect(await messageTester.data1()).to.be.equal(messageTestData2); + expect(await messageTester.data2()).to.be.equal(messageTestData3); + }); + + it('Should not be able to relay message from untrusted sender', async () => { + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + messageBadSender, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + await expect( + fuelMessagePortal.relayMessage( + messageBadSender, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ) + ).to.be.revertedWithCustomError(messageTester, 'InvalidMessageSender'); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + }); + + it('Should not be able to relay message to bad recipient', async () => { + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + messageBadRecipient, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + await expect( + fuelMessagePortal.relayMessage( + messageBadRecipient, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ) + ).to.be.revertedWithCustomError(fuelMessagePortal, 'MessageRelayFailed'); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + }); + + it('Should not be able to relay message with bad data', async () => { + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + messageBadData, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + await expect( + fuelMessagePortal.relayMessage( + messageBadData, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ) + ).to.be.revertedWithCustomError(fuelMessagePortal, 'MessageRelayFailed'); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + }); + + it('Should be able to relay message to EOA', async () => { + const expectedWithdrawnAmount = messageEOA.amount * BASE_ASSET_CONVERSION; + const expectedRecipient = b256ToAddress(messageEOA.recipient); + + await fuelMessagePortal.depositETH(messageEOA.sender, { + value: expectedWithdrawnAmount, + }); + + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + messageEOA, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + + const relayTx = fuelMessagePortal.relayMessage( + messageEOA, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ); + await expect(relayTx).to.not.be.reverted; + await expect(relayTx).to.changeEtherBalances( + [fuelMessagePortal, expectedRecipient], + [expectedWithdrawnAmount * -1n, expectedWithdrawnAmount] + ); + + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(true); + }); + + it('Should be able to relay message to EOA with no amount', async () => { + const messageRecipient = b256ToAddress(messageEOANoAmount.recipient); + + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + messageEOANoAmount, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + const relayTx = fuelMessagePortal.relayMessage( + messageEOANoAmount, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ); + await expect(relayTx).to.not.be.reverted; + await expect(relayTx).to.changeEtherBalances( + [fuelMessagePortal, messageRecipient], + [0, 0] + ); + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(true); + }); + + it('Should not be able to relay valid message with different amount', async () => { + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + message2, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + + const diffBlock = { + sender: message2.sender, + recipient: message2.recipient, + nonce: message2.nonce, + amount: message2.amount + parseEther('1.0'), + data: message2.data, + }; + + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + + await expect( + fuelMessagePortal.relayMessage( + diffBlock, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ) + ).to.be.revertedWithCustomError( + fuelMessagePortal, + 'InvalidMessageInBlockProof' + ); + }); + + it('Should not be able to relay non-existent message', async () => { + const [, msgBlockHeader, blockInRoot] = generateProof( + message2, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + const msgInBlock = { + key: 0, + proof: [], + }; + await expect( + fuelMessagePortal.relayMessage( + message2, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ) + ).to.be.revertedWithCustomError( + fuelMessagePortal, + 'InvalidMessageInBlockProof' + ); + }); + + it('Should not be able to relay reentrant messages', async () => { + // create a message that attempts to relay another message + const [, rTestMsgBlockHeader, rTestBlockInRoot, rTestMsgInBlock] = + generateProof( + message1, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + const reentrantTestData = fuelMessagePortal.interface.encodeFunctionData( + 'relayMessage', + [ + message1, + endOfCommitIntervalHeaderLite, + rTestMsgBlockHeader, + rTestBlockInRoot, + rTestMsgInBlock, + ] + ); + const messageReentrant = new Message( + trustedSenderAddress, + b256_fuelMessagePortalAddress, + BigInt(0), + randomBytes32(), + reentrantTestData + ); + const messageReentrantId = computeMessageId(messageReentrant); + const messageReentrantMessages = [messageReentrantId]; + const messageReentrantMessageNodes = constructTree( + messageReentrantMessages + ); + + // create block that contains this message + const tai64Time = + BigInt(Math.floor(new Date().getTime() / 1000)) + 4611686018427387914n; + const reentrantTestMessageBlock = createBlock( + '', + blockIds.length, + toBeHex(tai64Time), + '1', + calcRoot(messageReentrantMessages) + ); + const reentrantTestMessageBlockId = computeBlockId( + reentrantTestMessageBlock + ); + blockIds.push(reentrantTestMessageBlockId); + + // commit and finalize a block that contains the block with the message + const reentrantTestRootBlock = createBlock( + calcRoot(blockIds), + blockIds.length, + toBeHex(tai64Time) + ); + const reentrantTestPrevBlockNodes = constructTree(blockIds); + const reentrantTestRootBlockId = computeBlockId(reentrantTestRootBlock); + await fuelChainState.commit(reentrantTestRootBlockId, 1); + provider.send('evm_increaseTime', [TIME_TO_FINALIZE]); + + // generate proof for relaying reentrant message + const messageBlockLeafIndexKey = getLeafIndexKey( + reentrantTestPrevBlockNodes, + reentrantTestMessageBlockId + ); + const blockInHistoryProof = { + key: messageBlockLeafIndexKey, + proof: getProof(reentrantTestPrevBlockNodes, messageBlockLeafIndexKey), + }; + const messageLeafIndexKey = getLeafIndexKey( + messageReentrantMessageNodes, + messageReentrantId + ); + const messageInBlockProof = { + key: messageLeafIndexKey, + proof: getProof(messageReentrantMessageNodes, messageLeafIndexKey), + }; + + // re-enter via relayMessage + expect( + await fuelMessagePortal.incomingMessageSuccessful(messageReentrantId) + ).to.be.equal(false); + await expect( + fuelMessagePortal.relayMessage( + messageReentrant, + generateBlockHeaderLite(reentrantTestRootBlock), + reentrantTestMessageBlock, + blockInHistoryProof, + messageInBlockProof + ) + ).to.be.revertedWith('ReentrancyGuard: reentrant call'); + }); + }); + + describe('Behaves like FuelMessagePortalV2 - Accounting', () => { + beforeEach('fixture', async () => { + await fixture(); + await setupMessages( + hre.ethers.provider, + await fuelMessagePortal.getAddress(), + messageTester, + fuelChainState, + addresses + ); + }); + + // Simulates the case when withdrawn amount < initial deposited amount + it('should update the amount of deposited ether', async () => { + const recipient = b256ToAddress(messageEOA.recipient); + const txSender = signers.find((_, i) => addresses[i] === recipient); + const withdrawnAmount = messageEOA.amount * BASE_ASSET_CONVERSION; + const depositedAmount = withdrawnAmount * 2n; + + await fuelMessagePortal + .connect(txSender) + .depositETH(messageEOA.recipient, { + value: depositedAmount, + }); + + const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( + messageEOA, + blockHeaders, + prevBlockNodes, + blockIds, + messageNodes + ); + + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(false); + + const relayTx = fuelMessagePortal.relayMessage( + messageEOA, + endOfCommitIntervalHeaderLite, + msgBlockHeader, + blockInRoot, + msgInBlock + ); + await expect(relayTx).to.not.be.reverted; + await expect(relayTx).to.changeEtherBalances( + [await fuelMessagePortal.getAddress(), recipient], + [withdrawnAmount * -1n, withdrawnAmount] + ); + + expect( + await fuelMessagePortal.incomingMessageSuccessful(msgID) + ).to.be.equal(true); + + const expectedDepositedAmount = depositedAmount - withdrawnAmount; + expect(await fuelMessagePortal.totalDeposited()).to.be.equal( + expectedDepositedAmount + ); + }); + }); +} From eb2356a6a7f61b90e2dad219d5cce59e265fb738 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Thu, 27 Jun 2024 11:37:52 +0200 Subject: [PATCH 16/21] test: add outgoing messages tests to FuelMessagePortalV4 --- .../test/FuelMessagePortalV4.L1toL2.test.ts | 421 +++++++ ....ts => FuelMessagePortalV4.L2toL1.test.ts} | 2 +- .../test/messagesIncomingV3.test.ts | 1114 +---------------- 3 files changed, 477 insertions(+), 1060 deletions(-) create mode 100644 packages/solidity-contracts/test/FuelMessagePortalV4.L1toL2.test.ts rename packages/solidity-contracts/test/{FuelMessagePortalV4.test.ts => FuelMessagePortalV4.L2toL1.test.ts} (98%) diff --git a/packages/solidity-contracts/test/FuelMessagePortalV4.L1toL2.test.ts b/packages/solidity-contracts/test/FuelMessagePortalV4.L1toL2.test.ts new file mode 100644 index 00000000..80dcb7c1 --- /dev/null +++ b/packages/solidity-contracts/test/FuelMessagePortalV4.L1toL2.test.ts @@ -0,0 +1,421 @@ +import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; +import chai from 'chai'; +import type { Provider } from 'ethers'; +import { + MaxUint256, + Wallet, + hexlify, + parseEther, + parseUnits, + randomBytes, + zeroPadValue, +} from 'ethers'; +import { deployments, ethers, upgrades } from 'hardhat'; + +import { randomBytes32 } from '../protocol/utils'; +import type { + MessageTester, + FuelChainState, + FuelMessagePortalV4, +} from '../typechain'; + +import { + BLOCKS_PER_COMMIT_INTERVAL, + COMMIT_COOLDOWN, + TIME_TO_FINALIZE, +} from './utils'; +import { addressToB256 } from './utils/addressConversion'; + +const { expect } = chai; + +const ETH_DECIMALS = 18n; +const FUEL_BASE_ASSET_DECIMALS = 9n; +const BASE_ASSET_CONVERSION = 10n ** (ETH_DECIMALS - FUEL_BASE_ASSET_DECIMALS); + +const ETH_GLOBAL_LIMIT = parseEther('20'); +const ETH_PER_ACCOUNT_LIMIT = parseEther('10'); + +const EMPTY_DATA = new Uint8Array([]); + +const GAS_LIMIT = 1_000; +const MIN_GAS_PRICE = parseUnits('1', 'gwei'); +const MIN_GAS_PER_TX = 1; + +describe.only('FuelMessagesPortalV4 - Outgoing messages', async () => { + const nonceList: string[] = []; + + let signers: HardhatEthersSigner[]; + let addresses: string[]; + + // Testing contracts + let messageTester: MessageTester; + let fuelMessagePortal: FuelMessagePortalV4; + let fuelChainState: FuelChainState; + + let provider: Provider; + + const fixture = deployments.createFixture( + async ( + { ethers, upgrades: { deployProxy } }, + options?: { globalEthLimit?: bigint } + ) => { + const provider = ethers.provider; + const signers = await ethers.getSigners(); + const [deployer] = signers; + + const proxyOptions = { + initializer: 'initialize', + }; + + const fuelChainState = (await ethers + .getContractFactory('FuelChainState', deployer) + .then(async (factory) => + deployProxy(factory, [], { + ...proxyOptions, + constructorArgs: [ + TIME_TO_FINALIZE, + BLOCKS_PER_COMMIT_INTERVAL, + COMMIT_COOLDOWN, + ], + }) + ) + .then((tx) => tx.waitForDeployment())) as FuelChainState; + + const FuelMessagePortalV4 = await ethers.getContractFactory( + 'FuelMessagePortalV4' + ); + + const fuelMessagePortal = (await upgrades.deployProxy( + FuelMessagePortalV4, + [await fuelChainState.getAddress()], + { + initializer: 'initialize', + constructorArgs: [ + options?.globalEthLimit || ETH_GLOBAL_LIMIT, + GAS_LIMIT, + MIN_GAS_PER_TX, + MIN_GAS_PRICE, + ], + } + )) as unknown as FuelMessagePortalV4; + + const messageTester = (await ethers + .getContractFactory('MessageTester', deployer) + .then(async (factory) => factory.deploy(fuelMessagePortal)) + .then((tx) => tx.waitForDeployment())) as MessageTester; + + await signers[0].sendTransaction({ + to: messageTester, + value: parseEther('2'), + }); + + return { + provider, + deployer, + signers, + fuelMessagePortal, + fuelChainState, + messageTester, + addresses: signers.map(({ address }) => address), + }; + } + ); + + describe('Behaves like V2 - Accounting', () => { + beforeEach('fixture', async () => { + const fixt = await fixture(); + ({ + messageTester, + provider, + addresses, + fuelMessagePortal, + fuelChainState, + signers, + } = fixt); + }); + + it('should track the amount of deposited ether', async () => { + const recipient = randomBytes32(); + const value = parseEther('1'); + const fuelMessagePortalAddress = await fuelMessagePortal.getAddress(); + + { + const sender = signers[1]; + const tx = fuelMessagePortal + .connect(sender) + .depositETH(recipient, { value }); + + await expect(tx).not.to.be.reverted; + await expect(tx).to.changeEtherBalances( + [sender.address, fuelMessagePortalAddress], + [value * -1n, value] + ); + + expect(await fuelMessagePortal.totalDeposited()).equal(value); + } + + { + const sender = signers[2]; + const tx = fuelMessagePortal + .connect(sender) + .depositETH(recipient, { value }); + + await expect(tx).not.to.be.reverted; + await expect(tx).to.changeEtherBalances( + [sender.address, fuelMessagePortalAddress], + [value * -1n, value] + ); + + expect(await fuelMessagePortal.totalDeposited()).equal(value * 2n); + } + }); + + it('should revert if the global limit is reached', async () => { + const recipient = randomBytes32(); + const sender = signers[1]; + await fuelMessagePortal + .connect(signers[2]) + .depositETH(recipient, { value: ETH_PER_ACCOUNT_LIMIT }); + + await fuelMessagePortal + .connect(signers[3]) + .depositETH(recipient, { value: ETH_PER_ACCOUNT_LIMIT }); + + const revertTx = fuelMessagePortal + .connect(sender) + .depositETH(recipient, { value: 1 }); + + await expect(revertTx).to.be.revertedWithCustomError( + fuelMessagePortal, + 'GlobalDepositLimit' + ); + }); + }); + + describe('Behaves like V1 - Send messages', async () => { + let messageTesterAddress: string; + + before(async () => { + const fixt = await fixture({ + globalEthLimit: MaxUint256, + }); + ({ + messageTester, + provider, + addresses, + fuelMessagePortal, + fuelChainState, + } = fixt); + + // Verify contract getters + expect(await fuelMessagePortal.fuelChainStateContract()).to.equal( + await fuelChainState.getAddress() + ); + expect(await messageTester.fuelMessagePortal()).to.equal( + await fuelMessagePortal.getAddress() + ); + + messageTesterAddress = await messageTester.getAddress(); + }); + + it('Should be able to send message with data', async () => { + const recipient = randomBytes32(); + const data = hexlify(randomBytes(16)); + await expect(messageTester.attemptSendMessage(recipient, data)).to.not.be + .reverted; + + // Check logs for message sent + const logs = await provider.getLogs({ + address: fuelMessagePortal, + }); + const messageSentEvent = fuelMessagePortal.interface.parseLog( + logs[logs.length - 1] + ); + expect(messageSentEvent.name).to.equal('MessageSent'); + expect(messageSentEvent.args.sender).to.equal( + addressToB256(messageTesterAddress).toLowerCase() + ); + expect(messageSentEvent.args.recipient).to.equal(recipient); + expect(messageSentEvent.args.data).to.equal(data); + expect(messageSentEvent.args.amount).to.equal(0); + + // Check that nonce is unique + expect(nonceList).to.not.include(messageSentEvent.args.nonce); + nonceList.push(messageSentEvent.args.nonce); + }); + + it('Should be able to send message without data', async () => { + const recipient = randomBytes32(); + await expect(messageTester.attemptSendMessage(recipient, EMPTY_DATA)).to + .not.be.reverted; + + // Check logs for message sent + const logs = await provider.getLogs({ + address: fuelMessagePortal, + }); + const messageSentEvent = fuelMessagePortal.interface.parseLog( + logs[logs.length - 1] + ); + expect(messageSentEvent.name).to.equal('MessageSent'); + expect(messageSentEvent.args.sender).to.equal( + zeroPadValue(messageTesterAddress, 32) + ); + expect(messageSentEvent.args.recipient).to.equal(recipient); + expect(messageSentEvent.args.data).to.equal('0x'); + expect(messageSentEvent.args.amount).to.equal(0); + + // Check that nonce is unique + expect(nonceList).to.not.include(messageSentEvent.args.nonce); + nonceList.push(messageSentEvent.args.nonce); + }); + + it('Should be able to send message with amount and data', async () => { + const recipient = randomBytes32(); + const data = hexlify(randomBytes(8)); + const portalBalance = await provider.getBalance(fuelMessagePortal); + await expect( + messageTester.attemptSendMessageWithAmount( + recipient, + parseEther('0.1'), + data + ) + ).to.not.be.reverted; + + // Check logs for message sent + const logs = await provider.getLogs({ + address: fuelMessagePortal, + }); + const messageSentEvent = fuelMessagePortal.interface.parseLog( + logs[logs.length - 1] + ); + expect(messageSentEvent.name).to.equal('MessageSent'); + expect(messageSentEvent.args.sender).to.equal( + zeroPadValue(messageTesterAddress, 32) + ); + expect(messageSentEvent.args.recipient).to.equal(recipient); + expect(messageSentEvent.args.data).to.equal(data); + expect(messageSentEvent.args.amount).to.equal( + parseEther('0.1') / BASE_ASSET_CONVERSION + ); + + // Check that nonce is unique + expect(nonceList).to.not.include(messageSentEvent.args.nonce); + nonceList.push(messageSentEvent.args.nonce); + + // Check that portal balance increased + expect(await provider.getBalance(fuelMessagePortal)).to.equal( + portalBalance + parseEther('0.1') + ); + }); + + it('Should be able to send message with amount and without data', async () => { + const recipient = randomBytes32(); + const portalBalance = await provider.getBalance(fuelMessagePortal); + await expect( + messageTester.attemptSendMessageWithAmount( + recipient, + parseEther('0.5'), + EMPTY_DATA + ) + ).to.not.be.reverted; + + // Check logs for message sent + const logs = await provider.getLogs({ + address: fuelMessagePortal, + }); + const messageSentEvent = fuelMessagePortal.interface.parseLog( + logs[logs.length - 1] + ); + expect(messageSentEvent.name).to.equal('MessageSent'); + expect(messageSentEvent.args.sender).to.equal( + zeroPadValue(messageTesterAddress, 32) + ); + expect(messageSentEvent.args.recipient).to.equal(recipient); + expect(messageSentEvent.args.data).to.equal('0x'); + expect(messageSentEvent.args.amount).to.equal( + parseEther('0.5') / BASE_ASSET_CONVERSION + ); + + // Check that nonce is unique + expect(nonceList).to.not.include(messageSentEvent.args.nonce); + nonceList.push(messageSentEvent.args.nonce); + + // Check that portal balance increased + expect(await provider.getBalance(fuelMessagePortal)).to.equal( + portalBalance + parseEther('0.5') + ); + }); + + it('Should not be able to send message with amount too small', async () => { + const recipient = randomBytes32(); + await expect( + fuelMessagePortal.sendMessage(recipient, EMPTY_DATA, { + value: 1, + }) + ).to.be.revertedWithCustomError( + fuelMessagePortal, + 'AmountPrecisionIncompatibility' + ); + }); + + it('Should not be able to send message with amount too big', async () => { + const recipient = randomBytes32(); + await ethers.provider.send('hardhat_setBalance', [ + addresses[0], + '0xf00000000000000000000000', + ]); + + const maxUint64 = BigInt('0xffffffffffffffff'); + const precision = 10n ** 9n; + + const maxAllowedValue = maxUint64 * precision; + await fuelMessagePortal.sendMessage(recipient, EMPTY_DATA, { + value: maxAllowedValue, + }); + + const minUnallowedValue = (maxUint64 + 1n) * precision; + await expect( + fuelMessagePortal.sendMessage(recipient, EMPTY_DATA, { + value: minUnallowedValue, + }) + ).to.be.revertedWithCustomError(fuelMessagePortal, 'AmountTooBig'); + }); + it('Should not be able to send message with too much data', async () => { + const recipient = randomBytes32(); + const data = new Uint8Array(65536 + 1); + await expect( + fuelMessagePortal.sendMessage(recipient, data) + ).to.be.revertedWithCustomError(fuelMessagePortal, 'MessageDataTooLarge'); + }); + + it('Should be able to send message with only ETH', async () => { + const recipient = randomBytes32(); + await expect( + fuelMessagePortal.depositETH(recipient, { + value: parseEther('1.234'), + }) + ).to.not.be.reverted; + + // Check logs for message sent + const logs = await provider.getLogs({ + address: fuelMessagePortal, + }); + const messageSentEvent = fuelMessagePortal.interface.parseLog( + logs[logs.length - 1] + ); + expect(messageSentEvent.name).to.equal('MessageSent'); + expect(messageSentEvent.args.sender).to.equal( + zeroPadValue(addresses[0], 32) + ); + expect(messageSentEvent.args.recipient).to.equal(recipient); + expect(messageSentEvent.args.data).to.equal('0x'); + expect(messageSentEvent.args.amount).to.equal( + parseEther('1.234') / BASE_ASSET_CONVERSION + ); + + // Check that nonce is unique + expect(nonceList).to.not.include(messageSentEvent.args.nonce); + nonceList.push(messageSentEvent.args.nonce); + }); + }); +}); diff --git a/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts b/packages/solidity-contracts/test/FuelMessagePortalV4.L2toL1.test.ts similarity index 98% rename from packages/solidity-contracts/test/FuelMessagePortalV4.test.ts rename to packages/solidity-contracts/test/FuelMessagePortalV4.L2toL1.test.ts index d7691c42..352807ab 100644 --- a/packages/solidity-contracts/test/FuelMessagePortalV4.test.ts +++ b/packages/solidity-contracts/test/FuelMessagePortalV4.L2toL1.test.ts @@ -20,7 +20,7 @@ const GAS_LIMIT = 1_000; const MIN_GAS_PRICE = parseUnits('1', 'gwei'); const MIN_GAS_PER_TX = 1; -describe.only('FuelMessagePortalV4', () => { +describe('FuelMessagePortalV4 - Incoming messages', () => { const fixture = hre.deployments.createFixture( async ({ ethers, upgrades }) => { const signers = await ethers.getSigners(); diff --git a/packages/solidity-contracts/test/messagesIncomingV3.test.ts b/packages/solidity-contracts/test/messagesIncomingV3.test.ts index ffef897d..ae30bc21 100644 --- a/packages/solidity-contracts/test/messagesIncomingV3.test.ts +++ b/packages/solidity-contracts/test/messagesIncomingV3.test.ts @@ -1,227 +1,25 @@ -import { calcRoot, constructTree, getProof } from '@fuel-ts/merkle'; -import type { HardhatEthersSigner } from '@nomicfoundation/hardhat-ethers/signers'; import { expect } from 'chai'; -import { MaxUint256, parseEther, toBeHex, type Provider } from 'ethers'; import { deployments, ethers, upgrades } from 'hardhat'; -import type BlockHeader from '../protocol/blockHeader'; -import type { BlockHeaderLite } from '../protocol/blockHeader'; -import { - computeBlockId, - generateBlockHeaderLite, -} from '../protocol/blockHeader'; -import Message, { computeMessageId } from '../protocol/message'; -import { randomBytes32, tai64Time } from '../protocol/utils'; import type { FuelChainState, MessageTester, FuelMessagePortalV3, } from '../typechain'; -import { createRandomWalletWithFunds } from './utils'; -import { addressToB256, b256ToAddress } from './utils/addressConversion'; -import { createBlock } from './utils/createBlock'; -import type { TreeNode } from './utils/merkle'; import { BLOCKS_PER_COMMIT_INTERVAL, COMMIT_COOLDOWN, TIME_TO_FINALIZE, - generateProof, - getLeafIndexKey, } from './utils/merkle'; +import { behavesLikeFuelMessagePortalV3 } from './behaviors/FuelMessagePortalV3.L2toL1.behavior.test'; +import { MaxUint256 } from 'ethers'; -const ETH_DECIMALS = 18n; -const FUEL_BASE_ASSET_DECIMALS = 9n; -const BASE_ASSET_CONVERSION = 10n ** (ETH_DECIMALS - FUEL_BASE_ASSET_DECIMALS); +const DEPOSIT_LIMIT = MaxUint256; describe('FuelMessagePortalV3 - Incoming messages', () => { - let provider: Provider; - let addresses: string[]; - let signers: HardhatEthersSigner[]; - let fuelMessagePortal: FuelMessagePortalV3; - let fuelChainState: FuelChainState; - - // Message data - const messageTestData1 = randomBytes32(); - const messageTestData2 = randomBytes32(); - const messageTestData3 = randomBytes32(); - let messageNodes: TreeNode[]; - let trustedSenderAddress: string; - - // Testing contracts - let messageTester: MessageTester; - let messageTesterAddress: string; - let b256_fuelMessagePortalAddress: string; - - // Messages - let message1: Message; - let message2: Message; - let messageWithAmount: Message; - let messageBadSender: Message; - let messageBadRecipient: Message; - let messageBadData: Message; - let messageEOA: Message; - let messageEOANoAmount: Message; - - // Arrays of committed block headers and their IDs - let blockHeaders: BlockHeader[]; - let blockIds: string[]; - let endOfCommitIntervalHeader: BlockHeader; - let endOfCommitIntervalHeaderLite: BlockHeaderLite; - let unfinalizedBlock: BlockHeader; - let prevBlockNodes: TreeNode[]; - - async function setupMessages( - portalAddr: string, - messageTester: MessageTester, - fuelChainState: FuelChainState, - addresses: string[] - ) { - blockIds = []; - blockHeaders = []; - // get data for building messages - messageTesterAddress = addressToB256(await messageTester.getAddress()); - b256_fuelMessagePortalAddress = addressToB256(portalAddr); - - trustedSenderAddress = await messageTester.getTrustedSender(); - - // message from trusted sender - message1 = new Message( - trustedSenderAddress, - messageTesterAddress, - BigInt(0), - randomBytes32(), - messageTester.interface.encodeFunctionData('receiveMessage', [ - messageTestData1, - messageTestData2, - ]) - ); - message2 = new Message( - trustedSenderAddress, - messageTesterAddress, - BigInt(0), - randomBytes32(), - messageTester.interface.encodeFunctionData('receiveMessage', [ - messageTestData2, - messageTestData1, - ]) - ); - // message from trusted sender with amount - messageWithAmount = new Message( - trustedSenderAddress, - messageTesterAddress, - parseEther('0.1') / BASE_ASSET_CONVERSION, - randomBytes32(), - messageTester.interface.encodeFunctionData('receiveMessage', [ - messageTestData2, - messageTestData3, - ]) - ); - // message from untrusted sender - messageBadSender = new Message( - randomBytes32(), - messageTesterAddress, - BigInt(0), - randomBytes32(), - messageTester.interface.encodeFunctionData('receiveMessage', [ - messageTestData3, - messageTestData1, - ]) - ); - // message to bad recipient - messageBadRecipient = new Message( - trustedSenderAddress, - addressToB256(portalAddr), - BigInt(0), - randomBytes32(), - messageTester.interface.encodeFunctionData('receiveMessage', [ - messageTestData2, - messageTestData2, - ]) - ); - // message with bad data - messageBadData = new Message( - trustedSenderAddress, - messageTesterAddress, - BigInt(0), - randomBytes32(), - randomBytes32() - ); - // message to EOA - messageEOA = new Message( - randomBytes32(), - addressToB256(addresses[2]), - parseEther('0.1') / BASE_ASSET_CONVERSION, - randomBytes32(), - '0x' - ); - // message to EOA no amount - messageEOANoAmount = new Message( - randomBytes32(), - addressToB256(addresses[3]), - BigInt(0), - randomBytes32(), - '0x' - ); - - // compile all message IDs - const messageIds: string[] = []; - messageIds.push(computeMessageId(message1)); - messageIds.push(computeMessageId(message2)); - messageIds.push(computeMessageId(messageWithAmount)); - messageIds.push(computeMessageId(messageBadSender)); - messageIds.push(computeMessageId(messageBadRecipient)); - messageIds.push(computeMessageId(messageBadData)); - messageIds.push(computeMessageId(messageEOA)); - messageIds.push(computeMessageId(messageEOANoAmount)); - messageNodes = constructTree(messageIds); - - // create blocks - const messageCount = messageIds.length.toString(); - const messagesRoot = calcRoot(messageIds); - for (let i = 0; i < BLOCKS_PER_COMMIT_INTERVAL - 1; i++) { - const blockHeader = createBlock('', i, '', messageCount, messagesRoot); - const blockId = computeBlockId(blockHeader); - - // append block header and Id to arrays - blockHeaders.push(blockHeader); - blockIds.push(blockId); - } - - // create end of commit interval block - const timestamp = tai64Time(new Date().getTime()); - endOfCommitIntervalHeader = createBlock( - calcRoot(blockIds), - blockIds.length, - timestamp, - messageCount, - messagesRoot - ); - endOfCommitIntervalHeaderLite = generateBlockHeaderLite( - endOfCommitIntervalHeader - ); - prevBlockNodes = constructTree(blockIds); - blockIds.push(computeBlockId(endOfCommitIntervalHeader)); - - // finalize blocks in the state contract - await fuelChainState.commit(computeBlockId(endOfCommitIntervalHeader), 0); - ethers.provider.send('evm_increaseTime', [TIME_TO_FINALIZE]); - - // create an unfinalized block - unfinalizedBlock = createBlock( - calcRoot(blockIds), - BLOCKS_PER_COMMIT_INTERVAL * 11 - 1, - timestamp, - messageCount, - messagesRoot - ); - - await fuelChainState.commit(computeBlockId(unfinalizedBlock), 10); - } - const fixture = deployments.createFixture( async ({ ethers, upgrades: { deployProxy } }) => { - const provider = ethers.provider; const signers = await ethers.getSigners(); const [deployer] = signers; @@ -243,28 +41,14 @@ describe('FuelMessagePortalV3 - Incoming messages', () => { ) .then((tx) => tx.waitForDeployment())) as FuelChainState; - const deployment = await ethers - .getContractFactory('FuelMessagePortal', deployer) - .then(async (factory) => - deployProxy( - factory, - [await fuelChainState.getAddress()], - proxyOptions - ) - ) - .then((tx) => tx.waitForDeployment()); - - const V2Implementation = await ethers.getContractFactory( - 'FuelMessagePortalV2' - ); - - const V3Implementation = await ethers.getContractFactory( + const FuelMessagePortalV3 = await ethers.getContractFactory( 'FuelMessagePortalV3' ); - - const fuelMessagePortal = V3Implementation.attach(deployment).connect( - deployment.runner - ) as FuelMessagePortalV3; + const fuelMessagePortal = (await upgrades.deployProxy( + FuelMessagePortalV3, + [await fuelChainState.getAddress()], + { ...proxyOptions, constructorArgs: [DEPOSIT_LIMIT] } + )) as unknown as FuelMessagePortalV3; const messageTester = await ethers .getContractFactory('MessageTester', deployer) @@ -274,13 +58,10 @@ describe('FuelMessagePortalV3 - Incoming messages', () => { ); return { - provider, deployer, signers, fuelMessagePortal, fuelChainState, - V2Implementation, - V3Implementation, messageTester, addresses: signers.map(({ address }) => address), }; @@ -288,8 +69,51 @@ describe('FuelMessagePortalV3 - Incoming messages', () => { ); it('can upgrade from V1 to V2 to V3', async () => { - const { fuelMessagePortal, V2Implementation, V3Implementation } = - await fixture(); + // const { fuelMessagePortal, V2Implementation, V3Implementation } = + // await fixture(); + + const [deployer] = await ethers.getSigners(); + + const V2Implementation = await ethers.getContractFactory( + 'FuelMessagePortalV2' + ); + + const V3Implementation = await ethers.getContractFactory( + 'FuelMessagePortalV3' + ); + + const proxyOptions = { + initializer: 'initialize', + }; + + const fuelChainState = (await ethers + .getContractFactory('FuelChainState', deployer) + .then(async (factory) => + upgrades.deployProxy(factory, [], { + ...proxyOptions, + constructorArgs: [ + TIME_TO_FINALIZE, + BLOCKS_PER_COMMIT_INTERVAL, + COMMIT_COOLDOWN, + ], + }) + ) + .then((tx) => tx.waitForDeployment())) as FuelChainState; + + const deployment = await ethers + .getContractFactory('FuelMessagePortal', deployer) + .then(async (factory) => + upgrades.deployProxy( + factory, + [await fuelChainState.getAddress()], + proxyOptions + ) + ) + .then((tx) => tx.waitForDeployment()); + + const fuelMessagePortal = V3Implementation.attach(deployment).connect( + deployment.runner + ) as FuelMessagePortalV3; await expect(fuelMessagePortal.depositLimitGlobal()).to.be.reverted; @@ -309,833 +133,5 @@ describe('FuelMessagePortalV3 - Incoming messages', () => { expect(await fuelMessagePortal.withdrawalsPaused()).to.be.true; }); - describe('Behaves like V3 - Blacklisting', () => { - beforeEach('fixture', async () => { - const fixt = await fixture(); - const { V2Implementation, V3Implementation } = fixt; - ({ - provider, - fuelMessagePortal, - fuelChainState, - messageTester, - addresses, - signers, - } = fixt); - - await upgrades.upgradeProxy(fuelMessagePortal, V2Implementation, { - unsafeAllow: ['constructor'], - constructorArgs: [MaxUint256], - }); - - await upgrades.upgradeProxy(fuelMessagePortal, V3Implementation, { - unsafeAllow: ['constructor'], - constructorArgs: [MaxUint256], - }); - - await setupMessages( - await fuelMessagePortal.getAddress(), - messageTester, - fuelChainState, - addresses - ); - }); - - describe('pauseWithdrawals', () => { - it('pauses all withdrawals', async () => { - const [, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - messageEOA, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - - await fuelMessagePortal.pauseWithdrawals(); - expect(await fuelMessagePortal.withdrawalsPaused()).to.be.true; - - const relayTx = fuelMessagePortal.relayMessage( - messageEOA, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ); - - await expect(relayTx).to.be.revertedWithCustomError( - fuelMessagePortal, - 'WithdrawalsPaused' - ); - }); - }); - - describe('unpauseWithdrawals', () => { - it('unpauses withdrawals', async () => { - const withdrawnAmount = messageEOA.amount * BASE_ASSET_CONVERSION; - const depositedAmount = withdrawnAmount * 2n; - await fuelMessagePortal.depositETH(messageEOA.recipient, { - value: depositedAmount, - }); - - await fuelMessagePortal.pauseWithdrawals(); - const [, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - messageEOA, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - - await fuelMessagePortal.unpauseWithdrawals(); - expect(await fuelMessagePortal.withdrawalsPaused()).to.be.false; - - const relayTx = fuelMessagePortal.relayMessage( - messageEOA, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ); - - await expect(relayTx).not.to.be.reverted; - }); - }); - - describe('addMessageToBlacklist', () => { - it('can only be called by pauser role', async () => { - const mallory = await createRandomWalletWithFunds(); - - const [msgID] = generateProof( - messageEOA, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - - const PAUSER_ROLE = await fuelMessagePortal.PAUSER_ROLE(); - - const tx = fuelMessagePortal - .connect(mallory) - .addMessageToBlacklist(msgID); - - const expectedErrorMsg = - `AccessControl: account ${mallory.address.toLowerCase()} ` + - `is missing role ${PAUSER_ROLE}`; - - await expect(tx).to.be.revertedWith(expectedErrorMsg); - }); - - it('prevents withdrawals', async () => { - // Blacklisted message - { - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = - generateProof( - messageEOA, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - - await fuelMessagePortal.addMessageToBlacklist(msgID); - - const relayTx = fuelMessagePortal.relayMessage( - messageEOA, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ); - - await expect(relayTx).to.be.revertedWithCustomError( - fuelMessagePortal, - 'MessageBlacklisted' - ); - } - - // Non blacklisted message - { - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = - generateProof( - messageEOANoAmount, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - const relayTx = fuelMessagePortal.relayMessage( - messageEOANoAmount, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ); - await expect(relayTx).to.not.be.reverted; - } - }); - }); - - describe('removeMessageFromBlacklist', () => { - it('can only be called by admin role', async () => { - const mallory = await createRandomWalletWithFunds(); - const [msgID] = generateProof( - messageEOA, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - - const ADMIN_ROLE = await fuelMessagePortal.DEFAULT_ADMIN_ROLE(); - const tx = fuelMessagePortal - .connect(mallory) - .removeMessageFromBlacklist(msgID); - - const expectedErrorMsg = - `AccessControl: account ${mallory.address.toLowerCase()} ` + - `is missing role ${ADMIN_ROLE}`; - expect(tx).to.be.revertedWith(expectedErrorMsg); - }); - it('restores ability to withdraw', async () => { - // Whitelist back the blacklisted message - { - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = - generateProof( - messageEOA, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - - const withdrawnAmount = messageEOA.amount * BASE_ASSET_CONVERSION; - const depositedAmount = withdrawnAmount * 2n; - await fuelMessagePortal.depositETH(messageEOA.recipient, { - value: depositedAmount, - }); - await fuelMessagePortal.removeMessageFromBlacklist(msgID); - - const relayTx = fuelMessagePortal.relayMessage( - messageEOA, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ); - - await expect(relayTx).not.to.be.reverted; - } - }); - }); - }); - - describe('Behaves like V2 - Accounting', () => { - beforeEach('fixture', async () => { - const fixt = await fixture(); - const { V2Implementation } = fixt; - ({ - provider, - fuelMessagePortal, - fuelChainState, - messageTester, - addresses, - signers, - } = fixt); - - await upgrades.upgradeProxy(fuelMessagePortal, V2Implementation, { - unsafeAllow: ['constructor'], - constructorArgs: [MaxUint256], - }); - - await setupMessages( - await fuelMessagePortal.getAddress(), - messageTester, - fuelChainState, - addresses - ); - }); - - // Simulates the case when withdrawn amount < initial deposited amount - it('should update the amount of deposited ether', async () => { - const recipient = b256ToAddress(messageEOA.recipient); - const txSender = signers.find((_, i) => addresses[i] === recipient); - const withdrawnAmount = messageEOA.amount * BASE_ASSET_CONVERSION; - const depositedAmount = withdrawnAmount * 2n; - - await fuelMessagePortal - .connect(txSender) - .depositETH(messageEOA.recipient, { - value: depositedAmount, - }); - - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - messageEOA, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - - const relayTx = fuelMessagePortal.relayMessage( - messageEOA, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ); - await expect(relayTx).to.not.be.reverted; - await expect(relayTx).to.changeEtherBalances( - [await fuelMessagePortal.getAddress(), recipient], - [withdrawnAmount * -1n, withdrawnAmount] - ); - - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(true); - - const expectedDepositedAmount = depositedAmount - withdrawnAmount; - expect(await fuelMessagePortal.totalDeposited()).to.be.equal( - expectedDepositedAmount - ); - }); - }); - - // This is essentially a copy - paste from `messagesIncoming.ts` - describe('Behaves like V1 - Relay both valid and invalid messages', async () => { - before(async () => { - const fixt = await fixture(); - const { V3Implementation } = fixt; - ({ - provider, - fuelMessagePortal, - fuelChainState, - messageTester, - addresses, - } = fixt); - - await upgrades.upgradeProxy(fuelMessagePortal, V3Implementation, { - unsafeAllow: ['constructor'], - constructorArgs: [MaxUint256], - }); - - await setupMessages( - await fuelMessagePortal.getAddress(), - messageTester, - fuelChainState, - addresses - ); - }); - - it('Should not get a valid message sender outside of relaying', async () => { - await expect( - fuelMessagePortal.messageSender() - ).to.be.revertedWithCustomError( - fuelMessagePortal, - 'CurrentMessageSenderNotSet' - ); - }); - - it('Should not be able to relay message with bad root block', async () => { - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - message1, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - await expect( - fuelMessagePortal.relayMessage( - message1, - generateBlockHeaderLite( - createBlock('', BLOCKS_PER_COMMIT_INTERVAL * 20 - 1) - ), - msgBlockHeader, - blockInRoot, - msgInBlock - ) - ).to.be.revertedWithCustomError(fuelChainState, 'UnknownBlock'); - - await expect( - fuelMessagePortal.relayMessage( - message1, - generateBlockHeaderLite(unfinalizedBlock), - msgBlockHeader, - blockInRoot, - msgInBlock - ) - ).to.be.revertedWithCustomError(fuelMessagePortal, 'UnfinalizedBlock'); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - }); - - it('Should not be able to relay message with bad proof in root block', async () => { - const portalBalance = await provider.getBalance(fuelMessagePortal); - const messageTesterBalance = await provider.getBalance(messageTester); - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - message1, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - blockInRoot.key = blockInRoot.key + 1; - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - await expect( - fuelMessagePortal.relayMessage( - message1, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ) - ).to.be.revertedWithCustomError( - fuelMessagePortal, - 'InvalidBlockInHistoryProof' - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - expect(await provider.getBalance(fuelMessagePortal)).to.be.equal( - portalBalance - ); - expect(await provider.getBalance(messageTester)).to.be.equal( - messageTesterBalance - ); - }); - - it('Should be able to relay valid message', async () => { - const portalBalance = await provider.getBalance(fuelMessagePortal); - const messageTesterBalance = await provider.getBalance(messageTester); - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - message1, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - await expect( - fuelMessagePortal.relayMessage( - message1, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ) - ).to.not.be.reverted; - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(true); - expect(await messageTester.data1()).to.be.equal(messageTestData1); - expect(await messageTester.data2()).to.be.equal(messageTestData2); - expect(await provider.getBalance(fuelMessagePortal)).to.be.equal( - portalBalance - ); - expect(await provider.getBalance(messageTester)).to.be.equal( - messageTesterBalance - ); - }); - - it('Should not be able to relay already relayed message', async () => { - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - message1, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(true); - await expect( - fuelMessagePortal.relayMessage( - message1, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ) - ).to.be.revertedWithCustomError(fuelMessagePortal, 'AlreadyRelayed'); - }); - - it('Should not be able to relay message with low gas', async () => { - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - messageWithAmount, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - const options = { - gasLimit: 140000, - }; - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - await expect( - fuelMessagePortal.relayMessage( - messageWithAmount, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock, - options - ) - ).to.be.reverted; - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - }); - - it('Should be able to relay message with amount', async () => { - const expectedWithdrawnAmount = - messageWithAmount.amount * BASE_ASSET_CONVERSION; - - await fuelMessagePortal.depositETH(messageWithAmount.sender, { - value: expectedWithdrawnAmount, - }); - - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - messageWithAmount, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - - const relayTx = fuelMessagePortal.relayMessage( - messageWithAmount, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ); - - await expect(relayTx).to.not.be.reverted; - await expect(relayTx).to.changeEtherBalances( - [fuelMessagePortal, messageTester], - [expectedWithdrawnAmount * -1n, expectedWithdrawnAmount] - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(true); - - expect(await messageTester.data1()).to.be.equal(messageTestData2); - expect(await messageTester.data2()).to.be.equal(messageTestData3); - }); - - it('Should not be able to relay message from untrusted sender', async () => { - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - messageBadSender, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - await expect( - fuelMessagePortal.relayMessage( - messageBadSender, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ) - ).to.be.revertedWithCustomError(messageTester, 'InvalidMessageSender'); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - }); - - it('Should not be able to relay message to bad recipient', async () => { - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - messageBadRecipient, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - await expect( - fuelMessagePortal.relayMessage( - messageBadRecipient, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ) - ).to.be.revertedWithCustomError(fuelMessagePortal, 'MessageRelayFailed'); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - }); - - it('Should not be able to relay message with bad data', async () => { - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - messageBadData, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - await expect( - fuelMessagePortal.relayMessage( - messageBadData, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ) - ).to.be.revertedWithCustomError(fuelMessagePortal, 'MessageRelayFailed'); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - }); - - it('Should be able to relay message to EOA', async () => { - const expectedWithdrawnAmount = messageEOA.amount * BASE_ASSET_CONVERSION; - const expectedRecipient = b256ToAddress(messageEOA.recipient); - - await fuelMessagePortal.depositETH(messageEOA.sender, { - value: expectedWithdrawnAmount, - }); - - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - messageEOA, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - - const relayTx = fuelMessagePortal.relayMessage( - messageEOA, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ); - await expect(relayTx).to.not.be.reverted; - await expect(relayTx).to.changeEtherBalances( - [fuelMessagePortal, expectedRecipient], - [expectedWithdrawnAmount * -1n, expectedWithdrawnAmount] - ); - - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(true); - }); - - it('Should be able to relay message to EOA with no amount', async () => { - const messageRecipient = b256ToAddress(messageEOANoAmount.recipient); - - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - messageEOANoAmount, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - const relayTx = fuelMessagePortal.relayMessage( - messageEOANoAmount, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ); - await expect(relayTx).to.not.be.reverted; - await expect(relayTx).to.changeEtherBalances( - [fuelMessagePortal, messageRecipient], - [0, 0] - ); - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(true); - }); - - it('Should not be able to relay valid message with different amount', async () => { - const [msgID, msgBlockHeader, blockInRoot, msgInBlock] = generateProof( - message2, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - - const diffBlock = { - sender: message2.sender, - recipient: message2.recipient, - nonce: message2.nonce, - amount: message2.amount + parseEther('1.0'), - data: message2.data, - }; - - expect( - await fuelMessagePortal.incomingMessageSuccessful(msgID) - ).to.be.equal(false); - - await expect( - fuelMessagePortal.relayMessage( - diffBlock, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ) - ).to.be.revertedWithCustomError( - fuelMessagePortal, - 'InvalidMessageInBlockProof' - ); - }); - - it('Should not be able to relay non-existent message', async () => { - const [, msgBlockHeader, blockInRoot] = generateProof( - message2, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - const msgInBlock = { - key: 0, - proof: [], - }; - await expect( - fuelMessagePortal.relayMessage( - message2, - endOfCommitIntervalHeaderLite, - msgBlockHeader, - blockInRoot, - msgInBlock - ) - ).to.be.revertedWithCustomError( - fuelMessagePortal, - 'InvalidMessageInBlockProof' - ); - }); - - it('Should not be able to relay reentrant messages', async () => { - // create a message that attempts to relay another message - const [, rTestMsgBlockHeader, rTestBlockInRoot, rTestMsgInBlock] = - generateProof( - message1, - blockHeaders, - prevBlockNodes, - blockIds, - messageNodes - ); - const reentrantTestData = fuelMessagePortal.interface.encodeFunctionData( - 'relayMessage', - [ - message1, - endOfCommitIntervalHeaderLite, - rTestMsgBlockHeader, - rTestBlockInRoot, - rTestMsgInBlock, - ] - ); - const messageReentrant = new Message( - trustedSenderAddress, - b256_fuelMessagePortalAddress, - BigInt(0), - randomBytes32(), - reentrantTestData - ); - const messageReentrantId = computeMessageId(messageReentrant); - const messageReentrantMessages = [messageReentrantId]; - const messageReentrantMessageNodes = constructTree( - messageReentrantMessages - ); - - // create block that contains this message - const tai64Time = - BigInt(Math.floor(new Date().getTime() / 1000)) + 4611686018427387914n; - const reentrantTestMessageBlock = createBlock( - '', - blockIds.length, - toBeHex(tai64Time), - '1', - calcRoot(messageReentrantMessages) - ); - const reentrantTestMessageBlockId = computeBlockId( - reentrantTestMessageBlock - ); - blockIds.push(reentrantTestMessageBlockId); - - // commit and finalize a block that contains the block with the message - const reentrantTestRootBlock = createBlock( - calcRoot(blockIds), - blockIds.length, - toBeHex(tai64Time) - ); - const reentrantTestPrevBlockNodes = constructTree(blockIds); - const reentrantTestRootBlockId = computeBlockId(reentrantTestRootBlock); - await fuelChainState.commit(reentrantTestRootBlockId, 1); - ethers.provider.send('evm_increaseTime', [TIME_TO_FINALIZE]); - - // generate proof for relaying reentrant message - const messageBlockLeafIndexKey = getLeafIndexKey( - reentrantTestPrevBlockNodes, - reentrantTestMessageBlockId - ); - const blockInHistoryProof = { - key: messageBlockLeafIndexKey, - proof: getProof(reentrantTestPrevBlockNodes, messageBlockLeafIndexKey), - }; - const messageLeafIndexKey = getLeafIndexKey( - messageReentrantMessageNodes, - messageReentrantId - ); - const messageInBlockProof = { - key: messageLeafIndexKey, - proof: getProof(messageReentrantMessageNodes, messageLeafIndexKey), - }; - - // re-enter via relayMessage - expect( - await fuelMessagePortal.incomingMessageSuccessful(messageReentrantId) - ).to.be.equal(false); - await expect( - fuelMessagePortal.relayMessage( - messageReentrant, - generateBlockHeaderLite(reentrantTestRootBlock), - reentrantTestMessageBlock, - blockInHistoryProof, - messageInBlockProof - ) - ).to.be.revertedWith('ReentrancyGuard: reentrant call'); - }); - }); + behavesLikeFuelMessagePortalV3(fixture); }); From afaff285b05f4993a76494abfc62a589a9e0947a Mon Sep 17 00:00:00 2001 From: DefiCake Date: Thu, 27 Jun 2024 11:38:10 +0200 Subject: [PATCH 17/21] chore: remove .only from tests --- .../solidity-contracts/test/FuelMessagePortalV4.L1toL2.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solidity-contracts/test/FuelMessagePortalV4.L1toL2.test.ts b/packages/solidity-contracts/test/FuelMessagePortalV4.L1toL2.test.ts index 80dcb7c1..576b42cf 100644 --- a/packages/solidity-contracts/test/FuelMessagePortalV4.L1toL2.test.ts +++ b/packages/solidity-contracts/test/FuelMessagePortalV4.L1toL2.test.ts @@ -41,7 +41,7 @@ const GAS_LIMIT = 1_000; const MIN_GAS_PRICE = parseUnits('1', 'gwei'); const MIN_GAS_PER_TX = 1; -describe.only('FuelMessagesPortalV4 - Outgoing messages', async () => { +describe('FuelMessagesPortalV4 - Outgoing messages', async () => { const nonceList: string[] = []; let signers: HardhatEthersSigner[]; From 6691b60069de6a03299d52e788ed6333c0d8b89a Mon Sep 17 00:00:00 2001 From: DefiCake Date: Thu, 27 Jun 2024 17:39:59 +0200 Subject: [PATCH 18/21] feat: gas golfing and comments on sendTransaction --- .../v4/FuelMessagePortalV4.sol | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol index 2b169ff3..14db5fee 100644 --- a/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol +++ b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol @@ -36,6 +36,12 @@ contract FuelMessagePortalV4 is FuelMessagePortalV3 { MIN_GAS_PRICE = minGasPrice; } + /// @notice sends a transaction to the L2. The sender pays the execution cost with a fee that + /// @notice depends on congestion of previous calls to this function. + /// @notice DA costs are paid by the ethereum transaction itself + /// @dev Excess fee will be returned to the sender. Checks-effects-interactions pattern followed + /// @param gas amount of gas forwarded for the transaction in L2 + /// @param serializedTx Complete fuel transaction function sendTransaction(uint64 gas, bytes calldata serializedTx) external payable virtual { if (gas < MIN_GAS_PER_TX) { revert MinGas(); @@ -46,23 +52,39 @@ contract FuelMessagePortalV4 is FuelMessagePortalV3 { uint256 _gasPrice = gasPrice; if (_lastSeenBlock < block.number) { + // Update gas price uint256 distance; unchecked { distance = block.number - uint256(_lastSeenBlock); } + // If we had transactions in the previous block, check previous block congestion if (distance == 1) { if (_usedGas > GAS_TARGET) { - // Max increment: x2 - _gasPrice = (_gasPrice * PRECISION * _usedGas) / GAS_TARGET; + /** + * Max increment: x2 (Gas limit = gas target x2, see constructor) + * usedGas + * new gasPrice = gasPrice x -------------- + * gasTarget + */ + _gasPrice = _divByNonZero((_gasPrice * PRECISION * _usedGas), GAS_TARGET); } else { - // Max decrement: x0.5 - _gasPrice = (_gasPrice * (PRECISION + (_usedGas * PRECISION) / GAS_TARGET)) / 2; + /** + * Max decrement: x0.5. Min decrement: x1 + * usedGas + * new gasPrice = gasPrice x (1 + ---------------- ) x 0.5 + * gasTarget + */ + _gasPrice = _divByNonZero( + _gasPrice * (PRECISION + _divByNonZero((_usedGas * PRECISION), GAS_TARGET)), + 2 + ); } _gasPrice /= PRECISION; } else { - _gasPrice /= distance; + // If there were no transactions in the previous block, use distance to last congested block + _gasPrice = _divByNonZero(_gasPrice, distance); } _usedGas = gas; @@ -88,6 +110,7 @@ contract FuelMessagePortalV4 is FuelMessagePortalV3 { uint256 fee = _gasPrice * gas; + // Check fee and return to sender if needed if (msg.value != fee) { if (msg.value < fee) { revert InsufficientFee(); @@ -138,6 +161,13 @@ contract FuelMessagePortalV4 is FuelMessagePortalV3 { if (!success) revert RecipientRejectedETH(); } + /// @dev gas efficient division. Must be used with care, `_div` must be non zero + function _divByNonZero(uint256 _num, uint256 _div) internal pure returns (uint256 result) { + assembly { + result := div(_num, _div) + } + } + /** * @dev This empty reserved space is put in place to allow future versions to add new * variables without shifting down storage in the inheritance chain. From 17adc4228fb1ddee23f752c15db2dcf71eab2e45 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Thu, 27 Jun 2024 17:40:33 +0200 Subject: [PATCH 19/21] chore: add gas reporter --- packages/solidity-contracts/hardhat.config.ts | 4 + packages/solidity-contracts/package.json | 1 + pnpm-lock.yaml | 174 ++++++++++++++++++ 3 files changed, 179 insertions(+) diff --git a/packages/solidity-contracts/hardhat.config.ts b/packages/solidity-contracts/hardhat.config.ts index 78a719a3..d95e268e 100644 --- a/packages/solidity-contracts/hardhat.config.ts +++ b/packages/solidity-contracts/hardhat.config.ts @@ -8,6 +8,7 @@ import '@typechain/hardhat'; import '@openzeppelin/hardhat-upgrades'; import 'hardhat-deploy'; import 'solidity-coverage'; +import 'hardhat-gas-reporter'; dotEnvConfig(); @@ -103,6 +104,9 @@ const config: HardhatUserConfig = { etherscan: { apiKey: ETHERSCAN_API_KEY, }, + gasReporter: { + enabled: true, + }, }; export default config; diff --git a/packages/solidity-contracts/package.json b/packages/solidity-contracts/package.json index 1d4c9b2b..030739fb 100644 --- a/packages/solidity-contracts/package.json +++ b/packages/solidity-contracts/package.json @@ -62,6 +62,7 @@ "express": "^4.18.2", "hardhat": "^2.20.1", "hardhat-deploy": "^0.11.44", + "hardhat-gas-reporter": "^2.2.0", "lodash": "^4.17.21", "markdownlint": "^0.26.2", "markdownlint-cli": "^0.32.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6184da0..c5650db2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -190,6 +190,9 @@ importers: hardhat-deploy: specifier: ^0.11.44 version: 0.11.45(bufferutil@4.0.5)(utf-8-validate@5.0.7) + hardhat-gas-reporter: + specifier: ^2.2.0 + version: 2.2.0(bufferutil@4.0.5)(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(typescript@4.9.5)(utf-8-validate@5.0.7) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -264,6 +267,9 @@ packages: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} + '@adraffy/ens-normalize@1.10.0': + resolution: {integrity: sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q==} + '@adraffy/ens-normalize@1.10.1': resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} @@ -351,6 +357,10 @@ packages: '@changesets/write@0.3.0': resolution: {integrity: sha512-slGLb21fxZVUYbyea+94uFiD6ntQW0M2hIKNznFizDhZPDgn2c/fv1UzzlW43RVzh1BEDuIqW6hzlJ1OflNmcw==} + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1216,12 +1226,18 @@ packages: '@scure/bip32@1.1.5': resolution: {integrity: sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==} + '@scure/bip32@1.3.2': + resolution: {integrity: sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==} + '@scure/bip32@1.3.3': resolution: {integrity: sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==} '@scure/bip39@1.1.1': resolution: {integrity: sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==} + '@scure/bip39@1.2.1': + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} + '@scure/bip39@1.2.2': resolution: {integrity: sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==} @@ -1520,6 +1536,17 @@ packages: abbrev@1.0.9: resolution: {integrity: sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==} + abitype@1.0.0: + resolution: {integrity: sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1808,6 +1835,9 @@ packages: brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + brotli-wasm@2.0.1: + resolution: {integrity: sha512-+3USgYsC7bzb5yU0/p2HnnynZl0ak0E6uoIm4UW4Aby/8s8HFCq6NCfrrf1E9c3O8OCSzq3oYO1tUVqIi61Nww==} + browser-stdout@1.3.1: resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} @@ -1907,6 +1937,9 @@ packages: chardet@0.7.0: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + charenc@0.0.2: + resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + check-error@1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} @@ -1940,6 +1973,10 @@ packages: resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} engines: {node: '>=4'} + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + cli-table@0.3.11: resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} engines: {node: '>= 0.2.0'} @@ -2071,6 +2108,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + crypt@0.0.2: + resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} + csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} @@ -2856,6 +2896,11 @@ packages: hardhat-deploy@0.11.45: resolution: {integrity: sha512-aC8UNaq3JcORnEUIwV945iJuvBwi65tjHVDU3v6mOcqik7WAzHVCJ7cwmkkipsHrWysrB5YvGF1q9S1vIph83w==} + hardhat-gas-reporter@2.2.0: + resolution: {integrity: sha512-eAlLWnyDpQ+wJXgSCZsM0yt+rQm3ryJia1I1Hoi94LzlIfuSPcsMQM12VO6UHmAFLvXvoKxXPJ3ZYk0Kz+7CDQ==} + peerDependencies: + hardhat: ^2.16.0 + hardhat@2.22.5: resolution: {integrity: sha512-9Zq+HonbXCSy6/a13GY1cgHglQRfh4qkzmj1tpPlhxJDwNVnhxlReV6K7hCWFKlOrV13EQwsdcD0rjcaQKWRZw==} hasBin: true @@ -3170,6 +3215,11 @@ packages: isomorphic-unfetch@3.1.0: resolution: {integrity: sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==} + isows@1.0.3: + resolution: {integrity: sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg==} + peerDependencies: + ws: 7.5.10 + iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} @@ -3373,6 +3423,9 @@ packages: resolution: {integrity: sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==} hasBin: true + markdown-table@2.0.0: + resolution: {integrity: sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==} + markdownlint-cli@0.32.2: resolution: {integrity: sha512-xmJT1rGueUgT4yGNwk6D0oqQr90UJ7nMyakXtqjgswAkEhYYqjHew9RY8wDbOmh2R270IWjuKSeZzHDEGPAUkQ==} engines: {node: '>=14'} @@ -3972,6 +4025,10 @@ packages: resolution: {integrity: sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==} engines: {node: '>=6.5.0'} + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4143,6 +4200,9 @@ packages: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} hasBin: true + sha1@1.1.1: + resolution: {integrity: sha512-dZBS6OrMjtgVkopB1Gmo4RQCDKiZsqcpAQpkV/aaj+FCrCg8r4I4qMkDPQjBgLIxlmu9k4nUbWq6ohXahOneYA==} + shebang-command@1.2.0: resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} engines: {node: '>=0.10.0'} @@ -4722,6 +4782,14 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + viem@2.7.14: + resolution: {integrity: sha512-5b1KB1gXli02GOQHZIUsRluNUwssl2t4hqdFAzyWPwJ744N83jAOBOjOkrGz7K3qMIv9b0GQt3DoZIErSQTPkQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -4821,6 +4889,18 @@ packages: utf-8-validate: optional: true + ws@8.13.0: + resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + ws@8.17.1: resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} @@ -4901,6 +4981,8 @@ snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} + '@adraffy/ens-normalize@1.10.0': {} + '@adraffy/ens-normalize@1.10.1': {} '@aws-crypto/sha256-js@1.2.2': @@ -5097,6 +5179,9 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 + '@colors/colors@1.5.0': + optional: true + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -6134,6 +6219,12 @@ snapshots: '@noble/secp256k1': 1.7.1 '@scure/base': 1.1.6 + '@scure/bip32@1.3.2': + dependencies: + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.3 + '@scure/base': 1.1.6 + '@scure/bip32@1.3.3': dependencies: '@noble/curves': 1.3.0 @@ -6145,6 +6236,11 @@ snapshots: '@noble/hashes': 1.2.0 '@scure/base': 1.1.6 + '@scure/bip39@1.2.1': + dependencies: + '@noble/hashes': 1.3.3 + '@scure/base': 1.1.6 + '@scure/bip39@1.2.2': dependencies: '@noble/hashes': 1.3.3 @@ -6551,6 +6647,10 @@ snapshots: abbrev@1.0.9: {} + abitype@1.0.0(typescript@4.9.5): + optionalDependencies: + typescript: 4.9.5 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -6872,6 +6972,8 @@ snapshots: brorand@1.1.0: {} + brotli-wasm@2.0.1: {} + browser-stdout@1.3.1: {} browserify-aes@1.2.0: @@ -6983,6 +7085,8 @@ snapshots: chardet@0.7.0: {} + charenc@0.0.2: {} + check-error@1.0.3: dependencies: get-func-name: 2.0.2 @@ -7028,6 +7132,12 @@ snapshots: dependencies: restore-cursor: 2.0.0 + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + cli-table@0.3.11: dependencies: colors: 1.0.3 @@ -7173,6 +7283,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypt@0.0.2: {} + csv-generate@3.4.3: {} csv-parse@4.16.3: {} @@ -8386,6 +8498,31 @@ snapshots: - supports-color - utf-8-validate + hardhat-gas-reporter@2.2.0(bufferutil@4.0.5)(hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7))(typescript@4.9.5)(utf-8-validate@5.0.7): + dependencies: + '@ethersproject/abi': 5.7.0 + '@ethersproject/bytes': 5.7.0 + '@ethersproject/units': 5.7.0 + '@solidity-parser/parser': 0.18.0 + axios: 1.6.8(debug@4.3.4) + brotli-wasm: 2.0.1 + chalk: 4.1.2 + cli-table3: 0.6.5 + ethereum-cryptography: 2.1.3 + glob: 10.3.12 + hardhat: 2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7) + jsonschema: 1.4.1 + lodash: 4.17.21 + markdown-table: 2.0.0 + sha1: 1.1.1 + viem: 2.7.14(bufferutil@4.0.5)(typescript@4.9.5)(utf-8-validate@5.0.7) + transitivePeerDependencies: + - bufferutil + - debug + - typescript + - utf-8-validate + - zod + hardhat@2.22.5(bufferutil@4.0.5)(ts-node@10.9.2(@types/node@18.19.31)(typescript@4.9.5))(typescript@4.9.5)(utf-8-validate@5.0.7): dependencies: '@ethersproject/abi': 5.7.0 @@ -8710,6 +8847,10 @@ snapshots: transitivePeerDependencies: - encoding + isows@1.0.3(ws@8.13.0(bufferutil@4.0.5)(utf-8-validate@5.0.7)): + dependencies: + ws: 8.13.0(bufferutil@4.0.5)(utf-8-validate@5.0.7) + iterator.prototype@1.1.2: dependencies: define-properties: 1.2.1 @@ -8911,6 +9052,10 @@ snapshots: mdurl: 1.0.1 uc.micro: 1.0.6 + markdown-table@2.0.0: + dependencies: + repeat-string: 1.6.1 + markdownlint-cli@0.32.2: dependencies: commander: 9.4.1 @@ -9512,6 +9657,8 @@ snapshots: regexpp@2.0.1: {} + repeat-string@1.6.1: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -9725,6 +9872,11 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + sha1@1.1.1: + dependencies: + charenc: 0.0.2 + crypt: 0.0.2 + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 @@ -10425,6 +10577,23 @@ snapshots: vary@1.1.2: {} + viem@2.7.14(bufferutil@4.0.5)(typescript@4.9.5)(utf-8-validate@5.0.7): + dependencies: + '@adraffy/ens-normalize': 1.10.0 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 + '@scure/bip32': 1.3.2 + '@scure/bip39': 1.2.1 + abitype: 1.0.0(typescript@4.9.5) + isows: 1.0.3(ws@8.13.0(bufferutil@4.0.5)(utf-8-validate@5.0.7)) + ws: 8.13.0(bufferutil@4.0.5)(utf-8-validate@5.0.7) + optionalDependencies: + typescript: 4.9.5 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -10552,6 +10721,11 @@ snapshots: bufferutil: 4.0.5 utf-8-validate: 5.0.7 + ws@8.13.0(bufferutil@4.0.5)(utf-8-validate@5.0.7): + optionalDependencies: + bufferutil: 4.0.5 + utf-8-validate: 5.0.7 + ws@8.17.1(bufferutil@4.0.5)(utf-8-validate@5.0.7): optionalDependencies: bufferutil: 4.0.5 From 4d2fff0c719b31ca42b70581ae3fdc164d084711 Mon Sep 17 00:00:00 2001 From: Mad <46090742+DefiCake@users.noreply.github.com> Date: Thu, 1 Aug 2024 19:56:03 +0200 Subject: [PATCH 20/21] Update packages/solidity-contracts/contracts/test/EthReceiver.sol Co-authored-by: K1-R1 <77465250+K1-R1@users.noreply.github.com> --- packages/solidity-contracts/contracts/test/EthReceiver.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/solidity-contracts/contracts/test/EthReceiver.sol b/packages/solidity-contracts/contracts/test/EthReceiver.sol index fe258c48..cc4ba5b2 100644 --- a/packages/solidity-contracts/contracts/test/EthReceiver.sol +++ b/packages/solidity-contracts/contracts/test/EthReceiver.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.9; contract EthReceiver { From f1c2464b8c67388c39594e1df2986265434f5ea4 Mon Sep 17 00:00:00 2001 From: DefiCake Date: Thu, 1 Aug 2024 20:02:46 +0200 Subject: [PATCH 21/21] chore: restore code --- .../contracts/test/EthReceiver.sol | 2 +- packages/test-utils/src/utils/setup.ts | 48 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/solidity-contracts/contracts/test/EthReceiver.sol b/packages/solidity-contracts/contracts/test/EthReceiver.sol index cc4ba5b2..17755961 100644 --- a/packages/solidity-contracts/contracts/test/EthReceiver.sol +++ b/packages/solidity-contracts/contracts/test/EthReceiver.sol @@ -1,4 +1,4 @@ -SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 pragma solidity 0.8.9; contract EthReceiver { diff --git a/packages/test-utils/src/utils/setup.ts b/packages/test-utils/src/utils/setup.ts index 0e35fcf4..471a819a 100644 --- a/packages/test-utils/src/utils/setup.ts +++ b/packages/test-utils/src/utils/setup.ts @@ -140,32 +140,32 @@ export async function setupEnvironment( ); } const fuel_deployer = Wallet.fromPrivateKey(pk_fuel_deployer, fuel_provider); - // const fuel_deployerBalance = await fuel_deployer.getBalance(); - // if (fuel_deployerBalance.lt(fuels_parseEther('5')) && skip_deployer_balance) { - // throw new Error( - // 'Fuel deployer balance is very low (' + - // fuels_formatEther(fuel_deployerBalance) + - // 'ETH)' - // ); - // } + const fuel_deployerBalance = await fuel_deployer.getBalance(); + if (fuel_deployerBalance.lt(fuels_parseEther('5')) && skip_deployer_balance) { + throw new Error( + 'Fuel deployer balance is very low (' + + fuels_formatEther(fuel_deployerBalance) + + 'ETH)' + ); + } const fuel_signer1 = Wallet.fromPrivateKey(pk_fuel_signer1, fuel_provider); - // const fuel_signer1Balance = await fuel_signer1.getBalance(); - // if (fuel_signer1Balance.lt(fuels_parseEther('1')) && skip_deployer_balance) { - // const tx = await fuel_deployer.transfer( - // fuel_signer1.address, - // fuels_parseEther('1').toHex() - // ); - // await tx.wait(); - // } + const fuel_signer1Balance = await fuel_signer1.getBalance(); + if (fuel_signer1Balance.lt(fuels_parseEther('1')) && skip_deployer_balance) { + const tx = await fuel_deployer.transfer( + fuel_signer1.address, + fuels_parseEther('1').toHex() + ); + await tx.wait(); + } const fuel_signer2 = Wallet.fromPrivateKey(pk_fuel_signer2, fuel_provider); - // const fuel_signer2Balance = await fuel_signer2.getBalance(); - // if (fuel_signer2Balance.lt(fuels_parseEther('1')) && skip_deployer_balance) { - // const tx = await fuel_deployer.transfer( - // fuel_signer2.address, - // fuels_parseEther('1').toHex() - // ); - // await tx.wait(); - // } + const fuel_signer2Balance = await fuel_signer2.getBalance(); + if (fuel_signer2Balance.lt(fuels_parseEther('1')) && skip_deployer_balance) { + const tx = await fuel_deployer.transfer( + fuel_signer2.address, + fuels_parseEther('1').toHex() + ); + await tx.wait(); + } // Create provider and signers from http_ethereum_client const eth_provider = new JsonRpcProvider(http_ethereum_client);