diff --git a/contracts/tholos-v2/src/lib.rs b/contracts/tholos-v2/src/lib.rs index 648226d..2328100 100644 --- a/contracts/tholos-v2/src/lib.rs +++ b/contracts/tholos-v2/src/lib.rs @@ -572,6 +572,7 @@ const INSTANCE_LIFETIME_THRESHOLD: u32 = INSTANCE_BUMP_AMOUNT - DAY_IN_LEDGERS; const MAX_REGISTRATION_DURATION_SECS: u64 = 7 * 24 * 60 * 60; const MAX_REVEAL_DURATION_SECS: u64 = 7 * 24 * 60 * 60; +const MAX_ANTI_SNIPE_HARD_MAX_SECS: u64 = 29 * 24 * 60 * 60; // 29 days /// Same 7-day cap as v1's `challenge_window_secs`, for the same reason: it /// must leave real margin within the 30-day persistent-storage TTL bump /// (`INSTANCE_BUMP_AMOUNT`) for `finalize` to actually get called before the @@ -666,6 +667,9 @@ impl TholosV2 { if anti_snipe_extension_secs > anti_snipe_hard_max_secs { return Err(Error::InvalidAntiSnipeParams); } + if anti_snipe_hard_max_secs > MAX_ANTI_SNIPE_HARD_MAX_SECS { + return Err(Error::InvalidAntiSnipeParams); + } // registration_hard_deadline is registration_opened_at + // anti_snipe_hard_max_secs (see dispute() below), an absolute // duration from registration opening, independent of diff --git a/contracts/tholos-v2/src/test.rs b/contracts/tholos-v2/src/test.rs index ff4341a..d187ec6 100644 --- a/contracts/tholos-v2/src/test.rs +++ b/contracts/tholos-v2/src/test.rs @@ -3159,3 +3159,49 @@ mod proptest_settlement { } } } + +#[test] +fn test_initialize_rejects_anti_snipe_hard_max_over_max() { + let env = Env::default(); + env.mock_all_auths(); + let token_id = setup(&env); + let contract_id = env.register(TholosV2, ()); + let client = TholosV2Client::new(&env, &contract_id); + let admin = Address::generate(&env); + + let result = init_full( + &client, + &admin, + &token_id, + DEFAULT_REGISTRATION_SECS, + DEFAULT_ANTI_SNIPE_EXT_SECS, + MAX_ANTI_SNIPE_HARD_MAX_SECS + 1, + DEFAULT_REVEAL_SECS, + DEFAULT_MAX_POSITION, + DEFAULT_MAX_TOTAL_WEIGHT, + ); + assert_eq!(result, Err(Ok(Error::InvalidAntiSnipeParams))); +} + +#[test] +fn test_initialize_accepts_anti_snipe_hard_max_at_max() { + let env = Env::default(); + env.mock_all_auths(); + let token_id = setup(&env); + let contract_id = env.register(TholosV2, ()); + let client = TholosV2Client::new(&env, &contract_id); + let admin = Address::generate(&env); + + let result = init_full( + &client, + &admin, + &token_id, + DEFAULT_REGISTRATION_SECS, + DEFAULT_ANTI_SNIPE_EXT_SECS, + MAX_ANTI_SNIPE_HARD_MAX_SECS, + DEFAULT_REVEAL_SECS, + DEFAULT_MAX_POSITION, + DEFAULT_MAX_TOTAL_WEIGHT, + ); + assert_eq!(result, Ok(Ok(()))); +} diff --git a/docs/src/CONTRACT_V2.md b/docs/src/CONTRACT_V2.md index 0a272f6..be5dcb6 100644 --- a/docs/src/CONTRACT_V2.md +++ b/docs/src/CONTRACT_V2.md @@ -175,7 +175,7 @@ only affect assertions created after the change. | `InvalidBondAmount` | `base_bond` isn't positive, or exceeds `MAX_BOND_AMOUNT`. | | `InvalidRegistrationDuration` | `registration_duration_secs` is zero or exceeds 7 days. | | `InvalidRevealDuration` | `reveal_duration_secs` is zero or exceeds 7 days. | -| `InvalidAntiSnipeParams` | `anti_snipe_extension_secs` exceeds `anti_snipe_hard_max_secs`, or `anti_snipe_hard_max_secs` is shorter than `registration_duration_secs`. | +| `InvalidAntiSnipeParams` | `anti_snipe_extension_secs` exceeds `anti_snipe_hard_max_secs`, `anti_snipe_hard_max_secs` is shorter than `registration_duration_secs`, or `anti_snipe_hard_max_secs` exceeds `MAX_ANTI_SNIPE_HARD_MAX_SECS` (29 days). | | `InvalidMaxPosition` | `max_position` isn't positive, or exceeds `max_total_weight`. | | `InvalidMaxTotalWeight` | `max_total_weight` isn't positive, or exceeds `MAX_SETTLEMENT_TOTAL_WEIGHT`. | | `InvalidChallengeWindow` | `challenge_window_secs` is zero or exceeds 7 days. | diff --git a/docs/src/DEPLOYMENT_V2.md b/docs/src/DEPLOYMENT_V2.md index b607fd1..8a0f462 100644 --- a/docs/src/DEPLOYMENT_V2.md +++ b/docs/src/DEPLOYMENT_V2.md @@ -32,7 +32,7 @@ a dispute arrives. Every timeline runs from the moment `dispute` is called. | --- | --- | | `registration_duration_secs` | How long a dispute stays in registration, during which the asserter, disputer, and any third party can lock capital and commit votes. Must be at least 1 second; the contract enforces a practical upper bound to keep lifetimes reasonable. Deposit this commitment time into your business model: typical Internet disputes might use 1 day; urgent or time-sensitive ones might use 1 hour. If your dispute is about a sports result, a stock price, or anything with a known announcement, set this shorter than the time until the external event resolves, so resolution bonds are visible in time. | | `anti_snipe_extension_secs` | How much longer the registration deadline moves if a position is funded within this many seconds of the ordinary cutoff. Prevents a late attacker from dominating an already-open dispute in the final second. Set it to 0 if you don't need anti-sniping (a trusted environment with no arms-race incentive), or to a reasonable backstab window (e.g., 5 minutes) if you expect contested disputes. The contract enforces an upper bound relative to `anti_snipe_hard_max_secs` (see below). | -| `anti_snipe_hard_max_secs` | The absolute maximum registration deadline, regardless of how many extensions occur. No deposit can extend registration past this time, even if extensions keep firing. Set it to at least `registration_duration_secs` (the contract enforces this), plus enough extension opportunities to feel fair (e.g., `registration_duration_secs + 3 * anti_snipe_extension_secs`). A very large hard max (e.g., `registration_duration_secs + 100 * anti_snipe_extension_secs`) defeats anti-sniping; a very small one (barely above the base window) defeats extensions. | +| `anti_snipe_hard_max_secs` | The absolute maximum registration deadline, regardless of how many extensions occur. No deposit can extend registration past this time, even if extensions keep firing. Set it to at least `registration_duration_secs` (the contract enforces this) and at most `MAX_ANTI_SNIPE_HARD_MAX_SECS` (29 days), plus enough extension opportunities to feel fair (e.g., `registration_duration_secs + 100 * anti_snipe_extension_secs`). A very large hard max defeats anti-sniping; a very small one (barely above the base window) defeats extensions. | | `reveal_duration_secs` | How long a dispute stays in the reveal phase after registration closes, during which all third-party commitments from registration become binding votes by revealing their salted choice. Must be at least 1 second; the contract enforces a practical upper bound. Typical disputes might use 6 hours to 1 day here: long enough for off-chain coordinators to run their own resolution process, short enough to finalize quickly. After the reveal deadline, any position that did not reveal is counted as abstaining (forfeited in settlement). | ### Arithmetic bounds