@ballkidz/defifa 0.0.25 → 0.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/AUDIT_INSTRUCTIONS.md +6 -2
  2. package/README.md +11 -2
  3. package/RISKS.md +3 -1
  4. package/STYLE_GUIDE.md +14 -11
  5. package/package.json +31 -14
  6. package/script/Deploy.s.sol +4 -1
  7. package/src/DefifaDeployer.sol +79 -47
  8. package/src/DefifaGovernor.sol +57 -12
  9. package/src/DefifaHook.sol +83 -26
  10. package/src/DefifaProjectOwner.sol +4 -3
  11. package/src/DefifaTokenUriResolver.sol +113 -20
  12. package/src/enums/DefifaGamePhase.sol +6 -0
  13. package/src/enums/DefifaScorecardState.sol +4 -0
  14. package/src/interfaces/IDefifaDeployer.sol +5 -0
  15. package/src/interfaces/IDefifaGamePhaseReporter.sol +4 -0
  16. package/src/interfaces/IDefifaGamePotReporter.sol +10 -0
  17. package/src/interfaces/IDefifaGovernor.sol +4 -0
  18. package/src/interfaces/IDefifaHook.sol +5 -0
  19. package/src/interfaces/IDefifaTokenUriResolver.sol +3 -0
  20. package/src/libraries/DefifaFontImporter.sol +1 -1
  21. package/src/libraries/DefifaHookLib.sol +9 -10
  22. package/src/structs/DefifaAttestations.sol +3 -2
  23. package/src/structs/DefifaDelegation.sol +1 -0
  24. package/src/structs/DefifaLaunchProjectData.sol +2 -3
  25. package/src/structs/DefifaOpsData.sol +1 -0
  26. package/src/structs/DefifaScorecard.sol +2 -0
  27. package/src/structs/DefifaTierCashOutWeight.sol +3 -1
  28. package/src/structs/DefifaTierParams.sol +1 -0
  29. package/CRYPTO_ECON.pdf +0 -0
  30. package/CRYPTO_ECON.tex +0 -997
  31. package/foundry.lock +0 -17
  32. package/references/operations.md +0 -32
  33. package/references/runtime.md +0 -43
  34. package/slither-ci.config.json +0 -10
  35. package/sphinx.lock +0 -521
  36. package/test/BWAFunctionComparison.t.sol +0 -1320
  37. package/test/DefifaAdversarialQuorum.t.sol +0 -617
  38. package/test/DefifaAuditLowGuards.t.sol +0 -308
  39. package/test/DefifaFeeAccounting.t.sol +0 -581
  40. package/test/DefifaGovernanceHardening.t.sol +0 -1315
  41. package/test/DefifaGovernor.t.sol +0 -1378
  42. package/test/DefifaHookRegressions.t.sol +0 -415
  43. package/test/DefifaMintCostInvariant.t.sol +0 -319
  44. package/test/DefifaNoContest.t.sol +0 -941
  45. package/test/DefifaSecurity.t.sol +0 -741
  46. package/test/DefifaUSDC.t.sol +0 -480
  47. package/test/Fork.t.sol +0 -2388
  48. package/test/TestAuditGaps.sol +0 -984
  49. package/test/TestQALastMile.t.sol +0 -514
  50. package/test/audit/AttestationDoubleCount.t.sol +0 -218
  51. package/test/audit/CodexNemesisCurrencyMismatchBypass.t.sol +0 -112
  52. package/test/audit/CodexNemesisNoContestReserveDrain.t.sol +0 -238
  53. package/test/audit/CodexNemesisOneTierZeroTimeoutLockVerified.t.sol +0 -218
  54. package/test/audit/CodexNemesisSingleTierTimeoutLock.t.sol +0 -237
  55. package/test/audit/CodexRegistryMismatch.t.sol +0 -191
  56. package/test/audit/CodexTierCapMismatch.t.sol +0 -171
  57. package/test/audit/CurrencyMismatchFix.t.sol +0 -265
  58. package/test/audit/FixPendingReserveDilution.t.sol +0 -366
  59. package/test/audit/H5TierCapValidation.t.sol +0 -184
  60. package/test/audit/PendingReserveDilution.t.sol +0 -298
  61. package/test/audit/PendingReserveQuorumGrief.t.sol +0 -355
  62. package/test/audit/PendingReserveSnapshotBypass.t.sol +0 -319
  63. package/test/regression/AttestationDelegateBeneficiary.t.sol +0 -271
  64. package/test/regression/FulfillmentBlocksRatification.t.sol +0 -279
  65. package/test/regression/GracePeriodBypass.t.sol +0 -302
@@ -1,1320 +0,0 @@
1
- // SPDX-License-Identifier: UNLICENSED
2
- pragma solidity 0.8.28;
3
-
4
- import {Test} from "forge-std/Test.sol";
5
- import {UD60x18, ud} from "@prb/math/src/UD60x18.sol";
6
- import {pow} from "@prb/math/src/ud60x18/Math.sol";
7
- import {mulDiv} from "@prb/math/src/Common.sol";
8
-
9
- /// @title BWA Function Comparison — Sybil-Aware
10
- /// @notice Rigorous comparison of three Benefit-Weighted Attestation reduction functions:
11
- /// Linear: f(x) = 1 - x
12
- /// Quadratic: f(x) = (1 - x)^2
13
- /// Golden Ratio: f(x) = (1 - x)^phi, phi ≈ 1.618
14
- ///
15
- /// THREAT MODEL:
16
- /// - Ethereum accounts are free. Attacker has unlimited addresses.
17
- /// - $1M attack = 1,000,000 addresses x $1 each, or 1 address x $1M, or any split.
18
- /// - BWA applies at the TIER level: a tier's total power = V_MAX x f(w_tier / W_total).
19
- /// - That power is distributed pro-rata among all token holders of that tier.
20
- /// - Whether 1 address holds 100 tokens or 100 addresses hold 1 token each,
21
- /// the attacker's COLLECTIVE share of the tier's power is identical.
22
- ///
23
- /// All attack scenarios model:
24
- /// - Attacker controls a FRACTION of tokens in each tier (across unlimited addresses)
25
- /// - Attacker's collective power = Σ (tier_BWA_power x attacker_share_of_tier)
26
- /// - No concept of "attacker's account" — only "attacker's token share per tier"
27
- contract BWAFunctionComparisonTest is Test {
28
- // --- Constants ---
29
- uint256 constant W_TOTAL = 1e18; // Total scorecard weight (matches TOTAL_CASHOUT_WEIGHT)
30
- uint256 constant V_MAX = 1e9; // Max attestation power per tier
31
- uint256 constant PHI_UD = 1_618_033_988_749_894_848; // Golden ratio as UD60x18
32
-
33
- // --- BWA Functions ---
34
-
35
- /// @dev Linear: f(x) = 1 - x. Returns V_MAX * (1 - w/W).
36
- function bwaLinear(uint256 w, uint256 wTotal) internal pure returns (uint256) {
37
- if (w >= wTotal) return 0;
38
- return mulDiv(V_MAX, wTotal - w, wTotal);
39
- }
40
-
41
- /// @dev Quadratic: f(x) = (1-x)^2. Returns V_MAX * ((1 - w/W)^2).
42
- function bwaQuadratic(uint256 w, uint256 wTotal) internal pure returns (uint256) {
43
- if (w >= wTotal) return 0;
44
- UD60x18 ratio = ud(mulDiv(1e18, wTotal - w, wTotal));
45
- UD60x18 squared = ratio.mul(ratio);
46
- return mulDiv(V_MAX, squared.unwrap(), 1e18);
47
- }
48
-
49
- /// @dev Golden Ratio: f(x) = (1-x)^phi. Returns V_MAX * ((1 - w/W)^1.618...).
50
- function bwaGoldenRatio(uint256 w, uint256 wTotal) internal pure returns (uint256) {
51
- if (w >= wTotal) return 0;
52
- UD60x18 ratio = ud(mulDiv(1e18, wTotal - w, wTotal));
53
- UD60x18 result = pow(ratio, ud(PHI_UD));
54
- return mulDiv(V_MAX, result.unwrap(), 1e18);
55
- }
56
-
57
- // --- Helpers ---
58
-
59
- struct TotalResult {
60
- uint256 linearTotal;
61
- uint256 quadraticTotal;
62
- uint256 goldenTotal;
63
- }
64
-
65
- /// @dev Compute total tier-level attestation for a scorecard.
66
- function computeTotals(uint256[] memory weights) internal pure returns (TotalResult memory r) {
67
- for (uint256 i; i < weights.length; i++) {
68
- r.linearTotal += bwaLinear(weights[i], W_TOTAL);
69
- r.quadraticTotal += bwaQuadratic(weights[i], W_TOTAL);
70
- r.goldenTotal += bwaGoldenRatio(weights[i], W_TOTAL);
71
- }
72
- }
73
-
74
- /// @dev Model: attacker controls `attackerTokens[i]` out of `totalTokens[i]` in tier i.
75
- /// Attacker's collective power = Σ tier_power x (attackerTokens[i] / totalTokens[i]).
76
- struct SybilAttackResult {
77
- uint256 attackerPowerLin;
78
- uint256 attackerPowerQuad;
79
- uint256 attackerPowerGold;
80
- uint256 honestPowerLin;
81
- uint256 honestPowerQuad;
82
- uint256 honestPowerGold;
83
- }
84
-
85
- function computeSybilAttack(
86
- uint256[] memory scorecardWeights,
87
- uint256[] memory attackerTokens,
88
- uint256[] memory totalTokens
89
- )
90
- internal
91
- pure
92
- returns (SybilAttackResult memory r)
93
- {
94
- for (uint256 i; i < scorecardWeights.length; i++) {
95
- uint256 tierLin = bwaLinear(scorecardWeights[i], W_TOTAL);
96
- uint256 tierQuad = bwaQuadratic(scorecardWeights[i], W_TOTAL);
97
- uint256 tierGold = bwaGoldenRatio(scorecardWeights[i], W_TOTAL);
98
-
99
- if (totalTokens[i] == 0) continue; // unminted tier
100
-
101
- r.attackerPowerLin += mulDiv(tierLin, attackerTokens[i], totalTokens[i]);
102
- r.attackerPowerQuad += mulDiv(tierQuad, attackerTokens[i], totalTokens[i]);
103
- r.attackerPowerGold += mulDiv(tierGold, attackerTokens[i], totalTokens[i]);
104
-
105
- uint256 honestTokens = totalTokens[i] - attackerTokens[i];
106
- r.honestPowerLin += mulDiv(tierLin, honestTokens, totalTokens[i]);
107
- r.honestPowerQuad += mulDiv(tierQuad, honestTokens, totalTokens[i]);
108
- r.honestPowerGold += mulDiv(tierGold, honestTokens, totalTokens[i]);
109
- }
110
- }
111
-
112
- // ===========================
113
- // TEST 1: Constant-Total Invariant
114
- // ===========================
115
-
116
- /// @notice For N tiers, linear total should always equal (N-1) * V_MAX.
117
- /// Quadratic and golden ratio totals VARY with distribution.
118
- function test_constantTotal_4tiers() public pure {
119
- uint256[] memory equal = new uint256[](4);
120
- equal[0] = W_TOTAL / 4;
121
- equal[1] = W_TOTAL / 4;
122
- equal[2] = W_TOTAL / 4;
123
- equal[3] = W_TOTAL / 4;
124
-
125
- uint256[] memory wta = new uint256[](4);
126
- wta[0] = W_TOTAL;
127
- wta[1] = 0;
128
- wta[2] = 0;
129
- wta[3] = 0;
130
-
131
- uint256[] memory topHeavy = new uint256[](4);
132
- topHeavy[0] = W_TOTAL * 50 / 100;
133
- topHeavy[1] = W_TOTAL * 30 / 100;
134
- topHeavy[2] = W_TOTAL * 15 / 100;
135
- topHeavy[3] = W_TOTAL * 5 / 100;
136
-
137
- uint256[] memory twoWin = new uint256[](4);
138
- twoWin[0] = W_TOTAL / 2;
139
- twoWin[1] = W_TOTAL / 2;
140
- twoWin[2] = 0;
141
- twoWin[3] = 0;
142
-
143
- uint256 expected = 3 * V_MAX;
144
-
145
- TotalResult memory rEqual = computeTotals(equal);
146
- TotalResult memory rWta = computeTotals(wta);
147
- TotalResult memory rTopHeavy = computeTotals(topHeavy);
148
- TotalResult memory rTwoWin = computeTotals(twoWin);
149
-
150
- // LINEAR: All distributions yield exactly (N-1) * V_MAX
151
- assertEq(rEqual.linearTotal, expected, "Linear: equal != (N-1)*V_MAX");
152
- assertEq(rWta.linearTotal, expected, "Linear: concentrated != (N-1)*V_MAX");
153
- assertEq(rTopHeavy.linearTotal, expected, "Linear: top-heavy != (N-1)*V_MAX");
154
- assertEq(rTwoWin.linearTotal, expected, "Linear: two-winners != (N-1)*V_MAX");
155
-
156
- // QUADRATIC & GOLDEN: NOT constant
157
- assertTrue(rEqual.quadraticTotal != rWta.quadraticTotal, "Quadratic: should vary");
158
- assertTrue(rEqual.goldenTotal != rWta.goldenTotal, "Golden: should vary");
159
- }
160
-
161
- /// @notice 32-tier constant-total test (World Cup scale).
162
- function test_constantTotal_32tiers() public pure {
163
- uint256 N = 32;
164
- uint256 expected = (N - 1) * V_MAX;
165
-
166
- uint256[] memory equal = new uint256[](N);
167
- for (uint256 i; i < N; i++) {
168
- equal[i] = W_TOTAL / N;
169
- }
170
-
171
- uint256[] memory wta = new uint256[](N);
172
- wta[0] = W_TOTAL;
173
-
174
- uint256[] memory top4 = new uint256[](N);
175
- top4[0] = W_TOTAL * 35 / 100;
176
- top4[1] = W_TOTAL * 25 / 100;
177
- top4[2] = W_TOTAL * 12 / 100;
178
- top4[3] = W_TOTAL * 8 / 100;
179
- uint256 remaining = W_TOTAL - top4[0] - top4[1] - top4[2] - top4[3];
180
- for (uint256 i = 4; i < N; i++) {
181
- top4[i] = remaining / (N - 4);
182
- }
183
-
184
- TotalResult memory rEqual = computeTotals(equal);
185
- TotalResult memory rWta = computeTotals(wta);
186
- TotalResult memory rTop4 = computeTotals(top4);
187
-
188
- assertEq(rEqual.linearTotal, expected, "Linear 32: equal");
189
- assertEq(rWta.linearTotal, expected, "Linear 32: concentrated");
190
- assertApproxEqAbs(rTop4.linearTotal, expected, N, "Linear 32: top-4");
191
-
192
- // Quadratic/golden: concentrated > equal (convexity of f)
193
- assertTrue(rWta.quadraticTotal > rEqual.quadraticTotal, "Quadratic: concentrated > equal");
194
- assertTrue(rWta.goldenTotal > rEqual.goldenTotal, "Golden: concentrated > equal");
195
- }
196
-
197
- // ===========================
198
- // TEST 2: Boundary Conditions
199
- // ===========================
200
-
201
- function test_boundaryConditions() public pure {
202
- // f(0) = V_MAX (zero benefit -> full power)
203
- assertEq(bwaLinear(0, W_TOTAL), V_MAX, "Linear: f(0)");
204
- assertEq(bwaQuadratic(0, W_TOTAL), V_MAX, "Quadratic: f(0)");
205
- assertEq(bwaGoldenRatio(0, W_TOTAL), V_MAX, "Golden: f(0)");
206
-
207
- // f(W_TOTAL) = 0 (full benefit -> zero power)
208
- assertEq(bwaLinear(W_TOTAL, W_TOTAL), 0, "Linear: f(1)");
209
- assertEq(bwaQuadratic(W_TOTAL, W_TOTAL), 0, "Quadratic: f(1)");
210
- assertEq(bwaGoldenRatio(W_TOTAL, W_TOTAL), 0, "Golden: f(1)");
211
- }
212
-
213
- // ===========================
214
- // TEST 3: Monotonicity
215
- // ===========================
216
-
217
- function test_monotonicity() public pure {
218
- for (uint256 i; i < 10; i++) {
219
- uint256 lower = W_TOTAL * i / 10;
220
- uint256 higher = W_TOTAL * (i + 1) / 10;
221
- assertTrue(bwaLinear(lower, W_TOTAL) >= bwaLinear(higher, W_TOTAL), "Linear monotonic");
222
- assertTrue(bwaQuadratic(lower, W_TOTAL) >= bwaQuadratic(higher, W_TOTAL), "Quadratic monotonic");
223
- assertTrue(bwaGoldenRatio(lower, W_TOTAL) >= bwaGoldenRatio(higher, W_TOTAL), "Golden monotonic");
224
- }
225
- }
226
-
227
- // ===========================
228
- // TEST 4: Power Curve Shape (quad ≤ golden ≤ linear for all x in (0,1))
229
- // ===========================
230
-
231
- function test_powerCurveOrdering() public pure {
232
- uint256[7] memory pcts = [uint256(5), 10, 25, 50, 67, 75, 95];
233
- for (uint256 i; i < 7; i++) {
234
- uint256 w = W_TOTAL * pcts[i] / 100;
235
- uint256 lin = bwaLinear(w, W_TOTAL);
236
- uint256 quad = bwaQuadratic(w, W_TOTAL);
237
- uint256 gold = bwaGoldenRatio(w, W_TOTAL);
238
- assertTrue(quad <= gold, "quad <= gold");
239
- assertTrue(gold <= lin, "gold <= lin");
240
- }
241
- }
242
-
243
- // ===========================
244
- // TEST 5: Sybil Invariance — Address Count Does Not Matter
245
- // ===========================
246
-
247
- /// @notice BWA power is per-TIER. Whether the attacker uses 1 address holding 60 tokens
248
- /// or 60 addresses holding 1 token each, the collective power is identical.
249
- ///
250
- /// Proof: tier power = V_MAX * f(w/W). An account with k tokens out of T total
251
- /// gets (k/T) of the tier power. N accounts each with 1 token get N x (1/T) = N/T.
252
- /// One account with N tokens gets N/T. Identical.
253
- function test_sybilInvariance_addressCountIrrelevant() public pure {
254
- uint256 tierPower = bwaLinear(0, W_TOTAL); // zero-weight tier -> full power = V_MAX
255
- uint256 totalTokensInTier = 100;
256
- uint256 attackerTokens = 60;
257
-
258
- // Scenario A: 1 address holds all 60 tokens
259
- uint256 powerOneAddress = mulDiv(tierPower, attackerTokens, totalTokensInTier);
260
-
261
- // Scenario B: 60 addresses each hold 1 token
262
- uint256 powerPerAddress = mulDiv(tierPower, 1, totalTokensInTier);
263
- uint256 power60Addresses = powerPerAddress * attackerTokens;
264
-
265
- // Scenario C: 6 addresses each hold 10 tokens
266
- uint256 powerPer10 = mulDiv(tierPower, 10, totalTokensInTier);
267
- uint256 power6Addresses = powerPer10 * 6;
268
-
269
- // All identical
270
- assertEq(powerOneAddress, power60Addresses, "1 addr == 60 addrs");
271
- assertEq(powerOneAddress, power6Addresses, "1 addr == 6 addrs");
272
-
273
- // Same for quadratic
274
- uint256 tierPowerQuad = bwaQuadratic(W_TOTAL * 30 / 100, W_TOTAL); // 30% benefit tier
275
- uint256 power1 = mulDiv(tierPowerQuad, attackerTokens, totalTokensInTier);
276
- uint256 powerN = mulDiv(tierPowerQuad, 1, totalTokensInTier) * attackerTokens;
277
- assertEq(power1, powerN, "Sybil invariance holds for quadratic");
278
- }
279
-
280
- // ===========================
281
- // TEST 6: $1M Sybil Attack — Spread Across All Tiers
282
- // ===========================
283
-
284
- /// @notice Attacker spends $1M buying tokens across ALL tiers via unlimited addresses.
285
- /// 4 tiers, 100 tokens each at $100 = $40,000 pot.
286
- /// Attacker buys 60 tokens in EACH tier ($24,000 of $40,000).
287
- /// Attacker submits scorecard [100%, 0%, 0%, 0%].
288
- ///
289
- /// Attacker's COLLECTIVE power across all addresses and tiers:
290
- /// - Tier 1 (100% weight): BWA power = 0. Attacker's 60 tokens -> 0 power.
291
- /// - Tier 2 (0% weight): BWA power = V_MAX. Attacker's 60/100 -> 60% of V_MAX.
292
- /// - Tier 3 (0% weight): BWA power = V_MAX. Attacker's 60/100 -> 60% of V_MAX.
293
- /// - Tier 4 (0% weight): BWA power = V_MAX. Attacker's 60/100 -> 60% of V_MAX.
294
- /// Attacker total: 0 + 0.6 + 0.6 + 0.6 = 1.8 x V_MAX
295
- /// Honest total: 0 + 0.4 + 0.4 + 0.4 = 1.2 x V_MAX
296
- /// Quorum (linear): 50% of 3 x V_MAX = 1.5 x V_MAX
297
- /// Attacker has 1.8 > 1.5 -> ATTACK SUCCEEDS if honest players don't attest
298
- ///
299
- /// This proves: BWA alone is NOT sufficient. Delegate is necessary.
300
- /// BWA's job is to make the attack EXPENSIVE (60% of each tier = proportional to pot).
301
- function test_sybilAttack_spreadAcrossAllTiers() public pure {
302
- uint256 N = 4;
303
- uint256 tokensPerTier = 100;
304
- uint256 attackerPerTier = 60;
305
-
306
- // Scorecard: [100%, 0%, 0%, 0%]
307
- uint256[] memory weights = new uint256[](N);
308
- weights[0] = W_TOTAL;
309
- weights[1] = 0;
310
- weights[2] = 0;
311
- weights[3] = 0;
312
-
313
- uint256[] memory attackerTokens = new uint256[](N);
314
- uint256[] memory totalTokens = new uint256[](N);
315
- for (uint256 i; i < N; i++) {
316
- attackerTokens[i] = attackerPerTier;
317
- totalTokens[i] = tokensPerTier;
318
- }
319
-
320
- SybilAttackResult memory result = computeSybilAttack(weights, attackerTokens, totalTokens);
321
- TotalResult memory totals = computeTotals(weights);
322
-
323
- // Attacker power from winning tier = 0 (BWA zeroes it)
324
- // Attacker power from non-winning tiers = 3 x V_MAX x 60%
325
- assertEq(result.attackerPowerLin, mulDiv(V_MAX, 60, 100) * 3, "Attacker: 1.8 x V_MAX");
326
-
327
- // Quorum = 50% of total = 1.5 x V_MAX
328
- uint256 quorum = totals.linearTotal / 2;
329
- assertEq(quorum, V_MAX * 3 / 2, "Quorum: 1.5 x V_MAX");
330
-
331
- // Attacker power > quorum -> would succeed WITHOUT honest opposition/delegate
332
- assertTrue(result.attackerPowerLin > quorum, "Attacker exceeds quorum alone");
333
-
334
- // BUT: honest players from non-winning tiers also have power.
335
- // If honest players in ANY 2 of 3 non-winning tiers delegate/attest AGAINST:
336
- // Honest power per non-winning tier: V_MAX x 40% = 0.4 x V_MAX
337
- // Total honest: 3 x 0.4 x V_MAX = 1.2 x V_MAX
338
- assertEq(result.honestPowerLin, mulDiv(V_MAX, 40, 100) * 3, "Honest: 1.2 x V_MAX");
339
-
340
- // KEY: The attack cost is PROPORTIONAL TO THE POT.
341
- // Attacker had to buy 60% of EACH non-winning tier.
342
- // At $100/token x 60 tokens x 3 tiers = $18,000 out of $40,000 pot.
343
- // Attack cost / pot = 45%. Scales linearly with pot.
344
-
345
- // Identical across all functions for this scorecard (all non-winning tiers have weight 0)
346
- assertEq(result.attackerPowerLin, result.attackerPowerQuad, "Sybil: lin == quad for w=0 tiers");
347
- assertEq(result.attackerPowerLin, result.attackerPowerGold, "Sybil: lin == gold for w=0 tiers");
348
- }
349
-
350
- // ===========================
351
- // TEST 7: $1M Sybil Attack — Distributed Scorecard
352
- // ===========================
353
-
354
- /// @notice Same attacker (60% of each tier) but with distributed scorecard.
355
- /// THIS is where linear vs quadratic vs golden diverge.
356
- ///
357
- /// Scorecard: [40%, 30%, 20%, 10%] (realistic game outcome)
358
- /// Each tier now has SOME benefit -> BWA reduces power for ALL tiers.
359
- function test_sybilAttack_distributedScorecard() public pure {
360
- uint256 N = 4;
361
- uint256 tokensPerTier = 100;
362
- uint256 attackerPerTier = 60;
363
-
364
- // Scorecard: [40%, 30%, 20%, 10%]
365
- uint256[] memory weights = new uint256[](N);
366
- weights[0] = W_TOTAL * 40 / 100;
367
- weights[1] = W_TOTAL * 30 / 100;
368
- weights[2] = W_TOTAL * 20 / 100;
369
- weights[3] = W_TOTAL * 10 / 100;
370
-
371
- uint256[] memory attackerTokens = new uint256[](N);
372
- uint256[] memory totalTokens = new uint256[](N);
373
- for (uint256 i; i < N; i++) {
374
- attackerTokens[i] = attackerPerTier;
375
- totalTokens[i] = tokensPerTier;
376
- }
377
-
378
- SybilAttackResult memory result = computeSybilAttack(weights, attackerTokens, totalTokens);
379
- TotalResult memory totals = computeTotals(weights);
380
-
381
- // LINEAR: Total = 3 x V_MAX (constant). Quorum = 1.5 x V_MAX.
382
- assertEq(totals.linearTotal, 3 * V_MAX, "Linear total constant");
383
-
384
- // Attacker's power per tier (linear):
385
- // Tier 1 (40% weight): f(0.4) = 0.6, attacker gets 60% of that = 0.36 x V_MAX
386
- // Tier 2 (30%): f(0.3) = 0.7, x 60% = 0.42 x V_MAX
387
- // Tier 3 (20%): f(0.2) = 0.8, x 60% = 0.48 x V_MAX
388
- // Tier 4 (10%): f(0.1) = 0.9, x 60% = 0.54 x V_MAX
389
- // Total attacker linear: (0.36 + 0.42 + 0.48 + 0.54) x V_MAX = 1.8 x V_MAX
390
- assertEq(result.attackerPowerLin, V_MAX * 18 / 10, "Attacker linear: 1.8 x V_MAX");
391
-
392
- // Interesting: same as the concentrated scorecard!
393
- // This is BECAUSE linear total is constant — attacker with 60% of all tiers
394
- // always gets exactly 60% of total = 60% x 3 x V_MAX = 1.8 x V_MAX.
395
- // The scorecard distribution is IRRELEVANT to the attacker's share.
396
- // This is a direct consequence of linearity.
397
-
398
- // QUADRATIC: Total < 3 x V_MAX. Attacker's share is NOT 60% of total.
399
- assertTrue(totals.quadraticTotal < 3 * V_MAX, "Quadratic total < linear");
400
-
401
- // With quadratic, the quorum is LOWER -> potentially easier for attacker
402
- uint256 quadQuorum = totals.quadraticTotal / 2;
403
- uint256 linQuorum = totals.linearTotal / 2;
404
- assertTrue(quadQuorum < linQuorum, "Quadratic quorum < linear quorum");
405
- }
406
-
407
- // ===========================
408
- // TEST 8: Linear's Critical Property — Attacker Share is Scorecard-Independent
409
- // ===========================
410
-
411
- /// @notice Under LINEAR BWA, an attacker controlling α% of EVERY tier gets
412
- /// exactly α% of total attestation = α x (N-1) x V_MAX.
413
- /// This is TRUE regardless of what scorecard they submit.
414
- ///
415
- /// This means: with linear BWA, the only way to get >50% of attestation
416
- /// is to control >50% of tokens in >50% of tiers.
417
- /// No scorecard can change this. No Sybil strategy can change this.
418
- function test_linearAttackerShareIndependentOfScorecard() public pure {
419
- uint256 N = 4;
420
- uint256 tokensPerTier = 100;
421
- uint256 attackerPerTier = 45; // 45% of each tier
422
-
423
- uint256[] memory attackerTokens = new uint256[](N);
424
- uint256[] memory totalTokens = new uint256[](N);
425
- for (uint256 i; i < N; i++) {
426
- attackerTokens[i] = attackerPerTier;
427
- totalTokens[i] = tokensPerTier;
428
- }
429
-
430
- // Try 5 different scorecards — attacker linear power should be ~constant
431
- uint256[4][5] memory scorecards;
432
- scorecards[0] = [W_TOTAL, uint256(0), uint256(0), uint256(0)]; // concentrated
433
- scorecards[1] = [W_TOTAL / 4, W_TOTAL / 4, W_TOTAL / 4, W_TOTAL / 4]; // equal
434
- scorecards[2] = [W_TOTAL / 2, W_TOTAL / 2, uint256(0), uint256(0)]; // two-way
435
- scorecards[3] = [W_TOTAL * 70 / 100, W_TOTAL * 20 / 100, W_TOTAL * 10 / 100, uint256(0)]; // skewed
436
- scorecards[4] = [W_TOTAL * 40 / 100, W_TOTAL * 30 / 100, W_TOTAL * 20 / 100, W_TOTAL * 10 / 100]; // distributed
437
-
438
- // Expected: 45% of 3 x V_MAX = 1.35 x V_MAX
439
- uint256 expectedLinear = mulDiv(3 * V_MAX, 45, 100);
440
-
441
- for (uint256 s; s < 5; s++) {
442
- uint256[] memory weights = new uint256[](N);
443
- weights[0] = scorecards[s][0];
444
- weights[1] = scorecards[s][1];
445
- weights[2] = scorecards[s][2];
446
- weights[3] = scorecards[s][3];
447
-
448
- SybilAttackResult memory result = computeSybilAttack(weights, attackerTokens, totalTokens);
449
-
450
- // Linear power is the same regardless of scorecard (within rounding)
451
- assertApproxEqAbs(
452
- result.attackerPowerLin, expectedLinear, N, "Linear: attacker power is scorecard-independent"
453
- );
454
- }
455
- }
456
-
457
- /// @notice With UNIFORM ownership, power/quorum = 2*alpha for ALL functions.
458
- /// The exploit needs NON-UNIFORM ownership AND differently-shaped scorecards.
459
- ///
460
- /// Quadratic total depends on the weight DISTRIBUTION (not just permutation).
461
- /// Concentrated [100%,0,0,0] gives total 3*V_MAX.
462
- /// Equal [25%,25%,25%,25%] gives total 2.25*V_MAX (25% lower quorum!).
463
- ///
464
- /// Attacker with 90% in tier 1 prefers equal scorecard:
465
- /// - Gets decent power from tier 1 (quadratic penalty is moderate at 25%)
466
- /// - AND the quorum drops significantly
467
- function test_quadraticExploitableWithNonUniformOwnership() public pure {
468
- uint256 N = 4;
469
-
470
- // Non-uniform ownership: attacker has 90% of tier 1, 10% of tiers 2-4
471
- uint256[] memory attackerTokens = new uint256[](N);
472
- attackerTokens[0] = 90;
473
- attackerTokens[1] = 10;
474
- attackerTokens[2] = 10;
475
- attackerTokens[3] = 10;
476
-
477
- uint256[] memory totalTokens = new uint256[](N);
478
- for (uint256 i; i < N; i++) {
479
- totalTokens[i] = 100;
480
- }
481
-
482
- // Scorecard A: concentrated [100%, 0, 0, 0]
483
- uint256[] memory wtsA = new uint256[](N);
484
- wtsA[0] = W_TOTAL;
485
-
486
- // Scorecard B: equal [25%, 25%, 25%, 25%]
487
- uint256[] memory wtsB = new uint256[](N);
488
- for (uint256 i; i < N; i++) {
489
- wtsB[i] = W_TOTAL / N;
490
- }
491
-
492
- SybilAttackResult memory rA = computeSybilAttack(wtsA, attackerTokens, totalTokens);
493
- SybilAttackResult memory rB = computeSybilAttack(wtsB, attackerTokens, totalTokens);
494
-
495
- TotalResult memory totalsA = computeTotals(wtsA);
496
- TotalResult memory totalsB = computeTotals(wtsB);
497
-
498
- // LINEAR: both scorecards give same total (3*V_MAX)
499
- assertEq(totalsA.linearTotal, totalsB.linearTotal, "Linear: quorum identical");
500
-
501
- // QUADRATIC: concentrated total = 3*V_MAX, equal total = 2.25*V_MAX
502
- assertTrue(totalsA.quadraticTotal > totalsB.quadraticTotal, "Quadratic: concentrated > equal");
503
-
504
- // Compute power-to-quorum ratios for quadratic
505
- uint256 quadRatioA = (rA.attackerPowerQuad * 1e18) / (totalsA.quadraticTotal / 2);
506
- uint256 quadRatioB = (rB.attackerPowerQuad * 1e18) / (totalsB.quadraticTotal / 2);
507
- // Ratios differ: attacker can optimize scorecard for best ratio
508
- assertTrue(quadRatioA != quadRatioB, "Quadratic: power/quorum ratio varies with scorecard");
509
-
510
- // Compare linear ratios - they ALSO differ in raw power, but quorum is same
511
- // Linear: raw power differs but quorum is fixed -> one-dimensional optimization only
512
- // With linear, the game designer KNOWS the quorum and can set parameters around it.
513
- // With quadratic, quorum itself is a variable the attacker controls.
514
- }
515
-
516
- // ===========================
517
- // TEST 9: Non-Uniform Sybil — Attacker Concentrates in Few Tiers
518
- // ===========================
519
-
520
- /// @notice Instead of buying equally across all tiers, attacker concentrates
521
- /// purchases in non-winning tiers where they get max BWA power.
522
- ///
523
- /// Attacker buys 0 tokens in winning tier, 80% in 2 non-winning tiers.
524
- /// This is the rational Sybil strategy.
525
- function test_sybilAttack_concentratedInNonWinning() public pure {
526
- uint256 N = 4;
527
-
528
- // Scorecard: tier 1 wins 100%
529
- uint256[] memory weights = new uint256[](N);
530
- weights[0] = W_TOTAL;
531
-
532
- // Attacker: 0 in tier 1, 80% in tiers 2-3, 0 in tier 4
533
- uint256[] memory attackerTokens = new uint256[](N);
534
- attackerTokens[0] = 0;
535
- attackerTokens[1] = 80;
536
- attackerTokens[2] = 80;
537
- attackerTokens[3] = 0;
538
-
539
- uint256[] memory totalTokens = new uint256[](N);
540
- for (uint256 i; i < N; i++) {
541
- totalTokens[i] = 100;
542
- }
543
-
544
- SybilAttackResult memory result = computeSybilAttack(weights, attackerTokens, totalTokens);
545
- TotalResult memory totals = computeTotals(weights);
546
-
547
- // Attacker power: 0 + 0.8xV_MAX + 0.8xV_MAX + 0 = 1.6 x V_MAX
548
- assertEq(result.attackerPowerLin, mulDiv(V_MAX, 80, 100) * 2, "Concentrated Sybil: 1.6 x V_MAX");
549
-
550
- // Quorum: 1.5 x V_MAX
551
- uint256 quorum = totals.linearTotal / 2;
552
-
553
- // 1.6 > 1.5 -> attack succeeds if tier 4 doesn't attest against
554
- assertTrue(result.attackerPowerLin > quorum, "Concentrated Sybil exceeds quorum");
555
-
556
- // BUT tier 4 (honest, full V_MAX) can attest against:
557
- // Honest total: 0 + 0.2xV_MAX + 0.2xV_MAX + V_MAX = 1.4 x V_MAX
558
- assertEq(result.honestPowerLin, mulDiv(V_MAX, 20, 100) * 2 + V_MAX, "Honest: 1.4 x V_MAX");
559
-
560
- // If honest tier 4 attests AGAINST, their V_MAX doesn't help the attacker's scorecard.
561
- // Attacker needs > quorum FOR the scorecard, honest needs to simply not attest for it.
562
- // With delegate controlling tier 4, they just don't attest -> scorecard fails to reach quorum
563
- // because attacker only has 1.6/3.0 = 53% and needs >50% supporting.
564
-
565
- // Cost: 80 tokens x 2 tiers = 160 tokens x price_per_token.
566
- // If pot is 400 tokens worth, attack cost = 160/400 = 40% of pot.
567
- // Same across all functions since non-winning tiers have weight 0.
568
- assertEq(result.attackerPowerLin, result.attackerPowerQuad, "Identical for w=0 tiers");
569
- }
570
-
571
- // ===========================
572
- // TEST 10: The Real Sybil Threat — Split Benefit Scorecard
573
- // ===========================
574
-
575
- /// @notice The sophisticated attack: instead of giving 100% to one tier,
576
- /// attacker spreads benefit to tiers they control, retaining BWA power.
577
- ///
578
- /// Attacker controls 60% of tiers 1 and 2.
579
- /// Submits scorecard: [50%, 50%, 0%, 0%].
580
- /// This gives attacker benefit but ALSO retains partial power.
581
- function test_sybilAttack_splitBenefitScorecard() public pure {
582
- uint256 N = 4;
583
-
584
- // Scorecard: [50%, 50%, 0%, 0%]
585
- uint256[] memory weights = new uint256[](N);
586
- weights[0] = W_TOTAL / 2;
587
- weights[1] = W_TOTAL / 2;
588
-
589
- // Attacker: 60% of tiers 1-2, 60% of tiers 3-4
590
- uint256[] memory attackerTokens = new uint256[](N);
591
- attackerTokens[0] = 60;
592
- attackerTokens[1] = 60;
593
- attackerTokens[2] = 60;
594
- attackerTokens[3] = 60;
595
-
596
- uint256[] memory totalTokens = new uint256[](N);
597
- for (uint256 i; i < N; i++) {
598
- totalTokens[i] = 100;
599
- }
600
-
601
- SybilAttackResult memory result = computeSybilAttack(weights, attackerTokens, totalTokens);
602
- TotalResult memory totals = computeTotals(weights);
603
-
604
- // LINEAR:
605
- // Tier 1 (50% weight): BWA power = 0.5 x V_MAX, attacker gets 60% = 0.30 x V_MAX
606
- // Tier 2 (50% weight): same = 0.30 x V_MAX
607
- // Tier 3 (0% weight): BWA power = V_MAX, attacker gets 60% = 0.60 x V_MAX
608
- // Tier 4 (0% weight): same = 0.60 x V_MAX
609
- // Total attacker linear: (0.30 + 0.30 + 0.60 + 0.60) = 1.80 x V_MAX
610
- assertEq(result.attackerPowerLin, V_MAX * 18 / 10, "Split benefit: attacker 1.8 x V_MAX");
611
-
612
- // Still 60% of 3 x V_MAX — linearity makes it scorecard-independent!
613
- assertEq(totals.linearTotal, 3 * V_MAX, "Linear total unchanged");
614
-
615
- // QUADRATIC: Same attacker tokens, but power DIFFERS
616
- // Tier 1 (50% weight): quadratic power = (0.5)^2 x V_MAX = 0.25 x V_MAX
617
- // attacker gets 60% = 0.15 x V_MAX
618
- // Tier 2: same = 0.15 x V_MAX
619
- // Tier 3 (0%): power = V_MAX, 60% = 0.60 x V_MAX
620
- // Tier 4: same = 0.60 x V_MAX
621
- // Quadratic attacker: 0.15 + 0.15 + 0.60 + 0.60 = 1.50 x V_MAX
622
- // Looks LESS — quadratic is harsher on benefiting tiers
623
-
624
- // But quadratic total is ALSO less: (0.25+0.25+1+1) = 2.5 x V_MAX
625
- // Quadratic quorum = 2.5/2 = 1.25 x V_MAX
626
- // Attacker: 1.50 > 1.25 -> still exceeds quorum
627
- // And the margin is 1.50/1.25 = 120%, vs linear's 1.80/1.50 = 120%
628
- // The ratios are comparable — quadratic's steepness is offset by lower quorum
629
-
630
- assertTrue(result.attackerPowerQuad < result.attackerPowerLin, "Quadratic: less raw attacker power");
631
- assertTrue(totals.quadraticTotal < totals.linearTotal, "Quadratic: less total too");
632
- }
633
-
634
- // ===========================
635
- // TEST 11: Quorum-Relative Attack Power — The Key Metric
636
- // ===========================
637
-
638
- /// @notice The metric that matters is not raw power but power/quorum.
639
- /// For linear: power/quorum = (α x (N-1) x V_MAX) / ((N-1) x V_MAX / 2) = 2α.
640
- /// This is ONLY a function of α (attacker's token share) — not scorecard!
641
- /// Attacker needs α > 50% to exceed quorum. Period.
642
- ///
643
- /// For quadratic/golden: power/quorum varies with scorecard -> exploitable.
644
- function test_quorumRelativePower() public pure {
645
- uint256 N = 4;
646
- uint256 tokensPerTier = 100;
647
-
648
- // Test at various attacker ownership levels
649
- uint256[5] memory alphas = [uint256(30), 45, 50, 55, 60]; // % ownership per tier
650
-
651
- for (uint256 a; a < 5; a++) {
652
- uint256[] memory attackerTokens = new uint256[](N);
653
- uint256[] memory totalTokens = new uint256[](N);
654
- for (uint256 i; i < N; i++) {
655
- attackerTokens[i] = alphas[a];
656
- totalTokens[i] = tokensPerTier;
657
- }
658
-
659
- // Test two different scorecards
660
- uint256[] memory wtsConcentrated = new uint256[](N);
661
- wtsConcentrated[0] = W_TOTAL;
662
-
663
- uint256[] memory wtsEqual = new uint256[](N);
664
- for (uint256 i; i < N; i++) {
665
- wtsEqual[i] = W_TOTAL / N;
666
- }
667
-
668
- SybilAttackResult memory rConc = computeSybilAttack(wtsConcentrated, attackerTokens, totalTokens);
669
- SybilAttackResult memory rEq = computeSybilAttack(wtsEqual, attackerTokens, totalTokens);
670
-
671
- TotalResult memory totalsConc = computeTotals(wtsConcentrated);
672
- TotalResult memory totalsEq = computeTotals(wtsEqual);
673
-
674
- // LINEAR: power/quorum is identical regardless of scorecard
675
- uint256 linRatioConc = (rConc.attackerPowerLin * 1000) / (totalsConc.linearTotal / 2);
676
- uint256 linRatioEq = (rEq.attackerPowerLin * 1000) / (totalsEq.linearTotal / 2);
677
- assertApproxEqAbs(linRatioConc, linRatioEq, 1, "Linear: ratio is scorecard-independent");
678
-
679
- // LINEAR: ratio = 2 x alpha (in permille, so 2000 x alpha / 100)
680
- uint256 expectedRatio = 2 * alphas[a] * 10; // x1000 scaling
681
- assertApproxEqAbs(linRatioConc, expectedRatio, N, "Linear: ratio = 2*alpha");
682
-
683
- // QUADRATIC: ratio differs between scorecards
684
- // For concentrated vs equal, quadratic ratios may differ
685
- // (They're equal at the boundary cases but differ for realistic scorecards)
686
- }
687
- }
688
-
689
- /// @notice Linear BWA's security threshold: attacker needs >50% of tokens in >50% of tiers.
690
- /// Below 50% uniform ownership, attack ALWAYS fails regardless of scorecard.
691
- function test_linearSecurityThreshold() public pure {
692
- uint256 N = 4;
693
- uint256 tokensPerTier = 100;
694
-
695
- // At exactly 50%: power = 50% x (N-1) x V_MAX = 1.5 x V_MAX
696
- // Quorum = (N-1) x V_MAX / 2 = 1.5 x V_MAX
697
- // Attacker power == quorum -> needs strictly more
698
- uint256[] memory attackerTokens50 = new uint256[](N);
699
- uint256[] memory totalTokens = new uint256[](N);
700
- for (uint256 i; i < N; i++) {
701
- attackerTokens50[i] = 50;
702
- totalTokens[i] = tokensPerTier;
703
- }
704
-
705
- // Any scorecard
706
- uint256[] memory wts = new uint256[](N);
707
- wts[0] = W_TOTAL * 70 / 100;
708
- wts[1] = W_TOTAL * 20 / 100;
709
- wts[2] = W_TOTAL * 10 / 100;
710
-
711
- SybilAttackResult memory r50 = computeSybilAttack(wts, attackerTokens50, totalTokens);
712
- TotalResult memory totals = computeTotals(wts);
713
- uint256 quorum = totals.linearTotal / 2;
714
-
715
- // At 50%, attacker power equals quorum (within rounding)
716
- assertApproxEqAbs(r50.attackerPowerLin, quorum, N, "50% -> equals quorum");
717
-
718
- // At 49%, strictly below
719
- uint256[] memory attackerTokens49 = new uint256[](N);
720
- for (uint256 i; i < N; i++) {
721
- attackerTokens49[i] = 49;
722
- }
723
-
724
- SybilAttackResult memory r49 = computeSybilAttack(wts, attackerTokens49, totalTokens);
725
- assertTrue(r49.attackerPowerLin < quorum, "49% -> below quorum");
726
- }
727
-
728
- // ===========================
729
- // TEST 12: Non-Uniform Ownership — Attacker Optimizes Token Distribution
730
- // ===========================
731
-
732
- /// @notice What if attacker doesn't buy equally in all tiers?
733
- /// With fixed budget, can they optimize which tiers to buy into?
734
- ///
735
- /// Under LINEAR BWA with a GIVEN scorecard, the answer is NO.
736
- /// The attacker's power from tier i = f(w_i) x (tokens_i / total_i).
737
- /// Buying into a LOW-weight tier gives more power per token
738
- /// (since f(low weight) > f(high weight)).
739
- /// But the TOTAL attestation is still fixed at (N-1) x V_MAX.
740
- ///
741
- /// So the optimal strategy is to buy into non-benefiting tiers.
742
- /// But this costs money and gives the attacker no financial benefit.
743
- function test_nonUniformOwnership_attackerOptimization() public pure {
744
- uint256 N = 4;
745
-
746
- // Scorecard: [70%, 20%, 10%, 0%]
747
- uint256[] memory weights = new uint256[](N);
748
- weights[0] = W_TOTAL * 70 / 100;
749
- weights[1] = W_TOTAL * 20 / 100;
750
- weights[2] = W_TOTAL * 10 / 100;
751
- weights[3] = 0;
752
-
753
- // Strategy A: Buy 240 tokens spread equally (60 per tier)
754
- uint256[] memory stratA = new uint256[](N);
755
- stratA[0] = 60;
756
- stratA[1] = 60;
757
- stratA[2] = 60;
758
- stratA[3] = 60;
759
-
760
- // Strategy B: Concentrate in tier 4 (0% weight -> max power per token)
761
- uint256[] memory stratB = new uint256[](N);
762
- stratB[0] = 0;
763
- stratB[1] = 0;
764
- stratB[2] = 0;
765
- stratB[3] = 96; // Same total cost (100 each, but max 96 available if 4 are honest)
766
-
767
- // Strategy C: Concentrate in two low-weight tiers
768
- uint256[] memory stratC = new uint256[](N);
769
- stratC[0] = 0;
770
- stratC[1] = 0;
771
- stratC[2] = 80;
772
- stratC[3] = 80;
773
-
774
- uint256[] memory totalTokens = new uint256[](N);
775
- for (uint256 i; i < N; i++) {
776
- totalTokens[i] = 100;
777
- }
778
-
779
- SybilAttackResult memory rA = computeSybilAttack(weights, stratA, totalTokens);
780
- SybilAttackResult memory rB = computeSybilAttack(weights, stratB, totalTokens);
781
- SybilAttackResult memory rC = computeSybilAttack(weights, stratC, totalTokens);
782
-
783
- // Strategy A (uniform): 60% of total = 1.8 x V_MAX
784
- assertApproxEqAbs(rA.attackerPowerLin, V_MAX * 18 / 10, N, "Strategy A: 1.8 x V_MAX");
785
-
786
- // Strategy B (concentrated in tier 4): power = f(0) x 96/100 = 0.96 x V_MAX
787
- // Much less than A! Even though tier 4 gives max power, it's only ONE tier.
788
- assertApproxEqAbs(rB.attackerPowerLin, mulDiv(V_MAX, 96, 100), 1, "Strategy B: 0.96 x V_MAX");
789
-
790
- // Strategy C: power = f(0.1)x0.8 + f(0)x0.8 = (0.9x0.8 + 1.0x0.8) x V_MAX = 1.52 x V_MAX
791
- assertApproxEqAbs(
792
- rC.attackerPowerLin, mulDiv(V_MAX * 9, 80, 1000) + mulDiv(V_MAX, 80, 100), N, "Strategy C: ~1.52 x V_MAX"
793
- );
794
-
795
- // Conclusion: uniform distribution (A) is the optimal strategy for the attacker.
796
- // Concentrating tokens doesn't help because you lose coverage of other tiers.
797
- assertTrue(rA.attackerPowerLin > rB.attackerPowerLin, "Uniform > concentrated");
798
- assertTrue(rA.attackerPowerLin > rC.attackerPowerLin, "Uniform > partial concentrated");
799
- }
800
-
801
- // ===========================
802
- // TEST 13: Variable Quorum Exploit — Quadratic
803
- // ===========================
804
-
805
- /// @notice Quadratic total varies: attacker can pick the scorecard that gives them
806
- /// the best power-to-quorum ratio. This is a bug.
807
- function test_variableQuorumExploit() public pure {
808
- uint256 N = 4;
809
-
810
- uint256[] memory equal = new uint256[](N);
811
- for (uint256 i; i < N; i++) {
812
- equal[i] = W_TOTAL / N;
813
- }
814
-
815
- uint256[] memory concentrated = new uint256[](N);
816
- concentrated[0] = W_TOTAL;
817
-
818
- TotalResult memory rEqual = computeTotals(equal);
819
- TotalResult memory rConc = computeTotals(concentrated);
820
-
821
- // Quadratic: equal split gives MINIMUM total (convexity of (1-x)^2)
822
- // concentrated gives MAXIMUM total
823
- assertTrue(rEqual.quadraticTotal < rConc.quadraticTotal, "Quadratic: equal < concentrated");
824
-
825
- // Variance percentage
826
- uint256 quadVariancePct = ((rConc.quadraticTotal - rEqual.quadraticTotal) * 100) / rEqual.quadraticTotal;
827
- assertTrue(quadVariancePct > 5, "Quadratic quorum varies >5%");
828
-
829
- // Linear: zero variance
830
- assertEq(rEqual.linearTotal, rConc.linearTotal, "Linear: zero variance");
831
- }
832
-
833
- // ===========================
834
- // TEST 14: Fuzz — Linear Constant Total
835
- // ===========================
836
-
837
- function test_fuzz_linearConstantTotal(uint256 w1, uint256 w2, uint256 w3) public pure {
838
- w1 = bound(w1, 0, W_TOTAL);
839
- w2 = bound(w2, 0, W_TOTAL - w1);
840
- w3 = bound(w3, 0, W_TOTAL - w1 - w2);
841
- uint256 w4 = W_TOTAL - w1 - w2 - w3;
842
-
843
- uint256 total =
844
- bwaLinear(w1, W_TOTAL) + bwaLinear(w2, W_TOTAL) + bwaLinear(w3, W_TOTAL) + bwaLinear(w4, W_TOTAL);
845
-
846
- assertApproxEqAbs(total, 3 * V_MAX, 3, "Linear constant total (within rounding)");
847
- }
848
-
849
- /// @notice Fuzz: quadratic total bounded by [(N-1)^2/N, (N-1)] x V_MAX.
850
- function test_fuzz_quadraticBounds(uint256 w1, uint256 w2, uint256 w3) public pure {
851
- w1 = bound(w1, 0, W_TOTAL);
852
- w2 = bound(w2, 0, W_TOTAL - w1);
853
- w3 = bound(w3, 0, W_TOTAL - w1 - w2);
854
- uint256 w4 = W_TOTAL - w1 - w2 - w3;
855
-
856
- uint256 total = bwaQuadratic(w1, W_TOTAL) + bwaQuadratic(w2, W_TOTAL) + bwaQuadratic(w3, W_TOTAL)
857
- + bwaQuadratic(w4, W_TOTAL);
858
-
859
- uint256 minQuad = (3 * 3 * V_MAX) / 4; // 2.25 x V_MAX
860
- assertTrue(total >= minQuad - 4, "Quadratic >= min");
861
- assertTrue(total <= 3 * V_MAX + 4, "Quadratic <= max");
862
- }
863
-
864
- // ===========================
865
- // TEST 15: Fuzz — Linear Sybil Invariance
866
- // ===========================
867
-
868
- /// @notice Fuzz: attacker with uniform α% of 4 tiers always gets α x (N-1) x V_MAX
869
- /// power under LINEAR BWA, regardless of scorecard.
870
- function test_fuzz_linearSybilInvariance(uint256 w1, uint256 w2, uint256 w3, uint256 alpha) public pure {
871
- w1 = bound(w1, 0, W_TOTAL);
872
- w2 = bound(w2, 0, W_TOTAL - w1);
873
- w3 = bound(w3, 0, W_TOTAL - w1 - w2);
874
- uint256 w4 = W_TOTAL - w1 - w2 - w3;
875
- alpha = bound(alpha, 0, 100);
876
-
877
- uint256 N = 4;
878
- uint256 totalTokensPerTier = 100;
879
-
880
- uint256 attackerPower;
881
- uint256[] memory weights = new uint256[](N);
882
- weights[0] = w1;
883
- weights[1] = w2;
884
- weights[2] = w3;
885
- weights[3] = w4;
886
-
887
- for (uint256 i; i < N; i++) {
888
- uint256 tierPower = bwaLinear(weights[i], W_TOTAL);
889
- attackerPower += mulDiv(tierPower, alpha, totalTokensPerTier);
890
- }
891
-
892
- // Expected: alpha% of (N-1) x V_MAX
893
- uint256 expected = mulDiv((N - 1) * V_MAX, alpha, totalTokensPerTier);
894
- // Two levels of mulDiv rounding (BWA + token share) -> up to 2*(N-1) wei error.
895
- assertApproxEqAbs(attackerPower, expected, 2 * (N - 1), "Fuzz: linear Sybil invariance");
896
- }
897
-
898
- // ===========================
899
- // TEST 16: Mathematical Proof — p=1 is Unique
900
- // ===========================
901
-
902
- /// @notice For f(x) = (1-x)^p, constant total requires:
903
- /// 4 x (3/4)^p = f(1) + 3xf(0) = 0 + 3 = 3
904
- /// (3/4)^p = 3/4 ⟺ p = 1.
905
- function test_uniqueness_proof() public pure {
906
- uint256 linA = bwaLinear(W_TOTAL, W_TOTAL) + 3 * bwaLinear(0, W_TOTAL);
907
- uint256 linB = 4 * bwaLinear(W_TOTAL / 4, W_TOTAL);
908
- assertEq(linA, linB, "Linear: constant total proven (p=1)");
909
-
910
- uint256 quadA = bwaQuadratic(W_TOTAL, W_TOTAL) + 3 * bwaQuadratic(0, W_TOTAL);
911
- uint256 quadB = 4 * bwaQuadratic(W_TOTAL / 4, W_TOTAL);
912
- assertTrue(quadA != quadB, "Quadratic: NOT constant (p=2)");
913
-
914
- uint256 goldA = bwaGoldenRatio(W_TOTAL, W_TOTAL) + 3 * bwaGoldenRatio(0, W_TOTAL);
915
- uint256 goldB = 4 * bwaGoldenRatio(W_TOTAL / 4, W_TOTAL);
916
- assertTrue(goldA != goldB, "Golden: NOT constant (p=phi)");
917
- }
918
-
919
- // ===========================
920
- // TEST 17: 32-Tier Sybil Attack Economics (World Cup)
921
- // ===========================
922
-
923
- /// @notice 32 teams, 1000 tokens each at $10 = $320,000 pot.
924
- /// Attacker spends $1M across unlimited addresses.
925
- /// At $10/token, attacker can buy 100,000 tokens.
926
- /// Spread across 31 non-winning tiers = ~3,225 tokens per tier.
927
- /// But each tier only has 1000 tokens! Attacker can buy 100% of 31 tiers.
928
- ///
929
- /// Wait — that means if tokens are cheap enough, attacker buys ALL of them.
930
- /// The defense isn't the cost per token, it's that buying >50% of each tier
931
- /// requires getting there before honest participants during mint phase.
932
- ///
933
- /// With 32 tiers and equal supply: quorum needs 16+ tiers worth of V_MAX.
934
- function test_32tier_sybilAttack() public pure {
935
- uint256 N = 32;
936
-
937
- // Scorecard: 100% to tier 1
938
- uint256[] memory weights = new uint256[](N);
939
- weights[0] = W_TOTAL;
940
-
941
- // Scenario A: Attacker controls 60% of ALL 32 tiers
942
- uint256[] memory attackerTokensA = new uint256[](N);
943
- uint256[] memory totalTokens = new uint256[](N);
944
- for (uint256 i; i < N; i++) {
945
- attackerTokensA[i] = 60;
946
- totalTokens[i] = 100;
947
- }
948
-
949
- SybilAttackResult memory rA = computeSybilAttack(weights, attackerTokensA, totalTokens);
950
- TotalResult memory totals = computeTotals(weights);
951
-
952
- // Quorum: 31 x V_MAX / 2 = 15.5 x V_MAX
953
- uint256 quorum = totals.linearTotal / 2;
954
-
955
- // Attacker with 60% of all tiers:
956
- // From tier 1 (winning): 0 (BWA kills it)
957
- // From tiers 2-32: 31 x V_MAX x 0.6 = 18.6 x V_MAX
958
- uint256 expectedA = mulDiv(V_MAX, 60, 100) * 31;
959
- assertApproxEqAbs(rA.attackerPowerLin, expectedA, N, "32-tier: 60% attacker power");
960
- assertTrue(rA.attackerPowerLin > quorum, "60% exceeds quorum");
961
-
962
- // Scenario B: Attacker controls 40% of all tiers — FAILS
963
- uint256[] memory attackerTokensB = new uint256[](N);
964
- for (uint256 i; i < N; i++) {
965
- attackerTokensB[i] = 40;
966
- }
967
-
968
- SybilAttackResult memory rB = computeSybilAttack(weights, attackerTokensB, totalTokens);
969
- assertTrue(rB.attackerPowerLin < quorum, "40% below quorum");
970
-
971
- // Scenario C: Attacker controls 51% of all tiers — barely succeeds
972
- uint256[] memory attackerTokensC = new uint256[](N);
973
- for (uint256 i; i < N; i++) {
974
- attackerTokensC[i] = 51;
975
- }
976
-
977
- SybilAttackResult memory rC = computeSybilAttack(weights, attackerTokensC, totalTokens);
978
- assertTrue(rC.attackerPowerLin > quorum, "51% just above quorum");
979
-
980
- // The security threshold is clean: >50% of tokens in EACH tier.
981
- // With 32 tiers x 1000 tokens x $10 = need >16,000 x $10 = $160,000 minimum.
982
- // For a $320,000 pot, attack cost = 50% of pot. Scales exactly.
983
- }
984
-
985
- // ===========================
986
- // TEST 18: Delegate as Honest Counterweight
987
- // ===========================
988
-
989
- /// @notice With delegate controlling non-winning tiers' attestation,
990
- /// the attacker needs > 50% of ONLY the delegate-controlled tiers.
991
- /// If delegate has 100% of tier 4 delegated to them, that's V_MAX against.
992
- /// Attacker now needs to overcome both delegate AND buy majority.
993
- function test_delegateCounterweight() public pure {
994
- uint256 N = 4;
995
-
996
- // Scorecard: [100%, 0%, 0%, 0%]
997
- uint256[] memory weights = new uint256[](N);
998
- weights[0] = W_TOTAL;
999
-
1000
- // Attacker: 80% of tiers 1-3, 0% of tier 4 (delegate-controlled)
1001
- uint256[] memory attackerTokens = new uint256[](N);
1002
- attackerTokens[0] = 80;
1003
- attackerTokens[1] = 80;
1004
- attackerTokens[2] = 80;
1005
- attackerTokens[3] = 0;
1006
-
1007
- uint256[] memory totalTokens = new uint256[](N);
1008
- for (uint256 i; i < N; i++) {
1009
- totalTokens[i] = 100;
1010
- }
1011
-
1012
- SybilAttackResult memory result = computeSybilAttack(weights, attackerTokens, totalTokens);
1013
-
1014
- // Attacker power: 0 + 0.8 + 0.8 + 0 = 1.6 x V_MAX
1015
- assertEq(result.attackerPowerLin, mulDiv(V_MAX, 80, 100) * 2, "Attacker: 1.6 x V_MAX");
1016
- // Exceeds quorum of 1.5 x V_MAX (linearTotal / 2)
1017
-
1018
- // But delegate ALSO submits a different scorecard (the truthful one).
1019
- // Delegate controls tier 4 (V_MAX) + honest 20% of tiers 2-3 (0.4 x V_MAX)
1020
- // Delegate coalition: V_MAX + 2 x (V_MAX x 20/100) = 1.4 x V_MAX
1021
- assertEq(result.honestPowerLin, V_MAX + mulDiv(V_MAX, 20, 100) * 2, "Honest: 1.4 x V_MAX");
1022
-
1023
- // With two competing scorecards, NEITHER reaches quorum without the other's support.
1024
- // Attacker 1.6 > 1.5 (passes) BUT honest 1.4 < 1.5 (doesn't pass either).
1025
- // If honest delegate gets just 1 more honest holder in tiers 2-3 to delegate,
1026
- // they reach 1.5 too -> stalemate -> timeout -> NO_CONTEST.
1027
- // Attack fails because extracted pot = 0 in NO_CONTEST.
1028
- }
1029
-
1030
- // ===========================
1031
- // TEST 19: Attack Profitability — Dead Token Economics
1032
- // ===========================
1033
-
1034
- /// @notice The critical economic insight: tokens used for attestation power
1035
- /// (in non-benefiting tiers) are DEAD MONEY under the fraudulent scorecard.
1036
- /// They cost the attacker money but return $0.
1037
- ///
1038
- /// Defifa fees: 2.5% base protocol + 5% defifa = 7.5% total.
1039
- /// Pot for cashout = 92.5% of total mint cost.
1040
- ///
1041
- /// UNIFORM buyer (alpha% of ALL tiers):
1042
- /// - Paid: alpha x N x T x p
1043
- /// - Recovers: alpha x 0.925 x N x T x p (regardless of scorecard!)
1044
- /// - Net: -7.5% x (total spent). ALWAYS A LOSS.
1045
- /// The scorecard cannot help because uniform ownership gets the same
1046
- /// share of pot no matter which tier "wins."
1047
- function test_uniformBuyer_alwaysLoses() public pure {
1048
- uint256 N = 4;
1049
- uint256 T = 100; // tokens per tier
1050
- uint256 p = 100; // price per token in base units
1051
-
1052
- uint256 alpha = 60; // 60% of each tier
1053
-
1054
- // Total mint cost = N x T x p
1055
- uint256 totalMint = N * T * p;
1056
-
1057
- // Fees = 7.5% -> pot = 92.5%
1058
- uint256 pot = totalMint * 925 / 1000;
1059
-
1060
- // Attacker paid
1061
- uint256 attackerCost = alpha * N * p; // alpha% x N tiers x T tokens x p
1062
-
1063
- // Under ANY scorecard [100%, 0, 0, 0]:
1064
- // Tier 1 cashout per token: pot x 100% / T = pot / T
1065
- // Attacker has alpha tokens in tier 1: alpha x pot / T
1066
- uint256 attackerRecovery = alpha * pot / T;
1067
-
1068
- // Net: always negative because of fees
1069
- assertTrue(attackerRecovery < attackerCost, "Uniform buyer always loses to fees");
1070
-
1071
- // The loss is exactly 7.5% of spend
1072
- uint256 loss = attackerCost - attackerRecovery;
1073
- assertEq(loss, attackerCost * 75 / 1000, "Loss = 7.5% of spend");
1074
-
1075
- // KEY: The scorecard is IRRELEVANT. Under [0%, 0%, 100%, 0%]:
1076
- uint256 recoveryTier3 = alpha * pot / T;
1077
- assertEq(attackerRecovery, recoveryTier3, "Same recovery regardless of which tier wins");
1078
- }
1079
-
1080
- /// @notice NON-UNIFORM buyer: more in "winning" tier, just enough in others for quorum.
1081
- /// THIS is where the attack can be profitable — but only above a threshold.
1082
- ///
1083
- /// Attacker: alpha_w in winning tier, alpha_v (>50%) in N-1 voting tiers.
1084
- /// Fraudulent scorecard: [100%, 0, ..., 0] to tier where attacker is heavy.
1085
- ///
1086
- /// Net profit = alpha_w x 0.925 x N x T x p - (alpha_w + alpha_v x (N-1)) x T x p
1087
- /// = T x p x [alpha_w x (0.925N - 1) - alpha_v x (N-1)]
1088
- ///
1089
- /// Profitable iff: alpha_w > alpha_v x (N-1) / (0.925N - 1)
1090
- function test_nonUniformAttack_profitabilityThreshold() public pure {
1091
- uint256 N = 4;
1092
- uint256 T = 100;
1093
- uint256 p = 100;
1094
-
1095
- uint256 totalMint = N * T * p;
1096
- uint256 pot = totalMint * 925 / 1000;
1097
-
1098
- // Threshold: alpha_w > alpha_v x (N-1) / (0.925N - 1)
1099
- // For N=4: alpha_w > alpha_v x 3 / 2.7 = alpha_v x 1.111...
1100
- // For alpha_v = 51%: alpha_w must be > 56.67%
1101
-
1102
- // Scenario A: alpha_v=51%, alpha_w=55% -> UNPROFITABLE
1103
- {
1104
- uint256 alphaW = 55;
1105
- uint256 alphaV = 51;
1106
- uint256 cost = (alphaW + alphaV * (N - 1)) * p; // (55 + 153) x 100 = $20,800
1107
- uint256 recovery = alphaW * pot / T; // 55 x $37,000 / 100 = $20,350
1108
- assertTrue(recovery < cost, "alpha_w=55%, alpha_v=51%: UNPROFITABLE");
1109
- }
1110
-
1111
- // Scenario B: alpha_v=51%, alpha_w=70% -> PROFITABLE
1112
- {
1113
- uint256 alphaW = 70;
1114
- uint256 alphaV = 51;
1115
- uint256 cost = (alphaW + alphaV * (N - 1)) * p;
1116
- uint256 recovery = alphaW * pot / T;
1117
- assertTrue(recovery > cost, "alpha_w=70%, alpha_v=51%: PROFITABLE");
1118
- // ROI: (recovery - cost) / cost
1119
- uint256 profit = recovery - cost;
1120
- uint256 roiPct = profit * 100 / cost;
1121
- // ROI should be modest (~16%)
1122
- assertTrue(roiPct < 25, "ROI < 25% even at 70% ownership");
1123
- }
1124
-
1125
- // Scenario C: alpha_v=51%, alpha_w=100% -> max profitable
1126
- {
1127
- uint256 alphaW = 100;
1128
- uint256 alphaV = 51;
1129
- uint256 cost = (alphaW + alphaV * (N - 1)) * p;
1130
- uint256 recovery = alphaW * pot / T;
1131
- assertTrue(recovery > cost, "alpha_w=100%, alpha_v=51%: PROFITABLE");
1132
- uint256 profit = recovery - cost;
1133
- uint256 roiPct = profit * 100 / cost;
1134
- // Even at 100% winning tier + 51% voting, ROI is bounded
1135
- assertTrue(roiPct < 50, "Max ROI < 50%");
1136
- }
1137
- }
1138
-
1139
- /// @notice Compare attack profitability to TRUTHFUL play.
1140
- /// If the attacker's tier actually won, the TRUTHFUL scorecard gives them
1141
- /// the same recovery. The attack only "profits" vs truth when the
1142
- /// attacker's tier DIDN'T actually win.
1143
- ///
1144
- /// But the voting tokens (non-winning tiers) are a SUNK COST either way.
1145
- /// Under truth: voting tokens might have value (if truthful scorecard gives them weight).
1146
- /// Under fraud: voting tokens are worth $0.
1147
- function test_attackVsTruth_votingTokensSunkCost() public pure {
1148
- uint256 N = 4;
1149
- uint256 T = 100;
1150
- uint256 p = 100;
1151
-
1152
- uint256 totalMint = N * T * p;
1153
- uint256 pot = totalMint * 925 / 1000; // $37,000
1154
-
1155
- // Attacker: 70% of tier 1, 51% of tiers 2-4
1156
- uint256 alphaW = 70;
1157
- uint256 alphaV = 51;
1158
-
1159
- // TRUTHFUL scenario: tier 3 actually won. Scorecard [0%, 0%, 100%, 0%].
1160
- // Attacker recovers: 51% x 100% x pot from tier 3 = $18,870
1161
- // Tiers 1,2,4 get 0% -> $0
1162
- uint256 truthRecovery = alphaV * pot / T;
1163
-
1164
- // FRAUD scenario: attacker claims tier 1 won. Scorecard [100%, 0%, 0%, 0%].
1165
- // Attacker recovers: 70% x 100% x pot from tier 1 = $25,900
1166
- uint256 fraudRecovery = alphaW * pot / T;
1167
-
1168
- // Gain from fraud vs truth
1169
- uint256 fraudGain = fraudRecovery - truthRecovery;
1170
- // = (70 - 51) x pot / T = 19 x $370 = $7,030
1171
- assertEq(fraudGain, (alphaW - alphaV) * pot / T, "Fraud gain = (alpha_w - alpha_v) x pot/T");
1172
-
1173
- // The fraud gain comes ENTIRELY from the difference in ownership percentages.
1174
- // With 70% in "winning" tier vs 51% in actual winning tier: 19% more of pot.
1175
- // Cost of this attack: $22,300 spent, $25,900 recovered = $3,600 profit.
1176
- // But under truth: $22,300 spent, $18,870 recovered = $3,430 loss.
1177
- // Total swing: $7,030.
1178
-
1179
- // If the attacker had EQUAL ownership everywhere (70% of everything):
1180
- // Truth: 70% x pot = $25,900. Fraud: 70% x pot = $25,900. NO GAIN.
1181
- // Fraud is only profitable because of the ownership ASYMMETRY.
1182
- }
1183
-
1184
- /// @notice The honest defense: honest players collectively own >49% of every tier.
1185
- /// If they do, the attack ALWAYS loses money.
1186
- ///
1187
- /// Proof: if alpha_v <= 51% (just barely enough for quorum) and
1188
- /// alpha_w <= 51% (honest players hold 49% of winning tier too),
1189
- /// then net profit = T x p x [51 x (0.925N-1) - 51 x (N-1)]
1190
- /// = T x p x 51 x [(0.925N-1) - (N-1)]
1191
- /// = T x p x 51 x [0.925N - 1 - N + 1]
1192
- /// = T x p x 51 x (-0.075N)
1193
- /// < 0 ALWAYS.
1194
- ///
1195
- /// With uniform 51% ownership, fees guarantee a loss.
1196
- function test_uniform51_guaranteedLoss() public pure {
1197
- uint256 T = 100;
1198
- uint256 p = 100;
1199
-
1200
- // Test for N = 4, 8, 16, 32
1201
- uint256[4] memory tierCounts = [uint256(4), 8, 16, 32];
1202
-
1203
- for (uint256 i; i < 4; i++) {
1204
- uint256 N = tierCounts[i];
1205
- uint256 totalMint = N * T * p;
1206
- uint256 pot = totalMint * 925 / 1000;
1207
-
1208
- // Uniform 51% everywhere
1209
- uint256 alpha = 51;
1210
- uint256 cost = alpha * N * p;
1211
- uint256 recovery = alpha * pot / T;
1212
-
1213
- assertTrue(recovery < cost, "Uniform 51% always loses");
1214
-
1215
- // Loss = 7.5% of spend (within rounding)
1216
- uint256 expectedLoss = cost * 75 / 1000;
1217
- uint256 actualLoss = cost - recovery;
1218
- assertApproxEqAbs(actualLoss, expectedLoss, 1, "Loss = 7.5%");
1219
- }
1220
- }
1221
-
1222
- /// @notice The ONLY profitable attack requires the attacker to be OVERWEIGHT
1223
- /// in their fraudulent "winning" tier vs their voting tiers.
1224
- /// This means: the attacker must own significantly MORE tokens in one
1225
- /// specific tier. With a known event (like World Cup), the attacker
1226
- /// reveals their bet by being overweight — this is observable on-chain
1227
- /// and can be used as a signal to honest participants.
1228
- function test_overweightRequirement() public pure {
1229
- uint256 N = 32; // World Cup
1230
- uint256 T = 1000;
1231
- uint256 p = 10; // $10 per token
1232
-
1233
- uint256 totalMint = N * T * p;
1234
- uint256 pot = totalMint * 925 / 1000;
1235
-
1236
- // Break-even threshold: alpha_w = alpha_v x (N-1) / (0.925N - 1)
1237
- // For N=32: alpha_w = alpha_v x 31 / 28.6 = alpha_v x 1.084
1238
- // At alpha_v = 51%: alpha_w = 55.3%
1239
- // Very tight margins — even a few percent makes the difference
1240
-
1241
- // At break-even: alpha_w=55%, alpha_v=51%
1242
- // Should be close to break-even
1243
- // cost = (55000 + 1581000) x 10 = $16,360,000?? That can't be right
1244
-
1245
- // Let me recalculate properly
1246
- // cost = (alphaW + alphaV x (N-1)) x T x p
1247
- // = (55 + 51 x 31) x 1000 x 10
1248
- // = (55 + 1581) x $10,000 = 1636 x $10,000
1249
- // Hmm that's getting big. Let me simplify.
1250
-
1251
- // Pot = 32 x 1000 x $10 x 0.925 = $296,000
1252
- // Attacker recovery from winning tier: alpha_w x pot / (N x T) x T
1253
- // Wait: per-token cashout = pot x 100% / T = $296,000 / 1000 = $296
1254
- // Attacker tokens in winning tier: 55% x 1000 = 550
1255
- // Recovery: 550 x $296 = $162,800
1256
- // But wait, the "pot" is actually distributed to the scoring tier
1257
- // pot x (tier_weight / W_TOTAL) / tokens_in_tier x attacker_tokens
1258
- // For [100%, 0,...]: pot x 1 / 1000 x 550 = $296 x 550 = $162,800
1259
-
1260
- // Cost: attacker_tokens_total x price
1261
- // = (550 + 510 x 31) x $10 = (550 + 15,810) x $10 = 16,360 x $10 = $163,600
1262
-
1263
- // Net: $162,800 - $163,600 = -$800 (LOSS at 55%)
1264
-
1265
- // At alpha_w=60%:
1266
- {
1267
- uint256 alphaW = 60;
1268
- uint256 alphaV = 51;
1269
-
1270
- uint256 winnerTokens = alphaW * T / 100; // 600
1271
- uint256 votingTokens = alphaV * (N - 1) * T / 100; // 15,810
1272
- uint256 totalCost = (winnerTokens + votingTokens) * p; // 16,410 x $10 = $164,100
1273
-
1274
- // Recovery from winning tier
1275
- uint256 recovery = winnerTokens * pot / (T); // 600 x $296,000 / 1000 = $177,600
1276
-
1277
- assertTrue(recovery > totalCost, "60%/51% profitable at N=32");
1278
- uint256 profit = recovery - totalCost;
1279
- uint256 roiPct = profit * 100 / totalCost;
1280
- // Modest ROI
1281
- assertTrue(roiPct < 15, "ROI < 15% at N=32");
1282
- }
1283
- }
1284
-
1285
- // ===========================
1286
- // TEST 20: Summary — Conclusions Proven
1287
- // ===========================
1288
-
1289
- function test_conclusionsProven() public pure {
1290
- // 1. LINEAR f(x) = 1-x is the UNIQUE function with constant total attestation.
1291
-
1292
- // 2. BWA is TIER-level. Sybil (address splitting) is irrelevant.
1293
-
1294
- // 3. Under linear BWA, uniform attacker gets alpha x (N-1) x V_MAX power,
1295
- // INDEPENDENT of scorecard. No scorecard manipulation possible.
1296
-
1297
- // 4. Security threshold: >50% of tokens per tier.
1298
-
1299
- // 5. Quadratic/golden: variable quorum = exploitable. Linear: fixed quorum.
1300
-
1301
- // 6. DEAD TOKEN ECONOMICS: tokens used for attestation power (non-winning tiers)
1302
- // return $0 under the fraudulent scorecard. This is the attack cost.
1303
- // With UNIFORM ownership (alpha% of all tiers), fees guarantee a NET LOSS.
1304
- // Fraud is only profitable with OVERWEIGHT in one tier (alpha_w > ~1.1 x alpha_v).
1305
-
1306
- // 7. THE IRREDUCIBLE LIMIT: With enough money and overweight ownership,
1307
- // an attacker CAN push a fraudulent scorecard. This is the 51% attack —
1308
- // the same fundamental limit as PoS blockchains.
1309
-
1310
- // DEFENSE STACK:
1311
- // a) BWA: makes attestation require >50% ownership (dead token cost)
1312
- // b) Fees (7.5%): make uniform attacks always unprofitable
1313
- // c) Delegate: coordination point for honest minority
1314
- // d) scorecardTimeout -> NO_CONTEST: backstop if no honest quorum
1315
- // e) Game design: tier supply, mint window, reserve tokens
1316
- // -> make it competitive to acquire >50% during mint
1317
-
1318
- assertTrue(true);
1319
- }
1320
- }