diff --git a/.changeset/thin-ways-dress.md b/.changeset/thin-ways-dress.md new file mode 100644 index 00000000..33a25e4b --- /dev/null +++ b/.changeset/thin-ways-dress.md @@ -0,0 +1,6 @@ +--- +'@fuel-bridge/solidity-contracts': minor +'@fuel-bridge/test-utils': minor +--- + +ci for contract upgrade test suite diff --git a/.github/workflows/upgrade-test-suite.yml b/.github/workflows/upgrade-test-suite.yml new file mode 100644 index 00000000..0d4e2638 --- /dev/null +++ b/.github/workflows/upgrade-test-suite.yml @@ -0,0 +1,46 @@ +name: Upgrade Test Suite + +on: + push: + branches: + - main + pull_request: + branches: + - main # Target branch for the PR + release: + types: [published] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + upgrade-test-suite: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v3 + - uses: FuelLabs/github-actions/setups/node@master + with: + node-version: 20.16.0 + pnpm-version: 9.0.6 + - uses: FuelLabs/github-actions/setups/docker@master + with: + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: ./.github/actions/setup-rust + - name: Build project + run: pnpm build + - name: Sets the tenderly rpc endpoint in the L1 docker container env and sets forking variable for fuel core setup + run: | + cat << EOF > l1_chain.env + TENDERLY_RPC_URL=${{ secrets.TENDERLY_RPC_URL }} + EOF + + cat << EOF > fuel_core.env + FORKING=true + EOF + working-directory: docker/envs + - name: Run integration tests on a L1 fork after upgrading contracts + run: | + pnpm run test:fork diff --git a/docker/envs/fuel_core.env b/docker/envs/fuel_core.env index 5afd14ac..1f6ad282 100644 --- a/docker/envs/fuel_core.env +++ b/docker/envs/fuel_core.env @@ -1,5 +1,6 @@ RUST_BACKTRACE=1 -CONSENSUS_KEY_SECRET="0xa449b1ffee0e2205fa924c6740cc48b3b473aa28587df6dab12abc245d1f5298" # Uncommend to use the variables by removing # # FUEL_IP= # FUEL_PORT= +# set true when running integration tests over forked environment +FORKING=false diff --git a/docker/envs/l1_chain.env b/docker/envs/l1_chain.env index 2334abdb..1218fb96 100644 --- a/docker/envs/l1_chain.env +++ b/docker/envs/l1_chain.env @@ -5,3 +5,5 @@ # L1_PORT= # SERVE_PORT= DEPLOYER_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d +# set tenderly rpc for spinning up forked network +TENDERLY_RPC_URL= \ No newline at end of file diff --git a/docker/fuel-core/Dockerfile b/docker/fuel-core/Dockerfile index 3d8b69db..012142f4 100644 --- a/docker/fuel-core/Dockerfile +++ b/docker/fuel-core/Dockerfile @@ -7,7 +7,8 @@ FROM ghcr.io/fuellabs/fuel-core:v0.40.0 ARG FUEL_IP=0.0.0.0 ARG FUEL_PORT=4001 -ARG CONSENSUS_KEY_SECRET="" +# since we need to set the FORKING var in the upgrade-test-suite ci so setting it here +ARG CONSENSUS_KEY_SECRET="0xa449b1ffee0e2205fa924c6740cc48b3b473aa28587df6dab12abc245d1f5298" # dependencies ENV DEBIAN_FRONTEND=noninteractive diff --git a/docker/fuel-core/fuel_core.sh b/docker/fuel-core/fuel_core.sh index 3f9d2b65..34b579d7 100644 --- a/docker/fuel-core/fuel_core.sh +++ b/docker/fuel-core/fuel_core.sh @@ -49,19 +49,40 @@ export FUEL_MESSAGE_PORTAL_CONTRACT_ADDRESS=$(cat "./addresses.json" | jq -r .Fu echo "FUEL_MESSAGE_PORTAL_CONTRACT_ADDRESS: $FUEL_MESSAGE_PORTAL_CONTRACT_ADDRESS" echo "L1_CHAIN_HTTP: $L1_CHAIN_HTTP" +export FORKING=${FORKING} + # start the Fuel client -echo "Starting fuel node." -exec /root/fuel-core run \ - --ip $FUEL_IP \ - --port $FUEL_PORT \ - --utxo-validation \ - --vm-backtrace \ - --enable-relayer \ - --relayer $L1_CHAIN_HTTP \ - --relayer-v2-listening-contracts $FUEL_MESSAGE_PORTAL_CONTRACT_ADDRESS \ - --poa-interval-period 1sec \ - --debug \ - --da-compression $DA_COMPRESSION \ - --graphql-max-complexity $GRAPHQL_COMPLEXITY \ - --min-gas-price 0 \ - --snapshot ./ \ No newline at end of file +if [ "$FORKING" = "true" ]; then + echo "FORKING is enabled. Running with da deploy height" + exec /root/fuel-core run \ + --ip $FUEL_IP \ + --port $FUEL_PORT \ + --utxo-validation \ + --vm-backtrace \ + --enable-relayer \ + --relayer $L1_CHAIN_HTTP \ + --relayer-v2-listening-contracts $FUEL_MESSAGE_PORTAL_CONTRACT_ADDRESS \ + --poa-interval-period 1sec \ + --relayer-da-deploy-height=21371952 \ + --debug \ + --da-compression $DA_COMPRESSION \ + --graphql-max-complexity $GRAPHQL_COMPLEXITY \ + --min-gas-price 0 \ + --snapshot ./ +else + echo "FORKING is disabled. Running without da deploy height" + exec /root/fuel-core run \ + --ip $FUEL_IP \ + --port $FUEL_PORT \ + --utxo-validation \ + --vm-backtrace \ + --enable-relayer \ + --relayer $L1_CHAIN_HTTP \ + --relayer-v2-listening-contracts $FUEL_MESSAGE_PORTAL_CONTRACT_ADDRESS \ + --poa-interval-period 1sec \ + --debug \ + --da-compression $DA_COMPRESSION \ + --graphql-max-complexity $GRAPHQL_COMPLEXITY \ + --min-gas-price 0 \ + --snapshot ./ +fi \ No newline at end of file diff --git a/docker/l1-chain/Dockerfile b/docker/l1-chain/Dockerfile index 7970a8b3..1f6bbe87 100644 --- a/docker/l1-chain/Dockerfile +++ b/docker/l1-chain/Dockerfile @@ -11,6 +11,7 @@ RUN npm i -g pnpm # clone the contracts repo ADD ./packages/solidity-contracts/package.json /l1chain/fuel-v2-contracts/ + # copy over the fuel chain and replace consts values WORKDIR /l1chain/fuel-v2-contracts @@ -27,16 +28,11 @@ RUN pnpm compile ADD ./docker/l1-chain/.fuelChainConsts.env /l1chain/fuel-v2-contracts/.fuelChainConsts.env ADD ./packages/solidity-contracts/contracts /l1chain/fuel-v2-contracts/contracts ADD ./packages/solidity-contracts/deploy /l1chain/fuel-v2-contracts/deploy +ADD ./packages/solidity-contracts/deployments/ /l1chain/fuel-v2-contracts/deployments/ ADD ./packages/solidity-contracts/protocol /l1chain/fuel-v2-contracts/protocol - -# remove build dependencies -# RUN pnpm prune --prod RUN pnpm compile -# Create deployments dir -RUN mkdir deployments - # expose node and server port ENV L1_IP="${L1_IP}" ENV L1_PORT="${L1_PORT}" diff --git a/package.json b/package.json index 84ceb1a5..d448cb1e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "node:clean": "make -C ./docker clean", "node:logs": "make -C ./docker logs", "test": "sh ./scripts/test.sh", + "test:fork": "sh ./scripts/test-fork.sh", "test:integration": "DEBUG=true pnpm --filter @fuel-bridge/integration-tests test", + "test:integration:fork": "DEBUG=true pnpm --filter @fuel-bridge/integration-tests test-fork", "lint:check": "eslint . --ext .ts,.js", "lint:fix": "pnpm lint:check --fix", "prettier:check": "prettier --check .", diff --git a/packages/fungible-token/package.json b/packages/fungible-token/package.json index 4f179bd1..d1125a70 100644 --- a/packages/fungible-token/package.json +++ b/packages/fungible-token/package.json @@ -10,7 +10,6 @@ }, "devDependencies": { "fuels": "0.96.1", - "@fuel-bridge/esbuild-bin-loader": "workspace:../esbuild-bin-loader", - "@fuel-bridge/solidity-contracts": "workspace:*" + "@fuel-bridge/esbuild-bin-loader": "workspace:../esbuild-bin-loader" } } diff --git a/packages/integration-tests/fork-tests/bridge_erc20.ts b/packages/integration-tests/fork-tests/bridge_erc20.ts new file mode 100644 index 00000000..43865cb6 --- /dev/null +++ b/packages/integration-tests/fork-tests/bridge_erc20.ts @@ -0,0 +1,669 @@ +import type { BridgeFungibleToken } from '@fuel-bridge/fungible-token'; +import { + RATE_LIMIT_AMOUNT, + RATE_LIMIT_DURATION, +} from '@fuel-bridge/solidity-contracts/protocol/constants'; +import type { Token } from '@fuel-bridge/solidity-contracts/typechain'; +import type { TestEnvironment } from '@fuel-bridge/test-utils'; +import { + setupEnvironment, + relayCommonMessage, + waitForMessage, + createRelayMessageParams, + getOrDeployECR20Contract, + getOrDeployL2Bridge, + FUEL_TX_PARAMS, + getMessageOutReceipt, + fuel_to_eth_address, + waitForBlockFinalization, + getTokenId, + getBlock, + FUEL_CALL_TX_PARAMS, + hardhatSkipTime, +} from '@fuel-bridge/test-utils'; +import chai from 'chai'; +import { toBeHex, parseEther } from 'ethers'; +import type { JsonRpcProvider, Signer } from 'ethers'; +import { Address, BN } from 'fuels'; +import type { + AbstractAddress, + WalletUnlocked as FuelWallet, + MessageProof, + Provider, +} from 'fuels'; + +const { expect } = chai; + +describe('Bridging ERC20 tokens', async function () { + // Timeout 6 minutes + const DEFAULT_TIMEOUT_MS: number = 400_000; + const FUEL_MESSAGE_TIMEOUT_MS: number = 30_000; + const DECIMAL_DIFF = 1_000_000_000n; + + let env: TestEnvironment; + let eth_testToken: Token; + let eth_testTokenAddress: string; + let eth_erc20GatewayAddress: string; + let fuel_bridge: BridgeFungibleToken; + let fuel_bridgeImpl: BridgeFungibleToken; + let fuel_bridgeContractId: string; + let fuel_testAssetId: string; + + // override the default test timeout from 2000ms + this.timeout(DEFAULT_TIMEOUT_MS); + + async function forwardFuelChain(provider: Provider, blocksToForward: string) { + await provider.produceBlocks(Number(blocksToForward)).catch(console.error); + } + + async function generateWithdrawalMessageProof( + fuel_bridge: BridgeFungibleToken, + fuelTokenSender: FuelWallet, + ethereumTokenReceiverAddress: string, + NUM_TOKENS: bigint, + DECIMAL_DIFF: bigint + ): Promise { + // withdraw tokens back to the base chain + fuel_bridge.account = fuelTokenSender; + const paddedAddress = + '0x' + ethereumTokenReceiverAddress.slice(2).padStart(64, '0'); + const fuelTokenSenderBalance = await fuelTokenSender.getBalance( + fuel_testAssetId + ); + const transactionRequest = await fuel_bridge.functions + .withdraw(paddedAddress) + .addContracts([fuel_bridge, fuel_bridgeImpl]) + .txParams({ + tip: 0, + maxFee: 1, + }) + .callParams({ + forward: { + amount: new BN(NUM_TOKENS.toString()).div( + new BN(DECIMAL_DIFF.toString()) + ), + assetId: fuel_testAssetId, + }, + }) + .fundWithRequiredCoins(); + + const tx = await fuelTokenSender.sendTransaction(transactionRequest); + const fWithdrawTxResult = await tx.waitForResult(); + expect(fWithdrawTxResult.status).to.equal('success'); + + // check that the sender balance has decreased by the expected amount + const newSenderBalance = await fuelTokenSender.getBalance(fuel_testAssetId); + + expect( + newSenderBalance.eq( + fuelTokenSenderBalance.sub(toBeHex(NUM_TOKENS / DECIMAL_DIFF)) + ) + ).to.be.true; + + // Wait for the commited block + const withdrawBlock = await getBlock( + env.fuel.provider.url, + fWithdrawTxResult.blockId! + ); + + const TIME_TO_FINALIZE = await env.eth.fuelChainState.TIME_TO_FINALIZE(); + + const blocksPerCommitInterval = ( + await env.eth.fuelChainState.BLOCKS_PER_COMMIT_INTERVAL() + ).toString(); + + // Add + 1 to the block height to wait the next block + // that enable to proof the message + const nextBlockHeight = new BN(withdrawBlock.header.height).add(new BN(1)); + const commitHeight = new BN(nextBlockHeight).div(blocksPerCommitInterval); + + const cooldown = await env.eth.fuelChainState.COMMIT_COOLDOWN(); + + // fast forward post the commit cooldown period + await env.eth.provider.send('evm_increaseTime', [Number(cooldown) * 10]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + // produce more blocks to fetch the block height + await forwardFuelChain(env.fuel.provider, blocksPerCommitInterval); + + const block = await env.fuel.provider.getBlock(nextBlockHeight.toString()); + + // reset the commit hash in the local L2 network + await env.eth.fuelChainState + .connect(env.eth.signers[1]) + .commit(block.id, commitHeight.toString()); + + // fast forward to the block finalization time + await env.eth.provider.send('evm_increaseTime', [ + Number(TIME_TO_FINALIZE) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + const messageOutReceipt = getMessageOutReceipt(fWithdrawTxResult.receipts); + return await fuelTokenSender.provider.getMessageProof( + tx.id, + messageOutReceipt.nonce, + block.id + ); + } + + async function relayMessageFromFuel( + env: TestEnvironment, + withdrawMessageProof: MessageProof + ) { + // wait for block finalization + await waitForBlockFinalization(env, withdrawMessageProof); + + // construct relay message proof data + const relayMessageParams = createRelayMessageParams(withdrawMessageProof); + + // relay message + await env.eth.fuelMessagePortal.relayMessage( + relayMessageParams.message, + relayMessageParams.rootBlockHeader, + relayMessageParams.blockHeader, + relayMessageParams.blockInHistoryProof, + relayMessageParams.messageInBlockProof + ); + } + + async function relayMessageFromEthereum( + env: TestEnvironment, + fuelTokenMessageReceiver: AbstractAddress, + fuelTokenMessageNonce: BN, + fuel_AssetId: string, + amount: bigint + ) { + // relay the message ourselves + const message = await waitForMessage( + env.fuel.provider, + fuelTokenMessageReceiver, + fuelTokenMessageNonce, + FUEL_MESSAGE_TIMEOUT_MS + ); + expect(message).to.not.be.null; + + const tx = await relayCommonMessage(env.fuel.deployer, message, { + maturity: undefined, + contractIds: [fuel_bridgeImpl.id.toHexString()], + }); + + const txResult = await tx.waitForResult(); + + expect(txResult.status).to.equal('success'); + expect(txResult.mintedAssets.length).to.equal(1); + + const [mintedAsset] = txResult.mintedAssets; + + expect(mintedAsset.assetId).to.equal(fuel_AssetId); + expect(mintedAsset.amount.toString()).to.equal( + (amount / DECIMAL_DIFF).toString() + ); + } + + before(async () => { + env = await setupEnvironment({}); + eth_erc20GatewayAddress = ( + await env.eth.fuelERC20Gateway.getAddress() + ).toLowerCase(); + + eth_testToken = await getOrDeployECR20Contract(env); + eth_testTokenAddress = (await eth_testToken.getAddress()).toLowerCase(); + + const { contract, implementation } = await getOrDeployL2Bridge( + env, + env.eth.fuelERC20Gateway + ); + + fuel_bridge = contract; + fuel_bridgeImpl = implementation; + + fuel_bridgeContractId = fuel_bridge.id.toHexString(); + + await env.eth.fuelERC20Gateway + .connect(env.eth.deployer) + .setAssetIssuerId(fuel_bridgeContractId); + fuel_testAssetId = getTokenId(fuel_bridge, eth_testTokenAddress); + + // initializing rate limit params for the token + await env.eth.fuelERC20Gateway + .connect(env.eth.deployer) + .resetRateLimitAmount( + eth_testTokenAddress, + RATE_LIMIT_AMOUNT.toString(), + RATE_LIMIT_DURATION + ); + + await env.eth.fuelERC20Gateway + .connect(env.eth.deployer) + .updateRateLimitStatus(eth_testTokenAddress, true); + + const { value: expectedGatewayContractId } = await fuel_bridge.functions + .bridged_token_gateway() + .addContracts([fuel_bridge, fuel_bridgeImpl]) + .txParams(FUEL_CALL_TX_PARAMS) + .dryRun(); + + // check that values for the test token and gateway contract match what + // was compiled into the bridge-fungible-token binaries + + expect(fuel_to_eth_address(expectedGatewayContractId)).to.equal( + eth_erc20GatewayAddress + ); + expect(await eth_testToken.decimals()).to.equal(18n); + + // mint tokens as starting balances + await eth_testToken + .mint(await env.eth.deployer.getAddress(), 10_000) + .then((tx) => tx.wait()); + + await eth_testToken + .mint(await env.eth.signers[0].getAddress(), 10_000) + .then((tx) => tx.wait()); + + await eth_testToken + .mint(await env.eth.signers[1].getAddress(), 10_000) + .then((tx) => tx.wait()); + }); + + describe('Bridge ERC20 to Fuel', async () => { + const NUM_TOKENS = 100000000000000000000n; + let ethereumTokenSender: Signer; + let ethereumTokenSenderAddress: string; + let ethereumTokenSenderBalance: bigint; + let fuelTokenReceiver: FuelWallet; + let fuelTokenReceiverAddress: string; + let fuelTokenReceiverBalance: BN; + let fuelTokenMessageNonce: BN; + let fuelTokenMessageReceiver: AbstractAddress; + + before(async () => { + ethereumTokenSender = env.eth.signers[0]; + ethereumTokenSenderAddress = await ethereumTokenSender.getAddress(); + + await eth_testToken + .mint(ethereumTokenSenderAddress, NUM_TOKENS) + .then((tx) => tx.wait()); + + ethereumTokenSenderBalance = await eth_testToken.balanceOf( + ethereumTokenSenderAddress + ); + + fuelTokenReceiver = env.fuel.signers[0]; + fuelTokenReceiverAddress = fuelTokenReceiver.address.toHexString(); + fuelTokenReceiverBalance = await fuelTokenReceiver.getBalance( + fuel_testAssetId + ); + }); + + it('Bridge ERC20 via FuelERC20Gateway', async () => { + // approve FuelERC20Gateway to spend the tokens + await eth_testToken + .connect(ethereumTokenSender) + .approve(eth_erc20GatewayAddress, NUM_TOKENS) + .then((tx) => tx.wait()); + + // use the FuelERC20Gateway to deposit test tokens and receive equivalent tokens on Fuel + const receipt = await env.eth.fuelERC20Gateway + .connect(ethereumTokenSender) + .deposit(fuelTokenReceiverAddress, eth_testTokenAddress, NUM_TOKENS) + .then((tx) => tx.wait()); + + expect(receipt!.status).to.equal(1); + // parse events from logs + const [event, ...restOfEvents] = + await env.eth.fuelMessagePortal.queryFilter( + env.eth.fuelMessagePortal.filters.MessageSent, + receipt!.blockNumber, + receipt!.blockNumber + ); + expect(restOfEvents.length).to.be.eq(0); // Should be only 1 event + + fuelTokenMessageNonce = new BN(event.args.nonce.toString()); + fuelTokenMessageReceiver = Address.fromB256(event.args.recipient); + + // check that the sender balance has decreased by the expected amount + const newSenderBalance = await eth_testToken.balanceOf( + ethereumTokenSenderAddress + ); + expect(newSenderBalance === ethereumTokenSenderBalance - NUM_TOKENS).to.be + .true; + }); + + it('Relay messages from Ethereum on Fuel', async () => { + // override the default test timeout from 2000ms + this.timeout(FUEL_MESSAGE_TIMEOUT_MS); + // relay the standard erc20 deposit + await relayMessageFromEthereum( + env, + fuelTokenMessageReceiver, + fuelTokenMessageNonce, + fuel_testAssetId, + NUM_TOKENS + ); + + // override the default test timeout from 2000ms + this.timeout(FUEL_MESSAGE_TIMEOUT_MS); + }); + + it('Check metadata was registered', async () => { + await fuel_bridge.functions + .asset_to_l1_address({ bits: fuel_testAssetId }) + .addContracts([fuel_bridge, fuel_bridgeImpl]) + .call(); + + const { value: l2_decimals } = await fuel_bridge.functions + .decimals({ bits: fuel_testAssetId }) + .addContracts([fuel_bridge, fuel_bridgeImpl]) + .get(); + + expect(l2_decimals).to.be.equal(9); + }); + + it('Check ERC20 arrived on Fuel', async () => { + // check that the recipient balance has increased by the expected amount + const newReceiverBalance = await fuelTokenReceiver.getBalance( + fuel_testAssetId + ); + + expect( + newReceiverBalance.eq( + fuelTokenReceiverBalance.add(toBeHex(NUM_TOKENS / DECIMAL_DIFF)) + ) + ).to.be.true; + }); + + it('Bridge metadata', async () => { + // use the FuelERC20Gateway to deposit test tokens and receive equivalent tokens on Fuel + const receipt = await env.eth.fuelERC20Gateway + .connect(ethereumTokenSender) + .sendMetadata(eth_testTokenAddress) + .then((tx) => tx.wait()); + + // parse events from logs + const [event, ...restOfEvents] = + await env.eth.fuelMessagePortal.queryFilter( + env.eth.fuelMessagePortal.filters.MessageSent, + receipt!.blockNumber, + receipt!.blockNumber + ); + expect(restOfEvents.length).to.be.eq(0); // Should be only 1 event + + const nonce = new BN(event.args.nonce.toString()); + const fuelReceiver = Address.fromB256(event.args.recipient); + + // relay the message ourselves + const message = await waitForMessage( + env.fuel.provider, + fuelReceiver, + nonce, + FUEL_MESSAGE_TIMEOUT_MS + ); + expect(message).to.not.be.null; + + const tx = await relayCommonMessage(env.fuel.deployer, message, { + ...FUEL_TX_PARAMS, + maturity: undefined, + contractIds: [fuel_bridgeImpl.id.toHexString()], + }); + + const txResult = await tx.waitForResult(); + expect(txResult.status).to.equal('success'); + + const fuel_name = ( + await fuel_bridge.functions.name({ bits: fuel_testAssetId }).dryRun() + ).value; + const fuel_symbol = ( + await fuel_bridge.functions.symbol({ bits: fuel_testAssetId }).dryRun() + ).value; + + const eth_name = await eth_testToken.name(); + const eth_symbol = await eth_testToken.symbol(); + + expect(fuel_name).to.equal(eth_name); + expect(fuel_symbol).to.equal(eth_symbol); + }); + }); + + describe('Bridge ERC20 from Fuel', async () => { + const NUM_TOKENS = 10000000000000000000n; + const largeRateLimit = `30`; + let fuelTokenSender: FuelWallet; + let ethereumTokenReceiver: Signer; + let ethereumTokenReceiverAddress: string; + let ethereumTokenReceiverBalance: bigint; + let withdrawMessageProof: MessageProof | null; + let tokenBalanceBeforeWithdrawingOnFuel: BN; + + before(async () => { + fuelTokenSender = env.fuel.signers[0]; + ethereumTokenReceiver = env.eth.signers[0]; + ethereumTokenReceiverAddress = await ethereumTokenReceiver.getAddress(); + ethereumTokenReceiverBalance = await eth_testToken.balanceOf( + ethereumTokenReceiverAddress + ); + + tokenBalanceBeforeWithdrawingOnFuel = await fuelTokenSender.getBalance( + fuel_testAssetId + ); + }); + + it('Bridge ERC20 via Fuel token contract', async () => { + // withdraw tokens back to the base chain + withdrawMessageProof = await generateWithdrawalMessageProof( + fuel_bridge, + fuelTokenSender, + ethereumTokenReceiverAddress, + NUM_TOKENS, + DECIMAL_DIFF + ); + }); + + it('Relay Message from Fuel on Ethereum', async () => { + const withdrawnAmountBeforeRelay = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + const rateLimitEndDuratioBeforeRelay = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + + // relay message + await relayMessageFromFuel(env, withdrawMessageProof!); + + // check rate limit params + const withdrawnAmountAfterRelay = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + const rateLimitEndDuratioAfterRelay = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + + expect(rateLimitEndDuratioAfterRelay === rateLimitEndDuratioBeforeRelay) + .to.be.true; + + expect( + withdrawnAmountAfterRelay === NUM_TOKENS + withdrawnAmountBeforeRelay + ).to.be.true; + }); + + it('Check the remaining token balance on Fuel after the first withdrawal', async () => { + // fetch the remaining token balance + const currentTokenBalance = await fuelTokenSender.getBalance( + fuel_testAssetId + ); + + // currentTokenBalance has BN type by default hence the use of BN for conversion here + const expectedRemainingTokenBalanceOnFuel = + tokenBalanceBeforeWithdrawingOnFuel.sub( + new BN((NUM_TOKENS / DECIMAL_DIFF).toString()) + ); + + expect(currentTokenBalance.eq(expectedRemainingTokenBalanceOnFuel)).to.be + .true; + }); + + it('Rate limit parameters are updated when current withdrawn amount is more than the new limit & set a new higher limit', async () => { + const deployer = await env.eth.deployer; + const newRateLimit = '5'; + + let withdrawnAmountBeforeReset = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + await env.eth.fuelERC20Gateway + .connect(deployer) + .resetRateLimitAmount( + eth_testTokenAddress, + parseEther(newRateLimit), + RATE_LIMIT_DURATION + ); + + let currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + // current withdrawn amount doesn't change when rate limit is updated + + expect( + currentWithdrawnAmountAfterSettingLimit === withdrawnAmountBeforeReset + ).to.be.true; + + withdrawnAmountBeforeReset = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + await env.eth.fuelERC20Gateway + .connect(deployer) + .resetRateLimitAmount( + eth_testTokenAddress, + parseEther(largeRateLimit), + RATE_LIMIT_DURATION + ); + + currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + expect( + currentWithdrawnAmountAfterSettingLimit === withdrawnAmountBeforeReset + ).to.be.true; + }); + + it('Rate limit parameters are updated when the initial duration is over', async () => { + const rateLimitDuration = + await env.eth.fuelERC20Gateway.rateLimitDuration(eth_testTokenAddress); + + // fast forward time + await hardhatSkipTime( + env.eth.provider as JsonRpcProvider, + rateLimitDuration * 2n + ); + const currentPeriodEndBeforeRelay = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + + // withdraw tokens back to the base chain + withdrawMessageProof = await generateWithdrawalMessageProof( + fuel_bridge, + fuelTokenSender, + ethereumTokenReceiverAddress, + NUM_TOKENS, + DECIMAL_DIFF + ); + + // relay message + await relayMessageFromFuel(env, withdrawMessageProof!); + + const currentPeriodEndAfterRelay = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + + expect(currentPeriodEndAfterRelay > currentPeriodEndBeforeRelay).to.be + .true; + + const currentPeriodAmount = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + expect(currentPeriodAmount === NUM_TOKENS).to.be.true; + }); + + it('Check the remaining token balance on Fuel after the second withdrawal', async () => { + // fetch the remaining token balance + const currentTokenBalance = await fuelTokenSender.getBalance( + fuel_testAssetId + ); + + // currentTokenBalance has BN type by default hence the use of BN for conversion here + const expectedRemainingTokenBalanceOnFuel = + tokenBalanceBeforeWithdrawingOnFuel.sub( + new BN(((NUM_TOKENS * 2n) / DECIMAL_DIFF).toString()) + ); + + expect(currentTokenBalance.eq(expectedRemainingTokenBalanceOnFuel)).to.be + .true; + }); + + it('Rate limit parameters are updated when new limit is set after the initial duration', async () => { + const rateLimitDuration = + await env.eth.fuelERC20Gateway.rateLimitDuration(eth_testTokenAddress); + + const deployer = await env.eth.deployer; + const newRateLimit = `40`; + + // fast forward time + await hardhatSkipTime( + env.eth.provider as JsonRpcProvider, + rateLimitDuration * 2n + ); + + const currentWithdrawnAmountBeforeSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + const currentPeriodEndBeforeSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + + await env.eth.fuelERC20Gateway + .connect(deployer) + .resetRateLimitAmount( + eth_testTokenAddress, + parseEther(newRateLimit), + RATE_LIMIT_DURATION + ); + + const currentPeriodEndAfterSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodEnd(eth_testTokenAddress); + const currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelERC20Gateway.currentPeriodAmount( + eth_testTokenAddress + ); + + expect( + currentPeriodEndAfterSettingLimit > currentPeriodEndBeforeSettingLimit + ).to.be.true; + + expect( + currentWithdrawnAmountBeforeSettingLimit > + currentWithdrawnAmountAfterSettingLimit + ).to.be.true; + + expect(currentWithdrawnAmountAfterSettingLimit === 0n).to.be.true; + }); + + it('Check ERC20 arrived on Ethereum', async () => { + // check that the recipient balance has increased by the expected amount + const newReceiverBalance = await eth_testToken.balanceOf( + ethereumTokenReceiverAddress + ); + expect( + newReceiverBalance === ethereumTokenReceiverBalance + NUM_TOKENS * 2n + ).to.be.true; + }); + }); +}); diff --git a/packages/integration-tests/fork-tests/transfer_eth.ts b/packages/integration-tests/fork-tests/transfer_eth.ts new file mode 100644 index 00000000..f6781733 --- /dev/null +++ b/packages/integration-tests/fork-tests/transfer_eth.ts @@ -0,0 +1,468 @@ +import type { TestEnvironment } from '@fuel-bridge/test-utils'; +import { + setupEnvironment, + fuels_parseEther, + createRelayMessageParams, + getMessageOutReceipt, + waitForMessage, + waitForBlockFinalization, + getBlock, + FUEL_CALL_TX_PARAMS, +} from '@fuel-bridge/test-utils'; +import chai from 'chai'; +import { parseEther } from 'ethers'; +import type { Signer } from 'ethers'; +import { Address, BN, padFirst12BytesOfEvmAddress } from 'fuels'; +import type { + AbstractAddress, + WalletUnlocked as FuelWallet, + MessageProof, + Provider, +} from 'fuels'; + +const { expect } = chai; + +describe('Transferring ETH', async function () { + // Timeout 6 minutes + const DEFAULT_TIMEOUT_MS: number = 400_000; + const FUEL_MESSAGE_TIMEOUT_MS: number = 30_000; + let BASE_ASSET_ID: string; + + let env: TestEnvironment; + + // override the default test timeout of 2000ms + this.timeout(DEFAULT_TIMEOUT_MS); + + async function forwardFuelChain(provider: Provider, blocksToForward: string) { + await provider.produceBlocks(Number(blocksToForward)).catch(console.error); + } + + async function generateWithdrawalMessageProof( + fuelETHSender: FuelWallet, + ethereumETHReceiverAddress: string, + NUM_ETH: string + ): Promise { + // withdraw ETH back to the base chain + const fWithdrawTx = await fuelETHSender.withdrawToBaseLayer( + Address.fromString( + padFirst12BytesOfEvmAddress(ethereumETHReceiverAddress) + ), + fuels_parseEther(NUM_ETH), + FUEL_CALL_TX_PARAMS + ); + const fWithdrawTxResult = await fWithdrawTx.waitForResult(); + expect(fWithdrawTxResult.status).to.equal('success'); + + // Wait for the commited block + const withdrawBlock = await getBlock( + env.fuel.provider.url, + fWithdrawTxResult.blockId! + ); + + const TIME_TO_FINALIZE = await env.eth.fuelChainState.TIME_TO_FINALIZE(); + + const blocksPerCommitInterval = ( + await env.eth.fuelChainState.BLOCKS_PER_COMMIT_INTERVAL() + ).toString(); + + // Add + 1 to the block height to wait the next block + // that enable to proof the message + const nextBlockHeight = new BN(withdrawBlock.header.height).add(new BN(1)); + const commitHeight = new BN(nextBlockHeight).div(blocksPerCommitInterval); + + const cooldown = await env.eth.fuelChainState.COMMIT_COOLDOWN(); + + // fast forward post the commit cooldown period + await env.eth.provider.send('evm_increaseTime', [Number(cooldown) * 10]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + // produce more blocks to fetch the block height + await forwardFuelChain(env.fuel.provider, blocksPerCommitInterval); + + const block = await env.fuel.provider.getBlock(nextBlockHeight.toString()); + + // reset the commit hash in the local L2 network + await env.eth.fuelChainState + .connect(env.eth.signers[1]) + .commit(block.id, commitHeight.toString()); + + // fast forward to the block finalization time + await env.eth.provider.send('evm_increaseTime', [ + Number(TIME_TO_FINALIZE) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + // get message proof + const messageOutReceipt = getMessageOutReceipt(fWithdrawTxResult.receipts); + + return await fuelETHSender.provider.getMessageProof( + fWithdrawTx.id, + messageOutReceipt.nonce, + block.id + ); + } + + async function relayMessage( + env: TestEnvironment, + withdrawMessageProof: MessageProof + ) { + // wait for block finalization + await waitForBlockFinalization(env, withdrawMessageProof); + + // construct relay message proof data + const relayMessageParams = createRelayMessageParams(withdrawMessageProof); + + const TIME_TO_FINALIZE = await env.eth.fuelChainState.TIME_TO_FINALIZE(); + + // fast forward to the block finalization time + await env.eth.provider.send('evm_increaseTime', [ + Number(TIME_TO_FINALIZE) * 100, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + // relay message + await env.eth.fuelMessagePortal.relayMessage( + relayMessageParams.message, + relayMessageParams.rootBlockHeader, + relayMessageParams.blockHeader, + relayMessageParams.blockInHistoryProof, + relayMessageParams.messageInBlockProof + ); + } + + before(async () => { + env = await setupEnvironment({}); + BASE_ASSET_ID = env.fuel.provider.getBaseAssetId(); + }); + + describe('Send ETH to Fuel', async () => { + const NUM_ETH = '30'; + let ethereumETHSender: Signer; + let ethereumETHSenderAddress: string; + let fuelETHReceiver: AbstractAddress; + let fuelETHReceiverAddress: string; + let fuelETHReceiverBalance: BN; + let fuelETHMessageNonce: BN; + + before(async () => { + ethereumETHSender = env.eth.signers[0]; + ethereumETHSenderAddress = await ethereumETHSender.getAddress(); + fuelETHReceiver = env.fuel.signers[0].address; + fuelETHReceiverAddress = fuelETHReceiver.toHexString(); + + fuelETHReceiverBalance = await env.fuel.provider.getBalance( + fuelETHReceiver, + BASE_ASSET_ID + ); + }); + + it('Send ETH via MessagePortal', async () => { + // use the FuelMessagePortal to directly send ETH which should be immediately spendable + const tx = await env.eth.fuelMessagePortal + .connect(ethereumETHSender) + .depositETH(fuelETHReceiverAddress, { + value: parseEther(NUM_ETH), + }); + const receipt = await tx.wait(); + expect(receipt!.status).to.equal(1); + + // parse events from logs + const filter = env.eth.fuelMessagePortal.filters.MessageSent( + undefined, // Args set to null since there should be just 1 event for MessageSent + undefined, + undefined, + undefined, + undefined + ); + + const [event, ...restOfEvents] = + await env.eth.fuelMessagePortal.queryFilter( + filter, + receipt!.blockNumber, + receipt!.blockNumber + ); + expect(restOfEvents.length).to.be.eq(0); // Should be only 1 event + + fuelETHMessageNonce = new BN(event.args.nonce.toString()); + + // check that the sender balance has decreased by the expected amount + const newSenderBalance = await env.eth.provider.getBalance( + ethereumETHSenderAddress, + receipt!.blockNumber + ); + + const txCost = receipt!.fee; + + const expectedSenderBalance = + (await env.eth.provider.getBalance( + ethereumETHSender, + receipt!.blockNumber - 1 + )) - + txCost - + parseEther(NUM_ETH); + + expect(newSenderBalance).to.be.eq(expectedSenderBalance); + }); + + it('Wait for ETH to arrive on Fuel', async function () { + // wait for message to appear in fuel client + expect( + await waitForMessage( + env.fuel.provider, + fuelETHReceiver, + fuelETHMessageNonce, + FUEL_MESSAGE_TIMEOUT_MS + ) + ).to.not.be.null; + + // check that the recipient balance has increased by the expected amount + const newReceiverBalance = await env.fuel.provider.getBalance( + fuelETHReceiver, + BASE_ASSET_ID + ); + expect( + newReceiverBalance.eq( + fuelETHReceiverBalance.add(fuels_parseEther(NUM_ETH)) + ) + ).to.be.true; + }); + }); + + describe('Send ETH from Fuel', async () => { + const NUM_ETH = '0.001'; + let fuelETHSender: FuelWallet; + let fuelETHSenderBalance: BN; + let ethereumETHReceiver: Signer; + let ethereumETHReceiverAddress: string; + let ethereumETHReceiverBalance: bigint; + let withdrawMessageProof: MessageProof | null; + + before(async () => { + fuelETHSender = env.fuel.signers[1]; + fuelETHSenderBalance = await fuelETHSender.getBalance(BASE_ASSET_ID); + ethereumETHReceiver = env.eth.signers[1]; + ethereumETHReceiverAddress = await ethereumETHReceiver.getAddress(); + ethereumETHReceiverBalance = await env.eth.provider.getBalance( + ethereumETHReceiver + ); + }); + + it('Send ETH via OutputMessage', async () => { + withdrawMessageProof = await generateWithdrawalMessageProof( + fuelETHSender, + ethereumETHReceiverAddress, + NUM_ETH + ); + + // check that the sender balance has decreased by the expected amount + const newSenderBalance = await fuelETHSender.getBalance(BASE_ASSET_ID); + + // Get just the first 3 digits of the balance to compare to the expected balance + // this is required because the payment of gas fees is not deterministic + const diffOnSenderBalance = newSenderBalance + .sub(fuelETHSenderBalance) + .formatUnits(); + expect(diffOnSenderBalance.startsWith(NUM_ETH)).to.be.true; + }); + + it('Relay Message from Fuel on Ethereum', async () => { + await relayMessage(env, withdrawMessageProof!); + }); + + it('Check ETH arrived on Ethereum', async () => { + // check that the recipient balance has increased by the expected amount + const newReceiverBalance = await env.eth.provider.getBalance( + ethereumETHReceiver + ); + + expect( + newReceiverBalance <= ethereumETHReceiverBalance + parseEther(NUM_ETH) + ).to.be.true; + }); + }); + + describe('ETH Withdrawls based on rate limit updates', async () => { + const NUM_ETH = '9'; + const largeRateLimit = `30`; + let fuelETHSender: FuelWallet; + let ethereumETHReceiver: Signer; + let ethereumETHReceiverAddress: string; + let withdrawMessageProof: MessageProof | null; + let rateLimitDuration: bigint; + + before(async () => { + fuelETHSender = env.fuel.signers[1]; + ethereumETHReceiver = env.eth.signers[1]; + ethereumETHReceiverAddress = await ethereumETHReceiver.getAddress(); + + await env.eth.fuelMessagePortal + .connect(env.eth.deployer) + .updateRateLimitStatus(true); + rateLimitDuration = await env.eth.fuelMessagePortal.RATE_LIMIT_DURATION(); + }); + + it('Checks rate limit params after relaying', async () => { + withdrawMessageProof = await generateWithdrawalMessageProof( + fuelETHSender, + ethereumETHReceiverAddress, + NUM_ETH + ); + + const withdrawnAmountBeforeRelay = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + await relayMessage(env, withdrawMessageProof!); + + const currentPeriodAmount = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + expect( + currentPeriodAmount === parseEther(NUM_ETH) + withdrawnAmountBeforeRelay + ).to.be.true; + }); + + it('Relays ETH after the rate limit is updated', async () => { + const deployer = env.eth.deployer; + const newRateLimit = `30`; + + await env.eth.fuelMessagePortal + .connect(deployer) + .resetRateLimitAmount(parseEther(newRateLimit)); + + withdrawMessageProof = await generateWithdrawalMessageProof( + fuelETHSender, + ethereumETHReceiverAddress, + NUM_ETH + ); + + const withdrawnAmountBeforeRelay = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + let currentWIthdrawnAmountReset = false; + + if (withdrawnAmountBeforeRelay > parseEther(newRateLimit)) { + currentWIthdrawnAmountReset = true; + + // fast forward time + await env.eth.provider.send('evm_increaseTime', [ + Number(rateLimitDuration) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + } + + await relayMessage(env, withdrawMessageProof!); + + const currentPeriodAmount = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + if (currentWIthdrawnAmountReset) + expect(currentPeriodAmount === parseEther(NUM_ETH)).to.be.true; + else { + expect( + currentPeriodAmount === + parseEther(NUM_ETH) + withdrawnAmountBeforeRelay + ).to.be.true; + } + }); + + it('Rate limit parameters are updated when current withdrawn amount is more than the new limit & set a new higher limit', async () => { + const deployer = env.eth.deployer; + const newRateLimit = `10`; + + let withdrawnAmountBeforeReset = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + await env.eth.fuelMessagePortal + .connect(deployer) + .resetRateLimitAmount(parseEther(newRateLimit)); + + let currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + // current withdrawn amount doesn't change when rate limit is updated + expect( + currentWithdrawnAmountAfterSettingLimit === withdrawnAmountBeforeReset + ).to.be.true; + + withdrawnAmountBeforeReset = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + await env.eth.fuelMessagePortal + .connect(deployer) + .resetRateLimitAmount(parseEther(largeRateLimit)); + + currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + expect( + currentWithdrawnAmountAfterSettingLimit === withdrawnAmountBeforeReset + ).to.be.true; + }); + + it('Rate limit parameters are updated when the initial duration is over', async () => { + // fast forward time + await env.eth.provider.send('evm_increaseTime', [ + Number(rateLimitDuration) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + const currentPeriodEndBeforeRelay = + await env.eth.fuelMessagePortal.currentPeriodEnd(); + + withdrawMessageProof = await generateWithdrawalMessageProof( + fuelETHSender, + ethereumETHReceiverAddress, + NUM_ETH + ); + + await relayMessage(env, withdrawMessageProof!); + + const currentPeriodEndAfterRelay = + await env.eth.fuelMessagePortal.currentPeriodEnd(); + + expect(currentPeriodEndAfterRelay > currentPeriodEndBeforeRelay).to.be + .true; + + const currentPeriodAmount = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + expect(currentPeriodAmount === parseEther(NUM_ETH)).to.be.true; + }); + + it('Rate limit parameters are updated when new limit is set after the initial duration', async () => { + const deployer = await env.eth.deployer; + const newRateLimit = `40`; + + const currentWithdrawnAmountBeforeSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + const currentPeriodEndBeforeSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodEnd(); + + // fast forward time + await env.eth.provider.send('evm_increaseTime', [ + Number(rateLimitDuration) * 2, + ]); + await env.eth.provider.send('evm_mine', []); // Mine a new block + + await env.eth.fuelMessagePortal + .connect(deployer) + .resetRateLimitAmount(parseEther(newRateLimit)); + + const currentPeriodEndAfterSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodEnd(); + const currentWithdrawnAmountAfterSettingLimit = + await env.eth.fuelMessagePortal.currentPeriodAmount(); + + expect( + currentPeriodEndAfterSettingLimit > currentPeriodEndBeforeSettingLimit + ).to.be.true; + + expect( + currentWithdrawnAmountBeforeSettingLimit > + currentWithdrawnAmountAfterSettingLimit + ).to.be.true; + + expect(currentWithdrawnAmountAfterSettingLimit == 0n).to.be.true; + }); + }); +}); diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json index ed3fb160..fb635dd1 100644 --- a/packages/integration-tests/package.json +++ b/packages/integration-tests/package.json @@ -6,6 +6,7 @@ "license": "APACHE-2.0", "scripts": { "test": "pnpm mocha -b -r ts-node/register 'tests/**/*.ts'", + "test-fork": "pnpm mocha -b -r ts-node/register 'fork-tests/**/*.ts'", "test:erc20": "pnpm mocha -b -r ts-node/register 'tests/bridge_erc20.ts'", "test:erc721": "pnpm mocha -b -r ts-node/register 'tests/bridge_erc721.ts'", "test:transfer": "pnpm mocha -b -r ts-node/register 'tests/transfer_eth.ts'", diff --git a/packages/integration-tests/tsconfig.json b/packages/integration-tests/tsconfig.json index 1834f1a8..3f64fc4d 100644 --- a/packages/integration-tests/tsconfig.json +++ b/packages/integration-tests/tsconfig.json @@ -7,5 +7,5 @@ "resolveJsonModule": true, "lib": ["ES2021", "dom"] }, - "include": ["./tests", "./scripts", "./**/*.ts"] + "include": ["./tests", "./fork-tests", "./scripts", "./**/*.ts"] } diff --git a/packages/message-predicates/package.json b/packages/message-predicates/package.json index 6fc7d683..39babf10 100644 --- a/packages/message-predicates/package.json +++ b/packages/message-predicates/package.json @@ -12,7 +12,6 @@ "build": "tsup" }, "devDependencies": { - "@fuel-bridge/esbuild-bin-loader": "workspace:../esbuild-bin-loader", - "@fuel-bridge/solidity-contracts": "workspace:*" + "@fuel-bridge/esbuild-bin-loader": "workspace:../esbuild-bin-loader" } } diff --git a/packages/solidity-contracts/deploy/fork/001.token.ts b/packages/solidity-contracts/deploy/fork/001.token.ts new file mode 100644 index 00000000..9688ec73 --- /dev/null +++ b/packages/solidity-contracts/deploy/fork/001.token.ts @@ -0,0 +1,20 @@ +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { DeployFunction } from 'hardhat-deploy/dist/types'; + +/** + * @description Deployed for testing purposes + */ +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { + ethers, + deployments: { deploy }, + } = hre; + + const [deployer] = await ethers.getSigners(); + + await deploy('Token', { from: deployer.address, log: true }); +}; + +func.tags = ['token']; +func.id = 'token'; +export default func; diff --git a/packages/solidity-contracts/deploy/fork/002.set_canonical_token_bytecode.ts b/packages/solidity-contracts/deploy/fork/002.set_canonical_token_bytecode.ts new file mode 100644 index 00000000..f7ec8ccb --- /dev/null +++ b/packages/solidity-contracts/deploy/fork/002.set_canonical_token_bytecode.ts @@ -0,0 +1,86 @@ +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { DeployFunction } from 'hardhat-deploy/dist/types'; + +import { + USDT_ADDRESS, + USDC_ADDRESS, + WBTC_ADDRESS, + WETH_ADDRESS, +} from '../../protocol/constants'; +import { CustomToken__factory } from '../../typechain'; + +// script used to set custom token contract bytecodes for mainnet token addresses for testing purposes[USDC, USDT, WBTC, WETH] +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { ethers } = hre; + const [deployer] = await ethers.getSigners(); + + const customTokenFactory = await ethers.getContractFactory( + 'CustomToken', + deployer + ); + const customTokenWETHFactory = await ethers.getContractFactory( + 'CustomTokenWETH', + deployer + ); + + let customToken = await customTokenFactory.deploy(); + await customToken.waitForDeployment(); + + let runtimeBytecode = await ethers.provider.getCode( + await customToken.getAddress() + ); + + await ethers.provider.send('hardhat_setCode', [ + USDT_ADDRESS, + runtimeBytecode, + ]); + + let tokenInstance = CustomToken__factory.connect(USDT_ADDRESS, deployer); + await tokenInstance.setDecimals(6n); + + customToken = await customTokenFactory.deploy(); + await customToken.waitForDeployment(); + + runtimeBytecode = await ethers.provider.getCode( + await customToken.getAddress() + ); + + await ethers.provider.send('hardhat_setCode', [ + USDC_ADDRESS, + runtimeBytecode, + ]); + + tokenInstance = CustomToken__factory.connect(USDC_ADDRESS, deployer); + await tokenInstance.setDecimals(6n); + + customToken = await customTokenFactory.deploy(); + await customToken.waitForDeployment(); + + runtimeBytecode = await ethers.provider.getCode( + await customToken.getAddress() + ); + + await ethers.provider.send('hardhat_setCode', [ + WBTC_ADDRESS, + runtimeBytecode, + ]); + + tokenInstance = CustomToken__factory.connect(WBTC_ADDRESS, deployer); + await tokenInstance.setDecimals(8n); + + const customTokenWETH = await customTokenWETHFactory.deploy(); + await customTokenWETH.waitForDeployment(); + + runtimeBytecode = await ethers.provider.getCode( + await customTokenWETH.getAddress() + ); + + await ethers.provider.send('hardhat_setCode', [ + WETH_ADDRESS, + runtimeBytecode, + ]); +}; + +func.tags = ['set_canonical_token_bytecode']; +func.id = 'set_canonical_token_bytecode'; +export default func; diff --git a/packages/solidity-contracts/deploy/fork/003.chain_state_upgrade.ts b/packages/solidity-contracts/deploy/fork/003.chain_state_upgrade.ts new file mode 100644 index 00000000..3f0dfc05 --- /dev/null +++ b/packages/solidity-contracts/deploy/fork/003.chain_state_upgrade.ts @@ -0,0 +1,102 @@ +import fs from 'fs'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { DeployFunction } from 'hardhat-deploy/dist/types'; +import path from 'path'; + +import { FuelChainState__factory } from '../../typechain'; + +const BLOCKS_PER_COMMIT_INTERVAL = 30; +const TIME_TO_FINALIZE = 5; +const COMMIT_COOLDOWN = TIME_TO_FINALIZE; + +const ADMIN = '0x32da601374b38154f05904B16F44A1911Aa6f314'; +let COMMITTER_ADDRESS = '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { + ethers, + upgrades: { erc1967 }, + } = hre; + + const [deployer] = await ethers.getSigners(); + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + let address; + + if (isForking) { + const deploymentPath = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'mainnet', + 'FuelChainState.json' + ); + + const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8')); + address = deployment.address; + + const chainState = FuelChainState__factory.connect(address, deployer); + + const factory = await hre.ethers.getContractFactory('FuelChainState'); + + const newImplementation = await factory.deploy( + TIME_TO_FINALIZE, + BLOCKS_PER_COMMIT_INTERVAL, + COMMIT_COOLDOWN + ); + + const newImplementationAddress = await newImplementation.getAddress(); + + let txData = chainState.interface.encodeFunctionData('upgradeTo', [ + newImplementationAddress, + ]); + + await deployer.sendTransaction({ + to: ADMIN, + value: ethers.parseEther('100'), + }); + + const impersonatedSigner = await ethers.getImpersonatedSigner(ADMIN); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + const COMMITTER_ROLE = await chainState.COMMITTER_ROLE(); + + txData = await chainState.interface.encodeFunctionData('grantRole', [ + COMMITTER_ROLE, + COMMITTER_ADDRESS, + ]); + + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + // hardhat with forking sometimes throws a `nonce too low error` using only one committer, so added another to be used in tests + COMMITTER_ADDRESS = '0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65'; + + txData = await chainState.interface.encodeFunctionData('grantRole', [ + COMMITTER_ROLE, + COMMITTER_ADDRESS, + ]); + + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + const implementation = await erc1967.getImplementationAddress(address); + + console.log('Upgraded FuelChainState to', implementation); + + return true; + } +}; + +func.tags = ['upgrade_chain_state']; +func.id = 'upgrade_chain_state'; +export default func; diff --git a/packages/solidity-contracts/deploy/fork/004.portal_upgrade.ts b/packages/solidity-contracts/deploy/fork/004.portal_upgrade.ts new file mode 100644 index 00000000..14682a6a --- /dev/null +++ b/packages/solidity-contracts/deploy/fork/004.portal_upgrade.ts @@ -0,0 +1,86 @@ +import { MaxUint256 } from 'ethers'; +import fs from 'fs'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { DeployFunction } from 'hardhat-deploy/dist/types'; +import path from 'path'; + +import { FuelMessagePortalV3__factory as FuelMessagePortal } from '../../typechain'; + +const RATE_LIMIT_DURATION = 3600 * 24 * 7; + +const ADMIN = '0x32da601374b38154f05904B16F44A1911Aa6f314'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { + ethers, + upgrades: { erc1967 }, + } = hre; + + const [deployer] = await ethers.getSigners(); + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + let address; + + if (isForking) { + const deploymentPath = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'mainnet', + 'FuelMessagePortal.json' + ); + + const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8')); + address = deployment.address; + + const portal = FuelMessagePortal.connect(address, deployer); + + const SET_RATE_LIMITER_ROLE = await portal.SET_RATE_LIMITER_ROLE(); + + const factory = await hre.ethers.getContractFactory('FuelMessagePortalV3'); + + const newImplementation = await factory.deploy( + MaxUint256, + RATE_LIMIT_DURATION + ); + + const newImplementationAddress = await newImplementation.getAddress(); + + let txData = portal.interface.encodeFunctionData('upgradeTo', [ + newImplementationAddress, + ]); + + await deployer.sendTransaction({ + to: ADMIN, + value: ethers.parseEther('100'), + }); + + const impersonatedSigner = await ethers.getImpersonatedSigner(ADMIN); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + txData = await portal.interface.encodeFunctionData('grantRole', [ + SET_RATE_LIMITER_ROLE, + await deployer.getAddress(), + ]); + + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + const implementation = await erc1967.getImplementationAddress(address); + + console.log('Upgraded FuelMessagePortal to', implementation); + + return true; + } +}; + +func.tags = ['upgrade_portal']; +func.id = 'upgrade_portal'; +export default func; diff --git a/packages/solidity-contracts/deploy/fork/005.gateway_upgrade.ts b/packages/solidity-contracts/deploy/fork/005.gateway_upgrade.ts new file mode 100644 index 00000000..9b6e0d82 --- /dev/null +++ b/packages/solidity-contracts/deploy/fork/005.gateway_upgrade.ts @@ -0,0 +1,97 @@ +import fs from 'fs'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { DeployFunction } from 'hardhat-deploy/dist/types'; +import path from 'path'; + +import { FuelERC20GatewayV4__factory } from '../../typechain'; + +const ADMIN = '0x32da601374b38154f05904B16F44A1911Aa6f314'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { + ethers, + upgrades: { erc1967 }, + } = hre; + + const [deployer] = await ethers.getSigners(); + + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + let address; + + if (isForking) { + const deploymentPath = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'mainnet', + 'FuelERC20GatewayV4.json' + ); + + const deployment = JSON.parse(fs.readFileSync(deploymentPath, 'utf8')); + address = deployment.address; + + const portal = FuelERC20GatewayV4__factory.connect(address, deployer); + + const ADMIN_ROLE = await portal.DEFAULT_ADMIN_ROLE(); + const SET_RATE_LIMITER_ROLE = await portal.SET_RATE_LIMITER_ROLE(); + + const factory = await hre.ethers.getContractFactory('FuelERC20GatewayV4'); + + const newImplementation = await factory.deploy(); + + const newImplementationAddress = await newImplementation.getAddress(); + + let txData = portal.interface.encodeFunctionData('upgradeTo', [ + newImplementationAddress, + ]); + + await deployer.sendTransaction({ + to: ADMIN, + value: ethers.parseEther('100'), + }); + + const impersonatedSigner = await ethers.getImpersonatedSigner(ADMIN); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + txData = await portal.interface.encodeFunctionData('grantRole', [ + ADMIN_ROLE, + await deployer.getAddress(), + ]); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + txData = await portal.interface.encodeFunctionData('grantRole', [ + SET_RATE_LIMITER_ROLE, + await deployer.getAddress(), + ]); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + txData = await portal.interface.encodeFunctionData('requireWhitelist', [ + false, + ]); + await impersonatedSigner.sendTransaction({ + to: address, + data: txData, + }); + + const implementation = await erc1967.getImplementationAddress(address); + + console.log('Upgraded FuelGateway to', implementation); + + return true; + } +}; + +func.tags = ['upgrade_gateway']; +func.id = 'upgrade_gateway'; +export default func; diff --git a/packages/solidity-contracts/deploy/fork/999.serve_deployment_file.ts b/packages/solidity-contracts/deploy/fork/999.serve_deployment_file.ts new file mode 100644 index 00000000..e6fafd1b --- /dev/null +++ b/packages/solidity-contracts/deploy/fork/999.serve_deployment_file.ts @@ -0,0 +1,80 @@ +import fs, { writeFile } from 'fs'; +import type { HardhatRuntimeEnvironment } from 'hardhat/types'; +import type { DeployFunction } from 'hardhat-deploy/dist/types'; +import path from 'path'; +import { promisify } from 'util'; + +import { FuelChainState__factory } from '../../typechain'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { ethers } = hre; + + let address: any; + const isForking = hre.config.networks[hre.network.name]?.forking?.enabled; + const deploymentsFile: { [name: string]: string } = {}; + + if (isForking) { + let deploymentDir = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'mainnet' + ); + + fs.readdirSync(deploymentDir) + .filter((file) => path.extname(file) === '.json') + .forEach((file) => { + const filePath = path.join(deploymentDir, file); + try { + const deployment = JSON.parse(fs.readFileSync(filePath, 'utf8')); + // Use filename (without .json) as the key + const contractName = path.basename(file, '.json'); + deploymentsFile[contractName] = deployment.address; + } catch (error) { + console.error(`Error reading deployment file ${file}:`, error); + } + }); + + deploymentDir = path.join( + __dirname, + '..', + '..', + '/', + 'deployments', + 'mainnet', + 'FuelChainState.json' + ); + + const chainStateDeployment = JSON.parse( + fs.readFileSync(deploymentDir, 'utf8') + ); + address = chainStateDeployment.address; + } + + const state = FuelChainState__factory.connect(address, ethers.provider); + + deploymentsFile['BLOCKS_PER_COMMIT_INTERVAL'] = ( + await state.BLOCKS_PER_COMMIT_INTERVAL() + ).toString(); + deploymentsFile['NUM_COMMIT_SLOTS'] = ( + await state.NUM_COMMIT_SLOTS() + ).toString(); + deploymentsFile['TIME_TO_FINALIZE'] = ( + await state.TIME_TO_FINALIZE() + ).toString(); + + const writeFileAsync = promisify(writeFile); + + await writeFileAsync( + 'deployments/deployments.local.json', + JSON.stringify(deploymentsFile, null, 2), + 'utf8' + ); +}; + +func.tags = ['all']; +func.id = 'all'; +func.runAtTheEnd = true; +export default func; diff --git a/packages/solidity-contracts/deploy/hardhat/001.chain_state.ts b/packages/solidity-contracts/deploy/hardhat/001.chain_state.ts index 1d9f0e2d..e12339de 100644 --- a/packages/solidity-contracts/deploy/hardhat/001.chain_state.ts +++ b/packages/solidity-contracts/deploy/hardhat/001.chain_state.ts @@ -13,6 +13,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { upgrades: { deployProxy, erc1967 }, deployments: { save }, } = hre; + const [deployer] = await ethers.getSigners(); const contract = await deployProxy(new FuelChainState(deployer), [], { diff --git a/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v3.ts b/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v3.ts index 225aaf25..a662bb43 100644 --- a/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v3.ts +++ b/packages/solidity-contracts/deploy/hardhat/002.fuel_message_portal_v3.ts @@ -14,6 +14,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { upgrades: { deployProxy, erc1967 }, deployments: { get, save }, } = hre; + const [deployer] = await ethers.getSigners(); const { address: fuelChainState } = await get('FuelChainState'); diff --git a/packages/solidity-contracts/deploy/hardhat/003.erc20_gateway_v4.ts b/packages/solidity-contracts/deploy/hardhat/003.erc20_gateway_v4.ts index bef7f203..b548e213 100644 --- a/packages/solidity-contracts/deploy/hardhat/003.erc20_gateway_v4.ts +++ b/packages/solidity-contracts/deploy/hardhat/003.erc20_gateway_v4.ts @@ -9,6 +9,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { upgrades: { deployProxy, erc1967 }, deployments: { get, save }, } = hre; + const [deployer] = await ethers.getSigners(); const fuelMessagePortal = await get('FuelMessagePortal'); diff --git a/packages/solidity-contracts/deploy/hardhat/005.register_block_committer.ts b/packages/solidity-contracts/deploy/hardhat/005.register_block_committer.ts index f0fa19ab..4b037856 100644 --- a/packages/solidity-contracts/deploy/hardhat/005.register_block_committer.ts +++ b/packages/solidity-contracts/deploy/hardhat/005.register_block_committer.ts @@ -7,6 +7,7 @@ const COMMITTER_ADDRESS = '0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc'; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { ethers, deployments } = hre; + const [deployer] = await ethers.getSigners(); const { address } = await deployments.get('FuelChainState'); diff --git a/packages/solidity-contracts/deploy/hardhat/006._erc721_gateway_v3.ts b/packages/solidity-contracts/deploy/hardhat/006._erc721_gateway_v3.ts index 3f4734bf..0e020e70 100644 --- a/packages/solidity-contracts/deploy/hardhat/006._erc721_gateway_v3.ts +++ b/packages/solidity-contracts/deploy/hardhat/006._erc721_gateway_v3.ts @@ -9,6 +9,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { upgrades: { deployProxy, erc1967 }, deployments: { get, save }, } = hre; + const [deployer] = await ethers.getSigners(); const fuelMessagePortal = await get('FuelMessagePortal'); diff --git a/packages/solidity-contracts/deploy/hardhat/007.set_asset_issuer_id.ts b/packages/solidity-contracts/deploy/hardhat/007.set_asset_issuer_id.ts index 025d7345..5847683e 100644 --- a/packages/solidity-contracts/deploy/hardhat/007.set_asset_issuer_id.ts +++ b/packages/solidity-contracts/deploy/hardhat/007.set_asset_issuer_id.ts @@ -16,6 +16,7 @@ const ASSET_ISSUER_ID = const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const { ethers, deployments } = hre; + const [deployer] = await ethers.getSigners(); await deployments.execute( diff --git a/packages/solidity-contracts/deploy/hardhat/999.serve_deployment_file.ts b/packages/solidity-contracts/deploy/hardhat/999.serve_deployment_file.ts index 9900c7e0..a5ed9834 100644 --- a/packages/solidity-contracts/deploy/hardhat/999.serve_deployment_file.ts +++ b/packages/solidity-contracts/deploy/hardhat/999.serve_deployment_file.ts @@ -35,4 +35,4 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { func.tags = ['all']; func.id = 'all'; func.runAtTheEnd = true; -export default func; +export default func; \ No newline at end of file diff --git a/packages/solidity-contracts/hardhat.config.ts b/packages/solidity-contracts/hardhat.config.ts index 2815dc62..4c41912f 100644 --- a/packages/solidity-contracts/hardhat.config.ts +++ b/packages/solidity-contracts/hardhat.config.ts @@ -16,6 +16,7 @@ const CONTRACTS_DEPLOYER_KEY = process.env.CONTRACTS_DEPLOYER_KEY || ''; const ETHERSCAN_API_KEY = process.env.ETHERSCAN_API_KEY || ''; const INFURA_API_KEY = process.env.INFURA_API_KEY || ''; const RPC_URL = process.env.RPC_URL || 'http://127.0.0.1:8545'; +const isForkingEnabled = !!process.env.TENDERLY_RPC_URL; const config: HardhatUserConfig = { defaultNetwork: 'hardhat', @@ -41,7 +42,11 @@ const config: HardhatUserConfig = { accounts: { count: 128, }, - deploy: ['deploy/hardhat'], + forking: { + enabled: isForkingEnabled, + url: process.env.TENDERLY_RPC_URL ? process.env.TENDERLY_RPC_URL : '', + }, + deploy: isForkingEnabled ? ['deploy/fork'] : ['deploy/hardhat'], }, localhost: { url: 'http://127.0.0.1:8545/', diff --git a/packages/solidity-contracts/package.json b/packages/solidity-contracts/package.json index 70087cf7..cbacd48b 100644 --- a/packages/solidity-contracts/package.json +++ b/packages/solidity-contracts/package.json @@ -76,4 +76,4 @@ "typechain": "^8.3.2", "typescript": "^4.9.3" } -} +} \ No newline at end of file diff --git a/packages/solidity-contracts/scripts/test.sh b/packages/solidity-contracts/scripts/test.sh new file mode 100644 index 00000000..b8dbcdad --- /dev/null +++ b/packages/solidity-contracts/scripts/test.sh @@ -0,0 +1,31 @@ +# Wait for the nodes to be ready and run the tests +HEALTH_CHECK_COUNTER=0 +HELTH_CHECK_OUTPUT="" +MAX_CHECK_ATTEMPTS=50 + +waitForNodesToBeReady() { + NODE_URL="http://localhost:4000/v1/playground"; + + printf "\rWaiting for node.${HELTH_CHECK_OUTPUT}" + + if [ $HEALTH_CHECK_COUNTER -gt $MAX_CHECK_ATTEMPTS ]; then + echo "\n\nTests failed" + exit 1 + fi + + if curl --silent --head --request GET $NODE_URL | grep "200 OK" > /dev/null; then + # If the node responds with 200, it is ready + # to run the tests. + echo "\nRun tests..." + pnpm pnpm run test:integration + else + # If the request not returns 200 the node is not ready yet + # sleep for 6 seconds before and try again. + HEALTH_CHECK_COUNTER=$((HEALTH_CHECK_COUNTER+1)) + HELTH_CHECK_OUTPUT="${HELTH_CHECK_OUTPUT}." + sleep 6 + waitForNodesToBeReady + fi +} + +waitForNodesToBeReady \ No newline at end of file diff --git a/packages/test-utils/src/utils/setup.ts b/packages/test-utils/src/utils/setup.ts index 35640936..ca9d2aa4 100644 --- a/packages/test-utils/src/utils/setup.ts +++ b/packages/test-utils/src/utils/setup.ts @@ -66,6 +66,7 @@ export interface SetupOptions { pk_eth_deployer?: string; pk_eth_signer1?: string; pk_eth_signer2?: string; + pk_eth_signer3?: string; pk_fuel_deployer?: string; pk_fuel_signer1?: string; pk_fuel_signer2?: string; @@ -260,12 +261,10 @@ export async function setupEnvironment( } if (!eth_fuelERC721GatewayAddress) { - if (!deployerAddresses.FuelERC721Gateway) { - throw new Error( - 'Failed to get FuelERC721Gateway address from deployer' - ); + if (deployerAddresses.FuelERC721Gateway) { + // we don't use the erc71 gateway for the fork test suite + eth_fuelERC721GatewayAddress = deployerAddresses.FuelERC721Gateway; } - eth_fuelERC721GatewayAddress = deployerAddresses.FuelERC721Gateway; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc55af5d..bfa198cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,9 +68,6 @@ importers: '@fuel-bridge/esbuild-bin-loader': specifier: workspace:../esbuild-bin-loader version: link:../esbuild-bin-loader - '@fuel-bridge/solidity-contracts': - specifier: workspace:* - version: link:../solidity-contracts fuels: specifier: 0.96.1 version: 0.96.1 @@ -122,9 +119,6 @@ importers: '@fuel-bridge/esbuild-bin-loader': specifier: workspace:../esbuild-bin-loader version: link:../esbuild-bin-loader - '@fuel-bridge/solidity-contracts': - specifier: workspace:* - version: link:../solidity-contracts packages/solidity-contracts: devDependencies: diff --git a/scripts/build.sh b/scripts/build.sh index 4e93b7d2..dac65f6a 100644 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -2,4 +2,4 @@ forc build --release cargo run --bin fuel-contract-message-predicate -turbo run build +turbo run build \ No newline at end of file diff --git a/scripts/check.sh b/scripts/check.sh index f2f4be9b..f1b28c1d 100644 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -5,5 +5,4 @@ cargo fmt --check pnpm forc build --release cargo clippy --all-features --all-targets -- -D warnings pnpm prettier:check -pnpm lint:check - +pnpm lint:check \ No newline at end of file diff --git a/scripts/test-fork.sh b/scripts/test-fork.sh new file mode 100644 index 00000000..e3290966 --- /dev/null +++ b/scripts/test-fork.sh @@ -0,0 +1,35 @@ +# Start the docker compose file with L1 and Fuel Node +echo "\n\nStarting docker..." +pnpm run node:up + +# Wait for the nodes to be ready and run the tests +HEALTH_CHECK_COUNTER=0 +HELTH_CHECK_OUTPUT="" +MAX_CHECK_ATTEMPTS=50 + +waitForNodesToBeReady() { + NODE_URL="http://localhost:4000/v1/playground"; + + printf "\rWaiting for node.${HELTH_CHECK_OUTPUT}" + + if [ $HEALTH_CHECK_COUNTER -gt $MAX_CHECK_ATTEMPTS ]; then + echo "\n\nTests failed" + exit 1 + fi + + if curl --silent --head --request GET $NODE_URL | grep "200 OK" > /dev/null; then + # If the node responds with 200, it is ready + # to run the tests. + echo "\nRun tests..." + pnpm pnpm run test:integration:fork + else + # If the request not returns 200 the node is not ready yet + # sleep for 6 seconds before and try again. + HEALTH_CHECK_COUNTER=$((HEALTH_CHECK_COUNTER+1)) + HELTH_CHECK_OUTPUT="${HELTH_CHECK_OUTPUT}." + sleep 6 + waitForNodesToBeReady + fi +} + +waitForNodesToBeReady \ No newline at end of file