From c0987765667797231a0aec8346b2c1363888efc1 Mon Sep 17 00:00:00 2001 From: fakedev9999 Date: Sun, 11 Jan 2026 18:22:09 -0800 Subject: [PATCH 1/5] test(fault-proof): add test_bond_claim_with_multiple_recipients Add integration test verifying bond claiming works correctly when proposer and prover are different addresses. This tests the ChallengedAndValidProofProvided scenario where: - Proposer creates game with initial bond - Challenger challenges with challenger bond - Prover (different address) proves the claim - Game resolves as DEFENDER_WINS - Both proposer and prover can claim their respective credits Changes: - Add PROVER_ADDRESS and PROVER_PRIVATE_KEY constants (ANVIL_ACCOUNT_2) - Add Prover variant to Role enum - Add prove_game_with_role and claim_bond_with_role helper methods - Export Role enum from common module --- fault-proof/tests/common/constants.rs | 6 +++ fault-proof/tests/common/env.rs | 41 +++++++++++++++ fault-proof/tests/common/mod.rs | 2 +- fault-proof/tests/sync.rs | 75 +++++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/fault-proof/tests/common/constants.rs b/fault-proof/tests/common/constants.rs index bb0b19849..75f28b246 100644 --- a/fault-proof/tests/common/constants.rs +++ b/fault-proof/tests/common/constants.rs @@ -5,12 +5,15 @@ use alloy_primitives::{address, Address, B256, U256}; // Anvil predefined accounts pub const ANVIL_ACCOUNT_0: Address = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); pub const ANVIL_ACCOUNT_1: Address = address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); +pub const ANVIL_ACCOUNT_2: Address = address!("0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC"); // Account private keys pub const ANVIL_ACCOUNT_0_PRIVATE_KEY: &str = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"; pub const ANVIL_ACCOUNT_1_PRIVATE_KEY: &str = "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"; +pub const ANVIL_ACCOUNT_2_PRIVATE_KEY: &str = + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"; // Account roles pub const PROPOSER_ADDRESS: Address = ANVIL_ACCOUNT_0; @@ -19,6 +22,9 @@ pub const PROPOSER_PRIVATE_KEY: &str = ANVIL_ACCOUNT_0_PRIVATE_KEY; pub const CHALLENGER_ADDRESS: Address = ANVIL_ACCOUNT_1; pub const CHALLENGER_PRIVATE_KEY: &str = ANVIL_ACCOUNT_1_PRIVATE_KEY; +pub const PROVER_ADDRESS: Address = ANVIL_ACCOUNT_2; +pub const PROVER_PRIVATE_KEY: &str = ANVIL_ACCOUNT_2_PRIVATE_KEY; + // Default deployer is the same as proposer pub const DEPLOYER_PRIVATE_KEY: &str = ANVIL_ACCOUNT_0_PRIVATE_KEY; diff --git a/fault-proof/tests/common/env.rs b/fault-proof/tests/common/env.rs index 863a7d612..c74eec48b 100644 --- a/fault-proof/tests/common/env.rs +++ b/fault-proof/tests/common/env.rs @@ -53,6 +53,7 @@ pub enum Role { Proposer, Challenger, Deployer, + Prover, } /// Common test environment setup @@ -352,6 +353,7 @@ impl TestEnvironment { Role::Proposer => self.private_keys.proposer, Role::Challenger => self.private_keys.challenger, Role::Deployer => self.private_keys.deployer, + Role::Prover => self.private_keys.prover, }; let wallet = PrivateKeySigner::from_str(key)?; @@ -523,12 +525,50 @@ impl TestEnvironment { Ok(receipt) } + + /// Prove a game with a specific role (e.g., Prover instead of Proposer) + pub async fn prove_game_with_role( + &self, + address: Address, + role: Role, + ) -> Result { + let game = self.fault_dispute_game_with_role(address, role).await?; + let receipt = game + .prove(Bytes::new()) + .send() + .await? + .with_required_confirmations(1) + .get_receipt() + .await?; + + Ok(receipt) + } + + /// Claim bond with a specific role + pub async fn claim_bond_with_role( + &self, + game_address: Address, + recipient: Address, + role: Role, + ) -> Result { + let game = self.fault_dispute_game_with_role(game_address, role).await?; + let receipt = game + .claimCredit(recipient) + .send() + .await? + .with_required_confirmations(1) + .get_receipt() + .await?; + + Ok(receipt) + } } pub struct TestPrivateKeys { pub deployer: &'static str, pub proposer: &'static str, pub challenger: &'static str, + pub prover: &'static str, } impl Default for TestPrivateKeys { @@ -537,6 +577,7 @@ impl Default for TestPrivateKeys { deployer: DEPLOYER_PRIVATE_KEY, proposer: PROPOSER_PRIVATE_KEY, challenger: CHALLENGER_PRIVATE_KEY, + prover: PROVER_PRIVATE_KEY, } } } diff --git a/fault-proof/tests/common/mod.rs b/fault-proof/tests/common/mod.rs index 3f091f230..f9eb31200 100644 --- a/fault-proof/tests/common/mod.rs +++ b/fault-proof/tests/common/mod.rs @@ -8,5 +8,5 @@ pub mod monitor; pub mod process; pub use anvil::*; -pub use env::TestEnvironment; +pub use env::{Role, TestEnvironment}; pub use process::*; diff --git a/fault-proof/tests/sync.rs b/fault-proof/tests/sync.rs index 803cf003e..3de61e1c3 100644 --- a/fault-proof/tests/sync.rs +++ b/fault-proof/tests/sync.rs @@ -1389,6 +1389,81 @@ mod proposer_sync { Ok(()) } + /// Tests bond claiming when proposer and prover are different addresses. + /// + /// Scenario: ChallengedAndValidProofProvided with distinct proposer/prover + /// - Proposer creates game with initial bond + /// - Challenger challenges with challenger bond + /// - Prover (different address from proposer) proves the claim + /// - Game resolves as DEFENDER_WINS + /// - After finalization, both proposer and prover can claim their respective credits + /// + /// This test verifies that the system correctly distributes credits to both parties: + /// - Proposer receives their initial bond back + /// - Prover receives the challenger's bond as reward + #[tokio::test] + async fn test_bond_claim_with_multiple_recipients() -> Result<()> { + use crate::common::{constants::PROVER_ADDRESS, Role}; + + let (env, _proposer, init_bond) = setup().await?; + + let starting_l2_block = env.anvil.starting_l2_block_number; + + // Create game as proposer + let block = starting_l2_block + 1; + let root_claim = env.compute_output_root_at_block(block).await?; + env.create_game(root_claim, block, M, init_bond).await?; + let (_, game_address) = env.last_game_info().await?; + tracing::info!("✓ Created game at block {block}"); + + // Challenge the game + env.challenge_game(game_address).await?; + tracing::info!("✓ Challenger challenged the game"); + + // Prove with DIFFERENT address (Prover, not Proposer) + env.prove_game_with_role(game_address, Role::Prover).await?; + tracing::info!("✓ Prover (different from proposer) proved the game"); + + // Resolve the game + env.resolve_game(game_address).await?; + tracing::info!("✓ Resolved game as DEFENDER_WINS"); + + // Warp past finality delay + env.warp_time(DISPUTE_GAME_FINALITY_DELAY_SECONDS + 1).await?; + + // Get credits for both parties + let proposer_credit = env.get_credit(game_address, PROPOSER_ADDRESS).await?; + let prover_credit = env.get_credit(game_address, PROVER_ADDRESS).await?; + + tracing::info!( + "Credits - Proposer: {} wei, Prover: {} wei", + proposer_credit, + prover_credit + ); + + // Verify both have claimable credits + assert!(proposer_credit > U256::ZERO, "Proposer should have credit (bond return)"); + assert!(prover_credit > U256::ZERO, "Prover should have credit (challenger's bond)"); + + // Claim both credits + env.claim_bond(game_address, PROPOSER_ADDRESS).await?; + tracing::info!("✓ Proposer claimed bond"); + + env.claim_bond_with_role(game_address, PROVER_ADDRESS, Role::Prover).await?; + tracing::info!("✓ Prover claimed reward"); + + // Verify credits are now zero + let proposer_credit_after = env.get_credit(game_address, PROPOSER_ADDRESS).await?; + let prover_credit_after = env.get_credit(game_address, PROVER_ADDRESS).await?; + + assert_eq!(proposer_credit_after, U256::ZERO, "Proposer credit should be 0 after claim"); + assert_eq!(prover_credit_after, U256::ZERO, "Prover credit should be 0 after claim"); + + tracing::info!("✓ Both parties successfully claimed their credits"); + + Ok(()) + } + /// Verifies that defense tasks are spawned in deadline-ascending order. /// /// This test creates 2 games, then challenges them in reverse order (game 1 first, then From 6fdb61dd6c04e633a2b8a330484fc89ebada00c7 Mon Sep 17 00:00:00 2001 From: fakedev9999 Date: Sun, 11 Jan 2026 19:57:59 -0800 Subject: [PATCH 2/5] fix(fault-proof): use contract's credit() function in get_credit() The get_credit() helper was incorrectly summing both normalModeCredit and refundModeCredit, but the contract only pays out one type based on bondDistributionMode. This caused assertions to fail with 2x the expected credit amount. Use the contract's credit() view function which correctly returns only the applicable credit based on the current bond distribution mode. --- fault-proof/tests/common/env.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fault-proof/tests/common/env.rs b/fault-proof/tests/common/env.rs index c74eec48b..fa1ea1dd2 100644 --- a/fault-proof/tests/common/env.rs +++ b/fault-proof/tests/common/env.rs @@ -469,9 +469,8 @@ impl TestEnvironment { pub async fn get_credit(&self, game_address: Address, recipient: Address) -> Result { let provider = &self.anvil.provider; let game = OPSuccinctFaultDisputeGame::new(game_address, provider); - let normal_credit = game.normalModeCredit(recipient).call().await?; - let refund_credit = game.refundModeCredit(recipient).call().await?; - Ok(normal_credit + refund_credit) + let credit = game.credit(recipient).call().await?; + Ok(credit) } pub async fn last_game_info(&self) -> Result<(Uint<256, 4>, Address)> { From 84cacd49c196a12079304aba8adcd6f7e46e0f2b Mon Sep 17 00:00:00 2001 From: fakedev9999 Date: Sun, 11 Jan 2026 21:10:50 -0800 Subject: [PATCH 3/5] fix(fault-proof): handle bond distribution mode in get_credit() Fix get_credit() to correctly return credit for both resolved and unresolved games by checking bondDistributionMode: - UNDECIDED (0): Return refundModeCredit (bonds before resolution) - NORMAL/REFUND (1/2): Use contract's credit() function Also add close_game() helper and update test_bond_claim_with_multiple_recipients: - Call close_game() after resolution to finalize bond distribution mode - Use assert_eq! with exact expected values (init_bond, CHALLENGER_BOND) --- fault-proof/tests/common/env.rs | 32 ++++++++++++++++++++++++++++++-- fault-proof/tests/sync.rs | 12 ++++++++---- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/fault-proof/tests/common/env.rs b/fault-proof/tests/common/env.rs index fa1ea1dd2..27d34351f 100644 --- a/fault-proof/tests/common/env.rs +++ b/fault-proof/tests/common/env.rs @@ -450,6 +450,17 @@ impl TestEnvironment { Ok(receipt) } + /// Closes the game and sets the bond distribution mode. + /// + /// This must be called after resolution to finalize the bond distribution mode + /// (NORMAL or REFUND) before credits can be accurately queried or claimed. + pub async fn close_game(&self, address: Address) -> Result { + let game = self.fault_dispute_game(address).await?; + let receipt = + game.closeGame().send().await?.with_required_confirmations(1).get_receipt().await?; + Ok(receipt) + } + pub async fn claim_bond( &self, game_address: Address, @@ -466,11 +477,28 @@ impl TestEnvironment { Ok(receipt) } + /// Returns the credit balance for a recipient in a game. + /// + /// For resolved games (bondDistributionMode is NORMAL or REFUND), uses the contract's + /// `credit()` function which returns the correct claimable amount. + /// + /// For unresolved games (bondDistributionMode is UNDECIDED), returns `refundModeCredit` + /// since bonds are stored there during initialization before resolution. pub async fn get_credit(&self, game_address: Address, recipient: Address) -> Result { let provider = &self.anvil.provider; let game = OPSuccinctFaultDisputeGame::new(game_address, provider); - let credit = game.credit(recipient).call().await?; - Ok(credit) + + // BondDistributionMode: UNDECIDED = 0, NORMAL = 1, REFUND = 2 + let mode = game.bondDistributionMode().call().await?; + if mode == 0 { + // UNDECIDED: game not resolved yet, bonds are in refundModeCredit + let refund_credit = game.refundModeCredit(recipient).call().await?; + Ok(refund_credit) + } else { + // NORMAL or REFUND: use contract's credit() which handles both correctly + let credit = game.credit(recipient).call().await?; + Ok(credit) + } } pub async fn last_game_info(&self) -> Result<(Uint<256, 4>, Address)> { diff --git a/fault-proof/tests/sync.rs b/fault-proof/tests/sync.rs index 3de61e1c3..531f85fb3 100644 --- a/fault-proof/tests/sync.rs +++ b/fault-proof/tests/sync.rs @@ -1403,7 +1403,7 @@ mod proposer_sync { /// - Prover receives the challenger's bond as reward #[tokio::test] async fn test_bond_claim_with_multiple_recipients() -> Result<()> { - use crate::common::{constants::PROVER_ADDRESS, Role}; + use crate::common::{constants::{CHALLENGER_BOND, PROVER_ADDRESS}, Role}; let (env, _proposer, init_bond) = setup().await?; @@ -1431,6 +1431,10 @@ mod proposer_sync { // Warp past finality delay env.warp_time(DISPUTE_GAME_FINALITY_DELAY_SECONDS + 1).await?; + // Close game to finalize bond distribution mode (required before querying credits) + env.close_game(game_address).await?; + tracing::info!("✓ Game closed, bond distribution mode set"); + // Get credits for both parties let proposer_credit = env.get_credit(game_address, PROPOSER_ADDRESS).await?; let prover_credit = env.get_credit(game_address, PROVER_ADDRESS).await?; @@ -1441,9 +1445,9 @@ mod proposer_sync { prover_credit ); - // Verify both have claimable credits - assert!(proposer_credit > U256::ZERO, "Proposer should have credit (bond return)"); - assert!(prover_credit > U256::ZERO, "Prover should have credit (challenger's bond)"); + // Verify correct credit distribution + assert_eq!(proposer_credit, init_bond, "Proposer should get init_bond back"); + assert_eq!(prover_credit, CHALLENGER_BOND, "Prover should get challenger's bond"); // Claim both credits env.claim_bond(game_address, PROPOSER_ADDRESS).await?; From bb80c52a90312af82ce79ece10c7a5194f08c3db Mon Sep 17 00:00:00 2001 From: fakedev9999 Date: Sun, 11 Jan 2026 21:17:48 -0800 Subject: [PATCH 4/5] refactor(fault-proof): move CHALLENGER_BOND and PROVER_ADDRESS to module-level imports --- fault-proof/tests/sync.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fault-proof/tests/sync.rs b/fault-proof/tests/sync.rs index 531f85fb3..9dbd06393 100644 --- a/fault-proof/tests/sync.rs +++ b/fault-proof/tests/sync.rs @@ -6,8 +6,9 @@ mod proposer_sync { use crate::common::{ constants::{ - DISPUTE_GAME_FINALITY_DELAY_SECONDS, MAX_CHALLENGE_DURATION, MAX_PROVE_DURATION, - MOCK_PERMISSIONED_GAME_TYPE, PROPOSER_ADDRESS, TEST_GAME_TYPE, + CHALLENGER_BOND, DISPUTE_GAME_FINALITY_DELAY_SECONDS, MAX_CHALLENGE_DURATION, + MAX_PROVE_DURATION, MOCK_PERMISSIONED_GAME_TYPE, PROPOSER_ADDRESS, PROVER_ADDRESS, + TEST_GAME_TYPE, }, TestEnvironment, }; @@ -1403,7 +1404,7 @@ mod proposer_sync { /// - Prover receives the challenger's bond as reward #[tokio::test] async fn test_bond_claim_with_multiple_recipients() -> Result<()> { - use crate::common::{constants::{CHALLENGER_BOND, PROVER_ADDRESS}, Role}; + use crate::common::Role; let (env, _proposer, init_bond) = setup().await?; From 5f0ac00b904a49c5cdd877975819c4c5e31c91d8 Mon Sep 17 00:00:00 2001 From: fakedev9999 Date: Sun, 11 Jan 2026 21:27:23 -0800 Subject: [PATCH 5/5] refactor(fault-proof): move Role import to module level --- fault-proof/tests/sync.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fault-proof/tests/sync.rs b/fault-proof/tests/sync.rs index 9dbd06393..6e917df2c 100644 --- a/fault-proof/tests/sync.rs +++ b/fault-proof/tests/sync.rs @@ -10,7 +10,7 @@ mod proposer_sync { MAX_PROVE_DURATION, MOCK_PERMISSIONED_GAME_TYPE, PROPOSER_ADDRESS, PROVER_ADDRESS, TEST_GAME_TYPE, }, - TestEnvironment, + Role, TestEnvironment, }; use alloy_primitives::{Bytes, FixedBytes, Uint, U256}; use alloy_sol_types::{SolCall, SolValue}; @@ -1404,8 +1404,6 @@ mod proposer_sync { /// - Prover receives the challenger's bond as reward #[tokio::test] async fn test_bond_claim_with_multiple_recipients() -> Result<()> { - use crate::common::Role; - let (env, _proposer, init_bond) = setup().await?; let starting_l2_block = env.anvil.starting_l2_block_number;