From 8ec791c6ec80ee17f4baacbe870e9180adfcf069 Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 24 Mar 2026 16:27:17 +0100 Subject: [PATCH 01/45] fix: extract `L1_POL_TOKEN_ADDRESS` from kurtosis env --- core/helpers/pos-setup.bash | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/helpers/pos-setup.bash b/core/helpers/pos-setup.bash index f8be4494..6731c788 100644 --- a/core/helpers/pos-setup.bash +++ b/core/helpers/pos-setup.bash @@ -33,6 +33,7 @@ pos_setup() { [[ -z "${L1_STAKE_MANAGER_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_STAKING_INFO_ADDRESS:-}" ]] || [[ -z "${L1_MATIC_TOKEN_ADDRESS:-}" ]] || + [[ -z "${L1_POL_TOKEN_ADDRESS:-}" ]] || [[ -z "${L1_ERC20_TOKEN_ADDRESS:-}" ]] || [[ -z "${L1_ERC721_TOKEN_ADDRESS:-}" ]] || [[ -z "${L2_STATE_RECEIVER_ADDRESS:-}" ]] || @@ -56,6 +57,9 @@ pos_setup() { export L1_MATIC_TOKEN_ADDRESS=${L1_MATIC_TOKEN_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.tokens.MaticToken')} echo "L1_MATIC_TOKEN_ADDRESS=${L1_MATIC_TOKEN_ADDRESS}" + export L1_POL_TOKEN_ADDRESS=${L1_POL_TOKEN_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.tokens.PolToken')} + echo "L1_POL_TOKEN_ADDRESS=${L1_POL_TOKEN_ADDRESS}" + export L1_ERC20_TOKEN_ADDRESS=${L1_ERC20_TOKEN_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.tokens.TestToken')} echo "L1_ERC20_TOKEN_ADDRESS=${L1_ERC20_TOKEN_ADDRESS}" From cfa9ab1d9d66f0a22c28d7964ee5bb6fa4ce48fa Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 24 Mar 2026 16:27:29 +0100 Subject: [PATCH 02/45] fix: validator test to use POL instead of MATIC --- tests/pos/validator.bats | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pos/validator.bats b/tests/pos/validator.bats index a298eea8..bd63ba94 100644 --- a/tests/pos/validator.bats +++ b/tests/pos/validator.bats @@ -34,17 +34,17 @@ function generate_new_keypair() { echo "Funding the validator account with ETH..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" --value 1ether "${validator_address}" - echo "Funding the validator account with MATIC tokens..." + echo "Funding the validator account with POL tokens..." deposit_amount=$(cast to-unit 1ether wei) # minimum deposit: 1000000000000000000 (1 ether) heimdall_fee_amount=$(cast to-unit 1ether wei) # minimum heimdall fee: 1000000000000000000 (1 ether) funding_amount=$((deposit_amount + heimdall_fee_amount)) cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_MATIC_TOKEN_ADDRESS}" "transfer(address,uint)" "${validator_address}" "${funding_amount}" + "${L1_POL_TOKEN_ADDRESS}" "transfer(address,uint)" "${validator_address}" "${funding_amount}" - echo "Allowing the StakeManagerProxy contract to spend MATIC tokens on our behalf..." + echo "Allowing the StakeManagerProxy contract to spend POL tokens on our behalf..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${validator_private_key}" \ - "${L1_MATIC_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${funding_amount}" + "${L1_POL_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${funding_amount}" echo "Adding new validator to the validator set..." accept_delegation=false From a3498998b0f827edf95f0e52e0d16a350b904adf Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 24 Mar 2026 16:30:16 +0100 Subject: [PATCH 03/45] fix: use POL in all validator tests --- tests/pos/validator.bats | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/pos/validator.bats b/tests/pos/validator.bats index bd63ba94..c36bd64f 100644 --- a/tests/pos/validator.bats +++ b/tests/pos/validator.bats @@ -74,14 +74,14 @@ function generate_new_keypair() { initial_voting_power=$(eval "${VALIDATOR_POWER_CMD}") echo "Initial voting power of the validator (${VALIDATOR_ID}): ${initial_voting_power}." - echo "Funding the validator acount with MATIC/POL tokens..." + echo "Funding the validator acount with POL tokens..." stake_update_amount=$(cast to-unit 1ether wei) cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_MATIC_TOKEN_ADDRESS}" "transfer(address,uint)" "${validator_address}" "${stake_update_amount}" + "${L1_POL_TOKEN_ADDRESS}" "transfer(address,uint)" "${validator_address}" "${stake_update_amount}" - echo "Allowing the StakeManagerProxy contract to spend MATIC/POL tokens on our behalf..." + echo "Allowing the StakeManagerProxy contract to spend POL tokens on our behalf..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${VALIDATOR_PRIVATE_KEY}" \ - "${L1_MATIC_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${stake_update_amount}" + "${L1_POL_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${stake_update_amount}" echo "Updating the stake of the validator (${VALIDATOR_ID})..." stake_rewards=false @@ -108,10 +108,10 @@ function generate_new_keypair() { initial_top_up_balance=$(eval "${TOP_UP_FEE_BALANCE_CMD}") echo "${VALIDATOR_ADDRESS} initial top-up balance: ${initial_top_up_balance}." - echo "Allowing the StakeManagerProxy contract to spend MATIC/POL tokens on our behalf..." + echo "Allowing the StakeManagerProxy contract to spend POL tokens on our behalf..." top_up_amount=$(cast to-unit 1ether wei) cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_MATIC_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${top_up_amount}" + "${L1_POL_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${top_up_amount}" echo "Topping up the fee balance of the validator (${VALIDATOR_ADDRESS})..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ @@ -218,22 +218,22 @@ function generate_new_keypair() { echo "Transferring ETH from main address to foundry address for gas..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" --value 1ether "${DELEGATOR_ADDRESS}" - echo "Transferring MATIC/POL tokens from main address to foundry address..." + echo "Transferring POL tokens from main address to foundry address..." delegation_amount=$(cast to-unit 1ether wei) cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_MATIC_TOKEN_ADDRESS}" "transfer(address,uint)" "${DELEGATOR_ADDRESS}" "${delegation_amount}" + "${L1_POL_TOKEN_ADDRESS}" "transfer(address,uint)" "${DELEGATOR_ADDRESS}" "${delegation_amount}" echo "Verifying foundry address received the tokens..." delegator_pol_balance=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_MATIC_TOKEN_ADDRESS}" "balanceOf(address)(uint256)" "${DELEGATOR_ADDRESS}" | cut -d' ' -f1) + "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint256)" "${DELEGATOR_ADDRESS}" | cut -d' ' -f1) delegator_eth_balance=$(cast balance --rpc-url "${L1_RPC_URL}" "${DELEGATOR_ADDRESS}" --ether) - echo "Foundry address MATIC/POL balance: $(cast to-unit ${delegator_pol_balance} ether) POL" + echo "Foundry address POL balance: $(cast to-unit ${delegator_pol_balance} ether) POL" echo "Foundry address ETH balance: ${delegator_eth_balance} ETH" - echo "Allowing the StakeManager to spend MATIC/POL tokens on foundry address behalf..." + echo "Allowing the StakeManager to spend POL tokens on foundry address behalf..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${DELEGATOR_PRIVATE_KEY}" \ - "${L1_MATIC_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${delegation_amount}" + "${L1_POL_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${delegation_amount}" echo "Delegating ${delegation_amount} wei (1 MATIC/POL) to validator ${VALIDATOR_ID}..." min_shares_to_mint=0 From b20b02b94461993e325344969beb1896c779184a Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 24 Mar 2026 16:37:56 +0100 Subject: [PATCH 04/45] chore: nit --- tests/pos/validator.bats | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pos/validator.bats b/tests/pos/validator.bats index c36bd64f..bb602674 100644 --- a/tests/pos/validator.bats +++ b/tests/pos/validator.bats @@ -178,7 +178,7 @@ function generate_new_keypair() { } # bats test_tags=pos-validator,pos-delegate,transaction-pol -@test "delegate MATIC/POL to a validator" { +@test "delegate POL to a validator" { VALIDATOR_ID=${VALIDATOR_ID:-"1"} echo "VALIDATOR_ID=${VALIDATOR_ID}" @@ -235,7 +235,7 @@ function generate_new_keypair() { cast send --rpc-url "${L1_RPC_URL}" --private-key "${DELEGATOR_PRIVATE_KEY}" \ "${L1_POL_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${delegation_amount}" - echo "Delegating ${delegation_amount} wei (1 MATIC/POL) to validator ${VALIDATOR_ID}..." + echo "Delegating ${delegation_amount} wei (1 POL) to validator ${VALIDATOR_ID}..." min_shares_to_mint=0 cast send --rpc-url "${L1_RPC_URL}" --private-key "${DELEGATOR_PRIVATE_KEY}" \ "${validator_share_address}" "buyVoucherPOL(uint,uint)" "${delegation_amount}" "${min_shares_to_mint}" @@ -272,7 +272,7 @@ function generate_new_keypair() { } # bats test_tags=pos-validator,pos-undelegate,transaction-pol -@test "undelegate MATIC/POL from a validator" { +@test "undelegate POL from a validator" { VALIDATOR_ID=${VALIDATOR_ID:-"1"} echo "VALIDATOR_ID=${VALIDATOR_ID}" From cfe42f0a59470a7c99e53a901fe2940175880642 Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 24 Mar 2026 16:52:32 +0100 Subject: [PATCH 05/45] chore: use POL instead of MATIC during bridge tests --- tests/pos/bridge.bats | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 117e5706..0bbb068f 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -46,8 +46,8 @@ function wait_for_bor_state_sync() { assert_command_eventually_greater_or_equal "${BOR_STATE_SYNC_COUNT_CMD}" $((state_sync_count + 1)) "${timeout_seconds}" "${interval_seconds}" } -# bats test_tags=bridge,transaction-pol -@test "bridge MATIC/POL from L1 to L2 and confirm L2 MATIC/POL balance increased" { +# bats test_tags=bridge,transaction-matic +@test "bridge MATIC from L1 to L2 and confirm L2 native tokens balance increased" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get the initial balances. @@ -55,21 +55,21 @@ function wait_for_bor_state_sync() { initial_l2_balance=$(cast balance --rpc-url "${L2_RPC_URL}" "${address}") echo "Initial balances:" - echo "- L1 balance: ${initial_l1_balance} MATIC" - echo "- L2 balance: ${initial_l2_balance} wei" + echo "- L1 MATIC balance: ${initial_l1_balance}" + echo "- L2 native tokens balance: ${initial_l2_balance} wei" heimdall_state_sync_count=$(eval "${HEIMDALL_STATE_SYNC_COUNT_CMD}") bor_state_sync_count=$(eval "${BOR_STATE_SYNC_COUNT_CMD}") - # Bridge some MATIC/POL tokens from L1 to L2 to trigger a state sync. - # 1 MATIC/POL token = 1000000000000000000 wei. + # Bridge some MATIC tokens from L1 to L2 to trigger a state sync. + # 1 MATIC token = 1000000000000000000 wei. bridge_amount=$(cast to-unit 1ether wei) - echo "Approving the DepositManager contract to spend MATIC/POL tokens on our behalf..." + echo "Approving the DepositManager contract to spend MATIC tokens on our behalf..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_MATIC_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "${bridge_amount}" - echo "Depositing MATIC/POL tokens to trigger a state sync..." + echo "Depositing MATIC tokens to trigger a state sync..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "depositERC20(address,uint)" "${L1_MATIC_TOKEN_ADDRESS}" "${bridge_amount}" @@ -78,15 +78,15 @@ function wait_for_bor_state_sync() { wait_for_bor_state_sync "${bor_state_sync_count}" # Monitor the balances on L1 and L2. - echo "Monitoring MATIC/POL balance on L1..." + echo "Monitoring MATIC balance on L1..." assert_token_balance_eventually_lower_or_equal "${L1_MATIC_TOKEN_ADDRESS}" "${address}" $((initial_l1_balance - bridge_amount)) "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" - echo "Monitoring MATIC/POL balance on L2..." + echo "Monitoring native tokens balance on L2..." assert_ether_balance_eventually_greater_or_equal "${address}" $((initial_l2_balance + bridge_amount)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } # bats test_tags=bridge,transaction-pol -# @test "bridge MATIC/POL from L2 to L1 and confirm L1 MATIC/POL balance increased" { +# @test "bridge native tokens from L2 to L1 and confirm L1 POL balance increased" { # echo TODO # } From b76f51b3c9bf05a775df9f6cc93707d9e0ea57cc Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 24 Mar 2026 16:53:33 +0100 Subject: [PATCH 06/45] feat: add POL bridge test --- tests/pos/bridge.bats | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 0bbb068f..14d77fb6 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -46,6 +46,46 @@ function wait_for_bor_state_sync() { assert_command_eventually_greater_or_equal "${BOR_STATE_SYNC_COUNT_CMD}" $((state_sync_count + 1)) "${timeout_seconds}" "${interval_seconds}" } +# bats test_tags=bridge,transaction-pol +@test "bridge POL from L1 to L2 and confirm L2 native tokens balance increased" { + address=$(cast wallet address --private-key "${PRIVATE_KEY}") + + # Get the initial balances. + initial_l1_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') + initial_l2_balance=$(cast balance --rpc-url "${L2_RPC_URL}" "${address}") + + echo "Initial balances:" + echo "- L1 POL balance: ${initial_l1_balance}" + echo "- L2 native tokens balance: ${initial_l2_balance} wei" + + heimdall_state_sync_count=$(eval "${HEIMDALL_STATE_SYNC_COUNT_CMD}") + bor_state_sync_count=$(eval "${BOR_STATE_SYNC_COUNT_CMD}") + + # Bridge some POL tokens from L1 to L2 to trigger a state sync. + # The DepositManager remaps POL to MATIC internally before the state sync, + # so the L2 native token balance increases identically to bridging MATIC. + bridge_amount=$(cast to-unit 1ether wei) + + echo "Approving the DepositManager contract to spend POL tokens on our behalf..." + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ + "${L1_POL_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "${bridge_amount}" + + echo "Depositing POL tokens to trigger a state sync..." + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ + "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "depositERC20(address,uint)" "${L1_POL_TOKEN_ADDRESS}" "${bridge_amount}" + + # Wait for Heimdall and Bor to process the bridge event. + wait_for_heimdall_state_sync "${heimdall_state_sync_count}" + wait_for_bor_state_sync "${bor_state_sync_count}" + + # Monitor the balances on L1 and L2. + echo "Monitoring POL balance on L1..." + assert_token_balance_eventually_lower_or_equal "${L1_POL_TOKEN_ADDRESS}" "${address}" $((initial_l1_balance - bridge_amount)) "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + + echo "Monitoring native tokens balance on L2..." + assert_ether_balance_eventually_greater_or_equal "${address}" $((initial_l2_balance + bridge_amount)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" +} + # bats test_tags=bridge,transaction-matic @test "bridge MATIC from L1 to L2 and confirm L2 native tokens balance increased" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") From 6bb1629e088d978a7f96bebbcb2ffb3e8acd14f8 Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 24 Mar 2026 17:59:54 +0100 Subject: [PATCH 07/45] chore: update doc --- README.md | 1 + TESTSINVENTORY.md | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d287b4bc..0096cfab 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,7 @@ grep -hoR --include="*.bats" 'test_tags=[^ ]*' . | sed 's/.*test_tags=//' | tr ' - transaction-eoa - transaction-erc20 - transaction-erc721 +- transaction-matic - transaction-pol - transaction-uniswap - transient-storage diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index 8ae7230e..86266d6e 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -378,15 +378,16 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getAuthor returns a valid address for latest block | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L486) | | | bor_getCurrentValidators returns a non-empty validator list | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L509) | | | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | -| bridge MATIC/POL from L1 to L2 and confirm L2 MATIC/POL balance increased | [Link](./tests/pos/bridge.bats#L50) | | -| bridge MATIC/POL, ERC20, and ERC721 from L1 to L2 and confirm L2 balances increased | [Link](./tests/pos/bridge.bats#L187) | | -| bridge an ERC721 token from L1 to L2 and confirm L2 ERC721 balance increased | [Link](./tests/pos/bridge.bats#L138) | | -| bridge some ERC20 tokens from L1 to L2 and confirm L2 ERC20 balance increased | [Link](./tests/pos/bridge.bats#L94) | | +| bridge MATIC from L1 to L2 and confirm L2 native tokens balance increased | [Link](./tests/pos/bridge.bats#L90) | | +| bridge MATIC/POL, ERC20, and ERC721 from L1 to L2 and confirm L2 balances increased | [Link](./tests/pos/bridge.bats#L227) | | +| bridge POL from L1 to L2 and confirm L2 native tokens balance increased | [Link](./tests/pos/bridge.bats#L50) | | +| bridge an ERC721 token from L1 to L2 and confirm L2 ERC721 balance increased | [Link](./tests/pos/bridge.bats#L178) | | +| bridge some ERC20 tokens from L1 to L2 and confirm L2 ERC20 balance increased | [Link](./tests/pos/bridge.bats#L134) | | | coinbase balance increases by at least the priority fee portion of gas cost | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L318) | | | concurrent write/read race: tx submissions and state reads do not interfere | [Link](./tests/pos/execution-specs/rpc/rpc-concurrent-load-and-stress.bats#L248) | | | consecutive block baseFees are within ±5% of each other | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L183) | | | contract-to-contract call fuzz: CALL/STATICCALL/DELEGATECALL | [Link](./tests/pos/execution-specs/transactions/evm-transaction-fuzzing-and-liveness.bats#L792) | | -| delegate MATIC/POL to a validator | [Link](./tests/pos/validator.bats#L181) | | +| delegate POL to a validator | [Link](./tests/pos/validator.bats#L181) | | | deploy contract that returns 24577 runtime bytes is rejected by EIP-170 | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L124) | | | deploy contract that returns exactly 24576 runtime bytes succeeds (EIP-170 boundary) | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L150) | | | deploy contract that reverts in constructor leaves no code at deployed address | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L48) | | @@ -472,7 +473,7 @@ Table of tests currently implemented or being implemented in the E2E repository. | type 1 access list with multiple storage keys is accepted | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L207) | | | type 2 (EIP-1559) effectiveGasPrice = baseFee + min(priorityFee, maxFee - baseFee) | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L130) | | | type 2 maxFeePerGas below baseFee is rejected | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L175) | | -| undelegate MATIC/POL from a validator | [Link](./tests/pos/validator.bats#L275) | | +| undelegate POL from a validator | [Link](./tests/pos/validator.bats#L275) | | | update signer | [Link](./tests/pos/validator.bats#L147) | | | update validator stake | [Link](./tests/pos/validator.bats#L60) | | | update validator top-up fee | [Link](./tests/pos/validator.bats#L97) | | From 62a6afe4766cd7e3ccdecc46ea0bc30e7784c1af Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 24 Mar 2026 18:14:25 +0100 Subject: [PATCH 08/45] feat: first part of pos exit --- core/helpers/scripts/eventually.bash | 29 ++++++++++++++++ tests/pos/bridge.bats | 50 ++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/core/helpers/scripts/eventually.bash b/core/helpers/scripts/eventually.bash index c75d741d..47978162 100644 --- a/core/helpers/scripts/eventually.bash +++ b/core/helpers/scripts/eventually.bash @@ -149,6 +149,35 @@ function assert_token_balance_eventually_lower_or_equal() { done } +function assert_ether_balance_eventually_lower_or_equal() { + local address="$1" + local target="$2" + local rpc_url="$3" + local timeout="${4:-90}" + local interval="${5:-10}" + + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Target: ${target}" + + local start_time + start_time=$(date +%s) + local end_time + end_time=$((start_time + timeout)) + while true; do + if [[ "$(date +%s)" -ge "${end_time}" ]]; then + echo "Timeout reached." + exit 1 + fi + + balance=$(cast balance --rpc-url "${rpc_url}" "${address}") + echo "[$(date '+%Y-%m-%d %H:%M:%S')] Balance: ${balance} wei" + if [[ "${balance}" -le "${target}" ]]; then + break + fi + + sleep "${interval}" + done +} + function assert_ether_balance_eventually_greater_or_equal() { local address="$1" local target="$2" diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 14d77fb6..96da4040 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -126,9 +126,53 @@ function wait_for_bor_state_sync() { } # bats test_tags=bridge,transaction-pol -# @test "bridge native tokens from L2 to L1 and confirm L1 POL balance increased" { -# echo TODO -# } +@test "withdraw native tokens from L2 and confirm L2 native balance decreased and checkpoint submitted" { + address=$(cast wallet address --private-key "${PRIVATE_KEY}") + + # Get initial L2 native balance and latest checkpoint ID. + initial_l2_balance=$(cast balance --rpc-url "${L2_RPC_URL}" "${address}") + checkpoint_count_cmd='curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq --raw-output ".checkpoint.id"' + initial_checkpoint_id=$(eval "${checkpoint_count_cmd}") + + echo "Initial balances and state:" + echo "- L2 native balance: ${initial_l2_balance} wei" + echo "- Latest checkpoint ID: ${initial_checkpoint_id}" + + # Burn native tokens on L2 to initiate the Plasma exit. + withdraw_amount=$(cast to-unit 1ether wei) + echo "Burning ${withdraw_amount} wei on L2 (0x1010.withdraw)..." + withdraw_receipt=$(cast send \ + --rpc-url "${L2_RPC_URL}" \ + --private-key "${PRIVATE_KEY}" \ + --value "${withdraw_amount}" \ + --json \ + "0x0000000000000000000000000000000000001010" \ + "withdraw(uint256)" "${withdraw_amount}") + withdraw_tx_hash=$(echo "${withdraw_receipt}" | jq --raw-output ".transactionHash") + withdraw_block_hex=$(echo "${withdraw_receipt}" | jq --raw-output ".blockNumber") + withdraw_block=$(printf "%d" "${withdraw_block_hex}") + echo "Withdraw tx: ${withdraw_tx_hash} (block ${withdraw_block})" + + # Verify L2 native balance decreased. + echo "Verifying L2 native balance decreased..." + assert_ether_balance_eventually_lower_or_equal "${address}" $((initial_l2_balance - withdraw_amount)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + + # Wait for a new checkpoint on L1 that covers the withdrawal block. + # This confirms validators have attested to the burn, which is a prerequisite for building a valid exit Merkle proof. + echo "Waiting for a new checkpoint to cover L2 block ${withdraw_block}..." + assert_command_eventually_greater_or_equal "${checkpoint_count_cmd}" $((initial_checkpoint_id + 1)) "${timeout_seconds}" "${interval_seconds}" + + # TODO: Complete the Plasma exit on L1 to recover funds. + # After the burn block is checkpointed, the remaining steps are: + # 1. Build the exit payload: RLP-encoded receipt of the burn tx + Merkle + # proof of that receipt in the block's receipts trie + checkpoint proof. + # 2. L1: WithdrawManagerProxy.startExitWithBurntTokens(bytes exitTx) + # 3. L1: WithdrawManagerProxy.processExits(address token) + # 4. Assert L1 POL balance increased by withdraw_amount. + # Proof generation requires constructing the receipts MPT of the burn block, + # which is not feasible in pure bash. A dedicated tool (e.g. matic.js or a + # custom Go helper) is needed. +} # bats test_tags=bridge,transaction-erc20 @test "bridge some ERC20 tokens from L1 to L2 and confirm L2 ERC20 balance increased" { From ade16c3c53afd0e56351358a9bbdb3e6779ca2ff Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 24 Mar 2026 18:35:23 +0100 Subject: [PATCH 09/45] fix: remove validor test --- tests/pos/validator.bats | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/pos/validator.bats b/tests/pos/validator.bats index bb602674..0d0873b8 100644 --- a/tests/pos/validator.bats +++ b/tests/pos/validator.bats @@ -377,6 +377,18 @@ function generate_new_keypair() { cast send --rpc-url "${L1_RPC_URL}" --private-key "${VALIDATOR_PRIVATE_KEY}" \ "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "unstakePOL(uint)" "${VALIDATOR_ID}" + # Verify the unstake was initiated on L1: deactivationEpoch must be greater than zero. + # validators(uint256) returns (amount, reward, activationEpoch, deactivationEpoch, ...) + # where deactivationEpoch is the 4th return value. + deactivation_epoch=$(cast call --rpc-url "${L1_RPC_URL}" \ + "${L1_STAKE_MANAGER_PROXY_ADDRESS}" \ + "validators(uint256)(uint256,uint256,uint256,uint256,uint256,address,address,uint8)" \ + "${VALIDATOR_ID}" | sed -n '4p') + echo "Validator ${VALIDATOR_ID} deactivationEpoch: ${deactivation_epoch}" + [[ "${deactivation_epoch}" -gt "0" ]] + + # Wait for Heimdall to remove the validator at the epoch boundary. + # 1 epoch ≈ 256 blocks (~256s at 1s/block), so 300s gives enough headroom. echo "Monitoring the validator count on Heimdall..." - assert_command_eventually_equal "${VALIDATOR_COUNT_CMD}" $((initial_validator_count - 1)) 180 + assert_command_eventually_equal "${VALIDATOR_COUNT_CMD}" $((initial_validator_count - 1)) 300 } From 882c0975b1b013e1dfc7245c88af8d344b690e40 Mon Sep 17 00:00:00 2001 From: leovct Date: Tue, 24 Mar 2026 19:00:22 +0100 Subject: [PATCH 10/45] fix: delegate/undelegate tests --- tests/pos/validator.bats | 77 ++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 47 deletions(-) diff --git a/tests/pos/validator.bats b/tests/pos/validator.bats index 0d0873b8..ac912672 100644 --- a/tests/pos/validator.bats +++ b/tests/pos/validator.bats @@ -205,15 +205,14 @@ function generate_new_keypair() { fi # Check initial validator total stake (own stake + delegated amount). - initial_total_stake=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1) - echo "Initial total validator stake: ${initial_total_stake}" + initial_total_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ + "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1)" ether) + echo "Initial total validator stake: ${initial_total_stake_eth} POL" # Check initial delegator stake in this validator. - initial_delegator_stake_data=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${DELEGATOR_ADDRESS}") - initial_delegator_stake=$(echo "${initial_delegator_stake_data}" | head -1 | cut -d' ' -f1) - echo "Initial delegator stake: ${initial_delegator_stake}" + initial_delegator_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ + "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${DELEGATOR_ADDRESS}" | head -1 | cut -d' ' -f1)" ether) + echo "Initial delegator stake: ${initial_delegator_stake_eth} POL" echo "Transferring ETH from main address to foundry address for gas..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" --value 1ether "${DELEGATOR_ADDRESS}" @@ -243,28 +242,20 @@ function generate_new_keypair() { echo "Verifying delegation was successful..." # Check that validator's total stake increased. - expected_total_stake=$((initial_total_stake + delegation_amount)) - final_total_stake=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1) - echo "Expected total stake: ${expected_total_stake}" - echo "Final total stake: ${final_total_stake}" + final_total_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ + "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1)" ether) + echo "Final total stake: ${final_total_stake_eth} POL" + [[ $(echo "${final_total_stake_eth} == ${initial_total_stake_eth} + 1" | bc) -eq 1 ]] # Check that delegator's stake in validator increased. - final_delegator_stake_data=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${DELEGATOR_ADDRESS}") - final_delegator_stake=$(echo "${final_delegator_stake_data}" | head -1 | cut -d' ' -f1) - expected_delegator_stake=$((initial_delegator_stake + delegation_amount)) - echo "Expected delegator stake: ${expected_delegator_stake}" - echo "Final delegator stake: ${final_delegator_stake}" - - # Verify the stakes match expectations. - [[ "${final_total_stake}" -eq "${expected_total_stake}" ]] - [[ "${final_delegator_stake}" -eq "${expected_delegator_stake}" ]] + final_delegator_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ + "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${DELEGATOR_ADDRESS}" | head -1 | cut -d' ' -f1)" ether) + echo "Final delegator stake: ${final_delegator_stake_eth} POL" + [[ $(echo "${final_delegator_stake_eth} == ${initial_delegator_stake_eth} + 1" | bc) -eq 1 ]] # Verify L2 voting power matches the updated L1 stake. VALIDATOR_POWER_CMD='curl "${L2_CL_API_URL}/stake/validator/${VALIDATOR_ID}" | jq --raw-output ".validator.voting_power"' - - expected_voting_power=$(cast to-unit "${final_total_stake}" ether | cut -d'.' -f1) + expected_voting_power=$(echo "${final_total_stake_eth}" | cut -d'.' -f1) echo "Monitoring L2 voting power sync for validator ${VALIDATOR_ID}..." assert_command_eventually_equal "${VALIDATOR_POWER_CMD}" "${expected_voting_power}" 180 @@ -292,20 +283,20 @@ function generate_new_keypair() { # Check current delegator stake. current_delegator_stake_data=$(cast call --rpc-url "${L1_RPC_URL}" \ "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${DELEGATOR_ADDRESS}") - current_delegator_stake=$(echo "${current_delegator_stake_data}" | head -1 | cut -d' ' -f1) - echo "Current delegator stake: ${current_delegator_stake}" + current_delegator_stake_eth=$(cast to-unit "$(echo "${current_delegator_stake_data}" | head -1 | cut -d' ' -f1)" ether) + echo "Current delegator stake: ${current_delegator_stake_eth} POL" # Skip test if delegator has no stake. - if [[ "${current_delegator_stake}" == "0" ]]; then + if [[ $(echo "${current_delegator_stake_eth} == 0" | bc) -eq 1 ]]; then echo "Foundry address has no stake to undelegate, skipping test" echo "Run the delegation test first to create stake to undelegate" skip "No stake to undelegate" fi # Check current validator total stake. - initial_total_stake=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1) - echo "Initial total validator stake: ${initial_total_stake}" + initial_total_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ + "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1)" ether) + echo "Initial total validator stake: ${initial_total_stake_eth} POL" # Undelegate the current stake. undelegation_amount=$(cast to-unit 1ether wei) @@ -325,23 +316,16 @@ function generate_new_keypair() { echo "Verifying undelegation initiation was successful..." # Check that validator's total stake decreased. - expected_total_stake=$((initial_total_stake - undelegation_amount)) - new_total_stake=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1) - echo "Expected total stake: ${expected_total_stake}" - echo "New total stake: ${new_total_stake}" + new_total_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ + "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1)" ether) + echo "New total stake: ${new_total_stake_eth} POL" + [[ $(echo "${new_total_stake_eth} == ${initial_total_stake_eth} - 1" | bc) -eq 1 ]] # Check that delegator's active stake decreased. - new_delegator_stake_data=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${DELEGATOR_ADDRESS}") - new_delegator_stake=$(echo "${new_delegator_stake_data}" | head -1 | cut -d' ' -f1) - expected_delegator_stake=$((current_delegator_stake - undelegation_amount)) - echo "Expected delegator_stake: ${expected_delegator_stake}" - echo "New delegator stake: ${new_delegator_stake}" - - # Verify the stakes match expectations. - [[ "${new_total_stake}" -eq "${expected_total_stake}" ]] - [[ "${new_delegator_stake}" -eq "${expected_delegator_stake}" ]] + new_delegator_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ + "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${DELEGATOR_ADDRESS}" | head -1 | cut -d' ' -f1)" ether) + echo "New delegator stake: ${new_delegator_stake_eth} POL" + [[ $(echo "${new_delegator_stake_eth} == ${current_delegator_stake_eth} - 1" | bc) -eq 1 ]] # Check that unbond nonce was incremented. final_unbond_nonce=$(cast call --rpc-url "${L1_RPC_URL}" \ @@ -351,8 +335,7 @@ function generate_new_keypair() { # Verify L2 voting power matches the updated L1 stake. VALIDATOR_POWER_CMD='curl "${L2_CL_API_URL}/stake/validator/${VALIDATOR_ID}" | jq --raw-output ".validator.voting_power"' - - expected_voting_power=$(cast to-unit "${new_total_stake}" ether | cut -d'.' -f1) + expected_voting_power=$(echo "${new_total_stake_eth}" | cut -d'.' -f1) echo "Monitoring L2 voting power sync for validator ${VALIDATOR_ID}..." assert_command_eventually_equal "${VALIDATOR_POWER_CMD}" "${expected_voting_power}" 180 From ae3bfa652fa9b01d059b4179dc420da69f7d35be Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 11:00:37 +0100 Subject: [PATCH 11/45] fix: bash overflows --- core/helpers/scripts/eventually.bash | 8 ++++---- tests/pos/bridge.bats | 26 +++++++++++++------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/core/helpers/scripts/eventually.bash b/core/helpers/scripts/eventually.bash index 47978162..a27fd5d0 100644 --- a/core/helpers/scripts/eventually.bash +++ b/core/helpers/scripts/eventually.bash @@ -111,7 +111,7 @@ function assert_token_balance_eventually_greater_or_equal() { balance=$(cast call --json --rpc-url "${rpc_url}" "${contract_address}" "balanceOf(address)(uint)" "${eoa_address}" | jq --raw-output ".[0]") echo "[$(date '+%Y-%m-%d %H:%M:%S')] Balance: ${balance} tokens" - if [[ "${balance}" -ge "${target}" ]]; then + if [ "$(echo "${balance} >= ${target}" | bc)" -eq 1 ]; then break fi @@ -141,7 +141,7 @@ function assert_token_balance_eventually_lower_or_equal() { balance=$(cast call --json --rpc-url "${rpc_url}" "${contract_address}" "balanceOf(address)(uint)" "${eoa_address}" | jq --raw-output ".[0]") echo "[$(date '+%Y-%m-%d %H:%M:%S')] Balance: ${balance} tokens" - if [[ "${balance}" -le "${target}" ]]; then + if [ "$(echo "${balance} <= ${target}" | bc)" -eq 1 ]; then break fi @@ -170,7 +170,7 @@ function assert_ether_balance_eventually_lower_or_equal() { balance=$(cast balance --rpc-url "${rpc_url}" "${address}") echo "[$(date '+%Y-%m-%d %H:%M:%S')] Balance: ${balance} wei" - if [[ "${balance}" -le "${target}" ]]; then + if [ "$(echo "${balance} <= ${target}" | bc)" -eq 1 ]; then break fi @@ -199,7 +199,7 @@ function assert_ether_balance_eventually_greater_or_equal() { balance=$(cast balance --rpc-url "${rpc_url}" "${address}") echo "[$(date '+%Y-%m-%d %H:%M:%S')] Balance: ${balance} wei" - if [[ "${balance}" -ge "${target}" ]]; then + if [ "$(echo "${balance} >= ${target}" | bc)" -eq 1 ]; then break fi diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 96da4040..7cae2b24 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -80,10 +80,10 @@ function wait_for_bor_state_sync() { # Monitor the balances on L1 and L2. echo "Monitoring POL balance on L1..." - assert_token_balance_eventually_lower_or_equal "${L1_POL_TOKEN_ADDRESS}" "${address}" $((initial_l1_balance - bridge_amount)) "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_token_balance_eventually_lower_or_equal "${L1_POL_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_balance} - ${bridge_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" echo "Monitoring native tokens balance on L2..." - assert_ether_balance_eventually_greater_or_equal "${address}" $((initial_l2_balance + bridge_amount)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_ether_balance_eventually_greater_or_equal "${address}" "$(echo "${initial_l2_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } # bats test_tags=bridge,transaction-matic @@ -119,10 +119,10 @@ function wait_for_bor_state_sync() { # Monitor the balances on L1 and L2. echo "Monitoring MATIC balance on L1..." - assert_token_balance_eventually_lower_or_equal "${L1_MATIC_TOKEN_ADDRESS}" "${address}" $((initial_l1_balance - bridge_amount)) "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_token_balance_eventually_lower_or_equal "${L1_MATIC_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_balance} - ${bridge_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" echo "Monitoring native tokens balance on L2..." - assert_ether_balance_eventually_greater_or_equal "${address}" $((initial_l2_balance + bridge_amount)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_ether_balance_eventually_greater_or_equal "${address}" "$(echo "${initial_l2_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } # bats test_tags=bridge,transaction-pol @@ -155,7 +155,7 @@ function wait_for_bor_state_sync() { # Verify L2 native balance decreased. echo "Verifying L2 native balance decreased..." - assert_ether_balance_eventually_lower_or_equal "${address}" $((initial_l2_balance - withdraw_amount)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_ether_balance_eventually_lower_or_equal "${address}" "$(echo "${initial_l2_balance} - ${withdraw_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" # Wait for a new checkpoint on L1 that covers the withdrawal block. # This confirms validators have attested to the burn, which is a prerequisite for building a valid exit Merkle proof. @@ -207,10 +207,10 @@ function wait_for_bor_state_sync() { # Monitor the balances on L1 and L2. echo "Monitoring ERC20 balance on L1..." - assert_token_balance_eventually_lower_or_equal "${L1_ERC20_TOKEN_ADDRESS}" "${address}" $((initial_l1_balance - bridge_amount)) "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_token_balance_eventually_lower_or_equal "${L1_ERC20_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_balance} - ${bridge_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" echo "Monitoring ERC20 balance on L2..." - assert_token_balance_eventually_greater_or_equal "${L2_ERC20_TOKEN_ADDRESS}" "${address}" $((initial_l2_balance + bridge_amount)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_token_balance_eventually_greater_or_equal "${L2_ERC20_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } # bats test_tags=bridge,transaction-erc20 @@ -335,22 +335,22 @@ function wait_for_bor_state_sync() { assert_command_eventually_greater_or_equal "${BOR_STATE_SYNC_COUNT_CMD}" $((bor_state_sync_count + 3)) "${timeout_seconds}" "${interval_seconds}" echo "Verifying L1 MATIC/POL balance decreased..." - assert_token_balance_eventually_lower_or_equal "${L1_MATIC_TOKEN_ADDRESS}" "${address}" $((initial_l1_matic_balance - bridge_amount)) "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_token_balance_eventually_lower_or_equal "${L1_MATIC_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_matic_balance} - ${bridge_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" echo "Verifying L1 ERC20 balance decreased..." - assert_token_balance_eventually_lower_or_equal "${L1_ERC20_TOKEN_ADDRESS}" "${address}" $((initial_l1_erc20_balance - bridge_amount)) "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_token_balance_eventually_lower_or_equal "${L1_ERC20_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_erc20_balance} - ${bridge_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" echo "Verifying L1 ERC721 balance decreased..." - assert_token_balance_eventually_lower_or_equal "${L1_ERC721_TOKEN_ADDRESS}" "${address}" $((initial_l1_erc721_balance - 1)) "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_token_balance_eventually_lower_or_equal "${L1_ERC721_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_erc721_balance} - 1" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" echo "Verifying L2 native balance increased..." - assert_ether_balance_eventually_greater_or_equal "${address}" $((initial_l2_native_balance + bridge_amount)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_ether_balance_eventually_greater_or_equal "${address}" "$(echo "${initial_l2_native_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" echo "Verifying L2 ERC20 balance increased..." - assert_token_balance_eventually_greater_or_equal "${L2_ERC20_TOKEN_ADDRESS}" "${address}" $((initial_l2_erc20_balance + bridge_amount)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_token_balance_eventually_greater_or_equal "${L2_ERC20_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_erc20_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" echo "Verifying L2 ERC721 balance increased..." - assert_token_balance_eventually_greater_or_equal "${L2_ERC721_TOKEN_ADDRESS}" "${address}" $((initial_l2_erc721_balance + 1)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_token_balance_eventually_greater_or_equal "${L2_ERC721_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_erc721_balance} + 1" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" echo "✅ MATIC/POL, ERC20, and ERC721 bridge operations completed successfully!" echo "Summary:" From ba8ce6c9ae26237ad5792a598b1c0b43b40d0ab5 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 12:34:31 +0100 Subject: [PATCH 12/45] chore: nit --- tests/pos/bridge.bats | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 7cae2b24..132b678b 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -125,7 +125,7 @@ function wait_for_bor_state_sync() { assert_ether_balance_eventually_greater_or_equal "${address}" "$(echo "${initial_l2_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } -# bats test_tags=bridge,transaction-pol +# bats test_tags=withdraw,transaction-pol @test "withdraw native tokens from L2 and confirm L2 native balance decreased and checkpoint submitted" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") @@ -213,8 +213,8 @@ function wait_for_bor_state_sync() { assert_token_balance_eventually_greater_or_equal "${L2_ERC20_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } -# bats test_tags=bridge,transaction-erc20 -# @test "bridge some ERC20 tokens from L2 to L1 and confirm L1 ERC20 balance increased" { +# bats test_tags=withdraw,transaction-erc20 +# @test "withdraw some ERC20 tokens from L2 to L1 and confirm L1 ERC20 balance increased" { # echo TODO # } @@ -262,8 +262,8 @@ function wait_for_bor_state_sync() { assert_token_balance_eventually_greater_or_equal "${L2_ERC721_TOKEN_ADDRESS}" "${address}" $((initial_l2_balance + 1)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } -# bats test_tags=bridge,transaction-erc721 -# @test "bridge an ERC721 token from L2 to L1 and confirm L1 ERC721 balance increased" { +# bats test_tags=withdraw,transaction-erc721 +# @test "withdraw an ERC721 token from L2 to L1 and confirm L1 ERC721 balance increased" { # echo TODO # } From 6671b55ff85dd1bbc94dda9b751c2e6a330db7f5 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 12:38:21 +0100 Subject: [PATCH 13/45] chore: nit --- tests/pos/validator.bats | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pos/validator.bats b/tests/pos/validator.bats index ac912672..695d3b35 100644 --- a/tests/pos/validator.bats +++ b/tests/pos/validator.bats @@ -178,7 +178,7 @@ function generate_new_keypair() { } # bats test_tags=pos-validator,pos-delegate,transaction-pol -@test "delegate POL to a validator" { +@test "delegate to a validator" { VALIDATOR_ID=${VALIDATOR_ID:-"1"} echo "VALIDATOR_ID=${VALIDATOR_ID}" @@ -263,7 +263,7 @@ function generate_new_keypair() { } # bats test_tags=pos-validator,pos-undelegate,transaction-pol -@test "undelegate POL from a validator" { +@test "undelegate from a validator" { VALIDATOR_ID=${VALIDATOR_ID:-"1"} echo "VALIDATOR_ID=${VALIDATOR_ID}" From db40a367011ce17dcb14e1d236cdfb5234a90769 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 12:39:09 +0100 Subject: [PATCH 14/45] chore: update docs --- README.md | 1 + TESTSINVENTORY.md | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 0096cfab..00992fe7 100644 --- a/README.md +++ b/README.md @@ -359,6 +359,7 @@ grep -hoR --include="*.bats" 'test_tags=[^ ]*' . | sed 's/.*test_tags=//' | tr ' - validator-set - warm-cold - weth +- withdraw - zkevm-batch - zkevm-counters diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index 86266d6e..a67fce8f 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -379,15 +379,15 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getCurrentValidators returns a non-empty validator list | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L509) | | | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | | bridge MATIC from L1 to L2 and confirm L2 native tokens balance increased | [Link](./tests/pos/bridge.bats#L90) | | -| bridge MATIC/POL, ERC20, and ERC721 from L1 to L2 and confirm L2 balances increased | [Link](./tests/pos/bridge.bats#L227) | | +| bridge MATIC/POL, ERC20, and ERC721 from L1 to L2 and confirm L2 balances increased | [Link](./tests/pos/bridge.bats#L271) | | | bridge POL from L1 to L2 and confirm L2 native tokens balance increased | [Link](./tests/pos/bridge.bats#L50) | | -| bridge an ERC721 token from L1 to L2 and confirm L2 ERC721 balance increased | [Link](./tests/pos/bridge.bats#L178) | | -| bridge some ERC20 tokens from L1 to L2 and confirm L2 ERC20 balance increased | [Link](./tests/pos/bridge.bats#L134) | | +| bridge an ERC721 token from L1 to L2 and confirm L2 ERC721 balance increased | [Link](./tests/pos/bridge.bats#L222) | | +| bridge some ERC20 tokens from L1 to L2 and confirm L2 ERC20 balance increased | [Link](./tests/pos/bridge.bats#L178) | | | coinbase balance increases by at least the priority fee portion of gas cost | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L318) | | | concurrent write/read race: tx submissions and state reads do not interfere | [Link](./tests/pos/execution-specs/rpc/rpc-concurrent-load-and-stress.bats#L248) | | | consecutive block baseFees are within ±5% of each other | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L183) | | | contract-to-contract call fuzz: CALL/STATICCALL/DELEGATECALL | [Link](./tests/pos/execution-specs/transactions/evm-transaction-fuzzing-and-liveness.bats#L792) | | -| delegate POL to a validator | [Link](./tests/pos/validator.bats#L181) | | +| delegate to a validator | [Link](./tests/pos/validator.bats#L181) | | | deploy contract that returns 24577 runtime bytes is rejected by EIP-170 | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L124) | | | deploy contract that returns exactly 24576 runtime bytes succeeds (EIP-170 boundary) | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L150) | | | deploy contract that reverts in constructor leaves no code at deployed address | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L48) | | @@ -456,7 +456,7 @@ Table of tests currently implemented or being implemented in the E2E repository. | out-of-gas transaction still increments sender nonce | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L160) | | | prune TxIndexer | [Link](./tests/pos/heimdall-v2.bats#L86) | | | recipient balance increases by exactly the value sent | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L53) | | -| remove validator | [Link](./tests/pos/validator.bats#L363) | | +| remove validator | [Link](./tests/pos/validator.bats#L346) | | | replay protection: same signed tx submitted twice does not double-spend | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L445) | | | sender balance decreases by exactly gas cost plus value transferred | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L18) | | | sha3Uncles field is empty-list RLP hash (PoS has no uncles) | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L976) | | @@ -473,12 +473,13 @@ Table of tests currently implemented or being implemented in the E2E repository. | type 1 access list with multiple storage keys is accepted | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L207) | | | type 2 (EIP-1559) effectiveGasPrice = baseFee + min(priorityFee, maxFee - baseFee) | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L130) | | | type 2 maxFeePerGas below baseFee is rejected | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L175) | | -| undelegate POL from a validator | [Link](./tests/pos/validator.bats#L275) | | +| undelegate from a validator | [Link](./tests/pos/validator.bats#L266) | | | update signer | [Link](./tests/pos/validator.bats#L147) | | | update validator stake | [Link](./tests/pos/validator.bats#L60) | | | update validator top-up fee | [Link](./tests/pos/validator.bats#L97) | | | warm COINBASE access costs less than cold access to arbitrary address (EIP-3651) | [Link](./tests/pos/execution-specs/evm/evm-opcodes-cancun-shanghai-eips.bats#L457) | | | web3_clientVersion returns a non-empty version string | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L400) | | +| withdraw native tokens from L2 and confirm L2 native balance decreased and checkpoint submitted | [Link](./tests/pos/bridge.bats#L129) | | | zero-value self-transfer: only gas consumed, nonce increments | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L530) | | ## DApps Tests From 0b17f7e390323007fa468033a57b93ea41e5d7e3 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 12:44:55 +0100 Subject: [PATCH 15/45] chore: clean up --- tests/pos/bridge.bats | 92 ------------------------------------------- 1 file changed, 92 deletions(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 132b678b..d9e5a5c2 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -266,95 +266,3 @@ function wait_for_bor_state_sync() { # @test "withdraw an ERC721 token from L2 to L1 and confirm L1 ERC721 balance increased" { # echo TODO # } - -# bats test_tags=bridge,transaction-pol,transaction-erc20,transaction-erc721 -@test "bridge MATIC/POL, ERC20, and ERC721 from L1 to L2 and confirm L2 balances increased" { - address=$(cast wallet address --private-key "${PRIVATE_KEY}") - - # Get the initial balances. - initial_l1_matic_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_MATIC_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') - initial_l2_native_balance=$(cast balance --rpc-url "${L2_RPC_URL}" "${address}") - initial_l1_erc20_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_ERC20_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') - initial_l2_erc20_balance=$(cast call --rpc-url "${L2_RPC_URL}" --json "${L2_ERC20_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') - - # Mint a new ERC721 token. - total_supply=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_ERC721_TOKEN_ADDRESS}" "totalSupply()(uint)" | jq --raw-output '.[0]') - token_id=$((total_supply + 1)) - - echo "Minting ERC721 token (id: ${token_id})..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_ERC721_TOKEN_ADDRESS}" "mint(uint)" "${token_id}" - - initial_l1_erc721_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_ERC721_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') - initial_l2_erc721_balance=$(cast call --rpc-url "${L2_RPC_URL}" --json "${L2_ERC721_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') - - echo "Initial balances:" - echo "- L1 MATIC/POL: ${initial_l1_matic_balance}" - echo "- L2 MATIC/POL: ${initial_l2_native_balance} wei" - echo "- L1 ERC20: ${initial_l1_erc20_balance}" - echo "- L2 ERC20: ${initial_l2_erc20_balance}" - echo "- L1 ERC721: ${initial_l1_erc721_balance}" - echo "- L2 ERC721: ${initial_l2_erc721_balance}" - - # Get the initial state sync count. - heimdall_state_sync_count=$(eval "${HEIMDALL_STATE_SYNC_COUNT_CMD}") - bor_state_sync_count=$(eval "${BOR_STATE_SYNC_COUNT_CMD}") - - echo "Initial state sync counts:" - echo "- Heimdall: ${heimdall_state_sync_count}" - echo "- Bor: ${bor_state_sync_count}" - - # Bridge amount. - bridge_amount=$(cast to-unit 1ether wei) - - # Bridge MATIC/POL. - echo "Bridging MATIC/POL from L1 to L2..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_MATIC_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "${bridge_amount}" - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "depositERC20(address,uint)" "${L1_MATIC_TOKEN_ADDRESS}" "${bridge_amount}" - - # Bridge ERC20. - echo "Bridging ERC20 from L1 to L2..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_ERC20_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "${bridge_amount}" - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "depositERC20(address,uint)" "${L1_ERC20_TOKEN_ADDRESS}" "${bridge_amount}" - - # Bridge ERC721. - echo "Bridging ERC721 from L1 to L2..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_ERC721_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "${token_id}" - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "depositERC721(address,uint)" "${L1_ERC721_TOKEN_ADDRESS}" "${token_id}" - - # Wait for Heimdall and Bor to process the bridge events. - assert_command_eventually_greater_or_equal "${HEIMDALL_STATE_SYNC_COUNT_CMD}" $((heimdall_state_sync_count + 3)) "${timeout_seconds}" "${interval_seconds}" - - echo "Waiting for Bor to process all bridge events..." - assert_command_eventually_greater_or_equal "${BOR_STATE_SYNC_COUNT_CMD}" $((bor_state_sync_count + 3)) "${timeout_seconds}" "${interval_seconds}" - - echo "Verifying L1 MATIC/POL balance decreased..." - assert_token_balance_eventually_lower_or_equal "${L1_MATIC_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_matic_balance} - ${bridge_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" - - echo "Verifying L1 ERC20 balance decreased..." - assert_token_balance_eventually_lower_or_equal "${L1_ERC20_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_erc20_balance} - ${bridge_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" - - echo "Verifying L1 ERC721 balance decreased..." - assert_token_balance_eventually_lower_or_equal "${L1_ERC721_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_erc721_balance} - 1" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" - - echo "Verifying L2 native balance increased..." - assert_ether_balance_eventually_greater_or_equal "${address}" "$(echo "${initial_l2_native_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" - - echo "Verifying L2 ERC20 balance increased..." - assert_token_balance_eventually_greater_or_equal "${L2_ERC20_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_erc20_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" - - echo "Verifying L2 ERC721 balance increased..." - assert_token_balance_eventually_greater_or_equal "${L2_ERC721_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_erc721_balance} + 1" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" - - echo "✅ MATIC/POL, ERC20, and ERC721 bridge operations completed successfully!" - echo "Summary:" - echo "- 1 MATIC/POL bridged from L1 to L2" - echo "- 1 ERC20 token bridged from L1 to L2" - echo "- 1 ERC721 token (id: ${token_id}) bridged from L1 to L2" -} From bf5cb52d735469c87b396bec192d3a0f433b49ae Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 12:45:31 +0100 Subject: [PATCH 16/45] chore: nit --- TESTSINVENTORY.md | 1 - 1 file changed, 1 deletion(-) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index a67fce8f..03bd65ba 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -379,7 +379,6 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getCurrentValidators returns a non-empty validator list | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L509) | | | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | | bridge MATIC from L1 to L2 and confirm L2 native tokens balance increased | [Link](./tests/pos/bridge.bats#L90) | | -| bridge MATIC/POL, ERC20, and ERC721 from L1 to L2 and confirm L2 balances increased | [Link](./tests/pos/bridge.bats#L271) | | | bridge POL from L1 to L2 and confirm L2 native tokens balance increased | [Link](./tests/pos/bridge.bats#L50) | | | bridge an ERC721 token from L1 to L2 and confirm L2 ERC721 balance increased | [Link](./tests/pos/bridge.bats#L222) | | | bridge some ERC20 tokens from L1 to L2 and confirm L2 ERC20 balance increased | [Link](./tests/pos/bridge.bats#L178) | | From 2ec9135c0b0e2a808aff0722a5e38b4a916eeb43 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 12:49:24 +0100 Subject: [PATCH 17/45] feat: add bridge native L1 eth test --- core/helpers/pos-setup.bash | 8 ++++++ tests/pos/bridge.bats | 52 ++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/core/helpers/pos-setup.bash b/core/helpers/pos-setup.bash index 6731c788..7e8d466a 100644 --- a/core/helpers/pos-setup.bash +++ b/core/helpers/pos-setup.bash @@ -34,9 +34,11 @@ pos_setup() { [[ -z "${L1_STAKING_INFO_ADDRESS:-}" ]] || [[ -z "${L1_MATIC_TOKEN_ADDRESS:-}" ]] || [[ -z "${L1_POL_TOKEN_ADDRESS:-}" ]] || + [[ -z "${L1_WETH_TOKEN_ADDRESS:-}" ]] || [[ -z "${L1_ERC20_TOKEN_ADDRESS:-}" ]] || [[ -z "${L1_ERC721_TOKEN_ADDRESS:-}" ]] || [[ -z "${L2_STATE_RECEIVER_ADDRESS:-}" ]] || + [[ -z "${L2_WETH_TOKEN_ADDRESS:-}" ]] || [[ -z "${L2_ERC20_TOKEN_ADDRESS:-}" ]] || [[ -z "${L2_ERC721_TOKEN_ADDRESS:-}" ]]; then matic_contract_addresses=$(kurtosis files inspect "${ENCLAVE_NAME}" matic-contract-addresses contractAddresses.json | jq) @@ -75,5 +77,11 @@ pos_setup() { export L2_ERC721_TOKEN_ADDRESS=${L2_ERC721_TOKEN_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.child.tokens.RootERC721')} echo "L2_ERC721_TOKEN_ADDRESS=${L2_ERC721_TOKEN_ADDRESS}" + + export L1_WETH_TOKEN_ADDRESS=${L1_WETH_TOKEN_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.tokens.MaticWeth')} + echo "L1_WETH_TOKEN_ADDRESS=${L1_WETH_TOKEN_ADDRESS}" + + export L2_WETH_TOKEN_ADDRESS=${L2_WETH_TOKEN_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.child.tokens.MaticWeth')} + echo "L2_WETH_TOKEN_ADDRESS=${L2_WETH_TOKEN_ADDRESS}" fi } diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index d9e5a5c2..423a03b0 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -47,7 +47,7 @@ function wait_for_bor_state_sync() { } # bats test_tags=bridge,transaction-pol -@test "bridge POL from L1 to L2 and confirm L2 native tokens balance increased" { +@test "bridge POL from L1 to L2 and confirm native tokens balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get the initial balances. @@ -87,7 +87,7 @@ function wait_for_bor_state_sync() { } # bats test_tags=bridge,transaction-matic -@test "bridge MATIC from L1 to L2 and confirm L2 native tokens balance increased" { +@test "bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get the initial balances. @@ -125,8 +125,46 @@ function wait_for_bor_state_sync() { assert_ether_balance_eventually_greater_or_equal "${address}" "$(echo "${initial_l2_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } +# bats test_tags=bridge,transaction-eth +@test "bridge ETH from L1 to L2 and confirm WETH balance increased on L2" { + address=$(cast wallet address --private-key "${PRIVATE_KEY}") + + # Get the initial balances. + initial_l1_balance=$(cast balance --rpc-url "${L1_RPC_URL}" "${address}") + initial_l2_balance=$(cast call --rpc-url "${L2_RPC_URL}" --json "${L2_WETH_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') + + echo "Initial balances:" + echo "- L1 ETH balance: ${initial_l1_balance}" + echo "- L2 WETH balance: ${initial_l2_balance}" + + heimdall_state_sync_count=$(eval "${HEIMDALL_STATE_SYNC_COUNT_CMD}") + bor_state_sync_count=$(eval "${BOR_STATE_SYNC_COUNT_CMD}") + + # Bridge some ETH from L1 to L2 to trigger a state sync. + # The DepositManager wraps ETH into WETH (MaticWeth) on L1, so the L2 + # WETH balance increases rather than the native gas balance. + bridge_amount=$(cast to-unit 1ether wei) + + echo "Depositing ETH to trigger a state sync..." + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ + --value "${bridge_amount}" \ + "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "depositEther()" + + # Wait for Heimdall and Bor to process the bridge event. + wait_for_heimdall_state_sync "${heimdall_state_sync_count}" + wait_for_bor_state_sync "${bor_state_sync_count}" + + # Monitor the balances on L1 and L2. + # L1 ETH decreases by at least bridge_amount (gas costs make it decrease further). + echo "Monitoring ETH balance on L1..." + assert_ether_balance_eventually_lower_or_equal "${address}" "$(echo "${initial_l1_balance} - ${bridge_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + + echo "Monitoring WETH balance on L2..." + assert_token_balance_eventually_greater_or_equal "${L2_WETH_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" +} + # bats test_tags=withdraw,transaction-pol -@test "withdraw native tokens from L2 and confirm L2 native balance decreased and checkpoint submitted" { +@test "withdraw native tokens from L2 and confirm native balance decreased on L2 and checkpoint was submitted" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get initial L2 native balance and latest checkpoint ID. @@ -175,7 +213,7 @@ function wait_for_bor_state_sync() { } # bats test_tags=bridge,transaction-erc20 -@test "bridge some ERC20 tokens from L1 to L2 and confirm L2 ERC20 balance increased" { +@test "bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get the initial balances. @@ -214,12 +252,12 @@ function wait_for_bor_state_sync() { } # bats test_tags=withdraw,transaction-erc20 -# @test "withdraw some ERC20 tokens from L2 to L1 and confirm L1 ERC20 balance increased" { +# @test "withdraw ERC20 tokens from L2 to L1 and confirm ERC20 balance increased on L1" { # echo TODO # } # bats test_tags=bridge,transaction-erc721 -@test "bridge an ERC721 token from L1 to L2 and confirm L2 ERC721 balance increased" { +@test "bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Mint an ERC721 token. @@ -263,6 +301,6 @@ function wait_for_bor_state_sync() { } # bats test_tags=withdraw,transaction-erc721 -# @test "withdraw an ERC721 token from L2 to L1 and confirm L1 ERC721 balance increased" { +# @test "withdraw ERC721 token from L2 to L1 and confirm ERC721 balance increased on L1" { # echo TODO # } From 4e7d9e0421915a2ab0c54ac14ac1c106e5bfb01b Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 12:50:00 +0100 Subject: [PATCH 18/45] chore: update docs --- README.md | 1 + TESTSINVENTORY.md | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 00992fe7..5d0b4db3 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,7 @@ grep -hoR --include="*.bats" 'test_tags=[^ ]*' . | sed 's/.*test_tags=//' | tr ' - transaction-eoa - transaction-erc20 - transaction-erc721 +- transaction-eth - transaction-matic - transaction-pol - transaction-uniswap diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index 03bd65ba..7fa7c092 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -378,10 +378,11 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getAuthor returns a valid address for latest block | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L486) | | | bor_getCurrentValidators returns a non-empty validator list | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L509) | | | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | -| bridge MATIC from L1 to L2 and confirm L2 native tokens balance increased | [Link](./tests/pos/bridge.bats#L90) | | -| bridge POL from L1 to L2 and confirm L2 native tokens balance increased | [Link](./tests/pos/bridge.bats#L50) | | -| bridge an ERC721 token from L1 to L2 and confirm L2 ERC721 balance increased | [Link](./tests/pos/bridge.bats#L222) | | -| bridge some ERC20 tokens from L1 to L2 and confirm L2 ERC20 balance increased | [Link](./tests/pos/bridge.bats#L178) | | +| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L216) | | +| bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L260) | | +| bridge ETH from L1 to L2 and confirm WETH balance increased on L2 | [Link](./tests/pos/bridge.bats#L129) | | +| bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L90) | | +| bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L50) | | | coinbase balance increases by at least the priority fee portion of gas cost | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L318) | | | concurrent write/read race: tx submissions and state reads do not interfere | [Link](./tests/pos/execution-specs/rpc/rpc-concurrent-load-and-stress.bats#L248) | | | consecutive block baseFees are within ±5% of each other | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L183) | | @@ -478,7 +479,7 @@ Table of tests currently implemented or being implemented in the E2E repository. | update validator top-up fee | [Link](./tests/pos/validator.bats#L97) | | | warm COINBASE access costs less than cold access to arbitrary address (EIP-3651) | [Link](./tests/pos/execution-specs/evm/evm-opcodes-cancun-shanghai-eips.bats#L457) | | | web3_clientVersion returns a non-empty version string | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L400) | | -| withdraw native tokens from L2 and confirm L2 native balance decreased and checkpoint submitted | [Link](./tests/pos/bridge.bats#L129) | | +| withdraw native tokens from L2 and confirm native balance decreased on L2 and checkpoint was submitted | [Link](./tests/pos/bridge.bats#L167) | | | zero-value self-transfer: only gas consumed, nonce increments | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L530) | | ## DApps Tests From ed07bd5de705744707ca577f6415ef121cfe1967 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 12:51:37 +0100 Subject: [PATCH 19/45] chore: nit --- TESTSINVENTORY.md | 2 +- tests/pos/bridge.bats | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index 7fa7c092..5f85cb7d 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -380,9 +380,9 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | | bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L216) | | | bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L260) | | -| bridge ETH from L1 to L2 and confirm WETH balance increased on L2 | [Link](./tests/pos/bridge.bats#L129) | | | bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L90) | | | bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L50) | | +| bridge native token from L1 to L2 and confirm WETH balance increased on L2 | [Link](./tests/pos/bridge.bats#L129) | | | coinbase balance increases by at least the priority fee portion of gas cost | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L318) | | | concurrent write/read race: tx submissions and state reads do not interfere | [Link](./tests/pos/execution-specs/rpc/rpc-concurrent-load-and-stress.bats#L248) | | | consecutive block baseFees are within ±5% of each other | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L183) | | diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 423a03b0..1341576e 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -126,7 +126,7 @@ function wait_for_bor_state_sync() { } # bats test_tags=bridge,transaction-eth -@test "bridge ETH from L1 to L2 and confirm WETH balance increased on L2" { +@test "bridge native token from L1 to L2 and confirm WETH balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get the initial balances. From e5fca85abcb805b464fcc996fa2e47e7ccd2e4b0 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 13:02:53 +0100 Subject: [PATCH 20/45] fix: withdraw --- tests/pos/bridge.bats | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 1341576e..2b511b77 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -178,11 +178,13 @@ function wait_for_bor_state_sync() { # Burn native tokens on L2 to initiate the Plasma exit. withdraw_amount=$(cast to-unit 1ether wei) - echo "Burning ${withdraw_amount} wei on L2 (0x1010.withdraw)..." + echo "Burning ${withdraw_amount} wei on L2..." withdraw_receipt=$(cast send \ --rpc-url "${L2_RPC_URL}" \ --private-key "${PRIVATE_KEY}" \ --value "${withdraw_amount}" \ + --gas-price 30gwei \ + --priority-gas-price 30gwei \ --json \ "0x0000000000000000000000000000000000001010" \ "withdraw(uint256)" "${withdraw_amount}") From 828ab4704ade10e2983346fad06ebc872e649e4e Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 13:14:57 +0100 Subject: [PATCH 21/45] chore: clean up --- tests/pos/bridge.bats | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 2b511b77..3fec965a 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -14,6 +14,9 @@ setup() { # Define timeout and interval for eventually commands. timeout_seconds=${TIMEOUT_SECONDS:-"180"} interval_seconds=${INTERVAL_SECONDS:-"10"} + + # Amount to bridge in each test. + bridge_amount=$(cast to-unit 1ether wei) } function wait_for_heimdall_state_sync() { @@ -64,8 +67,6 @@ function wait_for_bor_state_sync() { # Bridge some POL tokens from L1 to L2 to trigger a state sync. # The DepositManager remaps POL to MATIC internally before the state sync, # so the L2 native token balance increases identically to bridging MATIC. - bridge_amount=$(cast to-unit 1ether wei) - echo "Approving the DepositManager contract to spend POL tokens on our behalf..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_POL_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "${bridge_amount}" @@ -103,8 +104,6 @@ function wait_for_bor_state_sync() { # Bridge some MATIC tokens from L1 to L2 to trigger a state sync. # 1 MATIC token = 1000000000000000000 wei. - bridge_amount=$(cast to-unit 1ether wei) - echo "Approving the DepositManager contract to spend MATIC tokens on our behalf..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_MATIC_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "${bridge_amount}" @@ -143,8 +142,6 @@ function wait_for_bor_state_sync() { # Bridge some ETH from L1 to L2 to trigger a state sync. # The DepositManager wraps ETH into WETH (MaticWeth) on L1, so the L2 # WETH balance increases rather than the native gas balance. - bridge_amount=$(cast to-unit 1ether wei) - echo "Depositing ETH to trigger a state sync..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ --value "${bridge_amount}" \ @@ -231,8 +228,6 @@ function wait_for_bor_state_sync() { # Bridge some ERC20 tokens from L1 to L2. # 1 ERC20 token = 1000000000000000000 wei. - bridge_amount=$(cast to-unit 1ether wei) - echo "Approving the DepositManager contract to spend ERC20 tokens on our behalf..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_ERC20_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" "${bridge_amount}" From d4ca7cb19854afa26bb55a2f057c4633ceaaa26b Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 13:16:15 +0100 Subject: [PATCH 22/45] chore: clean up --- tests/pos/bridge.bats | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 3fec965a..aea25ba1 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -8,8 +8,8 @@ setup() { pos_setup # Define state sync count commands. - HEIMDALL_STATE_SYNC_COUNT_CMD='curl "${L2_CL_API_URL}/clerk/event-records/count" | jq -r ".count"' - BOR_STATE_SYNC_COUNT_CMD='cast call --gas-limit 15000000 --rpc-url "${L2_RPC_URL}" "${L2_STATE_RECEIVER_ADDRESS}" "lastStateId()(uint)"' + heimdall_state_sync_count_cmd='curl "${L2_CL_API_URL}/clerk/event-records/count" | jq -r ".count"' + bor_state_sync_count_cmd='cast call --gas-limit 15000000 --rpc-url "${L2_RPC_URL}" "${L2_STATE_RECEIVER_ADDRESS}" "lastStateId()(uint)"' # Define timeout and interval for eventually commands. timeout_seconds=${TIMEOUT_SECONDS:-"180"} @@ -25,13 +25,13 @@ function wait_for_heimdall_state_sync() { echo "Error: state_sync_count is not set." exit 1 fi - if [[ -z "${HEIMDALL_STATE_SYNC_COUNT_CMD}" ]]; then - echo "Error: HEIMDALL_STATE_SYNC_COUNT_CMD environment variable is not set." + if [[ -z "${heimdall_state_sync_count_cmd}" ]]; then + echo "Error: heimdall_state_sync_count_cmd is not set." exit 1 fi echo "Monitoring state syncs on Heimdall..." - assert_command_eventually_greater_or_equal "${HEIMDALL_STATE_SYNC_COUNT_CMD}" $((state_sync_count + 1)) "${timeout_seconds}" "${interval_seconds}" + assert_command_eventually_greater_or_equal "${heimdall_state_sync_count_cmd}" $((state_sync_count + 1)) "${timeout_seconds}" "${interval_seconds}" } function wait_for_bor_state_sync() { @@ -40,13 +40,13 @@ function wait_for_bor_state_sync() { echo "Error: state_sync_count is not set." exit 1 fi - if [[ -z "${BOR_STATE_SYNC_COUNT_CMD}" ]]; then - echo "Error: BOR_STATE_SYNC_COUNT_CMD environment variable is not set." + if [[ -z "${bor_state_sync_count_cmd}" ]]; then + echo "Error: bor_state_sync_count_cmd is not set." exit 1 fi echo "Monitoring state syncs on Bor..." - assert_command_eventually_greater_or_equal "${BOR_STATE_SYNC_COUNT_CMD}" $((state_sync_count + 1)) "${timeout_seconds}" "${interval_seconds}" + assert_command_eventually_greater_or_equal "${bor_state_sync_count_cmd}" $((state_sync_count + 1)) "${timeout_seconds}" "${interval_seconds}" } # bats test_tags=bridge,transaction-pol @@ -61,8 +61,8 @@ function wait_for_bor_state_sync() { echo "- L1 POL balance: ${initial_l1_balance}" echo "- L2 native tokens balance: ${initial_l2_balance} wei" - heimdall_state_sync_count=$(eval "${HEIMDALL_STATE_SYNC_COUNT_CMD}") - bor_state_sync_count=$(eval "${BOR_STATE_SYNC_COUNT_CMD}") + heimdall_state_sync_count=$(eval "${heimdall_state_sync_count_cmd}") + bor_state_sync_count=$(eval "${bor_state_sync_count_cmd}") # Bridge some POL tokens from L1 to L2 to trigger a state sync. # The DepositManager remaps POL to MATIC internally before the state sync, @@ -99,8 +99,8 @@ function wait_for_bor_state_sync() { echo "- L1 MATIC balance: ${initial_l1_balance}" echo "- L2 native tokens balance: ${initial_l2_balance} wei" - heimdall_state_sync_count=$(eval "${HEIMDALL_STATE_SYNC_COUNT_CMD}") - bor_state_sync_count=$(eval "${BOR_STATE_SYNC_COUNT_CMD}") + heimdall_state_sync_count=$(eval "${heimdall_state_sync_count_cmd}") + bor_state_sync_count=$(eval "${bor_state_sync_count_cmd}") # Bridge some MATIC tokens from L1 to L2 to trigger a state sync. # 1 MATIC token = 1000000000000000000 wei. @@ -136,8 +136,8 @@ function wait_for_bor_state_sync() { echo "- L1 ETH balance: ${initial_l1_balance}" echo "- L2 WETH balance: ${initial_l2_balance}" - heimdall_state_sync_count=$(eval "${HEIMDALL_STATE_SYNC_COUNT_CMD}") - bor_state_sync_count=$(eval "${BOR_STATE_SYNC_COUNT_CMD}") + heimdall_state_sync_count=$(eval "${heimdall_state_sync_count_cmd}") + bor_state_sync_count=$(eval "${bor_state_sync_count_cmd}") # Bridge some ETH from L1 to L2 to trigger a state sync. # The DepositManager wraps ETH into WETH (MaticWeth) on L1, so the L2 @@ -223,8 +223,8 @@ function wait_for_bor_state_sync() { echo "- L1: ${initial_l1_balance}" echo "- L2: ${initial_l2_balance}" - heimdall_state_sync_count=$(eval "${HEIMDALL_STATE_SYNC_COUNT_CMD}") - bor_state_sync_count=$(eval "${BOR_STATE_SYNC_COUNT_CMD}") + heimdall_state_sync_count=$(eval "${heimdall_state_sync_count_cmd}") + bor_state_sync_count=$(eval "${bor_state_sync_count_cmd}") # Bridge some ERC20 tokens from L1 to L2. # 1 ERC20 token = 1000000000000000000 wei. @@ -273,8 +273,8 @@ function wait_for_bor_state_sync() { echo "- L1: ${initial_l1_balance}" echo "- L2: ${initial_l2_balance}" - heimdall_state_sync_count=$(eval "${HEIMDALL_STATE_SYNC_COUNT_CMD}") - bor_state_sync_count=$(eval "${BOR_STATE_SYNC_COUNT_CMD}") + heimdall_state_sync_count=$(eval "${heimdall_state_sync_count_cmd}") + bor_state_sync_count=$(eval "${bor_state_sync_count_cmd}") # Bridge the ERC721 token from L1 to L2. echo "Approving the DepositManager contract to spend ERC721 tokens on our behalf..." From 5c630c4669f95e9b617cfdd41139ad73b14d71dc Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 13:18:24 +0100 Subject: [PATCH 23/45] chore: nit --- tests/pos/bridge.bats | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index aea25ba1..bd53e0e3 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -7,6 +7,10 @@ setup() { load "../../core/helpers/scripts/eventually.bash" pos_setup + # Amount to bridge in each test. + bridge_amount=$(cast to-unit 1ether wei) + echo "bridge_amount=${bridge_amount}" + # Define state sync count commands. heimdall_state_sync_count_cmd='curl "${L2_CL_API_URL}/clerk/event-records/count" | jq -r ".count"' bor_state_sync_count_cmd='cast call --gas-limit 15000000 --rpc-url "${L2_RPC_URL}" "${L2_STATE_RECEIVER_ADDRESS}" "lastStateId()(uint)"' @@ -14,9 +18,8 @@ setup() { # Define timeout and interval for eventually commands. timeout_seconds=${TIMEOUT_SECONDS:-"180"} interval_seconds=${INTERVAL_SECONDS:-"10"} - - # Amount to bridge in each test. - bridge_amount=$(cast to-unit 1ether wei) + echo "timeout_seconds=${timeout_seconds}" + echo "interval_seconds=${interval_seconds}" } function wait_for_heimdall_state_sync() { From 7f3ca55515e1f5de3c60c0927d0af179739748e8 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 13:21:07 +0100 Subject: [PATCH 24/45] chore: clean up --- TESTSINVENTORY.md | 22 ++--- tests/pos/validator.bats | 195 ++++++++++++++++----------------------- 2 files changed, 88 insertions(+), 129 deletions(-) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index 5f85cb7d..ba7a8e6e 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -362,7 +362,7 @@ Table of tests currently implemented or being implemented in the E2E repository. | TSTORE gas cost is less than SSTORE for zero-to-nonzero write | [Link](./tests/pos/execution-specs/evm/eip1153-tstore-tload-transient-storage.bats#L393) | | | TSTORE in DELEGATECALL shares caller transient storage context | [Link](./tests/pos/execution-specs/evm/eip1153-tstore-tload-transient-storage.bats#L444) | | | TSTORE reverted by sub-call REVERT is undone | [Link](./tests/pos/execution-specs/evm/eip1153-tstore-tload-transient-storage.bats#L284) | | -| add new validator | [Link](./tests/pos/validator.bats#L20) | | +| add new validator | [Link](./tests/pos/validator.bats#L44) | | | all-opcode liveness smoke: deploy contracts exercising major opcode groups | [Link](./tests/pos/execution-specs/transactions/evm-transaction-fuzzing-and-liveness.bats#L896) | | | base fee adjusts between blocks following EIP-1559 dynamics | [Link](./tests/pos/execution-specs/evm/bor-chain-specific-evm-behavior.bats#L272) | | | base fee is present and positive on all recent blocks (PIP-79 invariant) | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L62) | | @@ -378,16 +378,16 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getAuthor returns a valid address for latest block | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L486) | | | bor_getCurrentValidators returns a non-empty validator list | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L509) | | | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | -| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L216) | | +| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L218) | | | bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L260) | | -| bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L90) | | -| bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L50) | | -| bridge native token from L1 to L2 and confirm WETH balance increased on L2 | [Link](./tests/pos/bridge.bats#L129) | | +| bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L94) | | +| bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L56) | | +| bridge native token from L1 to L2 and confirm WETH balance increased on L2 | [Link](./tests/pos/bridge.bats#L131) | | | coinbase balance increases by at least the priority fee portion of gas cost | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L318) | | | concurrent write/read race: tx submissions and state reads do not interfere | [Link](./tests/pos/execution-specs/rpc/rpc-concurrent-load-and-stress.bats#L248) | | | consecutive block baseFees are within ±5% of each other | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L183) | | | contract-to-contract call fuzz: CALL/STATICCALL/DELEGATECALL | [Link](./tests/pos/execution-specs/transactions/evm-transaction-fuzzing-and-liveness.bats#L792) | | -| delegate to a validator | [Link](./tests/pos/validator.bats#L181) | | +| delegate to a validator | [Link](./tests/pos/validator.bats#L168) | | | deploy contract that returns 24577 runtime bytes is rejected by EIP-170 | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L124) | | | deploy contract that returns exactly 24576 runtime bytes succeeds (EIP-170 boundary) | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L150) | | | deploy contract that reverts in constructor leaves no code at deployed address | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L48) | | @@ -456,7 +456,7 @@ Table of tests currently implemented or being implemented in the E2E repository. | out-of-gas transaction still increments sender nonce | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L160) | | | prune TxIndexer | [Link](./tests/pos/heimdall-v2.bats#L86) | | | recipient balance increases by exactly the value sent | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L53) | | -| remove validator | [Link](./tests/pos/validator.bats#L346) | | +| remove validator | [Link](./tests/pos/validator.bats#L313) | | | replay protection: same signed tx submitted twice does not double-spend | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L445) | | | sender balance decreases by exactly gas cost plus value transferred | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L18) | | | sha3Uncles field is empty-list RLP hash (PoS has no uncles) | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L976) | | @@ -473,10 +473,10 @@ Table of tests currently implemented or being implemented in the E2E repository. | type 1 access list with multiple storage keys is accepted | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L207) | | | type 2 (EIP-1559) effectiveGasPrice = baseFee + min(priorityFee, maxFee - baseFee) | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L130) | | | type 2 maxFeePerGas below baseFee is rejected | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L175) | | -| undelegate from a validator | [Link](./tests/pos/validator.bats#L266) | | -| update signer | [Link](./tests/pos/validator.bats#L147) | | -| update validator stake | [Link](./tests/pos/validator.bats#L60) | | -| update validator top-up fee | [Link](./tests/pos/validator.bats#L97) | | +| undelegate from a validator | [Link](./tests/pos/validator.bats#L243) | | +| update signer | [Link](./tests/pos/validator.bats#L143) | | +| update validator stake | [Link](./tests/pos/validator.bats#L81) | | +| update validator top-up fee | [Link](./tests/pos/validator.bats#L105) | | | warm COINBASE access costs less than cold access to arbitrary address (EIP-3651) | [Link](./tests/pos/execution-specs/evm/evm-opcodes-cancun-shanghai-eips.bats#L457) | | | web3_clientVersion returns a non-empty version string | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L400) | | | withdraw native tokens from L2 and confirm native balance decreased on L2 and checkpoint was submitted | [Link](./tests/pos/bridge.bats#L167) | | diff --git a/tests/pos/validator.bats b/tests/pos/validator.bats index 695d3b35..16bdaa46 100644 --- a/tests/pos/validator.bats +++ b/tests/pos/validator.bats @@ -6,6 +6,30 @@ setup() { load "../../core/helpers/pos-setup.bash" load "../../core/helpers/scripts/eventually.bash" pos_setup + + # Validator under test (genesis validator 1). + validator_private_key=${VALIDATOR_PRIVATE_KEY:-"0x2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2"} + validator_address=$(cast wallet address --private-key "${validator_private_key}") + validator_id=${VALIDATOR_ID:-"1"} + echo "validator_private_key=${validator_private_key}" + echo "validator_address=${validator_address}" + echo "validator_id=${validator_id}" + + # Delegator account (default foundry test address). + delegator_private_key=${DELEGATOR_PRIVATE_KEY:-"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"} + delegator_address=$(cast wallet address --private-key "${delegator_private_key}") + echo "delegator_private_key=${delegator_private_key}" + echo "delegator_address=${delegator_address}" + + # Common polling commands. + validator_count_cmd='curl -s "${L2_CL_API_URL}/stake/validators-set" | jq --raw-output ".validator_set.validators | length"' + validator_power_cmd='curl -s "${L2_CL_API_URL}/stake/validator/${validator_id}" | jq --raw-output ".validator.voting_power"' + validator_signer_cmd='curl -s "${L2_CL_API_URL}/stake/validator/${validator_id}" | jq --raw-output ".validator.signer"' + top_up_fee_balance_cmd='curl -s "${L2_CL_API_URL}/cosmos/bank/v1beta1/balances/${validator_address}" | jq --raw-output ".balances[] | select(.denom == \"pol\") | .amount"' + + # Define timeout and interval for eventually commands. + timeout_seconds=${TIMEOUT_SECONDS:-"180"} + interval_seconds=${INTERVAL_SECONDS:-"10"} } function generate_new_keypair() { @@ -18,10 +42,7 @@ function generate_new_keypair() { # bats test_tags=pos-validator @test "add new validator" { - VALIDATOR_COUNT_CMD='curl "${L2_CL_API_URL}/stake/validators-set" | jq --raw-output ".validator_set.validators | length"' - echo "VALIDATOR_COUNT_CMD=${VALIDATOR_COUNT_CMD}" - - initial_validator_count=$(eval "${VALIDATOR_COUNT_CMD}") + initial_validator_count=$(eval "${validator_count_cmd}") echo "Initial validator count: ${initial_validator_count}" echo "Generating a new validator keypair..." @@ -53,26 +74,13 @@ function generate_new_keypair() { "${validator_address}" "${deposit_amount}" "${heimdall_fee_amount}" "${accept_delegation}" "${validator_public_key}" echo "Monitoring the validator count on Heimdall..." - assert_command_eventually_equal "${VALIDATOR_COUNT_CMD}" $((initial_validator_count + 1)) 180 + assert_command_eventually_equal "${validator_count_cmd}" $((initial_validator_count + 1)) "${timeout_seconds}" } # bats test_tags=pos-validator @test "update validator stake" { - # First validator. - VALIDATOR_PRIVATE_KEY=${VALIDATOR_PRIVATE_KEY:-"0x2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2"} - echo "VALIDATOR_PRIVATE_KEY=${VALIDATOR_PRIVATE_KEY}" - - VALIDATOR_ID=${VALIDATOR_ID:-"1"} - echo "VALIDATOR_ID=${VALIDATOR_ID}" - - VALIDATOR_POWER_CMD='curl "${L2_CL_API_URL}/stake/validator/${VALIDATOR_ID}" | jq --raw-output ".validator.voting_power"' - echo "VALIDATOR_POWER_CMD=${VALIDATOR_POWER_CMD}" - - validator_address=$(cast wallet address --private-key "${VALIDATOR_PRIVATE_KEY}") - echo "validator_address=${validator_address}" - - initial_voting_power=$(eval "${VALIDATOR_POWER_CMD}") - echo "Initial voting power of the validator (${VALIDATOR_ID}): ${initial_voting_power}." + initial_voting_power=$(eval "${validator_power_cmd}") + echo "Initial voting power of the validator (${validator_id}): ${initial_voting_power}." echo "Funding the validator acount with POL tokens..." stake_update_amount=$(cast to-unit 1ether wei) @@ -80,50 +88,38 @@ function generate_new_keypair() { "${L1_POL_TOKEN_ADDRESS}" "transfer(address,uint)" "${validator_address}" "${stake_update_amount}" echo "Allowing the StakeManagerProxy contract to spend POL tokens on our behalf..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${VALIDATOR_PRIVATE_KEY}" \ + cast send --rpc-url "${L1_RPC_URL}" --private-key "${validator_private_key}" \ "${L1_POL_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${stake_update_amount}" - echo "Updating the stake of the validator (${VALIDATOR_ID})..." + echo "Updating the stake of the validator (${validator_id})..." stake_rewards=false - cast send --rpc-url "${L1_RPC_URL}" --private-key "${VALIDATOR_PRIVATE_KEY}" \ - "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "restakePOL(uint,uint,bool)" "${VALIDATOR_ID}" "${stake_update_amount}" "${stake_rewards}" + cast send --rpc-url "${L1_RPC_URL}" --private-key "${validator_private_key}" \ + "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "restakePOL(uint,uint,bool)" "${validator_id}" "${stake_update_amount}" "${stake_rewards}" echo "Monitoring the voting power of the validator..." voting_power_update=$(cast to-unit "${stake_update_amount}"wei ether) - assert_command_eventually_equal "${VALIDATOR_POWER_CMD}" $((initial_voting_power + voting_power_update)) 180 + assert_command_eventually_equal "${validator_power_cmd}" $((initial_voting_power + voting_power_update)) "${timeout_seconds}" } # bats test_tags=pos-validator @test "update validator top-up fee" { - # First validator. - VALIDATOR_PRIVATE_KEY=${VALIDATOR_PRIVATE_KEY:-"0x2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2"} - echo "VALIDATOR_PRIVATE_KEY=${VALIDATOR_PRIVATE_KEY}" - - VALIDATOR_ADDRESS=${VALIDATOR_ADDRESS:-"0x97538585a02A3f1B1297EB9979cE1b34ff953f1E"} - echo "VALIDATOR_ADDRESS=${VALIDATOR_ADDRESS}" - - TOP_UP_FEE_BALANCE_CMD='curl "${L2_CL_API_URL}/cosmos/bank/v1beta1/balances/${VALIDATOR_ADDRESS}" | jq --raw-output ".balances[] | select(.denom == \"pol\") | .amount"' - echo "TOP_UP_FEE_BALANCE_CMD=${TOP_UP_FEE_BALANCE_CMD}" - - initial_top_up_balance=$(eval "${TOP_UP_FEE_BALANCE_CMD}") - echo "${VALIDATOR_ADDRESS} initial top-up balance: ${initial_top_up_balance}." + initial_top_up_balance=$(eval "${top_up_fee_balance_cmd}") + echo "${validator_address} initial top-up balance: ${initial_top_up_balance}." echo "Allowing the StakeManagerProxy contract to spend POL tokens on our behalf..." top_up_amount=$(cast to-unit 1ether wei) cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_POL_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${top_up_amount}" - echo "Topping up the fee balance of the validator (${VALIDATOR_ADDRESS})..." + echo "Topping up the fee balance of the validator (${validator_address})..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "topUpForFee(address,uint)" "${VALIDATOR_ADDRESS}" "${top_up_amount}" + "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "topUpForFee(address,uint)" "${validator_address}" "${top_up_amount}" echo "Monitoring the top-up balance of the validator..." echo "Initial balance: ${initial_top_up_balance}" - timeout=180 - interval=10 start_time=$(date +%s) - end_time=$((start_time + timeout)) + end_time=$((start_time + timeout_seconds)) while true; do if [[ "$(date +%s)" -ge "${end_time}" ]]; then @@ -131,7 +127,7 @@ function generate_new_keypair() { exit 1 fi - current_balance=$(eval "${TOP_UP_FEE_BALANCE_CMD}") + current_balance=$(eval "${top_up_fee_balance_cmd}") echo "[$(date '+%Y-%m-%d %H:%M:%S')] Current balance: ${current_balance}" if [[ $(echo "${current_balance} > ${initial_top_up_balance}" | bc) -eq 1 ]]; then @@ -139,22 +135,13 @@ function generate_new_keypair() { break fi - sleep "${interval}" + sleep "${interval_seconds}" done } # bats test_tags=pos-validator @test "update signer" { - # First validator. - VALIDATOR_PRIVATE_KEY=${VALIDATOR_PRIVATE_KEY:-"0x2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2"} - echo "VALIDATOR_PRIVATE_KEY=${VALIDATOR_PRIVATE_KEY}" - - VALIDATOR_ID=${VALIDATOR_ID:-"1"} - echo "VALIDATOR_ID=${VALIDATOR_ID}" - - VALIDATOR_SIGNER_CMD='curl "${L2_CL_API_URL}/stake/validator/${VALIDATOR_ID}" | jq --raw-output ".validator.signer"' - - initial_signer=$(eval "${VALIDATOR_SIGNER_CMD}") + initial_signer=$(eval "${validator_signer_cmd}") echo "Initial signer: ${initial_signer}" # New account: @@ -170,29 +157,20 @@ function generate_new_keypair() { "$(cast calldata "updateSignerUpdateLimit(uint256)" "1")" echo "Updating signer..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${VALIDATOR_PRIVATE_KEY}" \ - "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "updateSigner(uint,bytes)" "${VALIDATOR_ID}" "${new_public_key}" + cast send --rpc-url "${L1_RPC_URL}" --private-key "${validator_private_key}" \ + "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "updateSigner(uint,bytes)" "${validator_id}" "${new_public_key}" echo "Monitoring signer change..." - assert_command_eventually_equal "${VALIDATOR_SIGNER_CMD}" "0xd74c0d3dee45a0a9516fb66e31c01536e8756e2a" 180 + assert_command_eventually_equal "${validator_signer_cmd}" "0xd74c0d3dee45a0a9516fb66e31c01536e8756e2a" "${timeout_seconds}" } # bats test_tags=pos-validator,pos-delegate,transaction-pol @test "delegate to a validator" { - VALIDATOR_ID=${VALIDATOR_ID:-"1"} - echo "VALIDATOR_ID=${VALIDATOR_ID}" - - # Use the default foundry test address as the delegator. - DELEGATOR_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - DELEGATOR_ADDRESS="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" - echo "DELEGATOR_PRIVATE_KEY=${DELEGATOR_PRIVATE_KEY}" - echo "DELEGATOR_ADDRESS=${DELEGATOR_ADDRESS}" - echo "L1_STAKING_INFO_ADDRESS=${L1_STAKING_INFO_ADDRESS}" # Get validator's ValidatorShare contract address. validator_share_address=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_STAKING_INFO_ADDRESS}" "getValidatorContractAddress(uint)(address)" "${VALIDATOR_ID}") + "${L1_STAKING_INFO_ADDRESS}" "getValidatorContractAddress(uint)(address)" "${validator_id}") echo "validator_share_address=${validator_share_address}" # Check if validator accepts delegation. @@ -206,83 +184,73 @@ function generate_new_keypair() { # Check initial validator total stake (own stake + delegated amount). initial_total_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1)" ether) + "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${validator_id}" | cut -d' ' -f1)" ether) echo "Initial total validator stake: ${initial_total_stake_eth} POL" # Check initial delegator stake in this validator. initial_delegator_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ - "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${DELEGATOR_ADDRESS}" | head -1 | cut -d' ' -f1)" ether) + "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${delegator_address}" | head -1 | cut -d' ' -f1)" ether) echo "Initial delegator stake: ${initial_delegator_stake_eth} POL" echo "Transferring ETH from main address to foundry address for gas..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" --value 1ether "${DELEGATOR_ADDRESS}" + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" --value 1ether "${delegator_address}" echo "Transferring POL tokens from main address to foundry address..." delegation_amount=$(cast to-unit 1ether wei) cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_POL_TOKEN_ADDRESS}" "transfer(address,uint)" "${DELEGATOR_ADDRESS}" "${delegation_amount}" + "${L1_POL_TOKEN_ADDRESS}" "transfer(address,uint)" "${delegator_address}" "${delegation_amount}" echo "Verifying foundry address received the tokens..." delegator_pol_balance=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint256)" "${DELEGATOR_ADDRESS}" | cut -d' ' -f1) - delegator_eth_balance=$(cast balance --rpc-url "${L1_RPC_URL}" "${DELEGATOR_ADDRESS}" --ether) + "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint256)" "${delegator_address}" | cut -d' ' -f1) + delegator_eth_balance=$(cast balance --rpc-url "${L1_RPC_URL}" "${delegator_address}" --ether) echo "Foundry address POL balance: $(cast to-unit ${delegator_pol_balance} ether) POL" echo "Foundry address ETH balance: ${delegator_eth_balance} ETH" echo "Allowing the StakeManager to spend POL tokens on foundry address behalf..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${DELEGATOR_PRIVATE_KEY}" \ + cast send --rpc-url "${L1_RPC_URL}" --private-key "${delegator_private_key}" \ "${L1_POL_TOKEN_ADDRESS}" "approve(address,uint)" "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "${delegation_amount}" - echo "Delegating ${delegation_amount} wei (1 POL) to validator ${VALIDATOR_ID}..." + echo "Delegating ${delegation_amount} wei (1 POL) to validator ${validator_id}..." min_shares_to_mint=0 - cast send --rpc-url "${L1_RPC_URL}" --private-key "${DELEGATOR_PRIVATE_KEY}" \ + cast send --rpc-url "${L1_RPC_URL}" --private-key "${delegator_private_key}" \ "${validator_share_address}" "buyVoucherPOL(uint,uint)" "${delegation_amount}" "${min_shares_to_mint}" echo "Verifying delegation was successful..." # Check that validator's total stake increased. final_total_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1)" ether) + "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${validator_id}" | cut -d' ' -f1)" ether) echo "Final total stake: ${final_total_stake_eth} POL" [[ $(echo "${final_total_stake_eth} == ${initial_total_stake_eth} + 1" | bc) -eq 1 ]] # Check that delegator's stake in validator increased. final_delegator_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ - "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${DELEGATOR_ADDRESS}" | head -1 | cut -d' ' -f1)" ether) + "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${delegator_address}" | head -1 | cut -d' ' -f1)" ether) echo "Final delegator stake: ${final_delegator_stake_eth} POL" [[ $(echo "${final_delegator_stake_eth} == ${initial_delegator_stake_eth} + 1" | bc) -eq 1 ]] # Verify L2 voting power matches the updated L1 stake. - VALIDATOR_POWER_CMD='curl "${L2_CL_API_URL}/stake/validator/${VALIDATOR_ID}" | jq --raw-output ".validator.voting_power"' expected_voting_power=$(echo "${final_total_stake_eth}" | cut -d'.' -f1) - echo "Monitoring L2 voting power sync for validator ${VALIDATOR_ID}..." - assert_command_eventually_equal "${VALIDATOR_POWER_CMD}" "${expected_voting_power}" 180 + echo "Monitoring L2 voting power sync for validator ${validator_id}..." + assert_command_eventually_equal "${validator_power_cmd}" "${expected_voting_power}" "${timeout_seconds}" echo "Delegation test completed successfully!" } # bats test_tags=pos-validator,pos-undelegate,transaction-pol @test "undelegate from a validator" { - VALIDATOR_ID=${VALIDATOR_ID:-"1"} - echo "VALIDATOR_ID=${VALIDATOR_ID}" - - # Use same foundry test address as delegator (consistent with delegation test). - DELEGATOR_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - DELEGATOR_ADDRESS="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" - echo "DELEGATOR_PRIVATE_KEY=${DELEGATOR_PRIVATE_KEY}" - echo "DELEGATOR_ADDRESS=${DELEGATOR_ADDRESS}" - echo "L1_STAKING_INFO_ADDRESS=${L1_STAKING_INFO_ADDRESS}" # Get validator's ValidatorShare contract address. validator_share_address=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_STAKING_INFO_ADDRESS}" "getValidatorContractAddress(uint)(address)" "${VALIDATOR_ID}") + "${L1_STAKING_INFO_ADDRESS}" "getValidatorContractAddress(uint)(address)" "${validator_id}") echo "validator_share_address=${validator_share_address}" # Check current delegator stake. current_delegator_stake_data=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${DELEGATOR_ADDRESS}") + "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${delegator_address}") current_delegator_stake_eth=$(cast to-unit "$(echo "${current_delegator_stake_data}" | head -1 | cut -d' ' -f1)" ether) echo "Current delegator stake: ${current_delegator_stake_eth} POL" @@ -295,7 +263,7 @@ function generate_new_keypair() { # Check current validator total stake. initial_total_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1)" ether) + "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${validator_id}" | cut -d' ' -f1)" ether) echo "Initial total validator stake: ${initial_total_stake_eth} POL" # Undelegate the current stake. @@ -304,61 +272,51 @@ function generate_new_keypair() { # Get current unbond nonce for the delegator. current_unbond_nonce=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${validator_share_address}" "unbondNonces(address)(uint)" "${DELEGATOR_ADDRESS}") + "${validator_share_address}" "unbondNonces(address)(uint)" "${delegator_address}") expected_unbond_nonce=$((current_unbond_nonce + 1)) echo "Expected unbond nonce: ${expected_unbond_nonce}" - echo "Initiating undelegation of ${undelegation_amount} wei POL from validator ${VALIDATOR_ID}..." + echo "Initiating undelegation of ${undelegation_amount} wei POL from validator ${validator_id}..." max_shares_to_burn=$(cast --max-uint) - cast send --rpc-url "${L1_RPC_URL}" --private-key "${DELEGATOR_PRIVATE_KEY}" \ + cast send --rpc-url "${L1_RPC_URL}" --private-key "${delegator_private_key}" \ "${validator_share_address}" "sellVoucher_newPOL(uint,uint)" "${undelegation_amount}" "${max_shares_to_burn}" echo "Verifying undelegation initiation was successful..." # Check that validator's total stake decreased. new_total_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ - "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${VALIDATOR_ID}" | cut -d' ' -f1)" ether) + "${L1_STAKING_INFO_ADDRESS}" "totalValidatorStake(uint)(uint)" "${validator_id}" | cut -d' ' -f1)" ether) echo "New total stake: ${new_total_stake_eth} POL" [[ $(echo "${new_total_stake_eth} == ${initial_total_stake_eth} - 1" | bc) -eq 1 ]] # Check that delegator's active stake decreased. new_delegator_stake_eth=$(cast to-unit "$(cast call --rpc-url "${L1_RPC_URL}" \ - "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${DELEGATOR_ADDRESS}" | head -1 | cut -d' ' -f1)" ether) + "${validator_share_address}" "getTotalStake(address)(uint,uint)" "${delegator_address}" | head -1 | cut -d' ' -f1)" ether) echo "New delegator stake: ${new_delegator_stake_eth} POL" [[ $(echo "${new_delegator_stake_eth} == ${current_delegator_stake_eth} - 1" | bc) -eq 1 ]] # Check that unbond nonce was incremented. final_unbond_nonce=$(cast call --rpc-url "${L1_RPC_URL}" \ - "${validator_share_address}" "unbondNonces(address)(uint)" "${DELEGATOR_ADDRESS}") + "${validator_share_address}" "unbondNonces(address)(uint)" "${delegator_address}") echo "Final unbond nonce: ${final_unbond_nonce}" [[ "${final_unbond_nonce}" -eq "${expected_unbond_nonce}" ]] # Verify L2 voting power matches the updated L1 stake. - VALIDATOR_POWER_CMD='curl "${L2_CL_API_URL}/stake/validator/${VALIDATOR_ID}" | jq --raw-output ".validator.voting_power"' expected_voting_power=$(echo "${new_total_stake_eth}" | cut -d'.' -f1) - echo "Monitoring L2 voting power sync for validator ${VALIDATOR_ID}..." - assert_command_eventually_equal "${VALIDATOR_POWER_CMD}" "${expected_voting_power}" 180 + echo "Monitoring L2 voting power sync for validator ${validator_id}..." + assert_command_eventually_equal "${validator_power_cmd}" "${expected_voting_power}" "${timeout_seconds}" echo "Undelegation test completed successfully!" } # bats test_tags=pos-validator @test "remove validator" { - # First validator. - VALIDATOR_PRIVATE_KEY=${VALIDATOR_PRIVATE_KEY:-"0x2a4ae8c4c250917781d38d95dafbb0abe87ae2c9aea02ed7c7524685358e49c2"} - echo "VALIDATOR_PRIVATE_KEY=${VALIDATOR_PRIVATE_KEY}" - - VALIDATOR_ID=${VALIDATOR_ID:-"1"} - echo "VALIDATOR_ID=${VALIDATOR_ID}" - - VALIDATOR_COUNT_CMD='curl "${L2_CL_API_URL}/stake/validators-set" | jq --raw-output ".validator_set.validators | length"' - - initial_validator_count=$(eval "${VALIDATOR_COUNT_CMD}") + initial_validator_count=$(eval "${validator_count_cmd}") echo "Initial validator count: ${initial_validator_count}" echo "Removing the validator from the validator set..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${VALIDATOR_PRIVATE_KEY}" \ - "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "unstakePOL(uint)" "${VALIDATOR_ID}" + cast send --rpc-url "${L1_RPC_URL}" --private-key "${validator_private_key}" \ + "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "unstakePOL(uint)" "${validator_id}" # Verify the unstake was initiated on L1: deactivationEpoch must be greater than zero. # validators(uint256) returns (amount, reward, activationEpoch, deactivationEpoch, ...) @@ -366,12 +324,13 @@ function generate_new_keypair() { deactivation_epoch=$(cast call --rpc-url "${L1_RPC_URL}" \ "${L1_STAKE_MANAGER_PROXY_ADDRESS}" \ "validators(uint256)(uint256,uint256,uint256,uint256,uint256,address,address,uint8)" \ - "${VALIDATOR_ID}" | sed -n '4p') - echo "Validator ${VALIDATOR_ID} deactivationEpoch: ${deactivation_epoch}" + "${validator_id}" | sed -n '4p') + echo "Validator ${validator_id} deactivationEpoch: ${deactivation_epoch}" [[ "${deactivation_epoch}" -gt "0" ]] # Wait for Heimdall to remove the validator at the epoch boundary. - # 1 epoch ≈ 256 blocks (~256s at 1s/block), so 300s gives enough headroom. + # 1 epoch ≈ 256 blocks (~256s at 1s/block), so a longer timeout than the default is needed; + # set TIMEOUT_SECONDS >= 300 when running this test. echo "Monitoring the validator count on Heimdall..." - assert_command_eventually_equal "${VALIDATOR_COUNT_CMD}" $((initial_validator_count - 1)) 300 + assert_command_eventually_equal "${validator_count_cmd}" $((initial_validator_count - 1)) "${timeout_seconds}" } From 9e3b08c9a32ef84c9e543d226bd93254d4f5cd1e Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 13:21:27 +0100 Subject: [PATCH 25/45] chore: nit --- TESTSINVENTORY.md | 12 ++++++------ tests/pos/validator.bats | 2 -- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index ba7a8e6e..fcbfb40a 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -387,7 +387,7 @@ Table of tests currently implemented or being implemented in the E2E repository. | concurrent write/read race: tx submissions and state reads do not interfere | [Link](./tests/pos/execution-specs/rpc/rpc-concurrent-load-and-stress.bats#L248) | | | consecutive block baseFees are within ±5% of each other | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L183) | | | contract-to-contract call fuzz: CALL/STATICCALL/DELEGATECALL | [Link](./tests/pos/execution-specs/transactions/evm-transaction-fuzzing-and-liveness.bats#L792) | | -| delegate to a validator | [Link](./tests/pos/validator.bats#L168) | | +| delegate to a validator | [Link](./tests/pos/validator.bats#L166) | | | deploy contract that returns 24577 runtime bytes is rejected by EIP-170 | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L124) | | | deploy contract that returns exactly 24576 runtime bytes succeeds (EIP-170 boundary) | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L150) | | | deploy contract that reverts in constructor leaves no code at deployed address | [Link](./tests/pos/execution-specs/evm/contract-creation-and-deployment-limits.bats#L48) | | @@ -456,7 +456,7 @@ Table of tests currently implemented or being implemented in the E2E repository. | out-of-gas transaction still increments sender nonce | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L160) | | | prune TxIndexer | [Link](./tests/pos/heimdall-v2.bats#L86) | | | recipient balance increases by exactly the value sent | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L53) | | -| remove validator | [Link](./tests/pos/validator.bats#L313) | | +| remove validator | [Link](./tests/pos/validator.bats#L311) | | | replay protection: same signed tx submitted twice does not double-spend | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L445) | | | sender balance decreases by exactly gas cost plus value transferred | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L18) | | | sha3Uncles field is empty-list RLP hash (PoS has no uncles) | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L976) | | @@ -473,10 +473,10 @@ Table of tests currently implemented or being implemented in the E2E repository. | type 1 access list with multiple storage keys is accepted | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L207) | | | type 2 (EIP-1559) effectiveGasPrice = baseFee + min(priorityFee, maxFee - baseFee) | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L130) | | | type 2 maxFeePerGas below baseFee is rejected | [Link](./tests/pos/execution-specs/transactions/transaction-types-and-gas-pricing.bats#L175) | | -| undelegate from a validator | [Link](./tests/pos/validator.bats#L243) | | -| update signer | [Link](./tests/pos/validator.bats#L143) | | -| update validator stake | [Link](./tests/pos/validator.bats#L81) | | -| update validator top-up fee | [Link](./tests/pos/validator.bats#L105) | | +| undelegate from a validator | [Link](./tests/pos/validator.bats#L241) | | +| update signer | [Link](./tests/pos/validator.bats#L141) | | +| update validator stake | [Link](./tests/pos/validator.bats#L79) | | +| update validator top-up fee | [Link](./tests/pos/validator.bats#L103) | | | warm COINBASE access costs less than cold access to arbitrary address (EIP-3651) | [Link](./tests/pos/execution-specs/evm/evm-opcodes-cancun-shanghai-eips.bats#L457) | | | web3_clientVersion returns a non-empty version string | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L400) | | | withdraw native tokens from L2 and confirm native balance decreased on L2 and checkpoint was submitted | [Link](./tests/pos/bridge.bats#L167) | | diff --git a/tests/pos/validator.bats b/tests/pos/validator.bats index 16bdaa46..c0784e0e 100644 --- a/tests/pos/validator.bats +++ b/tests/pos/validator.bats @@ -46,8 +46,6 @@ function generate_new_keypair() { echo "Initial validator count: ${initial_validator_count}" echo "Generating a new validator keypair..." - # Note: We're using the `generate_new_keypair` function defined below instead of `cast wallet new` - # because we need to generate a public key. read validator_address validator_public_key validator_private_key < <(generate_new_keypair) echo "Address: ${validator_address}" echo "Public key: ${validator_public_key}" From 5a55556fb58ca180dbfb9300d2765f06fe3d1702 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 13:39:43 +0100 Subject: [PATCH 26/45] chore: nit --- tests/pos/validator.bats | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pos/validator.bats b/tests/pos/validator.bats index c0784e0e..a7bd2e04 100644 --- a/tests/pos/validator.bats +++ b/tests/pos/validator.bats @@ -314,7 +314,7 @@ function generate_new_keypair() { echo "Removing the validator from the validator set..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${validator_private_key}" \ - "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "unstakePOL(uint)" "${validator_id}" + "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "unstakePOL(uint256)" "${validator_id}" # Verify the unstake was initiated on L1: deactivationEpoch must be greater than zero. # validators(uint256) returns (amount, reward, activationEpoch, deactivationEpoch, ...) From 66e9be9698279396e53a36734d92779c65668dba Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 13:40:57 +0100 Subject: [PATCH 27/45] feat: add test to withdraw rewards --- TESTSINVENTORY.md | 3 ++- tests/pos/validator.bats | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index fcbfb40a..fb411363 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -456,7 +456,7 @@ Table of tests currently implemented or being implemented in the E2E repository. | out-of-gas transaction still increments sender nonce | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L160) | | | prune TxIndexer | [Link](./tests/pos/heimdall-v2.bats#L86) | | | recipient balance increases by exactly the value sent | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L53) | | -| remove validator | [Link](./tests/pos/validator.bats#L311) | | +| remove validator | [Link](./tests/pos/validator.bats#L333) | | | replay protection: same signed tx submitted twice does not double-spend | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L445) | | | sender balance decreases by exactly gas cost plus value transferred | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L18) | | | sha3Uncles field is empty-list RLP hash (PoS has no uncles) | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L976) | | @@ -480,6 +480,7 @@ Table of tests currently implemented or being implemented in the E2E repository. | warm COINBASE access costs less than cold access to arbitrary address (EIP-3651) | [Link](./tests/pos/execution-specs/evm/evm-opcodes-cancun-shanghai-eips.bats#L457) | | | web3_clientVersion returns a non-empty version string | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L400) | | | withdraw native tokens from L2 and confirm native balance decreased on L2 and checkpoint was submitted | [Link](./tests/pos/bridge.bats#L167) | | +| withdraw validator rewards | [Link](./tests/pos/validator.bats#L311) | | | zero-value self-transfer: only gas consumed, nonce increments | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L530) | | ## DApps Tests diff --git a/tests/pos/validator.bats b/tests/pos/validator.bats index a7bd2e04..268b46f9 100644 --- a/tests/pos/validator.bats +++ b/tests/pos/validator.bats @@ -307,6 +307,28 @@ function generate_new_keypair() { echo "Undelegation test completed successfully!" } +# bats test_tags=pos-validator,transaction-pol +@test "withdraw validator rewards" { + initial_pol_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json \ + "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint256)" "${validator_address}" | jq --raw-output '.[0]') + claimable_reward=$(cast call --rpc-url "${L1_RPC_URL}" \ + "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "validatorReward(uint256)(uint256)" "${validator_id}") + echo "Initial POL balance: ${initial_pol_balance}" + echo "Claimable reward: ${claimable_reward}" + + echo "Withdrawing rewards for validator ${validator_id}..." + cast send --rpc-url "${L1_RPC_URL}" --private-key "${validator_private_key}" \ + "${L1_STAKE_MANAGER_PROXY_ADDRESS}" "withdrawRewardsPOL(uint256)" "${validator_id}" + + final_pol_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json \ + "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint256)" "${validator_address}" | jq --raw-output '.[0]') + echo "Final POL balance: ${final_pol_balance}" + + # The balance must have increased by at least the claimable reward observed before withdrawal. + # (It may be slightly higher since rewards accrue until the tx is mined.) + [[ $(echo "${final_pol_balance} >= ${initial_pol_balance} + ${claimable_reward}" | bc) -eq 1 ]] +} + # bats test_tags=pos-validator @test "remove validator" { initial_validator_count=$(eval "${validator_count_cmd}") From 6ce955f24d637f99c9200a35a69e2bdf7f040dc9 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 16:34:40 +0100 Subject: [PATCH 28/45] feat: generate exit paylod when withdrawing from pos --- TESTSINVENTORY.md | 6 +++--- core/helpers/pos-setup.bash | 4 ++++ tests/pos/bridge.bats | 35 +++++++++++++++++++++++------------ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index fb411363..535770c5 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -378,8 +378,8 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getAuthor returns a valid address for latest block | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L486) | | | bor_getCurrentValidators returns a non-empty validator list | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L509) | | | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | -| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L218) | | -| bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L260) | | +| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L229) | | +| bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L271) | | | bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L94) | | | bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L56) | | | bridge native token from L1 to L2 and confirm WETH balance increased on L2 | [Link](./tests/pos/bridge.bats#L131) | | @@ -479,7 +479,7 @@ Table of tests currently implemented or being implemented in the E2E repository. | update validator top-up fee | [Link](./tests/pos/validator.bats#L103) | | | warm COINBASE access costs less than cold access to arbitrary address (EIP-3651) | [Link](./tests/pos/execution-specs/evm/evm-opcodes-cancun-shanghai-eips.bats#L457) | | | web3_clientVersion returns a non-empty version string | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L400) | | -| withdraw native tokens from L2 and confirm native balance decreased on L2 and checkpoint was submitted | [Link](./tests/pos/bridge.bats#L167) | | +| withdraw native tokens from L2 and confirm POL balance increased on L1 | [Link](./tests/pos/bridge.bats#L167) | | | withdraw validator rewards | [Link](./tests/pos/validator.bats#L311) | | | zero-value self-transfer: only gas consumed, nonce increments | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L530) | | diff --git a/core/helpers/pos-setup.bash b/core/helpers/pos-setup.bash index 7e8d466a..403a327b 100644 --- a/core/helpers/pos-setup.bash +++ b/core/helpers/pos-setup.bash @@ -30,6 +30,7 @@ pos_setup() { if [[ -z "${L1_GOVERNANCE_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS:-}" ]] || + [[ -z "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_STAKE_MANAGER_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_STAKING_INFO_ADDRESS:-}" ]] || [[ -z "${L1_MATIC_TOKEN_ADDRESS:-}" ]] || @@ -50,6 +51,9 @@ pos_setup() { export L1_DEPOSIT_MANAGER_PROXY_ADDRESS=${L1_DEPOSIT_MANAGER_PROXY_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.DepositManagerProxy')} echo "L1_DEPOSIT_MANAGER_PROXY_ADDRESS=${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" + export L1_WITHDRAW_MANAGER_PROXY_ADDRESS=${L1_WITHDRAW_MANAGER_PROXY_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.WithdrawManagerProxy')} + echo "L1_WITHDRAW_MANAGER_PROXY_ADDRESS=${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" + export L1_STAKE_MANAGER_PROXY_ADDRESS=${L1_STAKE_MANAGER_PROXY_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.StakeManagerProxy')} echo "L1_STAKE_MANAGER_PROXY_ADDRESS=${L1_STAKE_MANAGER_PROXY_ADDRESS}" diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index bd53e0e3..b832ec7b 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -164,15 +164,17 @@ function wait_for_bor_state_sync() { } # bats test_tags=withdraw,transaction-pol -@test "withdraw native tokens from L2 and confirm native balance decreased on L2 and checkpoint was submitted" { +@test "withdraw native tokens from L2 and confirm POL balance increased on L1" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") - # Get initial L2 native balance and latest checkpoint ID. + # Get initial balances and latest checkpoint ID. + initial_l1_pol_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') initial_l2_balance=$(cast balance --rpc-url "${L2_RPC_URL}" "${address}") checkpoint_count_cmd='curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq --raw-output ".checkpoint.id"' initial_checkpoint_id=$(eval "${checkpoint_count_cmd}") echo "Initial balances and state:" + echo "- L1 POL balance: ${initial_l1_pol_balance}" echo "- L2 native balance: ${initial_l2_balance} wei" echo "- Latest checkpoint ID: ${initial_checkpoint_id}" @@ -202,16 +204,25 @@ function wait_for_bor_state_sync() { echo "Waiting for a new checkpoint to cover L2 block ${withdraw_block}..." assert_command_eventually_greater_or_equal "${checkpoint_count_cmd}" $((initial_checkpoint_id + 1)) "${timeout_seconds}" "${interval_seconds}" - # TODO: Complete the Plasma exit on L1 to recover funds. - # After the burn block is checkpointed, the remaining steps are: - # 1. Build the exit payload: RLP-encoded receipt of the burn tx + Merkle - # proof of that receipt in the block's receipts trie + checkpoint proof. - # 2. L1: WithdrawManagerProxy.startExitWithBurntTokens(bytes exitTx) - # 3. L1: WithdrawManagerProxy.processExits(address token) - # 4. Assert L1 POL balance increased by withdraw_amount. - # Proof generation requires constructing the receipts MPT of the burn block, - # which is not feasible in pure bash. A dedicated tool (e.g. matic.js or a - # custom Go helper) is needed. + # Generate the exit payload for the burn transaction. + # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + payload=(polycli pos exit-proof \ + --l1-rpc-url "${L1_RPC_URL}" \ + --l2-rpc-url "${L2_RPC_URL}" \ + --tx-hash "${withdraw_tx_hash}" \ + --checkpoint-id $((initial_checkpoint_id + 1))) + + # Start the exit on L1 with the generated payload. + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" + + # Process the exit on L1. + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_MATIC_TOKEN_ADDRESS}" + + # Verify L1 POL balance increased by the withdrawn amount. + echo "Verifying L1 POL balance increased..." + assert_token_balance_eventually_greater_or_equal "${L1_POL_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_pol_balance} + ${withdraw_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } # bats test_tags=bridge,transaction-erc20 From 97955403830a7ebeb77cb93336284d547f0c0ba9 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 16:46:21 +0100 Subject: [PATCH 29/45] feat: implement remaining withdraw tests --- tests/pos/bridge.bats | 230 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 204 insertions(+), 26 deletions(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index b832ec7b..fdfc1f7d 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -127,6 +127,68 @@ function wait_for_bor_state_sync() { assert_ether_balance_eventually_greater_or_equal "${address}" "$(echo "${initial_l2_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } +# bats test_tags=withdraw,transaction-pol +@test "withdraw native tokens from L2 and confirm POL balance increased on L1" { + address=$(cast wallet address --private-key "${PRIVATE_KEY}") + + # Get initial balances and latest checkpoint ID. + initial_l1_pol_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') + initial_l2_balance=$(cast balance --rpc-url "${L2_RPC_URL}" "${address}") + checkpoint_count_cmd='curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq --raw-output ".checkpoint.id"' + initial_checkpoint_id=$(eval "${checkpoint_count_cmd}") + + echo "Initial balances and state:" + echo "- L1 POL balance: ${initial_l1_pol_balance}" + echo "- L2 native balance: ${initial_l2_balance} wei" + echo "- Latest checkpoint ID: ${initial_checkpoint_id}" + + # Burn native tokens on L2 to initiate the Plasma exit. + withdraw_amount=$(cast to-unit 1ether wei) + echo "Burning ${withdraw_amount} wei on L2..." + withdraw_receipt=$(cast send \ + --rpc-url "${L2_RPC_URL}" \ + --private-key "${PRIVATE_KEY}" \ + --value "${withdraw_amount}" \ + --gas-price 30gwei \ + --priority-gas-price 30gwei \ + --json \ + "0x0000000000000000000000000000000000001010" \ + "withdraw(uint256)" "${withdraw_amount}") + withdraw_tx_hash=$(echo "${withdraw_receipt}" | jq --raw-output ".transactionHash") + withdraw_block_hex=$(echo "${withdraw_receipt}" | jq --raw-output ".blockNumber") + withdraw_block=$(printf "%d" "${withdraw_block_hex}") + echo "Withdraw tx: ${withdraw_tx_hash} (block ${withdraw_block})" + + # Verify L2 native balance decreased. + echo "Verifying L2 native balance decreased..." + assert_ether_balance_eventually_lower_or_equal "${address}" "$(echo "${initial_l2_balance} - ${withdraw_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + + # Wait for a new checkpoint on L1 that covers the withdrawal block. + # This confirms validators have attested to the burn, which is a prerequisite for building a valid exit Merkle proof. + echo "Waiting for a new checkpoint to cover L2 block ${withdraw_block}..." + assert_command_eventually_greater_or_equal "${checkpoint_count_cmd}" $((initial_checkpoint_id + 1)) "${timeout_seconds}" "${interval_seconds}" + + # Generate the exit payload for the burn transaction. + # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + payload=$(polycli pos exit-proof \ + --l1-rpc-url "${L1_RPC_URL}" \ + --l2-rpc-url "${L2_RPC_URL}" \ + --tx-hash "${withdraw_tx_hash}" \ + --checkpoint-id $((initial_checkpoint_id + 1))) + + # Start the exit on L1 with the generated payload. + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" + + # Process the exit on L1. + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_MATIC_TOKEN_ADDRESS}" + + # Verify L1 POL balance increased by the withdrawn amount. + echo "Verifying L1 POL balance increased..." + assert_token_balance_eventually_greater_or_equal "${L1_POL_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_pol_balance} + ${withdraw_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" +} + # bats test_tags=bridge,transaction-eth @test "bridge native token from L1 to L2 and confirm WETH balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") @@ -163,41 +225,41 @@ function wait_for_bor_state_sync() { assert_token_balance_eventually_greater_or_equal "${L2_WETH_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } -# bats test_tags=withdraw,transaction-pol -@test "withdraw native tokens from L2 and confirm POL balance increased on L1" { +# bats test_tags=withdraw,transaction-eth +@test "withdraw MaticWeth from L2 and confirm ETH balance increased on L1" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get initial balances and latest checkpoint ID. - initial_l1_pol_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') - initial_l2_balance=$(cast balance --rpc-url "${L2_RPC_URL}" "${address}") + initial_l1_balance=$(cast balance --rpc-url "${L1_RPC_URL}" "${address}") + initial_l2_balance=$(cast call --rpc-url "${L2_RPC_URL}" --json "${L2_WETH_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') checkpoint_count_cmd='curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq --raw-output ".checkpoint.id"' initial_checkpoint_id=$(eval "${checkpoint_count_cmd}") echo "Initial balances and state:" - echo "- L1 POL balance: ${initial_l1_pol_balance}" - echo "- L2 native balance: ${initial_l2_balance} wei" + echo "- L1 ETH balance: ${initial_l1_balance}" + echo "- L2 WETH balance: ${initial_l2_balance}" echo "- Latest checkpoint ID: ${initial_checkpoint_id}" - # Burn native tokens on L2 to initiate the Plasma exit. + # Burn MaticWeth on L2 to initiate the Plasma exit. + # The WithdrawManager will release the corresponding ETH locked on L1. withdraw_amount=$(cast to-unit 1ether wei) - echo "Burning ${withdraw_amount} wei on L2..." + echo "Burning ${withdraw_amount} MaticWeth on L2..." withdraw_receipt=$(cast send \ --rpc-url "${L2_RPC_URL}" \ --private-key "${PRIVATE_KEY}" \ - --value "${withdraw_amount}" \ --gas-price 30gwei \ --priority-gas-price 30gwei \ --json \ - "0x0000000000000000000000000000000000001010" \ + "${L2_WETH_TOKEN_ADDRESS}" \ "withdraw(uint256)" "${withdraw_amount}") withdraw_tx_hash=$(echo "${withdraw_receipt}" | jq --raw-output ".transactionHash") withdraw_block_hex=$(echo "${withdraw_receipt}" | jq --raw-output ".blockNumber") withdraw_block=$(printf "%d" "${withdraw_block_hex}") echo "Withdraw tx: ${withdraw_tx_hash} (block ${withdraw_block})" - # Verify L2 native balance decreased. - echo "Verifying L2 native balance decreased..." - assert_ether_balance_eventually_lower_or_equal "${address}" "$(echo "${initial_l2_balance} - ${withdraw_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + # Verify L2 WETH balance decreased. + echo "Verifying L2 WETH balance decreased..." + assert_token_balance_eventually_lower_or_equal "${L2_WETH_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_balance} - ${withdraw_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" # Wait for a new checkpoint on L1 that covers the withdrawal block. # This confirms validators have attested to the burn, which is a prerequisite for building a valid exit Merkle proof. @@ -206,23 +268,24 @@ function wait_for_bor_state_sync() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. - payload=(polycli pos exit-proof \ + payload=$(polycli pos exit-proof \ --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ --tx-hash "${withdraw_tx_hash}" \ --checkpoint-id $((initial_checkpoint_id + 1))) - + # Start the exit on L1 with the generated payload. + # MaticWeth is a mintable token on L2, so it uses startExitForMintableBurntTokens. cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitForMintableBurntTokens(bytes)" "${payload}" # Process the exit on L1. cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_MATIC_TOKEN_ADDRESS}" + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_WETH_TOKEN_ADDRESS}" - # Verify L1 POL balance increased by the withdrawn amount. - echo "Verifying L1 POL balance increased..." - assert_token_balance_eventually_greater_or_equal "${L1_POL_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_pol_balance} + ${withdraw_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + # Verify L1 ETH balance increased by the withdrawn amount (gas costs excluded). + echo "Verifying L1 ETH balance increased..." + assert_ether_balance_eventually_greater_or_equal "${address}" "$(echo "${initial_l1_balance} + ${withdraw_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } # bats test_tags=bridge,transaction-erc20 @@ -263,9 +326,65 @@ function wait_for_bor_state_sync() { } # bats test_tags=withdraw,transaction-erc20 -# @test "withdraw ERC20 tokens from L2 to L1 and confirm ERC20 balance increased on L1" { -# echo TODO -# } +@test "withdraw ERC20 tokens from L2 to L1 and confirm ERC20 balance increased on L1" { + address=$(cast wallet address --private-key "${PRIVATE_KEY}") + + # Get initial balances and latest checkpoint ID. + initial_l1_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_ERC20_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') + initial_l2_balance=$(cast call --rpc-url "${L2_RPC_URL}" --json "${L2_ERC20_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') + checkpoint_count_cmd='curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq --raw-output ".checkpoint.id"' + initial_checkpoint_id=$(eval "${checkpoint_count_cmd}") + + echo "Initial balances and state:" + echo "- L1 ERC20 balance: ${initial_l1_balance}" + echo "- L2 ERC20 balance: ${initial_l2_balance}" + echo "- Latest checkpoint ID: ${initial_checkpoint_id}" + + # Burn ERC20 tokens on L2 to initiate the Plasma exit. + withdraw_amount=$(cast to-unit 1ether wei) + echo "Burning ${withdraw_amount} ERC20 tokens on L2..." + withdraw_receipt=$(cast send \ + --rpc-url "${L2_RPC_URL}" \ + --private-key "${PRIVATE_KEY}" \ + --gas-price 30gwei \ + --priority-gas-price 30gwei \ + --json \ + "${L2_ERC20_TOKEN_ADDRESS}" \ + "withdraw(uint256)" "${withdraw_amount}") + withdraw_tx_hash=$(echo "${withdraw_receipt}" | jq --raw-output ".transactionHash") + withdraw_block_hex=$(echo "${withdraw_receipt}" | jq --raw-output ".blockNumber") + withdraw_block=$(printf "%d" "${withdraw_block_hex}") + echo "Withdraw tx: ${withdraw_tx_hash} (block ${withdraw_block})" + + # Verify L2 ERC20 balance decreased. + echo "Verifying L2 ERC20 balance decreased..." + assert_token_balance_eventually_lower_or_equal "${L2_ERC20_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_balance} - ${withdraw_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + + # Wait for a new checkpoint on L1 that covers the withdrawal block. + # This confirms validators have attested to the burn, which is a prerequisite for building a valid exit Merkle proof. + echo "Waiting for a new checkpoint to cover L2 block ${withdraw_block}..." + assert_command_eventually_greater_or_equal "${checkpoint_count_cmd}" $((initial_checkpoint_id + 1)) "${timeout_seconds}" "${interval_seconds}" + + # Generate the exit payload for the burn transaction. + # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + payload=$(polycli pos exit-proof \ + --l1-rpc-url "${L1_RPC_URL}" \ + --l2-rpc-url "${L2_RPC_URL}" \ + --tx-hash "${withdraw_tx_hash}" \ + --checkpoint-id $((initial_checkpoint_id + 1))) + + # Start the exit on L1 with the generated payload. + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" + + # Process the exit on L1. + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_ERC20_TOKEN_ADDRESS}" + + # Verify L1 ERC20 balance increased by the withdrawn amount. + echo "Verifying L1 ERC20 balance increased..." + assert_token_balance_eventually_greater_or_equal "${L1_ERC20_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_balance} + ${withdraw_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" +} # bats test_tags=bridge,transaction-erc721 @test "bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2" { @@ -312,6 +431,65 @@ function wait_for_bor_state_sync() { } # bats test_tags=withdraw,transaction-erc721 -# @test "withdraw ERC721 token from L2 to L1 and confirm ERC721 balance increased on L1" { -# echo TODO -# } +@test "withdraw ERC721 token from L2 to L1 and confirm ERC721 balance increased on L1" { + address=$(cast wallet address --private-key "${PRIVATE_KEY}") + + # Get a token ID owned by the address on L2. + token_id=$(cast call --rpc-url "${L2_RPC_URL}" --json "${L2_ERC721_TOKEN_ADDRESS}" "tokenOfOwnerByIndex(address,uint256)(uint256)" "${address}" 0 | jq --raw-output '.[0]') + echo "Withdrawing ERC721 token ID: ${token_id}" + + # Get initial balances and latest checkpoint ID. + initial_l1_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_ERC721_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') + initial_l2_balance=$(cast call --rpc-url "${L2_RPC_URL}" --json "${L2_ERC721_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') + checkpoint_count_cmd='curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq --raw-output ".checkpoint.id"' + initial_checkpoint_id=$(eval "${checkpoint_count_cmd}") + + echo "Initial balances and state:" + echo "- L1 ERC721 balance: ${initial_l1_balance}" + echo "- L2 ERC721 balance: ${initial_l2_balance}" + echo "- Latest checkpoint ID: ${initial_checkpoint_id}" + + # Burn the ERC721 token on L2 to initiate the Plasma exit. + echo "Burning ERC721 token (id: ${token_id}) on L2..." + withdraw_receipt=$(cast send \ + --rpc-url "${L2_RPC_URL}" \ + --private-key "${PRIVATE_KEY}" \ + --gas-price 30gwei \ + --priority-gas-price 30gwei \ + --json \ + "${L2_ERC721_TOKEN_ADDRESS}" \ + "withdraw(uint256)" "${token_id}") + withdraw_tx_hash=$(echo "${withdraw_receipt}" | jq --raw-output ".transactionHash") + withdraw_block_hex=$(echo "${withdraw_receipt}" | jq --raw-output ".blockNumber") + withdraw_block=$(printf "%d" "${withdraw_block_hex}") + echo "Withdraw tx: ${withdraw_tx_hash} (block ${withdraw_block})" + + # Verify L2 ERC721 balance decreased. + echo "Verifying L2 ERC721 balance decreased..." + assert_token_balance_eventually_lower_or_equal "${L2_ERC721_TOKEN_ADDRESS}" "${address}" $((initial_l2_balance - 1)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + + # Wait for a new checkpoint on L1 that covers the withdrawal block. + # This confirms validators have attested to the burn, which is a prerequisite for building a valid exit Merkle proof. + echo "Waiting for a new checkpoint to cover L2 block ${withdraw_block}..." + assert_command_eventually_greater_or_equal "${checkpoint_count_cmd}" $((initial_checkpoint_id + 1)) "${timeout_seconds}" "${interval_seconds}" + + # Generate the exit payload for the burn transaction. + # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + payload=$(polycli pos exit-proof \ + --l1-rpc-url "${L1_RPC_URL}" \ + --l2-rpc-url "${L2_RPC_URL}" \ + --tx-hash "${withdraw_tx_hash}" \ + --checkpoint-id $((initial_checkpoint_id + 1))) + + # Start the exit on L1 with the generated payload. + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" + + # Process the exit on L1. + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_ERC721_TOKEN_ADDRESS}" + + # Verify L1 ERC721 balance increased. + echo "Verifying L1 ERC721 balance increased..." + assert_token_balance_eventually_greater_or_equal "${L1_ERC721_TOKEN_ADDRESS}" "${address}" $((initial_l1_balance + 1)) "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" +} From 9a88eb34be5ba77ea7987817ee54c01f1cad4904 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 16:47:15 +0100 Subject: [PATCH 30/45] chore: add sections --- tests/pos/bridge.bats | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index fdfc1f7d..5e23cb8b 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -52,6 +52,10 @@ function wait_for_bor_state_sync() { assert_command_eventually_greater_or_equal "${bor_state_sync_count_cmd}" $((state_sync_count + 1)) "${timeout_seconds}" "${interval_seconds}" } +############################################################################## +# POL / MATIC <-> Native L2 +############################################################################## + # bats test_tags=bridge,transaction-pol @test "bridge POL from L1 to L2 and confirm native tokens balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") @@ -189,6 +193,10 @@ function wait_for_bor_state_sync() { assert_token_balance_eventually_greater_or_equal "${L1_POL_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_pol_balance} + ${withdraw_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } +############################################################################## +# ETH (Native L1) / WETH +############################################################################## + # bats test_tags=bridge,transaction-eth @test "bridge native token from L1 to L2 and confirm WETH balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") @@ -288,6 +296,10 @@ function wait_for_bor_state_sync() { assert_ether_balance_eventually_greater_or_equal "${address}" "$(echo "${initial_l1_balance} + ${withdraw_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } +############################################################################## +# ERC20 +############################################################################## + # bats test_tags=bridge,transaction-erc20 @test "bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") @@ -386,6 +398,10 @@ function wait_for_bor_state_sync() { assert_token_balance_eventually_greater_or_equal "${L1_ERC20_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_balance} + ${withdraw_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } +############################################################################## +# ERC721 +############################################################################## + # bats test_tags=bridge,transaction-erc721 @test "bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") From af2491f48cb61baeebc4bd78c3e0c93545621d5c Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 16:48:35 +0100 Subject: [PATCH 31/45] chore: nit --- TESTSINVENTORY.md | 15 +++++++++------ tests/pos/bridge.bats | 18 +++++++++--------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index 535770c5..ecd3fb8c 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -378,11 +378,11 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getAuthor returns a valid address for latest block | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L486) | | | bor_getCurrentValidators returns a non-empty validator list | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L509) | | | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | -| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L229) | | -| bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L271) | | -| bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L94) | | -| bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L56) | | -| bridge native token from L1 to L2 and confirm WETH balance increased on L2 | [Link](./tests/pos/bridge.bats#L131) | | +| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L304) | | +| bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L406) | | +| bridge ETH from L1 to L2 and confirm MaticWeth balance increased on L2 | [Link](./tests/pos/bridge.bats#L201) | | +| bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L98) | | +| bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L60) | | | coinbase balance increases by at least the priority fee portion of gas cost | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L318) | | | concurrent write/read race: tx submissions and state reads do not interfere | [Link](./tests/pos/execution-specs/rpc/rpc-concurrent-load-and-stress.bats#L248) | | | consecutive block baseFees are within ±5% of each other | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L183) | | @@ -479,7 +479,10 @@ Table of tests currently implemented or being implemented in the E2E repository. | update validator top-up fee | [Link](./tests/pos/validator.bats#L103) | | | warm COINBASE access costs less than cold access to arbitrary address (EIP-3651) | [Link](./tests/pos/execution-specs/evm/evm-opcodes-cancun-shanghai-eips.bats#L457) | | | web3_clientVersion returns a non-empty version string | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L400) | | -| withdraw native tokens from L2 and confirm POL balance increased on L1 | [Link](./tests/pos/bridge.bats#L167) | | +| withdraw ERC20 tokens from L2 to L1 and confirm ERC20 balance increased on L1 | [Link](./tests/pos/bridge.bats#L341) | | +| withdraw ERC721 token from L2 to L1 and confirm ERC721 balance increased on L1 | [Link](./tests/pos/bridge.bats#L450) | | +| withdraw MaticWeth from L2 and confirm ETH balance increased on L1 | [Link](./tests/pos/bridge.bats#L237) | | +| withdraw native tokens from L2 and confirm POL balance increased on L1 | [Link](./tests/pos/bridge.bats#L135) | | | withdraw validator rewards | [Link](./tests/pos/validator.bats#L311) | | | zero-value self-transfer: only gas consumed, nonce increments | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L530) | | diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 5e23cb8b..7cc0552e 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -194,11 +194,11 @@ function wait_for_bor_state_sync() { } ############################################################################## -# ETH (Native L1) / WETH +# ETH (Native L1) / MaticWeth ############################################################################## # bats test_tags=bridge,transaction-eth -@test "bridge native token from L1 to L2 and confirm WETH balance increased on L2" { +@test "bridge ETH from L1 to L2 and confirm MaticWeth balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get the initial balances. @@ -207,14 +207,14 @@ function wait_for_bor_state_sync() { echo "Initial balances:" echo "- L1 ETH balance: ${initial_l1_balance}" - echo "- L2 WETH balance: ${initial_l2_balance}" + echo "- L2 MaticWeth balance: ${initial_l2_balance}" heimdall_state_sync_count=$(eval "${heimdall_state_sync_count_cmd}") bor_state_sync_count=$(eval "${bor_state_sync_count_cmd}") # Bridge some ETH from L1 to L2 to trigger a state sync. - # The DepositManager wraps ETH into WETH (MaticWeth) on L1, so the L2 - # WETH balance increases rather than the native gas balance. + # The DepositManager wraps ETH into MaticWeth on L1, so the L2 + # MaticWeth balance increases rather than the native gas balance. echo "Depositing ETH to trigger a state sync..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ --value "${bridge_amount}" \ @@ -229,7 +229,7 @@ function wait_for_bor_state_sync() { echo "Monitoring ETH balance on L1..." assert_ether_balance_eventually_lower_or_equal "${address}" "$(echo "${initial_l1_balance} - ${bridge_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" - echo "Monitoring WETH balance on L2..." + echo "Monitoring MaticWeth balance on L2..." assert_token_balance_eventually_greater_or_equal "${L2_WETH_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } @@ -245,7 +245,7 @@ function wait_for_bor_state_sync() { echo "Initial balances and state:" echo "- L1 ETH balance: ${initial_l1_balance}" - echo "- L2 WETH balance: ${initial_l2_balance}" + echo "- L2 MaticWeth balance: ${initial_l2_balance}" echo "- Latest checkpoint ID: ${initial_checkpoint_id}" # Burn MaticWeth on L2 to initiate the Plasma exit. @@ -265,8 +265,8 @@ function wait_for_bor_state_sync() { withdraw_block=$(printf "%d" "${withdraw_block_hex}") echo "Withdraw tx: ${withdraw_tx_hash} (block ${withdraw_block})" - # Verify L2 WETH balance decreased. - echo "Verifying L2 WETH balance decreased..." + # Verify L2 MaticWeth balance decreased. + echo "Verifying L2 MaticWeth balance decreased..." assert_token_balance_eventually_lower_or_equal "${L2_WETH_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l2_balance} - ${withdraw_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" # Wait for a new checkpoint on L1 that covers the withdrawal block. From 94819b508aef0d1576580c9ec803377a2241386d Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 17:04:18 +0100 Subject: [PATCH 32/45] fix: root chain address is missing --- tests/pos/bridge.bats | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 7cc0552e..cb33a907 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -177,6 +177,7 @@ function wait_for_bor_state_sync() { payload=$(polycli pos exit-proof \ --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ + --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ --tx-hash "${withdraw_tx_hash}" \ --checkpoint-id $((initial_checkpoint_id + 1))) @@ -279,6 +280,7 @@ function wait_for_bor_state_sync() { payload=$(polycli pos exit-proof \ --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ + --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ --tx-hash "${withdraw_tx_hash}" \ --checkpoint-id $((initial_checkpoint_id + 1))) @@ -382,6 +384,7 @@ function wait_for_bor_state_sync() { payload=$(polycli pos exit-proof \ --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ + --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ --tx-hash "${withdraw_tx_hash}" \ --checkpoint-id $((initial_checkpoint_id + 1))) @@ -494,6 +497,7 @@ function wait_for_bor_state_sync() { payload=$(polycli pos exit-proof \ --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ + --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ --tx-hash "${withdraw_tx_hash}" \ --checkpoint-id $((initial_checkpoint_id + 1))) From 0256f1c3d7603cc060493b6ea85f5dc45d905656 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 17:08:41 +0100 Subject: [PATCH 33/45] fix: retrive root chain proxy --- core/helpers/pos-setup.bash | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/helpers/pos-setup.bash b/core/helpers/pos-setup.bash index 403a327b..0a5a86c7 100644 --- a/core/helpers/pos-setup.bash +++ b/core/helpers/pos-setup.bash @@ -29,6 +29,7 @@ pos_setup() { echo "L2_CL_API_URL=${L2_CL_API_URL}" if [[ -z "${L1_GOVERNANCE_PROXY_ADDRESS:-}" ]] || + [[ -z "${L1_ROOT_CHAIN_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_STAKE_MANAGER_PROXY_ADDRESS:-}" ]] || @@ -48,6 +49,9 @@ pos_setup() { export L1_GOVERNANCE_PROXY_ADDRESS=${L1_GOVERNANCE_PROXY_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.GovernanceProxy')} echo "L1_GOVERNANCE_PROXY_ADDRESS=${L1_GOVERNANCE_PROXY_ADDRESS}" + export L1_ROOT_CHAIN_PROXY_ADDRESS=${L1_ROOT_CHAIN_PROXY_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.RootChainProxy')} + echo "L1_ROOT_CHAIN_PROXY_ADDRESS=${L1_ROOT_CHAIN_PROXY_ADDRESS}" + export L1_DEPOSIT_MANAGER_PROXY_ADDRESS=${L1_DEPOSIT_MANAGER_PROXY_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.DepositManagerProxy')} echo "L1_DEPOSIT_MANAGER_PROXY_ADDRESS=${L1_DEPOSIT_MANAGER_PROXY_ADDRESS}" From 9b644b1da9355e93303ca63a1001b95bcdcb3061 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 17:15:05 +0100 Subject: [PATCH 34/45] chore: clean up --- tests/pos/bridge.bats | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index cb33a907..ee1758b3 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -178,8 +178,7 @@ function wait_for_bor_state_sync() { --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}" \ - --checkpoint-id $((initial_checkpoint_id + 1))) + --tx-hash "${withdraw_tx_hash}" # Start the exit on L1 with the generated payload. cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ @@ -281,8 +280,7 @@ function wait_for_bor_state_sync() { --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}" \ - --checkpoint-id $((initial_checkpoint_id + 1))) + --tx-hash "${withdraw_tx_hash}" # Start the exit on L1 with the generated payload. # MaticWeth is a mintable token on L2, so it uses startExitForMintableBurntTokens. @@ -385,8 +383,7 @@ function wait_for_bor_state_sync() { --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}" \ - --checkpoint-id $((initial_checkpoint_id + 1))) + --tx-hash "${withdraw_tx_hash}" # Start the exit on L1 with the generated payload. cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ @@ -498,8 +495,7 @@ function wait_for_bor_state_sync() { --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}" \ - --checkpoint-id $((initial_checkpoint_id + 1))) + --tx-hash "${withdraw_tx_hash}" # Start the exit on L1 with the generated payload. cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ From fc4d1411a7d54e8269a76858efb435b7f0c7c44c Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 17:17:24 +0100 Subject: [PATCH 35/45] fix: typos --- tests/pos/bridge.bats | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index ee1758b3..7a1d0532 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -178,7 +178,7 @@ function wait_for_bor_state_sync() { --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}" + --tx-hash "${withdraw_tx_hash}") # Start the exit on L1 with the generated payload. cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ @@ -280,7 +280,7 @@ function wait_for_bor_state_sync() { --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}" + --tx-hash "${withdraw_tx_hash}") # Start the exit on L1 with the generated payload. # MaticWeth is a mintable token on L2, so it uses startExitForMintableBurntTokens. @@ -383,7 +383,7 @@ function wait_for_bor_state_sync() { --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}" + --tx-hash "${withdraw_tx_hash}") # Start the exit on L1 with the generated payload. cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ @@ -495,7 +495,7 @@ function wait_for_bor_state_sync() { --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}" + --tx-hash "${withdraw_tx_hash}") # Start the exit on L1 with the generated payload. cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ From d30ac16edf1df5f657dad5f8996d5b9dce7c49a7 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 17:22:32 +0100 Subject: [PATCH 36/45] chore: nit --- tests/pos/bridge.bats | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 7a1d0532..49250ab8 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -174,17 +174,20 @@ function wait_for_bor_state_sync() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + echo "Generating the exit payload for the burn transaction..." payload=$(polycli pos exit-proof \ --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ --tx-hash "${withdraw_tx_hash}") - + # Start the exit on L1 with the generated payload. + echo "Starting the exit on L1 with the generated payload..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" # Process the exit on L1. + echo "Processing the exit on L1..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_MATIC_TOKEN_ADDRESS}" @@ -276,6 +279,7 @@ function wait_for_bor_state_sync() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + echo "Generating the exit payload for the burn transaction..." payload=$(polycli pos exit-proof \ --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ @@ -284,10 +288,12 @@ function wait_for_bor_state_sync() { # Start the exit on L1 with the generated payload. # MaticWeth is a mintable token on L2, so it uses startExitForMintableBurntTokens. + echo "Starting the exit on L1 with the generated payload..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitForMintableBurntTokens(bytes)" "${payload}" # Process the exit on L1. + echo "Processing the exit on L1..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_WETH_TOKEN_ADDRESS}" @@ -379,6 +385,7 @@ function wait_for_bor_state_sync() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + echo "Generating the exit payload for the burn transaction..." payload=$(polycli pos exit-proof \ --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ @@ -386,10 +393,12 @@ function wait_for_bor_state_sync() { --tx-hash "${withdraw_tx_hash}") # Start the exit on L1 with the generated payload. + echo "Starting the exit on L1 with the generated payload..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" # Process the exit on L1. + echo "Processing the exit on L1..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_ERC20_TOKEN_ADDRESS}" @@ -491,6 +500,7 @@ function wait_for_bor_state_sync() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + echo "Generating the exit payload for the burn transaction..." payload=$(polycli pos exit-proof \ --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ @@ -498,10 +508,12 @@ function wait_for_bor_state_sync() { --tx-hash "${withdraw_tx_hash}") # Start the exit on L1 with the generated payload. + echo "Starting the exit on L1 with the generated payload..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" # Process the exit on L1. + echo "Processing the exit on L1..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_ERC721_TOKEN_ADDRESS}" From 03a7e3b67746393a3f3079707f651b548cc5d51b Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 17:26:24 +0100 Subject: [PATCH 37/45] feat: allow to fail on exit payload generation and retry --- TESTSINVENTORY.md | 18 ++++++++-------- tests/pos/bridge.bats | 49 +++++++++++++++++++++++++------------------ 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index ecd3fb8c..77305c22 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -378,11 +378,11 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getAuthor returns a valid address for latest block | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L486) | | | bor_getCurrentValidators returns a non-empty validator list | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L509) | | | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | -| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L304) | | -| bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L406) | | -| bridge ETH from L1 to L2 and confirm MaticWeth balance increased on L2 | [Link](./tests/pos/bridge.bats#L201) | | -| bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L98) | | -| bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L60) | | +| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L325) | | +| bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L427) | | +| bridge ETH from L1 to L2 and confirm MaticWeth balance increased on L2 | [Link](./tests/pos/bridge.bats#L222) | | +| bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L119) | | +| bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L81) | | | coinbase balance increases by at least the priority fee portion of gas cost | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L318) | | | concurrent write/read race: tx submissions and state reads do not interfere | [Link](./tests/pos/execution-specs/rpc/rpc-concurrent-load-and-stress.bats#L248) | | | consecutive block baseFees are within ±5% of each other | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L183) | | @@ -479,10 +479,10 @@ Table of tests currently implemented or being implemented in the E2E repository. | update validator top-up fee | [Link](./tests/pos/validator.bats#L103) | | | warm COINBASE access costs less than cold access to arbitrary address (EIP-3651) | [Link](./tests/pos/execution-specs/evm/evm-opcodes-cancun-shanghai-eips.bats#L457) | | | web3_clientVersion returns a non-empty version string | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L400) | | -| withdraw ERC20 tokens from L2 to L1 and confirm ERC20 balance increased on L1 | [Link](./tests/pos/bridge.bats#L341) | | -| withdraw ERC721 token from L2 to L1 and confirm ERC721 balance increased on L1 | [Link](./tests/pos/bridge.bats#L450) | | -| withdraw MaticWeth from L2 and confirm ETH balance increased on L1 | [Link](./tests/pos/bridge.bats#L237) | | -| withdraw native tokens from L2 and confirm POL balance increased on L1 | [Link](./tests/pos/bridge.bats#L135) | | +| withdraw ERC20 tokens from L2 to L1 and confirm ERC20 balance increased on L1 | [Link](./tests/pos/bridge.bats#L362) | | +| withdraw ERC721 token from L2 to L1 and confirm ERC721 balance increased on L1 | [Link](./tests/pos/bridge.bats#L471) | | +| withdraw MaticWeth from L2 and confirm ETH balance increased on L1 | [Link](./tests/pos/bridge.bats#L258) | | +| withdraw native tokens from L2 and confirm POL balance increased on L1 | [Link](./tests/pos/bridge.bats#L156) | | | withdraw validator rewards | [Link](./tests/pos/validator.bats#L311) | | | zero-value self-transfer: only gas consumed, nonce increments | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L530) | | diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 49250ab8..177a5ee7 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -52,6 +52,27 @@ function wait_for_bor_state_sync() { assert_command_eventually_greater_or_equal "${bor_state_sync_count_cmd}" $((state_sync_count + 1)) "${timeout_seconds}" "${interval_seconds}" } +function generate_exit_payload() { + local tx_hash="$1" + local deadline=$((SECONDS + timeout_seconds)) + local payload="" + while [[ $SECONDS -lt $deadline ]]; do + echo "Trying to generate exit payload for tx ${tx_hash}..." + if payload=$(polycli pos exit-proof \ + --l1-rpc-url "${L1_RPC_URL}" \ + --l2-rpc-url "${L2_RPC_URL}" \ + --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ + --tx-hash "${tx_hash}" 2>/dev/null); then + echo "${payload}" + return 0 + fi + echo "Checkpoint not yet indexed, retrying in ${interval_seconds}s..." + sleep "${interval_seconds}" + done + echo "Error: failed to generate exit payload for tx ${tx_hash} within ${timeout_seconds} seconds." >&2 + return 1 +} + ############################################################################## # POL / MATIC <-> Native L2 ############################################################################## @@ -174,12 +195,9 @@ function wait_for_bor_state_sync() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + # Retried in a loop because the checkpoint may not yet be indexed by polycli even after being confirmed on L1. echo "Generating the exit payload for the burn transaction..." - payload=$(polycli pos exit-proof \ - --l1-rpc-url "${L1_RPC_URL}" \ - --l2-rpc-url "${L2_RPC_URL}" \ - --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}") + payload=$(generate_exit_payload "${withdraw_tx_hash}") # Start the exit on L1 with the generated payload. echo "Starting the exit on L1 with the generated payload..." @@ -279,12 +297,9 @@ function wait_for_bor_state_sync() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + # Retried in a loop because the checkpoint may not yet be indexed by polycli even after being confirmed on L1. echo "Generating the exit payload for the burn transaction..." - payload=$(polycli pos exit-proof \ - --l1-rpc-url "${L1_RPC_URL}" \ - --l2-rpc-url "${L2_RPC_URL}" \ - --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}") + payload=$(generate_exit_payload "${withdraw_tx_hash}") # Start the exit on L1 with the generated payload. # MaticWeth is a mintable token on L2, so it uses startExitForMintableBurntTokens. @@ -385,12 +400,9 @@ function wait_for_bor_state_sync() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + # Retried in a loop because the checkpoint may not yet be indexed by polycli even after being confirmed on L1. echo "Generating the exit payload for the burn transaction..." - payload=$(polycli pos exit-proof \ - --l1-rpc-url "${L1_RPC_URL}" \ - --l2-rpc-url "${L2_RPC_URL}" \ - --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}") + payload=$(generate_exit_payload "${withdraw_tx_hash}") # Start the exit on L1 with the generated payload. echo "Starting the exit on L1 with the generated payload..." @@ -500,12 +512,9 @@ function wait_for_bor_state_sync() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. + # Retried in a loop because the checkpoint may not yet be indexed by polycli even after being confirmed on L1. echo "Generating the exit payload for the burn transaction..." - payload=$(polycli pos exit-proof \ - --l1-rpc-url "${L1_RPC_URL}" \ - --l2-rpc-url "${L2_RPC_URL}" \ - --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}") + payload=$(generate_exit_payload "${withdraw_tx_hash}") # Start the exit on L1 with the generated payload. echo "Starting the exit on L1 with the generated payload..." From 773196bd67d12696ebc19f6c6122392c9dbe16c3 Mon Sep 17 00:00:00 2001 From: leovct Date: Wed, 25 Mar 2026 17:31:19 +0100 Subject: [PATCH 38/45] fix: typos --- tests/pos/bridge.bats | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 177a5ee7..e2bf29bd 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -57,7 +57,7 @@ function generate_exit_payload() { local deadline=$((SECONDS + timeout_seconds)) local payload="" while [[ $SECONDS -lt $deadline ]]; do - echo "Trying to generate exit payload for tx ${tx_hash}..." + echo "Trying to generate exit payload for tx ${tx_hash}..." >&2 if payload=$(polycli pos exit-proof \ --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ @@ -66,7 +66,7 @@ function generate_exit_payload() { echo "${payload}" return 0 fi - echo "Checkpoint not yet indexed, retrying in ${interval_seconds}s..." + echo "Checkpoint not yet indexed, retrying in ${interval_seconds}s..." >&2 sleep "${interval_seconds}" done echo "Error: failed to generate exit payload for tx ${tx_hash} within ${timeout_seconds} seconds." >&2 From 143de3c87d0d5c06883e9ae8639cee6224070d94 Mon Sep 17 00:00:00 2001 From: leovct Date: Fri, 27 Mar 2026 08:35:01 +0100 Subject: [PATCH 39/45] fix(pos): fix native token Plasma withdraw test - bridge.bats: route startExitWithBurntTokens to ERC20Predicate (not WithdrawManagerProxy), with --gas-limit 500000 - bridge.bats: pass --log-index 1 for native token burn (0x1010 emits LogTransfer at idx 0, Withdraw at idx 1) - bridge.bats: retry processExits(MATIC) in a loop with --gas-limit 500000; without explicit limit cast send under-estimates gas when the exit is not yet processable (~36K vs actual ~125K) - bridge.bats: check for null initial_checkpoint_id in all withdraw tests - pos-setup.bash: export L1_ERC20_PREDICATE_ADDRESS - eventually.bash: skip non-numeric results in assert_command_eventually_greater_or_equal Co-Authored-By: Claude Sonnet 4.6 --- core/helpers/pos-setup.bash | 4 ++ core/helpers/scripts/eventually.bash | 2 +- tests/pos/bridge.bats | 55 +++++++++++++++++++++------- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/core/helpers/pos-setup.bash b/core/helpers/pos-setup.bash index 0a5a86c7..fdf1618e 100644 --- a/core/helpers/pos-setup.bash +++ b/core/helpers/pos-setup.bash @@ -32,6 +32,7 @@ pos_setup() { [[ -z "${L1_ROOT_CHAIN_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS:-}" ]] || + [[ -z "${L1_ERC20_PREDICATE_ADDRESS:-}" ]] || [[ -z "${L1_STAKE_MANAGER_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_STAKING_INFO_ADDRESS:-}" ]] || [[ -z "${L1_MATIC_TOKEN_ADDRESS:-}" ]] || @@ -58,6 +59,9 @@ pos_setup() { export L1_WITHDRAW_MANAGER_PROXY_ADDRESS=${L1_WITHDRAW_MANAGER_PROXY_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.WithdrawManagerProxy')} echo "L1_WITHDRAW_MANAGER_PROXY_ADDRESS=${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" + export L1_ERC20_PREDICATE_ADDRESS=${L1_ERC20_PREDICATE_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.predicates.ERC20Predicate')} + echo "L1_ERC20_PREDICATE_ADDRESS=${L1_ERC20_PREDICATE_ADDRESS}" + export L1_STAKE_MANAGER_PROXY_ADDRESS=${L1_STAKE_MANAGER_PROXY_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.StakeManagerProxy')} echo "L1_STAKE_MANAGER_PROXY_ADDRESS=${L1_STAKE_MANAGER_PROXY_ADDRESS}" diff --git a/core/helpers/scripts/eventually.bash b/core/helpers/scripts/eventually.bash index a27fd5d0..ffc3e2e3 100644 --- a/core/helpers/scripts/eventually.bash +++ b/core/helpers/scripts/eventually.bash @@ -81,7 +81,7 @@ function assert_command_eventually_greater_or_equal() { result=$(eval "${command}") echo "[$(date '+%Y-%m-%d %H:%M:%S')] Result: ${result}" - if [[ "${result}" -ge "${target}" ]]; then + if [[ "${result}" =~ ^[0-9]+$ ]] && [[ "${result}" -ge "${target}" ]]; then break fi diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index e2bf29bd..5e63e987 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -54,15 +54,17 @@ function wait_for_bor_state_sync() { function generate_exit_payload() { local tx_hash="$1" + local log_index="${2:-0}" local deadline=$((SECONDS + timeout_seconds)) local payload="" while [[ $SECONDS -lt $deadline ]]; do - echo "Trying to generate exit payload for tx ${tx_hash}..." >&2 + echo "Trying to generate exit payload for tx ${tx_hash} (log-index=${log_index})..." >&2 if payload=$(polycli pos exit-proof \ --l1-rpc-url "${L1_RPC_URL}" \ --l2-rpc-url "${L2_RPC_URL}" \ --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${tx_hash}" 2>/dev/null); then + --tx-hash "${tx_hash}" \ + --log-index "${log_index}" 2>/dev/null); then echo "${payload}" return 0 fi @@ -161,6 +163,8 @@ function generate_exit_payload() { initial_l2_balance=$(cast balance --rpc-url "${L2_RPC_URL}" "${address}") checkpoint_count_cmd='curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq --raw-output ".checkpoint.id"' initial_checkpoint_id=$(eval "${checkpoint_count_cmd}") + # Default to 0 if no checkpoint has been produced yet (fresh enclave). + [[ "${initial_checkpoint_id}" == "null" ]] && initial_checkpoint_id=0 echo "Initial balances and state:" echo "- L1 POL balance: ${initial_l1_pol_balance}" @@ -196,22 +200,39 @@ function generate_exit_payload() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. # Retried in a loop because the checkpoint may not yet be indexed by polycli even after being confirmed on L1. + # The native token (0x1010) emits LogTransfer at log index 0 and Withdraw at log index 1, so we pass --log-index 1. echo "Generating the exit payload for the burn transaction..." - payload=$(generate_exit_payload "${withdraw_tx_hash}") + payload=$(generate_exit_payload "${withdraw_tx_hash}" 1) - # Start the exit on L1 with the generated payload. + # Start the exit on L1 via the ERC20Predicate contract. + # Note: startExitWithBurntTokens is on ERC20Predicate, not on WithdrawManagerProxy. echo "Starting the exit on L1 with the generated payload..." cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" - - # Process the exit on L1. - echo "Processing the exit on L1..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_MATIC_TOKEN_ADDRESS}" - - # Verify L1 POL balance increased by the withdrawn amount. - echo "Verifying L1 POL balance increased..." - assert_token_balance_eventually_greater_or_equal "${L1_POL_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_pol_balance} + ${withdraw_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + --gas-limit 500000 \ + "${L1_ERC20_PREDICATE_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" + + # Process the exit on L1 and verify the POL balance increased. + # Exits are queued under MATIC (the rootToken in the Withdraw event topic[1]). + # WithdrawManager converts MATIC exits to POL when releasing funds. + # processExits is retried in a loop because it may return without processing if the + # exit window (HALF_EXIT_PERIOD=1s) has not elapsed yet at the time of the first call. + echo "Processing the exit on L1 and verifying POL balance increased..." + target_l1_pol_balance="$(echo "${initial_l1_pol_balance} + ${withdraw_amount}" | bc)" + deadline=$((SECONDS + timeout_seconds)) + while [[ $SECONDS -lt $deadline ]]; do + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" --gas-limit 500000 \ + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_MATIC_TOKEN_ADDRESS}" >/dev/null 2>&1 || true + balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') + echo "[$(date '+%Y-%m-%d %H:%M:%S')] L1 POL balance: ${balance} (target: ${target_l1_pol_balance})" + if [ "$(echo "${balance} >= ${target_l1_pol_balance}" | bc)" -eq 1 ]; then + break + fi + sleep "${interval_seconds}" + done + if [ "$(echo "${balance} >= ${target_l1_pol_balance}" | bc)" -ne 1 ]; then + echo "Timeout: L1 POL balance did not reach target within ${timeout_seconds} seconds." + exit 1 + fi } ############################################################################## @@ -263,6 +284,8 @@ function generate_exit_payload() { initial_l2_balance=$(cast call --rpc-url "${L2_RPC_URL}" --json "${L2_WETH_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') checkpoint_count_cmd='curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq --raw-output ".checkpoint.id"' initial_checkpoint_id=$(eval "${checkpoint_count_cmd}") + # Default to 0 if no checkpoint has been produced yet (fresh enclave). + [[ "${initial_checkpoint_id}" == "null" ]] && initial_checkpoint_id=0 echo "Initial balances and state:" echo "- L1 ETH balance: ${initial_l1_balance}" @@ -367,6 +390,8 @@ function generate_exit_payload() { initial_l2_balance=$(cast call --rpc-url "${L2_RPC_URL}" --json "${L2_ERC20_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') checkpoint_count_cmd='curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq --raw-output ".checkpoint.id"' initial_checkpoint_id=$(eval "${checkpoint_count_cmd}") + # Default to 0 if no checkpoint has been produced yet (fresh enclave). + [[ "${initial_checkpoint_id}" == "null" ]] && initial_checkpoint_id=0 echo "Initial balances and state:" echo "- L1 ERC20 balance: ${initial_l1_balance}" @@ -480,6 +505,8 @@ function generate_exit_payload() { initial_l2_balance=$(cast call --rpc-url "${L2_RPC_URL}" --json "${L2_ERC721_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') checkpoint_count_cmd='curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq --raw-output ".checkpoint.id"' initial_checkpoint_id=$(eval "${checkpoint_count_cmd}") + # Default to 0 if no checkpoint has been produced yet (fresh enclave). + [[ "${initial_checkpoint_id}" == "null" ]] && initial_checkpoint_id=0 echo "Initial balances and state:" echo "- L1 ERC721 balance: ${initial_l1_balance}" From 7a9c490a8dbf5b927e80e634e2231dd01dd1f5bb Mon Sep 17 00:00:00 2001 From: leovct Date: Fri, 27 Mar 2026 08:35:16 +0100 Subject: [PATCH 40/45] docs: add Plasma exit debugging guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents the full L2→L1 Plasma exit flow with debugging commands, common revert messages, payload format details, and contract internals. Co-Authored-By: Claude Sonnet 4.6 --- docs/debugging-plasma-exits.md | 330 +++++++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 docs/debugging-plasma-exits.md diff --git a/docs/debugging-plasma-exits.md b/docs/debugging-plasma-exits.md new file mode 100644 index 00000000..821bee2f --- /dev/null +++ b/docs/debugging-plasma-exits.md @@ -0,0 +1,330 @@ +# Debugging Polygon PoS Plasma Exits + +This guide documents how to debug L2→L1 Plasma exit operations on the Polygon PoS bridge. +It covers the full flow, what can go wrong at each step, and how to investigate issues. + +## Overview: The Plasma Exit Flow + +``` +L2: burn tokens (withdraw(uint256) on 0x1010) + ↓ + wait for checkpoint (Heimdall submits block range to L1 RootChain) + ↓ + generate exit payload (polycli pos exit-proof) + ↓ +L1: startExitWithBurntTokens(bytes) on ERC20Predicate + ↓ + wait for HALF_EXIT_PERIOD (1s on devnet, ~7 days on mainnet) + ↓ +L1: processExits(MATIC) on WithdrawManagerProxy + ↓ + POL balance increases +``` + +## Key Contracts + +| Contract | Address (devnet) | Role | +|---|---|---| +| ERC20Predicate | `0x1D4b8c4d...CA35` | Verifies burn proof, queues exit | +| WithdrawManagerProxy | `0x862ff216...10` | Manages exit queue, processes exits | +| WithdrawManager (impl) | `0x489E1b7F...73` | WithdrawManager logic (behind proxy) | +| RootChainProxy | `0x39b4be5a...68` | Stores checkpoint data | +| ExitNFT | `0xF2dd130f...A4` | NFT minted when exit is queued | + +Get addresses from the enclave: +```bash +kurtosis files inspect pos matic-contract-addresses contractAddresses.json | jq '.root.predicates, .root.WithdrawManagerProxy' +``` + +## Step-by-Step Debugging + +### Step 1: Burn on L2 + +```bash +# Send the burn transaction +withdraw_receipt=$(cast send \ + --rpc-url "${L2_RPC_URL}" \ + --private-key "${PRIVATE_KEY}" \ + --value "1000000000000000000" \ + --json \ + "0x0000000000000000000000000000000000001010" \ + "withdraw(uint256)" "1000000000000000000") + +withdraw_tx_hash=$(echo "${withdraw_receipt}" | jq -r '.transactionHash') +withdraw_block=$(echo "${withdraw_receipt}" | jq -r '.blockNumber' | xargs printf "%d") +echo "Burn tx: ${withdraw_tx_hash} at block ${withdraw_block}" +``` + +**What to check:** +- `status: 1` — if status 0, the burn failed. The 0x1010 contract requires the amount to be bridge-backed. You must run a bridge (deposit) test first. +- Check log index 1 is `Withdraw` event: `cast receipt --rpc-url "${L2_RPC_URL}" --json "${withdraw_tx_hash}" | jq '.logs[1].topics[0]'` should be `0xebff2602...` + +**Common issue:** Burn reverts because no bridge-backed balance exists. Run the bridge POL test first. + +### Step 2: Wait for Checkpoint + +```bash +# Poll Heimdall for checkpoint count +curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq '.checkpoint.id' + +# A new checkpoint should appear covering withdraw_block +# This can take ~1-2 minutes on devnet +``` + +**What to check:** +- The checkpoint ID should increment, and the checkpoint's `end_block` should be >= `withdraw_block` +- Query a specific checkpoint: `curl -s "${L2_CL_API_URL}/checkpoints/${CHECKPOINT_ID}" | jq .` + +### Step 3: Generate Exit Payload + +```bash +# The native token burn is at log index 1 (LogTransfer=0, Withdraw=1, LogFeeTransfer=2) +payload=$(polycli pos exit-proof \ + --l1-rpc-url "${L1_RPC_URL}" \ + --l2-rpc-url "${L2_RPC_URL}" \ + --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ + --tx-hash "${withdraw_tx_hash}" \ + --log-index 1 2>/dev/null) +echo "Payload length: ${#payload}" +echo "First byte: 0x${payload:2:2}" # Should be 0xf9 (RLP long list prefix) +``` + +**What to check:** +- First byte `0xf9` or `0xf8` or anything `>= 0xc0` = RLP list (correct) +- First byte `0x00` = ABI encoding (wrong — polycli bug) +- If polycli fails: checkpoint not yet indexed, retry in a few seconds + +**Debugging payload format (RLP decode):** +```bash +# Check the branchMask field (field[8] in the RLP list) +# It should start with 0x00 (extension HP encoding) +# The branchMask for tx at index 0 should be 0x0080 +python3 -c " +import rlp +data = bytes.fromhex('${payload[2:]}') +fields = rlp.decode(data) +print('branchMask:', fields[8].hex()) # Should be 0080 for txIndex=0 +print('logIndex:', int.from_bytes(fields[9], 'big')) # Should be 1 +" +``` + +### Step 4: Simulate startExitWithBurntTokens + +Always simulate before sending: +```bash +address=$(cast wallet address --private-key "${PRIVATE_KEY}") +cast call \ + --rpc-url "${L1_RPC_URL}" \ + --from "${address}" \ + "${L1_ERC20_PREDICATE_ADDRESS}" \ + "startExitWithBurntTokens(bytes)" \ + "${payload}" 2>&1 +``` + +**Expected:** `0x` (empty return, no revert) + +**Common revert messages and their causes:** + +| Error | Cause | Fix | +|---|---|---| +| `incorrect mask` | branchMask[0] != 0 (HP leaf encoding used instead of extension) | polycli bug: don't include terminator in hexToCompact | +| `INVALID_RECEIPT_MERKLE_PROOF` | Proof nodes encoded as byte strings, not RLP lists | polycli bug: use `rlp.RawValue` for proof nodes | +| `WITHDRAW_BLOCK_NOT_A_PART_OF_SUBMITTED_HEADER` | Block Merkle leaf is wrong (using block hash instead of keccak256(n,ts,txRoot,receiptsRoot)) | polycli bug: compute correct leaf | +| `Not a withdraw event signature` | Wrong log index — pointed to LogTransfer instead of Withdraw | Use `--log-index 1` for native token | +| `Withdrawer and burn exit tx do not match` | `msg.sender` != topic[2] (from address in Withdraw event) | Send from the same address that did the burn | + +### Step 5: Send startExitWithBurntTokens + +```bash +cast send \ + --rpc-url "${L1_RPC_URL}" \ + --private-key "${PRIVATE_KEY}" \ + --gas-limit 500000 \ + "${L1_ERC20_PREDICATE_ADDRESS}" \ + "startExitWithBurntTokens(bytes)" \ + "${payload}" +``` + +**What to check after:** +```bash +# Verify ExitStarted event was emitted on WithdrawManager +# The tx receipt should have 2 logs: +# - Log 0: ExitNFT Transfer (minted to user) +# - Log 1: WithdrawManager ExitStarted +cast receipt --rpc-url "${L1_RPC_URL}" --json "${TX_HASH}" | jq '.logs | length' + +# Get the exit ID +cast receipt --rpc-url "${L1_RPC_URL}" --json "${TX_HASH}" | jq '.logs[1].topics[2]' + +# Verify queue has 1 exit under MATIC +queue=$(cast call --rpc-url "${L1_RPC_URL}" "${WITHDRAW_MANAGER}" "exitsQueues(address)(address)" "${MATIC}") +cast call --rpc-url "${L1_RPC_URL}" "${queue}" "currentSize()(uint256)" +# Should return 1 +``` + +### Step 6: Check Exit is Processable + +```bash +WITHDRAW_MANAGER="0x862ff216d822fBF2F381812627D4216d2150e810" +MATIC="0x8E1700577B7aE261753c67e1B93Fe60Dd3e205fa" +queue=$(cast call --rpc-url "${L1_RPC_URL}" "${WITHDRAW_MANAGER}" "exitsQueues(address)(address)" "${MATIC}") + +# Get exitableAt from the queue +cast call --rpc-url "${L1_RPC_URL}" "${queue}" "getMin()(uint256,uint256)" +# First return value = exitableAt (unix timestamp) + +# Get current block timestamp +curl -s -X POST "${L1_RPC_URL}" \ + -H "Content-Type: application/json" \ + -d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false],"id":1}' \ + | jq -r '.result.timestamp' | xargs printf "%d\n" + +# Exit is processable when: exitableAt <= block.timestamp +``` + +**Key insight:** `exitableAt = Math.max(checkpoint_createdAt + 2*HALF_EXIT_PERIOD, now_at_startExit + HALF_EXIT_PERIOD)`. With `HALF_EXIT_PERIOD=1`, the exit may not be processable for 1 second. On a fast devnet, `processExits` called in the very next block might still be too early. Retry in a loop. + +### Step 7: Process Exit + +```bash +cast send \ + --rpc-url "${L1_RPC_URL}" \ + --private-key "${PRIVATE_KEY}" \ + "${WITHDRAW_MANAGER}" \ + "processExits(address)" \ + "${MATIC}" +``` + +**What to check:** +- If `logs: []` — either exit not yet processable (retry) or already processed (queue empty now) +- If `Withdraw` event emitted — exit was processed +- The `Withdraw` event is on WithdrawManagerProxy (0x862ff2...) + +**Check if POL balance increased:** +```bash +cast call --rpc-url "${L1_RPC_URL}" --json \ + "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq -r '.[0]' +``` + +## Advanced Debugging: Tracing Transactions + +### Trace a failed transaction +```bash +# Requires archive node or recent block +cast run --rpc-url "${L1_RPC_URL}" "${TX_HASH}" + +# Or use debug_traceCall to simulate at latest block +curl -s -X POST "${L1_RPC_URL}" \ + -H "Content-Type: application/json" \ + -d '{ + "jsonrpc":"2.0", + "method":"debug_traceCall", + "params":[{ + "from":"YOUR_ADDRESS", + "to":"CONTRACT_ADDRESS", + "data":"CALLDATA", + "gas":"0x1E8480" + },"latest",{"tracer":"callTracer","tracerConfig":{"withLog":true}}], + "id":1 + }' | jq '.result' +``` + +### Get revert reason from a failed call +```bash +cast call --rpc-url "${L1_RPC_URL}" \ + --from "${address}" \ + "${CONTRACT}" "FUNCTION_SIG" "ARGS" 2>&1 +# Error: execution reverted: REVERT_REASON +``` + +### Inspect the exit queue state +```bash +# Check queue size +queue=$(cast call --rpc-url "${L1_RPC_URL}" "${WITHDRAW_MANAGER}" "exitsQueues(address)(address)" "${TOKEN}") +cast call --rpc-url "${L1_RPC_URL}" "${queue}" "currentSize()(uint256)" + +# Get minimum priority (exitableAt) and value (lower exitId bits) +cast call --rpc-url "${L1_RPC_URL}" "${queue}" "getMin()(uint256,uint256)" + +# Reconstruct the exitId +python3 -c " +exit_at = EXITABLE_AT +lower = LOWER_VALUE +exit_id = (exit_at << 128) | lower +print('exitId:', exit_id) +print('exitId hex:', hex(exit_id)) +" + +# Check exits[exitId] storage (amount, txHash, exitor, token, isRegularExit, predicate) +cast call --rpc-url "${L1_RPC_URL}" "${WITHDRAW_MANAGER}" \ + "exits(uint256)(uint256,bytes32,address,address,bool,address)" "${EXIT_ID}" + +# Check ExitNFT ownership +EXIT_NFT="0xF2dd130f8dfA4f55c6CCABB48f385c100c37D8A4" +cast call --rpc-url "${L1_RPC_URL}" "${EXIT_NFT}" "exists(uint256)(bool)" "${EXIT_ID}" +cast call --rpc-url "${L1_RPC_URL}" "${EXIT_NFT}" "ownerOf(uint256)(address)" "${EXIT_ID}" +``` + +## Understanding the Exit Payload Format + +The payload is an **RLP-encoded list** of 10 fields: + +``` +RLP([ + headerNumber, // uint: checkpoint ID × stride (e.g., checkpoint 2 → 20000) + blockProof, // bytes: binary Merkle proof of the block within the checkpoint + blockNumber, // uint: L2 block number containing the burn tx + blockTimestamp, // uint: timestamp of that L2 block + txRoot, // bytes32: transactions root of that L2 block + receiptRoot, // bytes32: receipts root of that L2 block + receipt, // bytes: RLP-encoded burn transaction receipt + receiptParentNodes, // bytes: RLP-encoded list of MPT proof nodes (root-to-leaf) + branchMask, // bytes: HP-encoded path through receipt MPT (e.g., [0x00, 0x80] for txIndex=0) + logIndex // uint: which log in the receipt is the Withdraw event (1 for native token) +]) +``` + +**branchMask explained:** +- The receipt MPT uses `rlp(txIndex)` as the key (e.g., txIndex=0 → key=`0x80`) +- The key nibbles are `[8, 0]` for key `0x80` +- HP encoding without terminator: `[0x00, 0x80]` (extension prefix 0x00 = even length) +- `verifyInclusion` requires `branchMaskBytes[0] == 0` — this is satisfied by extension encoding + +**blockProof (checkpoint Merkle proof) explained:** +- The checkpoint stores a Merkle root of all blocks in the range +- Each leaf is: `keccak256(abi.encodePacked(blockNum_32, blockTime_32, txRoot_32, receiptsRoot_32))` +- This is NOT the Ethereum block hash — it's a hash of specific fields +- The proof is an array of sibling hashes concatenated (32 bytes each) + +## Why MATIC and not POL for processExits? + +The 0x1010 contract on L2 emits: +```solidity +event Withdraw(address indexed token, address indexed from, uint256 amount, uint256 input1, uint256 output1) +``` + +where `token` = the L1 root token = MATIC (the original mapping before POL migration). + +`ERC20Predicate.startExitWithBurntTokens` reads `rootToken = topics[1] = MATIC` and calls `addExitToQueue(..., rootToken=MATIC, ...)`. + +`WithdrawManager` indexes exits under `exitsQueues[rootToken]` = `exitsQueues[MATIC]`. + +So `processExits(MATIC)` is correct. Internally, `onFinalizeExit` converts MATIC→POL via the registry and transfers POL to the user. + +## Proxy vs Implementation + +`WithdrawManagerProxy` delegates most calls to `WithdrawManager` (impl). BUT some functions are implemented DIRECTLY in the proxy (don't delegate): +- `exitsQueues(address)` — reads proxy storage directly +- `exits(uint256)` — reads proxy storage directly +- `exitNft()` — reads proxy storage + +When debugging, ensure you compute the **correct function selector**: +```bash +cast keccak "processExits(address)" | head -c 10 +# → 0x0f6795f2 + +cast keccak "startExitWithBurntTokens(bytes)" | head -c 10 +# → 0x6f7c1f04 +``` + +The wrong selector might accidentally hit a proxy-direct function (e.g., `0x72d8d524` is not `processExits`). From 2ab883ab2361becb57107bc75dc213e313fa164c Mon Sep 17 00:00:00 2001 From: leovct Date: Fri, 27 Mar 2026 10:19:08 +0100 Subject: [PATCH 41/45] fix(pos): fix WETH, ERC20, and ERC721 Plasma withdraw tests All three withdraw tests had the same bugs as the native token test: - Wrong target contract for startExitWithBurntTokens (was WithdrawManagerProxy, now ERC20Predicate for WETH/ERC20 and ERC721Predicate for ERC721) - Wrong log index (now --log-index 1; all tokens emit Transfer at 0 and Withdraw(rootToken, from, ...) at 1) - processExits called once without gas limit (now uses --gas-limit 500000) Also replace all processExits retry loops with a new process_plasma_exit helper: - Reads exitableAt from WithdrawManager.exitsQueues(token).getMin() - Sleeps precisely until exitableAt+2s instead of polling blindly - Calls processExits once with --gas-limit 500000 Other fixes: - export L1_ERC721_PREDICATE_ADDRESS in pos-setup.bash - generate_exit_payload accepts an optional 3rd timeout arg (withdraw tests pass 2*timeout_seconds to survive checkpoint boundary gaps) - WETH ETH balance check subtracts 0.01 ETH gas allowance (both startExit and processExits consume ETH as the gas token) Co-Authored-By: Claude Sonnet 4.6 --- core/helpers/pos-setup.bash | 4 ++ tests/pos/bridge.bats | 130 ++++++++++++++++++++++++------------ 2 files changed, 92 insertions(+), 42 deletions(-) diff --git a/core/helpers/pos-setup.bash b/core/helpers/pos-setup.bash index fdf1618e..74c27981 100644 --- a/core/helpers/pos-setup.bash +++ b/core/helpers/pos-setup.bash @@ -33,6 +33,7 @@ pos_setup() { [[ -z "${L1_DEPOSIT_MANAGER_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_ERC20_PREDICATE_ADDRESS:-}" ]] || + [[ -z "${L1_ERC721_PREDICATE_ADDRESS:-}" ]] || [[ -z "${L1_STAKE_MANAGER_PROXY_ADDRESS:-}" ]] || [[ -z "${L1_STAKING_INFO_ADDRESS:-}" ]] || [[ -z "${L1_MATIC_TOKEN_ADDRESS:-}" ]] || @@ -62,6 +63,9 @@ pos_setup() { export L1_ERC20_PREDICATE_ADDRESS=${L1_ERC20_PREDICATE_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.predicates.ERC20Predicate')} echo "L1_ERC20_PREDICATE_ADDRESS=${L1_ERC20_PREDICATE_ADDRESS}" + export L1_ERC721_PREDICATE_ADDRESS=${L1_ERC721_PREDICATE_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.predicates.ERC721Predicate')} + echo "L1_ERC721_PREDICATE_ADDRESS=${L1_ERC721_PREDICATE_ADDRESS}" + export L1_STAKE_MANAGER_PROXY_ADDRESS=${L1_STAKE_MANAGER_PROXY_ADDRESS:-$(echo "${matic_contract_addresses}" | jq --raw-output '.root.StakeManagerProxy')} echo "L1_STAKE_MANAGER_PROXY_ADDRESS=${L1_STAKE_MANAGER_PROXY_ADDRESS}" diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index 5e63e987..a4a9fb53 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -52,10 +52,55 @@ function wait_for_bor_state_sync() { assert_command_eventually_greater_or_equal "${bor_state_sync_count_cmd}" $((state_sync_count + 1)) "${timeout_seconds}" "${interval_seconds}" } +# process_plasma_exit queues → waits for the exit window → calls processExits once. +# +# Instead of a blind retry loop, it reads exitableAt from the on-chain priority queue +# (WithdrawManager.exitsQueues(token).getMin()) and sleeps precisely until the exit +# window opens (HALF_EXIT_PERIOD = 1s on devnet, ~7 days on mainnet), then calls +# processExits with an explicit gas limit. +# +# Usage: process_plasma_exit +# token_address — the L1 root token whose exit queue to check (MATIC, WETH, ERC20, ERC721…) +function process_plasma_exit() { + local token="$1" + + # Resolve the priority queue contract for this token. + local queue + queue=$(cast call --rpc-url "${L1_RPC_URL}" "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" \ + "exitsQueues(address)(address)" "${token}") + echo "Exit queue for ${token}: ${queue}" + + # Read exitableAt from the queue's min-heap entry. + local exitable_at + exitable_at=$(cast call --rpc-url "${L1_RPC_URL}" "${queue}" "getMin()(uint256,uint256)" \ + | awk 'NR==1{print $1}') + echo "exitableAt: ${exitable_at}" + + # Compute how many seconds until the exit window opens. + local current_ts + current_ts=$(cast block --rpc-url "${L1_RPC_URL}" --json | jq -r '.timestamp' | xargs printf "%d\n") + local wait_secs=$(( exitable_at - current_ts + 2 )) + if [[ $wait_secs -gt 0 ]]; then + echo "Exit window opens in ${wait_secs}s (now=${current_ts}, exitableAt=${exitable_at}), sleeping..." + sleep "$wait_secs" + fi + + # processExits must use --gas-limit: without it cast send auto-estimates gas at the time + # of the eth_estimateGas call (which may see a short early-return path when exitableAt has + # not elapsed yet), producing a gas estimate too low for the actual ~125K execution. + echo "Calling processExits(${token})..." + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" --gas-limit 500000 \ + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${token}" +} + function generate_exit_payload() { local tx_hash="$1" local log_index="${2:-0}" - local deadline=$((SECONDS + timeout_seconds)) + # Optional 3rd arg overrides timeout; defaults to timeout_seconds. + # Withdraw tests pass 2*timeout_seconds because the burn tx may land just before a + # checkpoint boundary, requiring polycli to wait for the NEXT checkpoint (~128 blocks later). + local timeout="${3:-${timeout_seconds}}" + local deadline=$((SECONDS + timeout)) local payload="" while [[ $SECONDS -lt $deadline ]]; do echo "Trying to generate exit payload for tx ${tx_hash} (log-index=${log_index})..." >&2 @@ -202,7 +247,7 @@ function generate_exit_payload() { # Retried in a loop because the checkpoint may not yet be indexed by polycli even after being confirmed on L1. # The native token (0x1010) emits LogTransfer at log index 0 and Withdraw at log index 1, so we pass --log-index 1. echo "Generating the exit payload for the burn transaction..." - payload=$(generate_exit_payload "${withdraw_tx_hash}" 1) + payload=$(generate_exit_payload "${withdraw_tx_hash}" 1 $((2 * timeout_seconds))) # Start the exit on L1 via the ERC20Predicate contract. # Note: startExitWithBurntTokens is on ERC20Predicate, not on WithdrawManagerProxy. @@ -211,28 +256,17 @@ function generate_exit_payload() { --gas-limit 500000 \ "${L1_ERC20_PREDICATE_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" - # Process the exit on L1 and verify the POL balance increased. + # Process the exit on L1. # Exits are queued under MATIC (the rootToken in the Withdraw event topic[1]). # WithdrawManager converts MATIC exits to POL when releasing funds. - # processExits is retried in a loop because it may return without processing if the - # exit window (HALF_EXIT_PERIOD=1s) has not elapsed yet at the time of the first call. - echo "Processing the exit on L1 and verifying POL balance increased..." - target_l1_pol_balance="$(echo "${initial_l1_pol_balance} + ${withdraw_amount}" | bc)" - deadline=$((SECONDS + timeout_seconds)) - while [[ $SECONDS -lt $deadline ]]; do - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" --gas-limit 500000 \ - "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_MATIC_TOKEN_ADDRESS}" >/dev/null 2>&1 || true - balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') - echo "[$(date '+%Y-%m-%d %H:%M:%S')] L1 POL balance: ${balance} (target: ${target_l1_pol_balance})" - if [ "$(echo "${balance} >= ${target_l1_pol_balance}" | bc)" -eq 1 ]; then - break - fi - sleep "${interval_seconds}" - done - if [ "$(echo "${balance} >= ${target_l1_pol_balance}" | bc)" -ne 1 ]; then - echo "Timeout: L1 POL balance did not reach target within ${timeout_seconds} seconds." - exit 1 - fi + echo "Processing the exit on L1..." + process_plasma_exit "${L1_MATIC_TOKEN_ADDRESS}" + + # Verify L1 POL balance increased by the withdrawn amount. + echo "Verifying L1 POL balance increased..." + assert_token_balance_eventually_greater_or_equal "${L1_POL_TOKEN_ADDRESS}" "${address}" \ + "$(echo "${initial_l1_pol_balance} + ${withdraw_amount}" | bc)" \ + "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } ############################################################################## @@ -321,23 +355,30 @@ function generate_exit_payload() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. # Retried in a loop because the checkpoint may not yet be indexed by polycli even after being confirmed on L1. + # The MaticWeth contract emits: log 0 = Transfer, log 1 = Withdraw(rootToken=WETH, from, ...). echo "Generating the exit payload for the burn transaction..." - payload=$(generate_exit_payload "${withdraw_tx_hash}") + payload=$(generate_exit_payload "${withdraw_tx_hash}" 1 $((2 * timeout_seconds))) # Start the exit on L1 with the generated payload. - # MaticWeth is a mintable token on L2, so it uses startExitForMintableBurntTokens. + # ERC20Predicate handles WETH exits: it reads rootToken=WETH from the Withdraw event and queues + # the exit. processExits(WETH) then calls DepositManager which unwraps WETH and sends ETH to user. echo "Starting the exit on L1 with the generated payload..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitForMintableBurntTokens(bytes)" "${payload}" + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" --gas-limit 500000 \ + "${L1_ERC20_PREDICATE_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" # Process the exit on L1. + # WETH is queued under L1_WETH_TOKEN_ADDRESS; processExits unwraps WETH to ETH internally. echo "Processing the exit on L1..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_WETH_TOKEN_ADDRESS}" + process_plasma_exit "${L1_WETH_TOKEN_ADDRESS}" - # Verify L1 ETH balance increased by the withdrawn amount (gas costs excluded). + # Verify L1 ETH balance increased by the withdrawn amount minus gas. + # ETH is the gas token: startExitWithBurntTokens and processExits both consume ETH as gas. + # We allow up to 0.01 ETH (10^16 wei) to cover gas across both transactions. echo "Verifying L1 ETH balance increased..." - assert_ether_balance_eventually_greater_or_equal "${address}" "$(echo "${initial_l1_balance} + ${withdraw_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + local gas_allowance=10000000000000000 + assert_ether_balance_eventually_greater_or_equal "${address}" \ + "$(echo "${initial_l1_balance} + ${withdraw_amount} - ${gas_allowance}" | bc)" \ + "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } ############################################################################## @@ -426,22 +467,25 @@ function generate_exit_payload() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. # Retried in a loop because the checkpoint may not yet be indexed by polycli even after being confirmed on L1. + # The ERC20 contract emits: log 0 = Transfer, log 1 = Withdraw(rootToken=ERC20, from, ...). echo "Generating the exit payload for the burn transaction..." - payload=$(generate_exit_payload "${withdraw_tx_hash}") + payload=$(generate_exit_payload "${withdraw_tx_hash}" 1 $((2 * timeout_seconds))) # Start the exit on L1 with the generated payload. + # ERC20Predicate handles ERC20 exits: reads rootToken from the Withdraw event and queues the exit. echo "Starting the exit on L1 with the generated payload..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" --gas-limit 500000 \ + "${L1_ERC20_PREDICATE_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" # Process the exit on L1. echo "Processing the exit on L1..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_ERC20_TOKEN_ADDRESS}" + process_plasma_exit "${L1_ERC20_TOKEN_ADDRESS}" # Verify L1 ERC20 balance increased by the withdrawn amount. echo "Verifying L1 ERC20 balance increased..." - assert_token_balance_eventually_greater_or_equal "${L1_ERC20_TOKEN_ADDRESS}" "${address}" "$(echo "${initial_l1_balance} + ${withdraw_amount}" | bc)" "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_token_balance_eventually_greater_or_equal "${L1_ERC20_TOKEN_ADDRESS}" "${address}" \ + "$(echo "${initial_l1_balance} + ${withdraw_amount}" | bc)" \ + "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } ############################################################################## @@ -540,20 +584,22 @@ function generate_exit_payload() { # Generate the exit payload for the burn transaction. # It includes the burn tx receipt, a Merkle proof of that receipt in the block's receipts trie, and a checkpoint proof. # Retried in a loop because the checkpoint may not yet be indexed by polycli even after being confirmed on L1. + # The ERC721 contract emits: log 0 = Transfer, log 1 = Withdraw(rootToken=ERC721, tokenId). echo "Generating the exit payload for the burn transaction..." - payload=$(generate_exit_payload "${withdraw_tx_hash}") + payload=$(generate_exit_payload "${withdraw_tx_hash}" 1 $((2 * timeout_seconds))) # Start the exit on L1 with the generated payload. + # ERC721Predicate handles ERC721 exits: reads rootToken from the Withdraw event and queues the exit. echo "Starting the exit on L1 with the generated payload..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" + cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" --gas-limit 500000 \ + "${L1_ERC721_PREDICATE_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" # Process the exit on L1. echo "Processing the exit on L1..." - cast send --rpc-url "${L1_RPC_URL}" --private-key "${PRIVATE_KEY}" \ - "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_ERC721_TOKEN_ADDRESS}" + process_plasma_exit "${L1_ERC721_TOKEN_ADDRESS}" # Verify L1 ERC721 balance increased. echo "Verifying L1 ERC721 balance increased..." - assert_token_balance_eventually_greater_or_equal "${L1_ERC721_TOKEN_ADDRESS}" "${address}" $((initial_l1_balance + 1)) "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + assert_token_balance_eventually_greater_or_equal "${L1_ERC721_TOKEN_ADDRESS}" "${address}" \ + $((initial_l1_balance + 1)) "${L1_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" } From 2279228f8efad5a15916c3ec4ad021edac54eb68 Mon Sep 17 00:00:00 2001 From: leovct Date: Fri, 27 Mar 2026 10:34:46 +0100 Subject: [PATCH 42/45] chore: remove plasma exit debugging doc (moved to vault) Co-Authored-By: Claude Sonnet 4.6 --- docs/debugging-plasma-exits.md | 330 --------------------------------- 1 file changed, 330 deletions(-) delete mode 100644 docs/debugging-plasma-exits.md diff --git a/docs/debugging-plasma-exits.md b/docs/debugging-plasma-exits.md deleted file mode 100644 index 821bee2f..00000000 --- a/docs/debugging-plasma-exits.md +++ /dev/null @@ -1,330 +0,0 @@ -# Debugging Polygon PoS Plasma Exits - -This guide documents how to debug L2→L1 Plasma exit operations on the Polygon PoS bridge. -It covers the full flow, what can go wrong at each step, and how to investigate issues. - -## Overview: The Plasma Exit Flow - -``` -L2: burn tokens (withdraw(uint256) on 0x1010) - ↓ - wait for checkpoint (Heimdall submits block range to L1 RootChain) - ↓ - generate exit payload (polycli pos exit-proof) - ↓ -L1: startExitWithBurntTokens(bytes) on ERC20Predicate - ↓ - wait for HALF_EXIT_PERIOD (1s on devnet, ~7 days on mainnet) - ↓ -L1: processExits(MATIC) on WithdrawManagerProxy - ↓ - POL balance increases -``` - -## Key Contracts - -| Contract | Address (devnet) | Role | -|---|---|---| -| ERC20Predicate | `0x1D4b8c4d...CA35` | Verifies burn proof, queues exit | -| WithdrawManagerProxy | `0x862ff216...10` | Manages exit queue, processes exits | -| WithdrawManager (impl) | `0x489E1b7F...73` | WithdrawManager logic (behind proxy) | -| RootChainProxy | `0x39b4be5a...68` | Stores checkpoint data | -| ExitNFT | `0xF2dd130f...A4` | NFT minted when exit is queued | - -Get addresses from the enclave: -```bash -kurtosis files inspect pos matic-contract-addresses contractAddresses.json | jq '.root.predicates, .root.WithdrawManagerProxy' -``` - -## Step-by-Step Debugging - -### Step 1: Burn on L2 - -```bash -# Send the burn transaction -withdraw_receipt=$(cast send \ - --rpc-url "${L2_RPC_URL}" \ - --private-key "${PRIVATE_KEY}" \ - --value "1000000000000000000" \ - --json \ - "0x0000000000000000000000000000000000001010" \ - "withdraw(uint256)" "1000000000000000000") - -withdraw_tx_hash=$(echo "${withdraw_receipt}" | jq -r '.transactionHash') -withdraw_block=$(echo "${withdraw_receipt}" | jq -r '.blockNumber' | xargs printf "%d") -echo "Burn tx: ${withdraw_tx_hash} at block ${withdraw_block}" -``` - -**What to check:** -- `status: 1` — if status 0, the burn failed. The 0x1010 contract requires the amount to be bridge-backed. You must run a bridge (deposit) test first. -- Check log index 1 is `Withdraw` event: `cast receipt --rpc-url "${L2_RPC_URL}" --json "${withdraw_tx_hash}" | jq '.logs[1].topics[0]'` should be `0xebff2602...` - -**Common issue:** Burn reverts because no bridge-backed balance exists. Run the bridge POL test first. - -### Step 2: Wait for Checkpoint - -```bash -# Poll Heimdall for checkpoint count -curl -s "${L2_CL_API_URL}/checkpoints/latest" | jq '.checkpoint.id' - -# A new checkpoint should appear covering withdraw_block -# This can take ~1-2 minutes on devnet -``` - -**What to check:** -- The checkpoint ID should increment, and the checkpoint's `end_block` should be >= `withdraw_block` -- Query a specific checkpoint: `curl -s "${L2_CL_API_URL}/checkpoints/${CHECKPOINT_ID}" | jq .` - -### Step 3: Generate Exit Payload - -```bash -# The native token burn is at log index 1 (LogTransfer=0, Withdraw=1, LogFeeTransfer=2) -payload=$(polycli pos exit-proof \ - --l1-rpc-url "${L1_RPC_URL}" \ - --l2-rpc-url "${L2_RPC_URL}" \ - --root-chain-address "${L1_ROOT_CHAIN_PROXY_ADDRESS}" \ - --tx-hash "${withdraw_tx_hash}" \ - --log-index 1 2>/dev/null) -echo "Payload length: ${#payload}" -echo "First byte: 0x${payload:2:2}" # Should be 0xf9 (RLP long list prefix) -``` - -**What to check:** -- First byte `0xf9` or `0xf8` or anything `>= 0xc0` = RLP list (correct) -- First byte `0x00` = ABI encoding (wrong — polycli bug) -- If polycli fails: checkpoint not yet indexed, retry in a few seconds - -**Debugging payload format (RLP decode):** -```bash -# Check the branchMask field (field[8] in the RLP list) -# It should start with 0x00 (extension HP encoding) -# The branchMask for tx at index 0 should be 0x0080 -python3 -c " -import rlp -data = bytes.fromhex('${payload[2:]}') -fields = rlp.decode(data) -print('branchMask:', fields[8].hex()) # Should be 0080 for txIndex=0 -print('logIndex:', int.from_bytes(fields[9], 'big')) # Should be 1 -" -``` - -### Step 4: Simulate startExitWithBurntTokens - -Always simulate before sending: -```bash -address=$(cast wallet address --private-key "${PRIVATE_KEY}") -cast call \ - --rpc-url "${L1_RPC_URL}" \ - --from "${address}" \ - "${L1_ERC20_PREDICATE_ADDRESS}" \ - "startExitWithBurntTokens(bytes)" \ - "${payload}" 2>&1 -``` - -**Expected:** `0x` (empty return, no revert) - -**Common revert messages and their causes:** - -| Error | Cause | Fix | -|---|---|---| -| `incorrect mask` | branchMask[0] != 0 (HP leaf encoding used instead of extension) | polycli bug: don't include terminator in hexToCompact | -| `INVALID_RECEIPT_MERKLE_PROOF` | Proof nodes encoded as byte strings, not RLP lists | polycli bug: use `rlp.RawValue` for proof nodes | -| `WITHDRAW_BLOCK_NOT_A_PART_OF_SUBMITTED_HEADER` | Block Merkle leaf is wrong (using block hash instead of keccak256(n,ts,txRoot,receiptsRoot)) | polycli bug: compute correct leaf | -| `Not a withdraw event signature` | Wrong log index — pointed to LogTransfer instead of Withdraw | Use `--log-index 1` for native token | -| `Withdrawer and burn exit tx do not match` | `msg.sender` != topic[2] (from address in Withdraw event) | Send from the same address that did the burn | - -### Step 5: Send startExitWithBurntTokens - -```bash -cast send \ - --rpc-url "${L1_RPC_URL}" \ - --private-key "${PRIVATE_KEY}" \ - --gas-limit 500000 \ - "${L1_ERC20_PREDICATE_ADDRESS}" \ - "startExitWithBurntTokens(bytes)" \ - "${payload}" -``` - -**What to check after:** -```bash -# Verify ExitStarted event was emitted on WithdrawManager -# The tx receipt should have 2 logs: -# - Log 0: ExitNFT Transfer (minted to user) -# - Log 1: WithdrawManager ExitStarted -cast receipt --rpc-url "${L1_RPC_URL}" --json "${TX_HASH}" | jq '.logs | length' - -# Get the exit ID -cast receipt --rpc-url "${L1_RPC_URL}" --json "${TX_HASH}" | jq '.logs[1].topics[2]' - -# Verify queue has 1 exit under MATIC -queue=$(cast call --rpc-url "${L1_RPC_URL}" "${WITHDRAW_MANAGER}" "exitsQueues(address)(address)" "${MATIC}") -cast call --rpc-url "${L1_RPC_URL}" "${queue}" "currentSize()(uint256)" -# Should return 1 -``` - -### Step 6: Check Exit is Processable - -```bash -WITHDRAW_MANAGER="0x862ff216d822fBF2F381812627D4216d2150e810" -MATIC="0x8E1700577B7aE261753c67e1B93Fe60Dd3e205fa" -queue=$(cast call --rpc-url "${L1_RPC_URL}" "${WITHDRAW_MANAGER}" "exitsQueues(address)(address)" "${MATIC}") - -# Get exitableAt from the queue -cast call --rpc-url "${L1_RPC_URL}" "${queue}" "getMin()(uint256,uint256)" -# First return value = exitableAt (unix timestamp) - -# Get current block timestamp -curl -s -X POST "${L1_RPC_URL}" \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest",false],"id":1}' \ - | jq -r '.result.timestamp' | xargs printf "%d\n" - -# Exit is processable when: exitableAt <= block.timestamp -``` - -**Key insight:** `exitableAt = Math.max(checkpoint_createdAt + 2*HALF_EXIT_PERIOD, now_at_startExit + HALF_EXIT_PERIOD)`. With `HALF_EXIT_PERIOD=1`, the exit may not be processable for 1 second. On a fast devnet, `processExits` called in the very next block might still be too early. Retry in a loop. - -### Step 7: Process Exit - -```bash -cast send \ - --rpc-url "${L1_RPC_URL}" \ - --private-key "${PRIVATE_KEY}" \ - "${WITHDRAW_MANAGER}" \ - "processExits(address)" \ - "${MATIC}" -``` - -**What to check:** -- If `logs: []` — either exit not yet processable (retry) or already processed (queue empty now) -- If `Withdraw` event emitted — exit was processed -- The `Withdraw` event is on WithdrawManagerProxy (0x862ff2...) - -**Check if POL balance increased:** -```bash -cast call --rpc-url "${L1_RPC_URL}" --json \ - "${L1_POL_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq -r '.[0]' -``` - -## Advanced Debugging: Tracing Transactions - -### Trace a failed transaction -```bash -# Requires archive node or recent block -cast run --rpc-url "${L1_RPC_URL}" "${TX_HASH}" - -# Or use debug_traceCall to simulate at latest block -curl -s -X POST "${L1_RPC_URL}" \ - -H "Content-Type: application/json" \ - -d '{ - "jsonrpc":"2.0", - "method":"debug_traceCall", - "params":[{ - "from":"YOUR_ADDRESS", - "to":"CONTRACT_ADDRESS", - "data":"CALLDATA", - "gas":"0x1E8480" - },"latest",{"tracer":"callTracer","tracerConfig":{"withLog":true}}], - "id":1 - }' | jq '.result' -``` - -### Get revert reason from a failed call -```bash -cast call --rpc-url "${L1_RPC_URL}" \ - --from "${address}" \ - "${CONTRACT}" "FUNCTION_SIG" "ARGS" 2>&1 -# Error: execution reverted: REVERT_REASON -``` - -### Inspect the exit queue state -```bash -# Check queue size -queue=$(cast call --rpc-url "${L1_RPC_URL}" "${WITHDRAW_MANAGER}" "exitsQueues(address)(address)" "${TOKEN}") -cast call --rpc-url "${L1_RPC_URL}" "${queue}" "currentSize()(uint256)" - -# Get minimum priority (exitableAt) and value (lower exitId bits) -cast call --rpc-url "${L1_RPC_URL}" "${queue}" "getMin()(uint256,uint256)" - -# Reconstruct the exitId -python3 -c " -exit_at = EXITABLE_AT -lower = LOWER_VALUE -exit_id = (exit_at << 128) | lower -print('exitId:', exit_id) -print('exitId hex:', hex(exit_id)) -" - -# Check exits[exitId] storage (amount, txHash, exitor, token, isRegularExit, predicate) -cast call --rpc-url "${L1_RPC_URL}" "${WITHDRAW_MANAGER}" \ - "exits(uint256)(uint256,bytes32,address,address,bool,address)" "${EXIT_ID}" - -# Check ExitNFT ownership -EXIT_NFT="0xF2dd130f8dfA4f55c6CCABB48f385c100c37D8A4" -cast call --rpc-url "${L1_RPC_URL}" "${EXIT_NFT}" "exists(uint256)(bool)" "${EXIT_ID}" -cast call --rpc-url "${L1_RPC_URL}" "${EXIT_NFT}" "ownerOf(uint256)(address)" "${EXIT_ID}" -``` - -## Understanding the Exit Payload Format - -The payload is an **RLP-encoded list** of 10 fields: - -``` -RLP([ - headerNumber, // uint: checkpoint ID × stride (e.g., checkpoint 2 → 20000) - blockProof, // bytes: binary Merkle proof of the block within the checkpoint - blockNumber, // uint: L2 block number containing the burn tx - blockTimestamp, // uint: timestamp of that L2 block - txRoot, // bytes32: transactions root of that L2 block - receiptRoot, // bytes32: receipts root of that L2 block - receipt, // bytes: RLP-encoded burn transaction receipt - receiptParentNodes, // bytes: RLP-encoded list of MPT proof nodes (root-to-leaf) - branchMask, // bytes: HP-encoded path through receipt MPT (e.g., [0x00, 0x80] for txIndex=0) - logIndex // uint: which log in the receipt is the Withdraw event (1 for native token) -]) -``` - -**branchMask explained:** -- The receipt MPT uses `rlp(txIndex)` as the key (e.g., txIndex=0 → key=`0x80`) -- The key nibbles are `[8, 0]` for key `0x80` -- HP encoding without terminator: `[0x00, 0x80]` (extension prefix 0x00 = even length) -- `verifyInclusion` requires `branchMaskBytes[0] == 0` — this is satisfied by extension encoding - -**blockProof (checkpoint Merkle proof) explained:** -- The checkpoint stores a Merkle root of all blocks in the range -- Each leaf is: `keccak256(abi.encodePacked(blockNum_32, blockTime_32, txRoot_32, receiptsRoot_32))` -- This is NOT the Ethereum block hash — it's a hash of specific fields -- The proof is an array of sibling hashes concatenated (32 bytes each) - -## Why MATIC and not POL for processExits? - -The 0x1010 contract on L2 emits: -```solidity -event Withdraw(address indexed token, address indexed from, uint256 amount, uint256 input1, uint256 output1) -``` - -where `token` = the L1 root token = MATIC (the original mapping before POL migration). - -`ERC20Predicate.startExitWithBurntTokens` reads `rootToken = topics[1] = MATIC` and calls `addExitToQueue(..., rootToken=MATIC, ...)`. - -`WithdrawManager` indexes exits under `exitsQueues[rootToken]` = `exitsQueues[MATIC]`. - -So `processExits(MATIC)` is correct. Internally, `onFinalizeExit` converts MATIC→POL via the registry and transfers POL to the user. - -## Proxy vs Implementation - -`WithdrawManagerProxy` delegates most calls to `WithdrawManager` (impl). BUT some functions are implemented DIRECTLY in the proxy (don't delegate): -- `exitsQueues(address)` — reads proxy storage directly -- `exits(uint256)` — reads proxy storage directly -- `exitNft()` — reads proxy storage - -When debugging, ensure you compute the **correct function selector**: -```bash -cast keccak "processExits(address)" | head -c 10 -# → 0x0f6795f2 - -cast keccak "startExitWithBurntTokens(bytes)" | head -c 10 -# → 0x6f7c1f04 -``` - -The wrong selector might accidentally hit a proxy-direct function (e.g., `0x72d8d524` is not `processExits`). From 37d404cb4a38143b8e116b58a4f4ff104c258b33 Mon Sep 17 00:00:00 2001 From: leovct Date: Fri, 27 Mar 2026 10:38:17 +0100 Subject: [PATCH 43/45] chore: update tests inventory Co-Authored-By: Claude Sonnet 4.6 --- TESTSINVENTORY.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index 77305c22..004393ca 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -378,11 +378,11 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getAuthor returns a valid address for latest block | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L486) | | | bor_getCurrentValidators returns a non-empty validator list | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L509) | | | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | -| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L325) | | -| bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L427) | | -| bridge ETH from L1 to L2 and confirm MaticWeth balance increased on L2 | [Link](./tests/pos/bridge.bats#L222) | | -| bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L119) | | -| bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L81) | | +| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L389) | | +| bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L496) | | +| bridge ETH from L1 to L2 and confirm MaticWeth balance increased on L2 | [Link](./tests/pos/bridge.bats#L277) | | +| bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L166) | | +| bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L128) | | | coinbase balance increases by at least the priority fee portion of gas cost | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L318) | | | concurrent write/read race: tx submissions and state reads do not interfere | [Link](./tests/pos/execution-specs/rpc/rpc-concurrent-load-and-stress.bats#L248) | | | consecutive block baseFees are within ±5% of each other | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L183) | | @@ -479,10 +479,10 @@ Table of tests currently implemented or being implemented in the E2E repository. | update validator top-up fee | [Link](./tests/pos/validator.bats#L103) | | | warm COINBASE access costs less than cold access to arbitrary address (EIP-3651) | [Link](./tests/pos/execution-specs/evm/evm-opcodes-cancun-shanghai-eips.bats#L457) | | | web3_clientVersion returns a non-empty version string | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L400) | | -| withdraw ERC20 tokens from L2 to L1 and confirm ERC20 balance increased on L1 | [Link](./tests/pos/bridge.bats#L362) | | -| withdraw ERC721 token from L2 to L1 and confirm ERC721 balance increased on L1 | [Link](./tests/pos/bridge.bats#L471) | | -| withdraw MaticWeth from L2 and confirm ETH balance increased on L1 | [Link](./tests/pos/bridge.bats#L258) | | -| withdraw native tokens from L2 and confirm POL balance increased on L1 | [Link](./tests/pos/bridge.bats#L156) | | +| withdraw ERC20 tokens from L2 to L1 and confirm ERC20 balance increased on L1 | [Link](./tests/pos/bridge.bats#L426) | | +| withdraw ERC721 token from L2 to L1 and confirm ERC721 balance increased on L1 | [Link](./tests/pos/bridge.bats#L540) | | +| withdraw MaticWeth from L2 and confirm ETH balance increased on L1 | [Link](./tests/pos/bridge.bats#L313) | | +| withdraw native tokens from L2 and confirm POL balance increased on L1 | [Link](./tests/pos/bridge.bats#L203) | | | withdraw validator rewards | [Link](./tests/pos/validator.bats#L311) | | | zero-value self-transfer: only gas consumed, nonce increments | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L530) | | From bafec8190a4371411694b43af46aedb0c4deea76 Mon Sep 17 00:00:00 2001 From: leovct Date: Fri, 27 Mar 2026 10:53:03 +0100 Subject: [PATCH 44/45] chore(pos): mention Plasma bridge in bridge test names Co-Authored-By: Claude Sonnet 4.6 --- TESTSINVENTORY.md | 18 +++++++++--------- tests/pos/bridge.bats | 18 +++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index 004393ca..eb255361 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -378,11 +378,11 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getAuthor returns a valid address for latest block | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L486) | | | bor_getCurrentValidators returns a non-empty validator list | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L509) | | | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | -| bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L389) | | -| bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L496) | | -| bridge ETH from L1 to L2 and confirm MaticWeth balance increased on L2 | [Link](./tests/pos/bridge.bats#L277) | | -| bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L166) | | -| bridge POL from L1 to L2 and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L128) | | +| bridge ERC20 tokens from L1 to L2 via Plasma bridge and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L389) | | +| bridge ERC721 token from L1 to L2 via Plasma bridge and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L496) | | +| bridge ETH from L1 to L2 via Plasma bridge and confirm MaticWeth balance increased on L2 | [Link](./tests/pos/bridge.bats#L277) | | +| bridge MATIC from L1 to L2 via Plasma bridge and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L166) | | +| bridge POL from L1 to L2 via Plasma bridge and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L128) | | | coinbase balance increases by at least the priority fee portion of gas cost | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L318) | | | concurrent write/read race: tx submissions and state reads do not interfere | [Link](./tests/pos/execution-specs/rpc/rpc-concurrent-load-and-stress.bats#L248) | | | consecutive block baseFees are within ±5% of each other | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L183) | | @@ -479,10 +479,10 @@ Table of tests currently implemented or being implemented in the E2E repository. | update validator top-up fee | [Link](./tests/pos/validator.bats#L103) | | | warm COINBASE access costs less than cold access to arbitrary address (EIP-3651) | [Link](./tests/pos/execution-specs/evm/evm-opcodes-cancun-shanghai-eips.bats#L457) | | | web3_clientVersion returns a non-empty version string | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L400) | | -| withdraw ERC20 tokens from L2 to L1 and confirm ERC20 balance increased on L1 | [Link](./tests/pos/bridge.bats#L426) | | -| withdraw ERC721 token from L2 to L1 and confirm ERC721 balance increased on L1 | [Link](./tests/pos/bridge.bats#L540) | | -| withdraw MaticWeth from L2 and confirm ETH balance increased on L1 | [Link](./tests/pos/bridge.bats#L313) | | -| withdraw native tokens from L2 and confirm POL balance increased on L1 | [Link](./tests/pos/bridge.bats#L203) | | +| withdraw ERC20 tokens from L2 to L1 via Plasma bridge and confirm ERC20 balance increased on L1 | [Link](./tests/pos/bridge.bats#L426) | | +| withdraw ERC721 token from L2 to L1 via Plasma bridge and confirm ERC721 balance increased on L1 | [Link](./tests/pos/bridge.bats#L540) | | +| withdraw MaticWeth from L2 via Plasma bridge and confirm ETH balance increased on L1 | [Link](./tests/pos/bridge.bats#L313) | | +| withdraw native tokens from L2 via Plasma bridge and confirm POL balance increased on L1 | [Link](./tests/pos/bridge.bats#L203) | | | withdraw validator rewards | [Link](./tests/pos/validator.bats#L311) | | | zero-value self-transfer: only gas consumed, nonce increments | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L530) | | diff --git a/tests/pos/bridge.bats b/tests/pos/bridge.bats index a4a9fb53..55a6fe3d 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -125,7 +125,7 @@ function generate_exit_payload() { ############################################################################## # bats test_tags=bridge,transaction-pol -@test "bridge POL from L1 to L2 and confirm native tokens balance increased on L2" { +@test "bridge POL from L1 to L2 via Plasma bridge and confirm native tokens balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get the initial balances. @@ -163,7 +163,7 @@ function generate_exit_payload() { } # bats test_tags=bridge,transaction-matic -@test "bridge MATIC from L1 to L2 and confirm native tokens balance increased on L2" { +@test "bridge MATIC from L1 to L2 via Plasma bridge and confirm native tokens balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get the initial balances. @@ -200,7 +200,7 @@ function generate_exit_payload() { } # bats test_tags=withdraw,transaction-pol -@test "withdraw native tokens from L2 and confirm POL balance increased on L1" { +@test "withdraw native tokens from L2 via Plasma bridge and confirm POL balance increased on L1" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get initial balances and latest checkpoint ID. @@ -274,7 +274,7 @@ function generate_exit_payload() { ############################################################################## # bats test_tags=bridge,transaction-eth -@test "bridge ETH from L1 to L2 and confirm MaticWeth balance increased on L2" { +@test "bridge ETH from L1 to L2 via Plasma bridge and confirm MaticWeth balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get the initial balances. @@ -310,7 +310,7 @@ function generate_exit_payload() { } # bats test_tags=withdraw,transaction-eth -@test "withdraw MaticWeth from L2 and confirm ETH balance increased on L1" { +@test "withdraw MaticWeth from L2 via Plasma bridge and confirm ETH balance increased on L1" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get initial balances and latest checkpoint ID. @@ -386,7 +386,7 @@ function generate_exit_payload() { ############################################################################## # bats test_tags=bridge,transaction-erc20 -@test "bridge ERC20 tokens from L1 to L2 and confirm ERC20 balance increased on L2" { +@test "bridge ERC20 tokens from L1 to L2 via Plasma bridge and confirm ERC20 balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get the initial balances. @@ -423,7 +423,7 @@ function generate_exit_payload() { } # bats test_tags=withdraw,transaction-erc20 -@test "withdraw ERC20 tokens from L2 to L1 and confirm ERC20 balance increased on L1" { +@test "withdraw ERC20 tokens from L2 to L1 via Plasma bridge and confirm ERC20 balance increased on L1" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get initial balances and latest checkpoint ID. @@ -493,7 +493,7 @@ function generate_exit_payload() { ############################################################################## # bats test_tags=bridge,transaction-erc721 -@test "bridge ERC721 token from L1 to L2 and confirm ERC721 balance increased on L2" { +@test "bridge ERC721 token from L1 to L2 via Plasma bridge and confirm ERC721 balance increased on L2" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Mint an ERC721 token. @@ -537,7 +537,7 @@ function generate_exit_payload() { } # bats test_tags=withdraw,transaction-erc721 -@test "withdraw ERC721 token from L2 to L1 and confirm ERC721 balance increased on L1" { +@test "withdraw ERC721 token from L2 to L1 via Plasma bridge and confirm ERC721 balance increased on L1" { address=$(cast wallet address --private-key "${PRIVATE_KEY}") # Get a token ID owned by the address on L2. From bf669bf729fba74a5bc04d126574ff0e51ec35c6 Mon Sep 17 00:00:00 2001 From: leovct Date: Fri, 27 Mar 2026 10:54:20 +0100 Subject: [PATCH 45/45] chore(pos): rename bridge.bats to plasma-bridge.bats Co-Authored-By: Claude Sonnet 4.6 --- TESTSINVENTORY.md | 18 +++++++++--------- tests/pos/{bridge.bats => plasma-bridge.bats} | 0 2 files changed, 9 insertions(+), 9 deletions(-) rename tests/pos/{bridge.bats => plasma-bridge.bats} (100%) diff --git a/TESTSINVENTORY.md b/TESTSINVENTORY.md index eb255361..0f9c3a64 100644 --- a/TESTSINVENTORY.md +++ b/TESTSINVENTORY.md @@ -378,11 +378,11 @@ Table of tests currently implemented or being implemented in the E2E repository. | bor_getAuthor returns a valid address for latest block | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L486) | | | bor_getCurrentValidators returns a non-empty validator list | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L509) | | | bor_getSnapshot returns snapshot with validator data | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L463) | | -| bridge ERC20 tokens from L1 to L2 via Plasma bridge and confirm ERC20 balance increased on L2 | [Link](./tests/pos/bridge.bats#L389) | | -| bridge ERC721 token from L1 to L2 via Plasma bridge and confirm ERC721 balance increased on L2 | [Link](./tests/pos/bridge.bats#L496) | | -| bridge ETH from L1 to L2 via Plasma bridge and confirm MaticWeth balance increased on L2 | [Link](./tests/pos/bridge.bats#L277) | | -| bridge MATIC from L1 to L2 via Plasma bridge and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L166) | | -| bridge POL from L1 to L2 via Plasma bridge and confirm native tokens balance increased on L2 | [Link](./tests/pos/bridge.bats#L128) | | +| bridge ERC20 tokens from L1 to L2 via Plasma bridge and confirm ERC20 balance increased on L2 | [Link](./tests/pos/plasma-bridge.bats#L389) | | +| bridge ERC721 token from L1 to L2 via Plasma bridge and confirm ERC721 balance increased on L2 | [Link](./tests/pos/plasma-bridge.bats#L496) | | +| bridge ETH from L1 to L2 via Plasma bridge and confirm MaticWeth balance increased on L2 | [Link](./tests/pos/plasma-bridge.bats#L277) | | +| bridge MATIC from L1 to L2 via Plasma bridge and confirm native tokens balance increased on L2 | [Link](./tests/pos/plasma-bridge.bats#L166) | | +| bridge POL from L1 to L2 via Plasma bridge and confirm native tokens balance increased on L2 | [Link](./tests/pos/plasma-bridge.bats#L128) | | | coinbase balance increases by at least the priority fee portion of gas cost | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L318) | | | concurrent write/read race: tx submissions and state reads do not interfere | [Link](./tests/pos/execution-specs/rpc/rpc-concurrent-load-and-stress.bats#L248) | | | consecutive block baseFees are within ±5% of each other | [Link](./tests/pos/execution-specs/protocol/pip79-bounded-basefee-validation.bats#L183) | | @@ -479,10 +479,10 @@ Table of tests currently implemented or being implemented in the E2E repository. | update validator top-up fee | [Link](./tests/pos/validator.bats#L103) | | | warm COINBASE access costs less than cold access to arbitrary address (EIP-3651) | [Link](./tests/pos/execution-specs/evm/evm-opcodes-cancun-shanghai-eips.bats#L457) | | | web3_clientVersion returns a non-empty version string | [Link](./tests/pos/execution-specs/rpc/rpc-method-conformance-and-validation.bats#L400) | | -| withdraw ERC20 tokens from L2 to L1 via Plasma bridge and confirm ERC20 balance increased on L1 | [Link](./tests/pos/bridge.bats#L426) | | -| withdraw ERC721 token from L2 to L1 via Plasma bridge and confirm ERC721 balance increased on L1 | [Link](./tests/pos/bridge.bats#L540) | | -| withdraw MaticWeth from L2 via Plasma bridge and confirm ETH balance increased on L1 | [Link](./tests/pos/bridge.bats#L313) | | -| withdraw native tokens from L2 via Plasma bridge and confirm POL balance increased on L1 | [Link](./tests/pos/bridge.bats#L203) | | +| withdraw ERC20 tokens from L2 to L1 via Plasma bridge and confirm ERC20 balance increased on L1 | [Link](./tests/pos/plasma-bridge.bats#L426) | | +| withdraw ERC721 token from L2 to L1 via Plasma bridge and confirm ERC721 balance increased on L1 | [Link](./tests/pos/plasma-bridge.bats#L540) | | +| withdraw MaticWeth from L2 via Plasma bridge and confirm ETH balance increased on L1 | [Link](./tests/pos/plasma-bridge.bats#L313) | | +| withdraw native tokens from L2 via Plasma bridge and confirm POL balance increased on L1 | [Link](./tests/pos/plasma-bridge.bats#L203) | | | withdraw validator rewards | [Link](./tests/pos/validator.bats#L311) | | | zero-value self-transfer: only gas consumed, nonce increments | [Link](./tests/pos/execution-specs/transactions/transaction-balance-nonce-and-replay-invariants.bats#L530) | | diff --git a/tests/pos/bridge.bats b/tests/pos/plasma-bridge.bats similarity index 100% rename from tests/pos/bridge.bats rename to tests/pos/plasma-bridge.bats