Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions contracts/tholos-v2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,10 @@ pub enum Error {
/// Rejected outright rather than treated as a no-op, so this call can
/// never be read as altering an already-decided result.
RoundAlreadyDecided = 36,
/// `max_total_weight` exceeded `MAX_TOTAL_WEIGHT_TO_POSITION_RATIO * max_position`.
/// Caps how many effective seats a single split actor can occupy, raising the
/// cost of Sybil-style address splitting.
InvalidWeightRatio = 37,
}

const DAY_IN_LEDGERS: u32 = 17280;
Expand Down Expand Up @@ -627,6 +631,13 @@ const MAX_BOND_AMOUNT: i128 = i128::MAX / (MAX_FINALIZE_REWARD_BPS as i128);
/// any deployment token's full realistic supply range.
const MAX_SETTLEMENT_TOTAL_WEIGHT: i128 = 10_000_000_000_000_000_000;

/// Maximum allowed ratio between `max_total_weight` and `max_position`.
///
/// Bounding this ratio raises the cost of Sybil-style splitting: a single actor
/// must control at least this many distinct positions to approach the
/// plutocratic threshold. See `docs/src/CONTRACT_V2.md` and issue #168.
const MAX_TOTAL_WEIGHT_TO_POSITION_RATIO: i128 = 10;

/// This proposal has exactly one weighted round: no recursive appeals or
/// repeated stake rounds, per V2_RESOLUTION.md's "Lifecycle and the single
/// weighted round". `round` is part of the commitment preimage now so a
Expand Down Expand Up @@ -709,6 +720,13 @@ impl TholosV2 {
return Err(Error::InvalidMaxPosition);
}

let max_allowed_total = max_position
.checked_mul(MAX_TOTAL_WEIGHT_TO_POSITION_RATIO)
.unwrap_or(i128::MAX);
if max_total_weight > max_allowed_total {
return Err(Error::InvalidWeightRatio);
}

let policy = PolicySnapshotV2 {
token,
base_bond,
Expand Down
52 changes: 51 additions & 1 deletion contracts/tholos-v2/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,56 @@ fn test_initialize_rejects_max_total_weight_over_max_bond() {
assert_eq!(result, Err(Ok(Error::InvalidMaxTotalWeight)));
}

#[test]
fn test_initialize_rejects_max_total_weight_above_ratio_bound() {
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 max_position = 100i128;
let max_total_weight = max_position * MAX_TOTAL_WEIGHT_TO_POSITION_RATIO + 1;
let result = init_full(
&client,
&admin,
&token_id,
DEFAULT_REGISTRATION_SECS,
DEFAULT_ANTI_SNIPE_EXT_SECS,
DEFAULT_ANTI_SNIPE_HARD_MAX_SECS,
DEFAULT_REVEAL_SECS,
max_position,
max_total_weight,
);
assert_eq!(result, Err(Ok(Error::InvalidWeightRatio)));
}

#[test]
fn test_initialize_accepts_max_total_weight_at_ratio_bound() {
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 max_position = 100i128;
let max_total_weight = max_position * MAX_TOTAL_WEIGHT_TO_POSITION_RATIO;
let result = init_full(
&client,
&admin,
&token_id,
DEFAULT_REGISTRATION_SECS,
DEFAULT_ANTI_SNIPE_EXT_SECS,
DEFAULT_ANTI_SNIPE_HARD_MAX_SECS,
DEFAULT_REVEAL_SECS,
max_position,
max_total_weight,
);
assert_eq!(result, Ok(Ok(())));
}

#[test]
fn test_initialize_rejects_zero_challenge_window() {
let env = Env::default();
Expand Down Expand Up @@ -1275,7 +1325,7 @@ fn test_register_exceeds_max_position_fails() {
DEFAULT_ANTI_SNIPE_HARD_MAX_SECS,
DEFAULT_REVEAL_SECS,
DEFAULT_BOND + 50,
DEFAULT_MAX_TOTAL_WEIGHT,
(DEFAULT_BOND + 50) * 10,
)
.unwrap()
.unwrap();
Expand Down
Loading