diff --git a/README.md b/README.md index d287b4bc..5d0b4db3 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,8 @@ 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 - transient-storage @@ -358,6 +360,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 8ae7230e..77305c22 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,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 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) | | | 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 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) | | @@ -455,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#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) | | @@ -472,12 +473,17 @@ 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) | | -| 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#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 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) | | ## DApps Tests diff --git a/core/helpers/pos-setup.bash b/core/helpers/pos-setup.bash index f8be4494..0a5a86c7 100644 --- a/core/helpers/pos-setup.bash +++ b/core/helpers/pos-setup.bash @@ -29,13 +29,18 @@ 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:-}" ]] || [[ -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) @@ -44,9 +49,15 @@ 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}" + 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}" @@ -56,6 +67,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}" @@ -71,5 +85,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/core/helpers/scripts/eventually.bash b/core/helpers/scripts/eventually.bash index c75d741d..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,36 @@ 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 + + sleep "${interval}" + 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 [ "$(echo "${balance} <= ${target}" | bc)" -eq 1 ]; then break fi @@ -170,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 117e5706..e2bf29bd 100644 --- a/tests/pos/bridge.bats +++ b/tests/pos/bridge.bats @@ -7,13 +7,19 @@ 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)"' + 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"} interval_seconds=${INTERVAL_SECONDS:-"10"} + echo "timeout_seconds=${timeout_seconds}" + echo "interval_seconds=${interval_seconds}" } function wait_for_heimdall_state_sync() { @@ -22,13 +28,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() { @@ -37,39 +43,100 @@ 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}" +} + +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}..." >&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 + echo "${payload}" + return 0 + fi + 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 + return 1 } +############################################################################## +# POL / MATIC <-> Native L2 +############################################################################## + # bats test_tags=bridge,transaction-pol -@test "bridge MATIC/POL from L1 to L2 and confirm L2 MATIC/POL 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. - initial_l1_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_MATIC_TOKEN_ADDRESS}" "balanceOf(address)(uint)" "${address}" | jq --raw-output '.[0]') + 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 balance: ${initial_l1_balance} MATIC" - echo "- L2 balance: ${initial_l2_balance} wei" + 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 MATIC/POL tokens from L1 to L2 to trigger a state sync. - # 1 MATIC/POL token = 1000000000000000000 wei. - bridge_amount=$(cast to-unit 1ether wei) + # 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. + 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}" - echo "Approving the DepositManager contract to spend MATIC/POL tokens on our behalf..." + # 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}" "$(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}" "$(echo "${initial_l2_balance} + ${bridge_amount}" | bc)" "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" +} + +# bats test_tags=bridge,transaction-matic +@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. + initial_l1_balance=$(cast call --rpc-url "${L1_RPC_URL}" --json "${L1_MATIC_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 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 tokens from L1 to L2 to trigger a state sync. + # 1 MATIC token = 1000000000000000000 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}" - 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,20 +145,184 @@ 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..." - 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 balance on L1..." + 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 MATIC/POL balance on L2..." - assert_ether_balance_eventually_greater_or_equal "${address}" $((initial_l2_balance + bridge_amount)) "${L2_RPC_URL}" "${timeout_seconds}" "${interval_seconds}" + echo "Monitoring native tokens balance on L2..." + 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 -# @test "bridge MATIC/POL from L2 to L1 and confirm L1 MATIC/POL balance increased" { -# echo TODO -# } +# 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. + # 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=$(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..." + 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}" +} + +############################################################################## +# ETH (Native L1) / MaticWeth +############################################################################## + +# bats test_tags=bridge,transaction-eth +@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. + 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 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 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}" \ + "${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 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}" +} + +# 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_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 ETH balance: ${initial_l1_balance}" + echo "- L2 MaticWeth balance: ${initial_l2_balance}" + echo "- Latest checkpoint ID: ${initial_checkpoint_id}" + + # 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} MaticWeth on L2..." + withdraw_receipt=$(cast send \ + --rpc-url "${L2_RPC_URL}" \ + --private-key "${PRIVATE_KEY}" \ + --gas-price 30gwei \ + --priority-gas-price 30gwei \ + --json \ + "${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 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. + # 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. + # 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=$(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. + 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}" + + # 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}" +} + +############################################################################## +# ERC20 +############################################################################## # 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. @@ -102,13 +333,11 @@ 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. - 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}" @@ -123,19 +352,79 @@ 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 -# @test "bridge some ERC20 tokens from L2 to L1 and confirm L1 ERC20 balance increased" { -# echo TODO -# } +# bats test_tags=withdraw,transaction-erc20 +@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. + # 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=$(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..." + 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}" + + # 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}" +} + +############################################################################## +# ERC721 +############################################################################## # 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. @@ -154,8 +443,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..." @@ -178,99 +467,66 @@ 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" { -# 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" { +# bats test_tags=withdraw,transaction-erc721 +@test "withdraw ERC721 token from L2 to L1 and confirm ERC721 balance increased on L1" { 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]') + # 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}" - # 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}" + # 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. + # 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=$(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..." 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}" + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "startExitWithBurntTokens(bytes)" "${payload}" - # 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}" + # Process the exit on L1. + echo "Processing the exit on L1..." 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}" $((initial_l1_matic_balance - bridge_amount)) "${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}" - - 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}" - - 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}" - - 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}" - - 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}" + "${L1_WITHDRAW_MANAGER_PROXY_ADDRESS}" "processExits(address)" "${L1_ERC721_TOKEN_ADDRESS}" - 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" + # 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}" } diff --git a/tests/pos/validator.bats b/tests/pos/validator.bats index a298eea8..268b46f9 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,15 +42,10 @@ 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..." - # 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}" @@ -34,17 +53,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 @@ -53,77 +72,52 @@ 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}" + initial_voting_power=$(eval "${validator_power_cmd}") + echo "Initial voting power of the validator (${validator_id}): ${initial_voting_power}." - 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}." - - 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..." - 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}" + 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_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 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})..." + 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 +125,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 +133,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 +155,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 MATIC/POL 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}" - +@test "delegate to a validator" { 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. @@ -205,107 +181,88 @@ 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}" + 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) - 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" + "${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 MATIC/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}" + 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_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}" \ + 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. - 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) - echo "Monitoring L2 voting power sync for validator ${VALIDATOR_ID}..." - assert_command_eventually_equal "${VALIDATOR_POWER_CMD}" "${expected_voting_power}" 180 + 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}" "${timeout_seconds}" echo "Delegation test completed successfully!" } # bats test_tags=pos-validator,pos-undelegate,transaction-pol -@test "undelegate MATIC/POL 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}" - +@test "undelegate from a validator" { 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}") - current_delegator_stake=$(echo "${current_delegator_stake_data}" | head -1 | cut -d' ' -f1) - echo "Current delegator stake: ${current_delegator_stake}" + "${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" # 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) @@ -313,70 +270,87 @@ 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. - 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}" \ - "${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=$(cast to-unit "${new_total_stake}" ether | cut -d'.' -f1) - echo "Monitoring L2 voting power sync for validator ${VALIDATOR_ID}..." - assert_command_eventually_equal "${VALIDATOR_POWER_CMD}" "${expected_voting_power}" 180 + 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}" "${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}" +# 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}" - VALIDATOR_ID=${VALIDATOR_ID:-"1"} - echo "VALIDATOR_ID=${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}" - VALIDATOR_COUNT_CMD='curl "${L2_CL_API_URL}/stake/validators-set" | jq --raw-output ".validator_set.validators | length"' + # 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 ]] +} - initial_validator_count=$(eval "${VALIDATOR_COUNT_CMD}") +# bats test_tags=pos-validator +@test "remove validator" { + 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(uint256)" "${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 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)) 180 + assert_command_eventually_equal "${validator_count_cmd}" $((initial_validator_count - 1)) "${timeout_seconds}" }