Skip to content
6 changes: 6 additions & 0 deletions fault-proof/tests/common/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down
74 changes: 71 additions & 3 deletions fault-proof/tests/common/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub enum Role {
Proposer,
Challenger,
Deployer,
Prover,
}

/// Common test environment setup
Expand Down Expand Up @@ -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)?;
Expand Down Expand Up @@ -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<TransactionReceipt> {
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,
Expand All @@ -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<U256> {
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)> {
Expand Down Expand Up @@ -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<TransactionReceipt> {
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<TransactionReceipt> {
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 {
Expand All @@ -537,6 +604,7 @@ impl Default for TestPrivateKeys {
deployer: DEPLOYER_PRIVATE_KEY,
proposer: PROPOSER_PRIVATE_KEY,
challenger: CHALLENGER_PRIVATE_KEY,
prover: PROVER_PRIVATE_KEY,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion fault-proof/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
84 changes: 81 additions & 3 deletions fault-proof/tests/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand Down
Loading