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 diff --git a/docker/fuel-core/Dockerfile b/docker/fuel-core/Dockerfile index 3b1c0baf..bd26ce56 100644 --- a/docker/fuel-core/Dockerfile +++ b/docker/fuel-core/Dockerfile @@ -1,8 +1,9 @@ -# IMPORTANT! When upgrading fuel-core version, -# Make sure to check +# 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 -FROM ghcr.io/fuellabs/fuel-core:v0.28.0 +# when upgrading fuel-core +FROM ghcr.io/fuellabs/fuel-core:v0.31.0 ARG FUEL_IP=0.0.0.0 ARG FUEL_PORT=4001 @@ -27,7 +28,7 @@ RUN cp -R /chain-configuration/local/* ./ # 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 \ +RUN cp /chain-configuration/upgradelog/ignition/state_transition_function/5.wasm \ ./state_transition_bytecode.wasm # update local state_config with custom genesis coins config diff --git a/packages/integration-tests/tests/fti.ts b/packages/integration-tests/tests/fti.ts new file mode 100644 index 00000000..ea58d130 --- /dev/null +++ b/packages/integration-tests/tests/fti.ts @@ -0,0 +1,225 @@ +import type { TestEnvironment } from '@fuel-bridge/test-utils'; +import { setupEnvironment, waitForTransaction } from '@fuel-bridge/test-utils'; +import chai from 'chai'; +import { hexlify, solidityPacked } from 'ethers'; +import { sha256, transactionRequestify } from 'fuels'; +import type { WalletUnlocked as FuelWallet, BN } from 'fuels'; + +const { expect } = chai; + +const MAX_GAS = 10000000n; + +describe('Forced Transaction Inclusion', async function () { + // Timeout 6 minutes + const DEFAULT_TIMEOUT_MS: number = 400_000; + let BASE_ASSET_ID: string; + let CHAIN_ID: number; + + 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(); + CHAIN_ID = env.fuel.provider.getChainId(); + }); + + describe('Send a transaction through Ethereum', async () => { + const NUM_ETH = '0.1'; + let ethSender: any; + let fuelSender: FuelWallet; + let fuelReceiver: FuelWallet; + let fuelSenderBalance: BN; + let fuelReceiverBalance: BN; + + before(async () => { + ethSender; + fuelSender = env.fuel.deployer; + fuelReceiver = env.fuel.signers[1]; + fuelSenderBalance = await fuelSender.getBalance(BASE_ASSET_ID); + fuelReceiverBalance = await fuelReceiver.getBalance(BASE_ASSET_ID); + }); + + it('allows to send transactions', 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 expectedFee = gasPrice * MAX_GAS; + + const ethTx = await env.eth.fuelMessagePortal.sendTransaction( + MAX_GAS, + 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, + } + ); + + if (error) { + throw new Error(error); + } + + const txResult = await response.waitForResult(); + + expect(txResult.status).to.equal('success'); + }); + + 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(); + + 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'); + }); + }); +}); diff --git a/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol new file mode 100644 index 00000000..14db5fee --- /dev/null +++ b/packages/solidity-contracts/contracts/fuelchain/FuelMessagePortal/v4/FuelMessagePortalV4.sol @@ -0,0 +1,177 @@ +// 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(); + 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; + 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, + uint64 minGasPerTx, + uint256 minGasPrice + ) FuelMessagePortalV3(depositLimitGlobal) { + GAS_LIMIT = gasLimit; + GAS_TARGET = gasLimit / 2; + MIN_GAS_PER_TX = minGasPerTx; + 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(); + } + + uint64 _usedGas = usedGas; + uint192 _lastSeenBlock = lastSeenBlock; + 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 (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. 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 { + // If there were no transactions in the previous block, use distance to last congested block + _gasPrice = _divByNonZero(_gasPrice, distance); + } + + _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); + } + + uint256 fee = _gasPrice * gas; + + // Check fee and return to sender if needed + 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) { + return uint256(lastSeenBlock); + } + + function getUsedGas() external view returns (uint64) { + return usedGas; + } + + 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; + } + + function collectFees() external onlyRole(FEE_COLLECTOR_ROLE) { + (bool success, ) = _msgSender().call{value: address(this).balance}(""); + 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. + * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + */ + uint256[49] private __gap; +} diff --git a/packages/solidity-contracts/contracts/test/EthReceiver.sol b/packages/solidity-contracts/contracts/test/EthReceiver.sol new file mode 100644 index 00000000..17755961 --- /dev/null +++ b/packages/solidity-contracts/contracts/test/EthReceiver.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +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/deploy/hardhat/002.fuel_message_portal_v3.ts b/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v4.ts similarity index 69% 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..3665ffeb 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 @@ -1,8 +1,13 @@ -import { MaxUint256 } from 'ethers'; +import { MaxUint256, parseUnits } 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 FTI_MIN_GAS_PRICE = parseUnits('1', 'gwei'); +const FTI_MIN_GAS_PER_TX = 1; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { @@ -19,7 +24,12 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { [fuelChainState], { initializer: 'initialize', - constructorArgs: [MaxUint256], + constructorArgs: [ + ETH_DEPOSIT_LIMIT, + FTI_GAS_LIMIT, + FTI_MIN_GAS_PER_TX, + FTI_MIN_GAS_PRICE, + ], } ); await contract.waitForDeployment(); @@ -30,7 +40,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { console.log('Deployed FuelMessagePortal at', address); await save('FuelMessagePortal', { address, - abi: [], + abi: [...FuelMessagePortal.abi], implementation, }); diff --git a/packages/solidity-contracts/hardhat.config.ts b/packages/solidity-contracts/hardhat.config.ts index 29d6e9f0..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(); @@ -40,6 +41,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/', @@ -102,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/packages/solidity-contracts/test/FuelMessagePortalV4.L1toL2.test.ts b/packages/solidity-contracts/test/FuelMessagePortalV4.L1toL2.test.ts new file mode 100644 index 00000000..576b42cf --- /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('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.L2toL1.test.ts b/packages/solidity-contracts/test/FuelMessagePortalV4.L2toL1.test.ts new file mode 100644 index 00000000..352807ab --- /dev/null +++ b/packages/solidity-contracts/test/FuelMessagePortalV4.L2toL1.test.ts @@ -0,0 +1,106 @@ +import { MaxUint256, parseUnits } from 'ethers'; +import hre from 'hardhat'; + +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; +const MIN_GAS_PRICE = parseUnits('1', 'gwei'); +const MIN_GAS_PER_TX = 1; + +describe('FuelMessagePortalV4 - Incoming messages', () => { + const fixture = hre.deployments.createFixture( + async ({ ethers, upgrades }) => { + const signers = await ethers.getSigners(); + const FuelMessagePortalV4 = 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, + ], + })) as unknown as FuelChainState; + + const fuelMessagePortal = (await upgrades.deployProxy( + FuelMessagePortalV4, + [await fuelChainState.getAddress()], + { + initializer: 'initialize', + constructorArgs: [ + DEPOSIT_LIMIT, + GAS_LIMIT, + MIN_GAS_PER_TX, + MIN_GAS_PRICE, + ], + } + )) as unknown as FuelMessagePortalV4; + + 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/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/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 + ); + }); + }); +} 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..5f787ff7 --- /dev/null +++ b/packages/solidity-contracts/test/behaviors/FuelMessagePortalV4.behavior.test.ts @@ -0,0 +1,597 @@ +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 { parseEther, parseUnits, randomBytes } from 'ethers'; + +import type { FuelMessagePortalV4 } from '../../typechain'; +import { + haltBlockProduction, + impersonateAccount, + resumeInstantBlockProduction, +} from '../utils'; + +export type FuelMessagePortalV4Fixture = { + signers: HardhatEthersSigner[]; + fuelMessagePortal: FuelMessagePortalV4; + [key: string]: any; +}; + +const SCALED_UNIT = 10n ** 18n; +const ONE_AND_A_HALF = SCALED_UNIT + SCALED_UNIT / 2n; + +export function behavesLikeFuelMessagePortalV4( + fixture: () => Promise +) { + let GAS_LIMIT: bigint; + 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 () => { + 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() + ); + FEE_COLLECTOR_ROLE = await fuelMessagePortal.FEE_COLLECTOR_ROLE(); + }); + + describe('sendTransaction()', () => { + afterEach('restore block production', async () => { + await resumeInstantBlockProduction(hre); + }); + + it('emits a Transaction event', 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, serializedTx, { + value: parseEther('1'), + }); + + 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(MIN_GAS_PER_TX, 256)); + const serializedTx = randomBytes(payloadLength); + + await fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); + const tx = fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); + + await expect(tx) + .to.emit(fuelMessagePortal, 'Transaction') + .withArgs(1, gas, serializedTx); + + expect(await fuelMessagePortal.getTransactionNonce()).to.be.equal(2); + }); + + it('increments used gas', async () => { + const { + fuelMessagePortal, + signers: [signer], + } = await fixture(); + + const payloadLength = Math.abs(randomInt(256)); + 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(); + + const tx1 = await fuelMessagePortal + .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(); + + expect((await tx1.wait()).blockNumber).to.be.equal( + (await tx2.wait()).blockNumber + ); + + expect(await fuelMessagePortal.getUsedGas()).to.be.equal(gas * 2); + expect(await fuelMessagePortal.getCurrentUsedGas()).to.equal(gas * 2); + }); + + 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); + + const { blockNumber } = await fuelMessagePortal + .sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }) + .then((tx) => tx.wait()); + + expect(await fuelMessagePortal.getLastSeenBlock()).to.equal( + blockNumber + ); + }); + + 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); + + await fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); + + expect(await fuelMessagePortal.getGasPrice()).to.be.equal( + await fuelMessagePortal.MIN_GAS_PRICE() + ); + }); + + 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('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)); + 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, + }); + }); + + 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(); + + const payloadLength = Math.abs(randomInt(256)); + const serializedTx = randomBytes(payloadLength); + + 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, { + 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 + ); + }); + + 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, { + value: parseEther('1'), + }); // Initialize to 1 gwei + const initialGasPrice = await fuelMessagePortal.getGasPrice(); + + await fuelMessagePortal.sendTransaction(gas, serializedTx, { + value: parseEther('1'), + }); + + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // 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 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'), + }); + + const initialGasPrice = await fuelMessagePortal.getGasPrice(); + expect(initialGasPrice).to.equal(parseUnits('8', 'gwei')); + + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // 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 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'), + }); + + const initialGasPrice = await fuelMessagePortal.getGasPrice(); + expect(initialGasPrice).to.equal(parseUnits('4', 'gwei')); + + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // 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 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'), + }); + + const initialGasPrice = await fuelMessagePortal.getGasPrice(); + expect(initialGasPrice).to.equal(parseUnits('4', 'gwei')); + + await fuelMessagePortal.sendTransaction(1, serializedTx, { + value: parseEther('1'), + }); // 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 serializedTx = randomBytes(payloadLength); + + 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(); + + const distance = 3; + await mine(distance - 1); // The last block will be mined by the tx + await fuelMessagePortal.sendTransaction(GAS_TARGET, serializedTx, { + value: parseEther('1'), + }); + + expect(await fuelMessagePortal.getGasPrice()).to.equal( + 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 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, { + value: parseEther('1'), + }); + + expect(await fuelMessagePortal.getGasPrice()).to.equal( + MIN_GAS_PRICE + ); + }); + }); + }); + }); + + 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('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, + 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] + ); + }); + }); + }); +} diff --git a/packages/solidity-contracts/test/behaviors/index.ts b/packages/solidity-contracts/test/behaviors/index.ts index 6a8fd980..46c37802 100644 --- a/packages/solidity-contracts/test/behaviors/index.ts +++ b/packages/solidity-contracts/test/behaviors/index.ts @@ -1,3 +1,5 @@ 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'; 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); }); diff --git a/packages/solidity-contracts/test/utils/blockProduction.ts b/packages/solidity-contracts/test/utils/blockProduction.ts new file mode 100644 index 00000000..4cb1928a --- /dev/null +++ b/packages/solidity-contracts/test/utils/blockProduction.ts @@ -0,0 +1,11 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +export async function haltBlockProduction(hre: HardhatRuntimeEnvironment) { + await hre.network.provider.send('evm_setAutomine', [false]); +} + +export async function resumeInstantBlockProduction( + hre: HardhatRuntimeEnvironment +) { + await hre.network.provider.send('evm_setAutomine', [true]); +} 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/packages/test-utils/src/utils/fuels/index.ts b/packages/test-utils/src/utils/fuels/index.ts index 3371f41c..a462610e 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..acfd8b91 --- /dev/null +++ b/packages/test-utils/src/utils/fuels/waitForTransaction.ts @@ -0,0 +1,69 @@ +/// @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; // This ID will only appear if the tx fails + 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) { + // Note: getRelayedTransactionStatus will only return if the transaction failed + 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 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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fcbf8ed8..007be7e4 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,10 +186,13 @@ 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) + 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 @@ -216,7 +219,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 @@ -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'} @@ -939,50 +949,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': @@ -1230,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==} @@ -1534,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'} @@ -1822,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==} @@ -1921,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==} @@ -1954,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'} @@ -2085,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==} @@ -2790,6 +2816,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 +2896,13 @@ packages: hardhat-deploy@0.11.45: resolution: {integrity: sha512-aC8UNaq3JcORnEUIwV945iJuvBwi65tjHVDU3v6mOcqik7WAzHVCJ7cwmkkipsHrWysrB5YvGF1q9S1vIph83w==} - hardhat@2.22.3: - resolution: {integrity: sha512-k8JV2ECWNchD6ahkg2BR5wKVxY0OiKot7fuxiIpRK0frRqyOljcR2vKwgWSLw6YIeDcNNA4xybj7Og7NSxr2hA==} + 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 peerDependencies: ts-node: '*' @@ -3183,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==} @@ -3386,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'} @@ -3985,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'} @@ -4051,6 +4095,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: @@ -4155,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'} @@ -4734,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==} @@ -4833,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'} @@ -4913,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': @@ -5109,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 @@ -5868,36 +5941,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 +5985,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 +6121,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 +6134,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 @@ -6153,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 @@ -6164,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 @@ -6246,12 +6323,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': @@ -6570,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 @@ -6891,6 +6972,8 @@ snapshots: brorand@1.1.0: {} + brotli-wasm@2.0.1: {} + browser-stdout@1.3.1: {} browserify-aes@1.2.0: @@ -7002,6 +7085,8 @@ snapshots: chardet@0.7.0: {} + charenc@0.0.2: {} + check-error@1.0.3: dependencies: get-func-name: 2.0.2 @@ -7047,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 @@ -7192,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: {} @@ -8405,11 +8498,36 @@ 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-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 '@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 @@ -8729,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 @@ -8930,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 @@ -9531,6 +9657,8 @@ snapshots: regexpp@2.0.1: {} + repeat-string@1.6.1: {} + require-directory@2.1.1: {} require-from-string@2.0.2: {} @@ -9582,7 +9710,7 @@ snapshots: rimraf@2.7.1: dependencies: - glob: 7.2.0 + glob: 7.2.3 rimraf@3.0.2: dependencies: @@ -9744,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 @@ -9851,7 +9984,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 +9995,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 @@ -10444,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 @@ -10571,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