//! Pure arithmetic and the resolution state machine for the `science_binary` //! Anchor program: fully collateralized USDC YES/NO binary-outcome markets. //! //! Nothing here touches Solana types. That is the point. The same functions the //! program calls are exercised by three different tools, so a result from any of //! them is about shipped code rather than a re-implementation: //! //! * `cargo test` runs the unit and randomized property tests; //! * `cargo +nightly miri test` runs those tests under the interpreter, which //! catches undefined behaviour; //! * `cargo kani` proves bounded properties of the money paths //! (see `src/kani_proofs.rs`). //! //! Money is `u64` base units of a token that the market mints one-for-one //! against USDC. `BPS_DENOM` is the basis-point denominator: a rate of `n` basis //! points is `n / 10_000`. //! //! Rounding is always down, and that is the safe direction. A fee rounded down //! is never larger than the exact fee, so a mint overcharges nobody by more than //! a base unit; a payout rounded down never exceeds the collateral behind it, so //! the reserve cannot be drained by a stream of dust redemptions. //! //! The economics, from `[internal location withheld]` section 1: //! //! mint N pairs user pays N + fee(N*m) exactly N enters the reserve //! merge N pairs user gets N - fee(N*r) exactly N leaves the reserve //! redeem N winning user gets N - fee(N*r) exactly N leaves the reserve //! losing token user gets 0, no fee //! //! The reserve invariant is checked in both directions: //! //! pre-resolution vault >= max(yes_supply, no_supply) //! post-resolution vault >= winning_supply //! cancelled vault >= payout(yes_supply) + payout(no_supply) #![no_std] #[cfg(test)] extern crate std; #[cfg(kani)] mod kani_proofs; /// Property-based model tests. Compiled only under `cargo test`, so the /// `proptest` dependency is a dev-dependency and never enters the deployed /// artifact. #[cfg(test)] mod proptest_model; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /// Basis-point denominator. A rate of `n` bps is `n / 10_000`. pub const BPS_DENOM: u64 = 10_000; /// Program cap on the mint and redemption fee rates: 1_000 bps, i.e. 10%. A cap /// is not a policy rate. It exists so a mistyped bps value cannot turn a fee into /// a confiscation. The published rates are a separate decision, fixed per market /// at creation. pub const FEE_BPS_CAP: u16 = 1_000; /// Program cap on the cancellation payout: 5_000 bps, i.e. 0.5 USDC per token. /// At the cap a complete pair redeems for exactly 1 USDC, so cancellation /// preserves the complete set. Anything at or below the cap stays solvent, which /// is why the cap is written as `2 * bps <= BPS_DENOM` in the check. pub const CANCEL_BPS_CAP: u16 = 5_000; /// Accepted challenge-window bounds, in seconds. One second is the floor so a /// test can exercise the timelock; the product default proposed in the policy is /// 72 hours, and nothing in the program treats the window as a legal safe /// harbour. Thirty days is a ceiling so a market cannot be parked in PROPOSED by /// an absurd parameter. pub const MIN_CHALLENGE_WINDOW: i64 = 1; pub const MAX_CHALLENGE_WINDOW: i64 = 30 * 24 * 60 * 60; // --------------------------------------------------------------------------- // Outcome encoding // --------------------------------------------------------------------------- /// No outcome recorded. Distinct from YES so a zeroed account is never mistaken /// for a resolved market. pub const OUTCOME_NONE: u8 = 0; pub const OUTCOME_YES: u8 = 1; pub const OUTCOME_NO: u8 = 2; pub fn outcome_valid(outcome: u8) -> bool { outcome == OUTCOME_YES || outcome == OUTCOME_NO } // --------------------------------------------------------------------------- // Resolution states. The encoding matches `[internal location withheld]`. // // OPEN -> PROPOSED -> CHALLENGE -> DISPUTED -> FINAL, plus CANCELLED. // // CHALLENGE is carried and enforced but nothing in the program enters it, and // nothing in `[internal location withheld]` enters it either: a proposal lands in // PROPOSED with the clock already running, and a challenge goes straight to // DISPUTED. The state is kept so the two implementations use the same alphabet // and so a later instruction can use it without a migration. This is recorded // as a discrepancy in SECURITY.md rather than quietly dropped. // --------------------------------------------------------------------------- pub const STATE_OPEN: u8 = 0; pub const STATE_PROPOSED: u8 = 1; pub const STATE_CHALLENGE: u8 = 2; pub const STATE_DISPUTED: u8 = 3; pub const STATE_FINAL: u8 = 4; pub const STATE_CANCELLED: u8 = 5; pub const STATE_COUNT: u8 = 6; pub fn state_valid(state: u8) -> bool { state < STATE_COUNT } /// True once the market is irreversibly finished, one way or the other. Neither /// state admits an outgoing transition, which is what makes FINAL mean FINAL. pub fn is_resolved(state: u8) -> bool { state == STATE_FINAL || state == STATE_CANCELLED } /// True while claims can still be merged. Merging is an exit, so it stays /// available in every state where a payout has not already been fixed. pub fn is_pre_resolution(state: u8) -> bool { state_valid(state) && !is_resolved(state) } /// The transition map. This is the same table as `ALLOWED_TRANSITIONS` in /// `[internal location withheld]`, transcribed one row at a time. The test /// `transition_map_matches_the_off_chain_table` asserts the two agree on all 36 /// ordered pairs, and the Kani harness `transition_map_is_exact` proves the same /// table exhaustively over the whole state space. pub fn transition_allowed(from: u8, to: u8) -> bool { if !state_valid(from) || !state_valid(to) { return false; } match from { STATE_OPEN => matches!(to, STATE_PROPOSED | STATE_CANCELLED), STATE_PROPOSED => matches!( to, STATE_CHALLENGE | STATE_FINAL | STATE_OPEN | STATE_DISPUTED | STATE_CANCELLED ), STATE_CHALLENGE => matches!( to, STATE_FINAL | STATE_DISPUTED | STATE_OPEN | STATE_CANCELLED ), STATE_DISPUTED => matches!(to, STATE_FINAL | STATE_OPEN | STATE_CANCELLED), STATE_FINAL | STATE_CANCELLED => false, _ => false, } } /// The challenge window, as a stored timestamp. A zero timestamp is "no window /// on record" and is never treated as elapsed, so a market that somehow reaches /// FINAL-eligibility without a proposal cannot be finalized by a clock of zero. pub fn challenge_window_elapsed(now: i64, challenge_until: i64) -> bool { challenge_until > 0 && now >= challenge_until } pub fn challenge_window_open(now: i64, challenge_until: i64) -> bool { challenge_until > 0 && now < challenge_until } // --------------------------------------------------------------------------- // Checked arithmetic // --------------------------------------------------------------------------- /// `(a * b) / c`, floored, refusing a zero divisor and refusing a result that /// does not fit back into a `u64`. The intermediate is `u128`, so no `u64` /// product can truncate, but the conversion back is checked because a large /// `a * b / c` can still exceed `u64::MAX` when `c` is small. pub fn mul_div(a: u64, b: u64, c: u64) -> Option { if c == 0 { return None; } let product = (a as u128).checked_mul(b as u128)?; let quotient = product / (c as u128); u64::try_from(quotient).ok() } /// The fee on an amount of dollars: `floor(amount * bps / 10_000)`. /// /// This is the only fee function. It is always called on dollars — a pair count /// at mint, a gross payout at merge or redemption — never on a notional or on a /// maximum payout. Charging on anything else would need a new line, not a flag. pub fn fee_on(amount: u64, bps: u16) -> Option { mul_div(amount, bps as u64, BPS_DENOM) } /// What a mint of `pairs` complete pairs costs, fee included. /// /// `gross = pairs + fee(pairs*m)`. Exactly `pairs` of that is collateral and /// goes into the reserve; `fee` goes to the disclosed fee account. `gross` is /// therefore always at least `pairs`, which is the property that keeps a mint /// from under-funding the pair it issues. pub fn mint_gross(pairs: u64, mint_fee_bps: u16) -> Option { let fee = fee_on(pairs, mint_fee_bps)?; pairs.checked_add(fee) } /// What a merge or a winning redemption of `gross` tokens returns. /// /// Returns `(fee, net)` with `fee + net == gross` and `fee <= gross`. The fee is /// charged on the gross that the reserve actually releases, so the reserve is /// drawn down by exactly `gross` and never by more. pub fn redeem_net(gross: u64, redeem_fee_bps: u16) -> Option<(u64, u64)> { let fee = fee_on(gross, redeem_fee_bps)?; let net = gross.checked_sub(fee)?; Some((fee, net)) } /// What one token pays under the cancellation rule: `floor(amount * bps / 10_000)`. /// The same rate applies to both sides, so the rule is one number, fixed at /// creation, and the complete-set property falls out of `2 * bps <= 10_000`. pub fn cancel_payout(amount: u64, cancel_payout_bps: u16) -> Option { mul_div(amount, cancel_payout_bps as u64, BPS_DENOM) } /// True when `cancel_payout_bps` keeps a complete set inside the ceiling: at or /// below 0.5 per token, so a pair pays at most 1 USDC in total. pub fn cancel_rule_preserves_the_complete_set(cancel_payout_bps: u16) -> bool { (cancel_payout_bps as u64) * 2 <= BPS_DENOM } // --------------------------------------------------------------------------- // Solvency predicates. These are the invariant, written once so the program and // every test read the same definition. // --------------------------------------------------------------------------- pub fn max_u64(a: u64, b: u64) -> u64 { if a > b { a } else { b } } /// Pre-resolution: the reserve covers the larger of the two outstanding token /// supplies. Paired issuance and merging normally keep the two equal; the /// conservative form covers the unequal case, which is what the policy requires. pub fn pre_resolution_covered(vault: u64, yes_supply: u64, no_supply: u64) -> bool { vault >= max_u64(yes_supply, no_supply) } /// Post-resolution: the reserve covers every unredeemed winning claim. The /// losing side cannot redeem, so it is not counted. pub fn post_resolution_covered(vault: u64, winning_supply: u64) -> bool { vault >= winning_supply } /// Cancelled: the reserve covers the sum of what the two sides may still /// redeem under the cancellation rule, both rounded down per payout. pub fn cancellation_covered( vault: u64, yes_supply: u64, no_supply: u64, cancel_payout_bps: u16, ) -> bool { let yes = match cancel_payout(yes_supply, cancel_payout_bps) { Some(v) => v, None => return false, }; let no = match cancel_payout(no_supply, cancel_payout_bps) { Some(v) => v, None => return false, }; match yes.checked_add(no) { Some(total) => vault >= total, None => false, } } // --------------------------------------------------------------------------- // Tests // // No accounts, no cluster, no dependencies. `cargo test` runs them on the base // toolchain and `cargo +nightly miri test` runs the same set under the // interpreter. // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; /// A reproducible xorshift, so the randomized checks are deterministic and /// need no dependency. This is the offline stand-in for a fuzzer: it drives /// the same functions the instructions call over a wide input space. The /// coverage-guided version of this is the Trident harness against the real /// `.so`. fn xorshift(state: &mut u64) -> u64 { let mut x = *state; x ^= x << 13; x ^= x >> 7; x ^= x << 17; *state = x; x } // -- fee arithmetic ---------------------------------------------------- #[test] fn fee_is_floor_division_of_the_dollars_it_is_charged_on() { // The published illustration: 100 USDC at 2% is exactly 2.00 USDC. assert_eq!(fee_on(100_000_000, 200).unwrap(), 2_000_000); // And it rounds down, never up. assert_eq!(fee_on(1, 200).unwrap(), 0); assert_eq!(fee_on(49, 200).unwrap(), 0); assert_eq!(fee_on(50, 200).unwrap(), 1); assert_eq!(fee_on(10_000, 1).unwrap(), 1); assert_eq!(fee_on(9_999, 1).unwrap(), 0); } #[test] fn a_zero_rate_charges_nothing() { for amount in [0u64, 1, 1_000_000, u64::MAX] { assert_eq!(fee_on(amount, 0).unwrap(), 0); } } #[test] fn fee_never_exceeds_the_amount_at_the_cap() { let mut state = 0xDEAD_BEEF_CAFE_F00Du64; for _ in 0..200_000 { let amount = xorshift(&mut state); let fee = fee_on(amount, FEE_BPS_CAP).unwrap(); assert!( fee <= amount, "a capped fee took more than the amount: {fee} of {amount}" ); // And it is exactly the truncated product. assert_eq!( fee as u128, (amount as u128 * FEE_BPS_CAP as u128) / BPS_DENOM as u128 ); } } #[test] fn mul_div_refuses_a_zero_divisor() { assert_eq!(mul_div(1, 1, 0), None); } // -- checked math at the u64 limits ------------------------------------ #[test] fn overflow_and_underflow_are_refused_at_the_u64_limits() { // The product fits in the u128 intermediate, so a full-range amount at a // rate of exactly the denominator comes back unchanged rather than // overflowing. assert_eq!(fee_on(u64::MAX, BPS_DENOM as u16), Some(u64::MAX)); assert_eq!(mul_div(u64::MAX, 1, 1), Some(u64::MAX)); // (2^64-1) * 2 / 1 cannot fit back into a u64. assert_eq!(mul_div(u64::MAX, 2, 1), None); // A merge of everything at a zero fee returns everything. assert_eq!(redeem_net(u64::MAX, 0).unwrap(), (0, u64::MAX)); // A merge whose fee cannot be represented is refused rather than wrapping. assert!(redeem_net(u64::MAX, FEE_BPS_CAP).is_some()); // The mint gross of the largest pair count overflows and is refused. assert_eq!(mint_gross(u64::MAX, 1), None); assert_eq!(mint_gross(u64::MAX, 0), Some(u64::MAX)); // Cancellation of the largest supply at a bps rate stays inside u64. assert_eq!( cancel_payout(u64::MAX, CANCEL_BPS_CAP).unwrap(), u64::MAX / 2 ); } // -- mint / merge conservation ----------------------------------------- #[test] fn a_mint_pays_the_pair_plus_the_fee_and_the_reserve_gets_exactly_the_pair() { let pairs = 166_666_667u64; // the seeding example's N at p = 0.70, rounded down let gross = mint_gross(pairs, 200).unwrap(); let fee = gross - pairs; assert_eq!(fee, fee_on(pairs, 200).unwrap()); assert_eq!(pairs + fee, gross); // The reserve receives the pair count and nothing less. assert!(gross >= pairs); } #[test] fn a_merge_returns_the_gross_less_the_fee_and_the_reserve_releases_exactly_the_gross() { for gross in [1u64, 2, 999, 1_000_000, 166_666_667, u64::MAX / 2] { let (fee, net) = redeem_net(gross, 200).unwrap(); assert_eq!(fee + net, gross, "the split must consume the gross exactly"); assert!(net <= gross); assert!(fee <= gross); } } #[test] fn merge_and_redeem_charge_the_same_rate() { // The policy applies `r` to pre-resolution merges and post-resolution // winning redemption alike. One function serves both, so this is a // restatement rather than a second implementation, but it pins intent. let amount = 12_345_678u64; assert_eq!(redeem_net(amount, 350), redeem_net(amount, 350)); assert_eq!(redeem_net(amount, 350).unwrap().0, 432_098); } // -- solvency at the boundaries ---------------------------------------- #[test] fn the_pre_resolution_invariant_is_two_sided_after_a_mint() { // Start from an empty market, mint, and check the reserve covers both // supplies. Then mint again. let (mut vault, mut yes, mut no) = (0u64, 0u64, 0u64); assert!(pre_resolution_covered(vault, yes, no)); for amount in [1u64, 1_000_000, 166_666_667] { vault += amount; yes += amount; no += amount; assert!(pre_resolution_covered(vault, yes, no)); } } #[test] fn the_invariant_catches_a_one_sided_supply_and_a_drained_vault() { // A market whose YES supply is larger than the reserve is not covered. assert!(!pre_resolution_covered(100, 101, 100)); assert!(pre_resolution_covered(100, 100, 100)); assert!(pre_resolution_covered(100, 100, 99)); // Post-resolution only the winning side is counted. assert!(post_resolution_covered(50, 50)); assert!(!post_resolution_covered(49, 50)); // An empty market is trivially covered, and a vault of zero with no // claims is covered too. assert!(pre_resolution_covered(0, 0, 0)); assert!(post_resolution_covered(0, 0)); } #[test] fn the_cancellation_rule_preserves_the_complete_set_at_the_cap() { assert!(cancel_rule_preserves_the_complete_set(CANCEL_BPS_CAP)); assert!(cancel_rule_preserves_the_complete_set(0)); assert!(!cancel_rule_preserves_the_complete_set(CANCEL_BPS_CAP + 1)); // At the cap a pair of equal supplies pays out at most the reserve. for supply in [0u64, 1, 7, 1_000_000, u64::MAX / 2] { let paid = 2 * cancel_payout(supply, CANCEL_BPS_CAP).unwrap(); assert!(paid <= supply, "cancellation overpaid: {paid} of {supply}"); } } #[test] fn cancellation_coverage_is_exact_at_the_boundary() { // Two equal supplies at the cap need exactly the reserve they have. assert!(cancellation_covered(1_000, 1_000, 1_000, CANCEL_BPS_CAP)); assert!(!cancellation_covered(999, 1_000, 1_000, CANCEL_BPS_CAP)); // Odd supplies round down, so the last unit is not owed. assert!(cancellation_covered(1, 1, 1, CANCEL_BPS_CAP)); assert!(cancellation_covered(0, 1, 1, CANCEL_BPS_CAP)); // A lower rate needs less. assert!(cancellation_covered(1, 1_000, 1_000, 1)); } // -- randomized property sweeps ---------------------------------------- /// Random sequences of mint and merge. The invariant must hold after every /// step, from an empty market. This is the offline half of the Trident run: /// it drives the same arithmetic over 20_000 sequences of 64 operations. #[test] fn random_mint_and_merge_sequences_keep_the_reserve_covering_both_supplies() { let mut state = 0x9E37_79B9_7F4A_7C15u64; let mut checked = 0u64; for _ in 0..20_000 { let (mut vault, mut yes, mut no) = (0u64, 0u64, 0u64); assert!(pre_resolution_covered(vault, yes, no)); for _ in 0..64 { let amount = 1 + xorshift(&mut state) % 1_000_000_000; if xorshift(&mut state) & 1 == 0 { let fee = fee_on(amount, FEE_BPS_CAP).unwrap(); let gross = mint_gross(amount, FEE_BPS_CAP).unwrap(); // The user pays the gross; the reserve receives `amount`. assert_eq!(gross, amount + fee); vault += amount; yes += amount; no += amount; } else if yes >= amount && no >= amount && vault >= amount { let (_fee, _net) = redeem_net(amount, FEE_BPS_CAP).unwrap(); vault -= amount; yes -= amount; no -= amount; } assert!( pre_resolution_covered(vault, yes, no), "invariant broke: vault={vault} yes={yes} no={no}" ); checked += 1; } } assert_eq!(checked, 1_280_000); } /// Random winning redemptions after FINAL. The supply and the vault must /// fall together, so coverage is preserved by construction and the check /// never fires. #[test] fn random_winning_redemptions_never_overdraw_the_reserve() { let mut state = 0x1234_5678_9ABC_DEF0u64; for _ in 0..20_000 { let supply = 1 + xorshift(&mut state) % 1_000_000_000; let (mut vault, mut winning) = (supply, supply); assert!(post_resolution_covered(vault, winning)); for _ in 0..32 { let amount = 1 + xorshift(&mut state) % (winning.max(1)); if amount > winning { continue; } let (_fee, net) = redeem_net(amount, FEE_BPS_CAP).unwrap(); assert!(net <= amount); vault -= amount; winning -= amount; assert!( post_resolution_covered(vault, winning), "post-resolution coverage broke: vault={vault} winning={winning}" ); } } } /// Random cancellation payouts. Both sides draw from the same reserve at /// the same rate, and the total must never exceed it. #[test] fn random_cancellation_payouts_never_exceed_the_reserve() { let mut state = 0x0FED_CBA9_8765_4321u64; for _ in 0..20_000 { let supply = xorshift(&mut state) % 1_000_000_000; let bps = (xorshift(&mut state) % (CANCEL_BPS_CAP as u64 + 1)) as u16; let (mut vault, mut yes, mut no) = (supply, supply, supply); assert!(cancellation_covered(vault, yes, no, bps)); let mut paid_total = 0u64; for _ in 0..64 { let side = xorshift(&mut state) & 1 == 0; let available = if side { yes } else { no }; if available == 0 { continue; } let amount = 1 + xorshift(&mut state) % available; let payout = cancel_payout(amount, bps).unwrap(); if payout > vault { // The invariant check upstream should have prevented this. panic!("cancellation payout {payout} exceeded the reserve {vault}"); } vault -= payout; paid_total = paid_total.saturating_add(payout); if side { yes -= amount; } else { no -= amount; } assert!(cancellation_covered(vault, yes, no, bps)); } assert!(paid_total <= supply); } } /// A mint never mints claims the reserve cannot back, at any fee rate up to /// the cap. This is the "no unbacked claims" property, stated as an identity /// on the arithmetic rather than as a run over a ledger. #[test] fn no_mint_can_create_an_unbacked_claim() { let mut state = 0xBEEF_0BAD_F00D_1234u64; for _ in 0..200_000 { let pairs = xorshift(&mut state) % 1_000_000_000_000; let bps = (xorshift(&mut state) as u16) % (FEE_BPS_CAP + 1); let gross = mint_gross(pairs, bps).unwrap(); // The reserve receives `pairs`, which is what the minted claims need. assert!(gross >= pairs); // And after the mint the vault holds exactly the pairs it issued. assert!(pre_resolution_covered(pairs, pairs, pairs)); } } // -- the state machine ------------------------------------------------- /// The table from `[internal location withheld]`, transcribed by hand. If the /// program's map and the off-chain map ever diverge, this test is where it /// shows up. const OFF_CHAIN_MAP: [[bool; STATE_COUNT as usize]; STATE_COUNT as usize] = [ // to: OPEN PROPOSED CHALLENGE DISPUTED FINAL CANCELLED /* OPEN */ [false, true, false, false, false, true], /* PROPOSED */ [true, false, true, true, true, true], /* CHALLENGE */ [true, false, false, true, true, true], /* DISPUTED */ [true, false, false, false, true, true], /* FINAL */ [false, false, false, false, false, false], /* CANCELLED */ [false, false, false, false, false, false], ]; #[test] fn transition_map_matches_the_off_chain_table() { for from in 0..STATE_COUNT { for to in 0..STATE_COUNT { assert_eq!( transition_allowed(from, to), OFF_CHAIN_MAP[from as usize][to as usize], "transition {from} -> {to} disagrees with resolution.py" ); } } } #[test] fn final_and_cancelled_are_absorbing() { for to in 0..STATE_COUNT { assert!(!transition_allowed(STATE_FINAL, to), "FINAL accepts {to}"); assert!( !transition_allowed(STATE_CANCELLED, to), "CANCELLED accepts {to}" ); } } #[test] fn every_transition_changes_the_state() { // A settlement instruction that leaves the market in the state it found // it is not a transition, and the map refuses it. That is what stops a // re-propose from being recorded as a new proposal. for state in 0..STATE_COUNT { assert!(!transition_allowed(state, state)); } } #[test] fn illegal_transitions_are_rejected() { // The ones the money paths depend on. assert!(!transition_allowed(STATE_OPEN, STATE_FINAL)); assert!(!transition_allowed(STATE_OPEN, STATE_DISPUTED)); assert!(!transition_allowed(STATE_OPEN, STATE_CHALLENGE)); assert!(!transition_allowed(STATE_FINAL, STATE_OPEN)); assert!(!transition_allowed(STATE_CANCELLED, STATE_OPEN)); assert!(!transition_allowed(STATE_PROPOSED, STATE_PROPOSED)); // An unset or out-of-range state is never a legal source or target. assert!(!transition_allowed(STATE_COUNT, STATE_OPEN)); assert!(!transition_allowed(255, STATE_OPEN)); assert!(!transition_allowed(STATE_OPEN, 255)); } #[test] fn the_legal_paths_are_the_ones_the_process_uses() { assert!(transition_allowed(STATE_OPEN, STATE_PROPOSED)); assert!(transition_allowed(STATE_PROPOSED, STATE_CHALLENGE)); assert!(transition_allowed(STATE_CHALLENGE, STATE_FINAL)); assert!(transition_allowed(STATE_PROPOSED, STATE_FINAL)); assert!(transition_allowed(STATE_PROPOSED, STATE_OPEN)); assert!(transition_allowed(STATE_PROPOSED, STATE_DISPUTED)); assert!(transition_allowed(STATE_DISPUTED, STATE_FINAL)); assert!(transition_allowed(STATE_DISPUTED, STATE_OPEN)); assert!(transition_allowed(STATE_OPEN, STATE_CANCELLED)); assert!(transition_allowed(STATE_DISPUTED, STATE_CANCELLED)); } #[test] fn is_pre_resolution_covers_exactly_the_unfixed_states() { assert!(is_pre_resolution(STATE_OPEN)); assert!(is_pre_resolution(STATE_PROPOSED)); assert!(is_pre_resolution(STATE_CHALLENGE)); assert!(is_pre_resolution(STATE_DISPUTED)); assert!(!is_pre_resolution(STATE_FINAL)); assert!(!is_pre_resolution(STATE_CANCELLED)); assert!(!is_pre_resolution(200)); assert!(is_resolved(STATE_FINAL)); assert!(is_resolved(STATE_CANCELLED)); assert!(!state_valid(200)); } #[test] fn outcomes_are_never_a_zeroed_account() { assert!(!outcome_valid(OUTCOME_NONE)); assert!(outcome_valid(OUTCOME_YES)); assert!(outcome_valid(OUTCOME_NO)); assert!(!outcome_valid(3)); } #[test] fn the_challenge_window_is_only_elapsed_when_one_was_recorded() { // A zero timestamp is not a window, so it is never elapsed and never open. assert!(!challenge_window_elapsed(1_000_000, 0)); assert!(!challenge_window_open(1_000_000, 0)); // Before, at, and after the deadline. assert!(!challenge_window_elapsed(100, 200)); assert!(challenge_window_elapsed(200, 200)); assert!(challenge_window_elapsed(201, 200)); assert!(challenge_window_open(199, 200)); assert!(!challenge_window_open(200, 200)); } }