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..27d34351f 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)?; @@ -448,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, @@ -464,12 +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 normal_credit = game.normalModeCredit(recipient).call().await?; - let refund_credit = game.refundModeCredit(recipient).call().await?; - Ok(normal_credit + refund_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)> { @@ -523,12 +552,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 +604,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..6e917df2c 100644 --- a/fault-proof/tests/sync.rs +++ b/fault-proof/tests/sync.rs @@ -6,10 +6,11 @@ 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, + Role, TestEnvironment, }; use alloy_primitives::{Bytes, FixedBytes, Uint, U256}; use alloy_sol_types::{SolCall, SolValue}; @@ -1389,6 +1390,83 @@ 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<()> { + 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?; + + // 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?; + + tracing::info!( + "Credits - Proposer: {} wei, Prover: {} wei", + proposer_credit, + prover_credit + ); + + // 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?; + 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