Skip to content

Commit c53bd1b

Browse files
authored
Make inherited security-pool outcomes final (#598)
* Make inherited pool outcomes final * Prevent fixed child escalation locks * Clarify fixed outcome settlement timing * Retry transient CI failure
1 parent 84fd73c commit c53bd1b

13 files changed

Lines changed: 291 additions & 147 deletions

docs/contract-interaction-reference.md

Lines changed: 7 additions & 7 deletions
Large diffs are not rendered by default.

docs/invariants.html

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,8 @@ <h2>Remediated Launch Findings</h2>
412412
</td>
413413
<td>
414414
A branch selected by a matching fork becomes the immutable
415-
continuation outcome, unrelated forks preserve the previous fixed
416-
outcome, and escalation-deposit withdrawal rejects a pool/game
415+
continuation outcome, later universe forks cannot transition that
416+
fixed pool, and escalation-deposit withdrawal rejects a pool/game
417417
mismatch.
418418
</td>
419419
</tr>
@@ -1748,16 +1748,21 @@ <h2>Escalation Games and Carried Claims</h2>
17481748
<div class="invariant-property">
17491749
When a universe forks on a pool's question, each child pool and its
17501750
continuation game use that child's Invalid, Yes, or No branch as
1751-
the final pool-question outcome. A later fork on the same question
1752-
replaces the inherited outcome with its newly selected branch; a
1753-
fork on another question preserves the inherited outcome. Deposit
1754-
withdrawal reverts unless the pool and game report the same outcome.
1751+
the final pool-question outcome. Once a pool inherits that fixed
1752+
outcome, new local escalation deposits and every later fork
1753+
transition revert, whether the new universe fork uses the same
1754+
question or another one. Deposit withdrawal reverts unless the pool
1755+
and game report the same outcome.
17551756
</div>
1756-
<p class="invariant-example"><strong>Example:</strong> A Yes child created by a fork on the pool question pays carried Yes deposits even if copied game balances favored No; a later unrelated fork keeps Yes as the payout outcome.</p>
1757+
<p class="invariant-example"><strong>Example:</strong> A Yes child created by a fork on the pool question pays carried Yes deposits even if copied game balances favored No. It rejects new local escalation REP before escrow, so a later local non-decision cannot lock vault redemption. A later matching or unrelated universe fork leaves the stored <code>SystemState.Operational</code> and fixed Yes outcome unchanged, and eligible share, vault REP, and carried-proof redemption paths remain available, while universe-fork guards still freeze normal operating calls.</p>
17571758
<dl class="invariant-metadata">
17581759
<div><dt>Type</dt><dd>Safety</dd></div>
17591760
<div><dt>Enforcement status</dt><dd>Reviewed preservation</dd></div>
17601761
<div><dt>Primary evidence</dt><dd>
1762+
<a
1763+
href="../solidity/contracts/peripherals/SecurityPool.sol"
1764+
><code>activateForkMode</code></a
1765+
>,
17611766
<a
17621767
href="../solidity/contracts/peripherals/EscalationGameCalculations.sol"
17631768
><code>getQuestionResolution</code></a

docs/mainnet-deployment-addresses.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
{
1414
"id": "deploymentStatusOracle",
1515
"label": "Deployment Status Oracle",
16-
"address": "0x99c01b10dd0FFa4e111a1F024523f618F4040743"
16+
"address": "0xf81e0586e246C0c46C6e5A60C89645402eFfF7d8"
1717
},
1818
{
1919
"id": "multicall3",
@@ -53,34 +53,34 @@
5353
{
5454
"id": "shareTokenFactory",
5555
"label": "ShareTokenFactory",
56-
"address": "0xCbec97Fc9699af3366e4a86A94Fb7478b2ea419d"
56+
"address": "0xDa7655f600D2A298e44E3649983c800C08F746F1"
5757
},
5858
{
5959
"id": "priceOracleManagerAndOperatorQueuerFactory",
6060
"label": "OpenOracle Price Coordinator Factory",
61-
"address": "0x34fd97b4DA610998A871cA35c89603D56EDC9354"
61+
"address": "0x2675524c278277161474CA2Bb61DdFC52E0E187e"
6262
},
6363
{
6464
"id": "securityPoolForker",
6565
"label": "Security Pool Forker",
66-
"address": "0x89655E77A6A623934C6f9c2Ccb28154febC16043"
66+
"address": "0x836da20a717298e55f03Ce98cDBf73F3C011B585"
6767
},
6868
{
6969
"id": "escalationGameFactory",
7070
"label": "Escalation Game Factory",
71-
"address": "0x3352BA09Be76FAB7795e7319B4Ec4daA4afE9AA9"
71+
"address": "0xB16Cf95eDf6b8BA654555e72d55d58faE992C4B6"
7272
},
7373
{
7474
"id": "securityPoolFactory",
7575
"label": "Security Pool Factory",
76-
"address": "0x01dbD6b3576EfcE2B0C7F9FeD1538bD20B673182"
76+
"address": "0xdcc473e4a941fC651309e8D1042DfB4a9C31c957"
7777
}
7878
],
7979
"derivedContracts": [
8080
{
8181
"id": "escalationGameProofVerifier",
8282
"label": "Escalation Game Proof Verifier",
83-
"address": "0xcbb9f8b87F4A6cFB3776a356C57A865320c72C29"
83+
"address": "0x9DcA4f0Ed5A7E9F560f8074E5A70AeD41Fdb8894"
8484
}
8585
]
8686
}

docs/operator-reference.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ contract-first form.
7777
| Escrowed REP withdrawal lock | `performWithdrawRep` rejects withdrawal while the vault still has REP escrowed in an escalation game; the vault must settle those locks first. | [SecurityPool.sol](../solidity/contracts/peripherals/SecurityPool.sol) |
7878
| External fork withdrawal lock | If the universe forked before the local escalation game ended and non-decision was not reached, parent-pool escalation withdrawal reverts. The child continuation already has the canonical snapshot and aggregate backing: winning inherited deposits settle there by proof, inherited losers require no transaction, and clearing the vault's parent lock accounting is optional. | [SecurityPool.sol](../solidity/contracts/peripherals/SecurityPool.sol), [`EscalationGameSettlement.withdrawDeposit`](../solidity/contracts/peripherals/EscalationGameSettlement.sol), [`EscalationGameCarry._getEffectiveInheritedUnresolvedTotal`](../solidity/contracts/peripherals/EscalationGameCarry.sol) |
7979
| REP-to-ownership round-up | `repToPoolOwnershipRoundUp` uses ceiling division when the pool removes vault ownership for an escalation deposit. That intentionally burns enough ownership to cover the requested REP even when the ownership conversion is fractional. | [`SecurityPool.repToPoolOwnershipRoundUp`](../solidity/contracts/peripherals/SecurityPool.sol), [`SecurityPool.depositToEscalationGame`](../solidity/contracts/peripherals/SecurityPool.sol) |
80-
| Escalation deposit wrapper | `depositToEscalationGame` deploys the game on the first valid post-end deposit, previews the accepted amount, removes vault REP ownership with round-up accounting, checks local and global solvency, transfers REP into the game, and records the deposit. | [`SecurityPool.depositToEscalationGame`](../solidity/contracts/peripherals/SecurityPool.sol), [`EscalationGame.recordDepositFromSecurityPool`](../solidity/contracts/peripherals/EscalationGame.sol) |
80+
| Escalation deposit wrapper | `depositToEscalationGame` rejects pools with an inherited fixed outcome because they cannot enter another fork or safely unwind a later local non-decision. Otherwise it deploys the game on the first valid post-end deposit, previews the accepted amount, removes vault REP ownership with round-up accounting, checks local and global solvency, transfers REP into the game, and records the deposit. | [`SecurityPool.depositToEscalationGame`](../solidity/contracts/peripherals/SecurityPool.sol), [`EscalationGame.recordDepositFromSecurityPool`](../solidity/contracts/peripherals/EscalationGame.sol) |
8181
| Direct ETH receiver | Ordinary calls to `receive()` accept ETH only from the forker, its truth auction, or its parent pool. Forced ETH can bypass `receive()`; it remains raw, unaccounted surplus and is not collateral or accrued fees. | [SecurityPool.sol](../solidity/contracts/peripherals/SecurityPool.sol) |
8282
| Vault enumeration | `getVaults(startIndex, count)` pages the append-only vault list in insertion order. `getActiveVaults(startIndex, count)` pages only active vaults in newest-first order by walking the active-vault linked list from `latestActiveVault`. Both return an empty array when `count == 0` or the start index is out of range. | [`SecurityPool.getVaults`](../solidity/contracts/peripherals/SecurityPool.sol), [`SecurityPool.getActiveVaults`](../solidity/contracts/peripherals/SecurityPool.sol) |
8383

@@ -112,12 +112,12 @@ consumption all live in this section.
112112
| Outcome room | Accepted deposit amount is capped to the selected outcome's remaining room under `nonDecisionThreshold`. | [`EscalationGameCalculations._getAcceptedDepositAmount`](../solidity/contracts/peripherals/EscalationGameCalculations.sol) |
113113
| Tie adjustment | If the accepted amount would create a tie with the current maximum balance while still below non-decision, the contract reduces the accepted amount by `1 wei`; if that breaks the accepted-amount rule, the deposit is rejected. | [`EscalationGameCalculations._getAcceptedDepositAmount`](../solidity/contracts/peripherals/EscalationGameCalculations.sol) |
114114
| Unresolved resolution state | If two or more outcomes meet the current running cost, `getQuestionResolution()` returns `None`. | [`EscalationGameCalculations.getQuestionResolution`](../solidity/contracts/peripherals/EscalationGameCalculations.sol) |
115-
| Matching-fork continuation resolution | When a universe fork uses the pool's question, every child stores the selected branch as its fixed question outcome whether the fork began through the pool-specific or direct Zoltar entrypoint. After the continuation deadline, the game uses that same fixed outcome for deposit settlement; unrelated descendant forks inherit an existing fixed outcome, while unrelated forks of unresolved pools retain local continuation semantics. Payout settlement also rejects any pool/game outcome mismatch. | [`EscalationGameCalculations.getQuestionResolution`](../solidity/contracts/peripherals/EscalationGameCalculations.sol), [`EscalationGameSettlement._getPayoutQuestionResolution`](../solidity/contracts/peripherals/EscalationGameSettlement.sol), [`SecurityPoolForker.getQuestionOutcome`](../solidity/contracts/peripherals/SecurityPoolForker.sol) |
115+
| Matching-fork continuation resolution | After the continuation deadline, a game with a fixed child outcome settles deposits against that outcome. Payout settlement rejects any pool/game outcome mismatch. See [Matching-question child outcome](#fork-migration) for the pool-level finality rule. | [`EscalationGameCalculations.getQuestionResolution`](../solidity/contracts/peripherals/EscalationGameCalculations.sol), [`EscalationGameSettlement._getPayoutQuestionResolution`](../solidity/contracts/peripherals/EscalationGameSettlement.sol), [`SecurityPoolForker.getQuestionOutcome`](../solidity/contracts/peripherals/SecurityPoolForker.sol) |
116116
| Empty-game fallback | If all outcome balances are zero after the running cost is non-zero, `getQuestionResolution()` returns `Invalid`; if the running cost is still zero, the unresolved check returns `None` first. | [`EscalationGameCalculations.getQuestionResolution`](../solidity/contracts/peripherals/EscalationGameCalculations.sol) |
117117
| Strict leading resolution | After the unresolved-cost check and the all-zero `Invalid` fallback, a strict `Invalid`, `Yes`, or `No` lead returns that outcome. Valid local deposits prevent tied maxima below non-decision by reducing the accepted amount by `1 wei`, or reverting if that adjusted amount becomes invalid. Continuation snapshots preserve the parent balances exactly, including ties, so every selected branch starts from the same unresolved game state. | [`EscalationGameCalculations.getQuestionResolution`](../solidity/contracts/peripherals/EscalationGameCalculations.sol), [`EscalationGameCalculations._getStrictLeaderOrNone`](../solidity/contracts/peripherals/EscalationGameCalculations.sol), [`EscalationGameCalculations._getAcceptedDepositAmount`](../solidity/contracts/peripherals/EscalationGameCalculations.sol), [`EscalationGameCarry.initializeForkCarrySnapshotWithResolutionBalances`](../solidity/contracts/peripherals/EscalationGameCarry.sol) |
118118
| Structural non-decision predicate | `hasReachedNonDecision()` becomes true when two or more outcomes reach `nonDecisionThreshold`; `nonDecisionState` separately records how that balance condition entered the lifecycle. New games use `ceil(forkThreshold / 2)`, so two threshold balances always contain at least the REP required to fund an own fork even when the fork threshold is odd. | [`Zoltar.getNonDecisionThreshold`](../solidity/contracts/Zoltar.sol), [`EscalationGameCalculations.hasReachedNonDecision`](../solidity/contracts/peripherals/EscalationGameCalculations.sol) |
119119
| `nonDecisionState = None` | No explicit non-decision transition has occurred. Deposits may remain available subject to the ordinary activation, continuation, timing, and amount guards. | [`EscalationGame.previewDepositOnOutcome`](../solidity/contracts/peripherals/EscalationGame.sol), [`EscalationGameDepositDelegate.recordDepositFromSecurityPool`](../solidity/contracts/peripherals/EscalationGameDepositDelegate.sol) |
120-
| `nonDecisionState = Local` | A local deposit brought a second outcome to the threshold. The game stores the real `nonDecisionTimestamp`, closes further deposits, and `canTriggerOwnFork()` returns true. | [`EscalationGameDepositDelegate.recordDepositFromSecurityPool`](../solidity/contracts/peripherals/EscalationGameDepositDelegate.sol), [`EscalationGameCalculations.canTriggerOwnFork`](../solidity/contracts/peripherals/EscalationGameCalculations.sol) |
120+
| `nonDecisionState = Local` | A local deposit brought a second outcome to the threshold. The game stores the real `nonDecisionTimestamp`, closes further deposits, and `canTriggerOwnFork()` returns true. That predicate is game-local: a pool with an inherited fixed outcome still rejects the fork transition in `activateForkMode()`. | [`EscalationGameDepositDelegate.recordDepositFromSecurityPool`](../solidity/contracts/peripherals/EscalationGameDepositDelegate.sol), [`EscalationGameCalculations.canTriggerOwnFork`](../solidity/contracts/peripherals/EscalationGameCalculations.sol), [`SecurityPool.activateForkMode`](../solidity/contracts/peripherals/SecurityPool.sol) |
121121
| `nonDecisionState = InheritedThresholdTie` | Snapshot initialization preserved two or more threshold-full balances without fabricating a local timestamp. The game closes further deposits. With a fixed child outcome it follows the continuation clock and cannot trigger its own fork; without one, `canTriggerOwnFork()` returns true directly. | [`EscalationGameCarry.initializeForkCarrySnapshotWithResolutionBalances`](../solidity/contracts/peripherals/EscalationGameCarry.sol), [`EscalationGameCalculations.getEscalationGameEndDate`](../solidity/contracts/peripherals/EscalationGameCalculations.sol), [`EscalationGameCalculations.canTriggerOwnFork`](../solidity/contracts/peripherals/EscalationGameCalculations.sol) |
122122
| Carry proofs | Inherited carry uses Merkle Mountain Range peaks and nullifier roots so child games can consume proofs without replaying already-spent parent deposits. | [`EscalationGameSettlement.withdrawDeposit`](../solidity/contracts/peripherals/EscalationGameSettlement.sol), [`EscalationGameCarry._verifyAndConsumeCarriedDepositProof`](../solidity/contracts/peripherals/EscalationGameCarry.sol) |
123123
| Continuation withdrawal | Anyone may relay a batch of winning carried proofs for one beneficiary after a child continuation resolves. Each proof authenticates its recorded depositor, consumes that leaf once, and pays the depositor from aggregate game REP. Inherited losing outcomes retire in constant-size work when the result is final and require no proof transaction; locally created losing deposits retain ordinary settlement. | [`SecurityPool.withdrawForkedEscalationDeposits`](../solidity/contracts/peripherals/SecurityPool.sol), [`EscalationGameSettlement.withdrawDeposit`](../solidity/contracts/peripherals/EscalationGameSettlement.sol), [`EscalationGameCarry._getEffectiveInheritedUnresolvedTotal`](../solidity/contracts/peripherals/EscalationGameCarry.sol) |
@@ -143,7 +143,7 @@ child-pool creation, REP splitting, and child outcome selection.
143143
| Independent continuation liveness | Child creation initializes the canonical carry and aggregate backing without waiting for vault transactions. Once final, authenticated winning proofs can be relayed permissionlessly, inherited losers retire without proofs, and optional parent cleanup may happen independently. | [`SecurityPoolForkerBase._finalizeAwaitingForkContinuationIfReady`](../solidity/contracts/peripherals/SecurityPoolForkerBase.sol), [`EscalationGameSettlement.withdrawDeposit`](../solidity/contracts/peripherals/EscalationGameSettlement.sol), [`EscalationGameCarry._getEffectiveInheritedUnresolvedTotal`](../solidity/contracts/peripherals/EscalationGameCarry.sol) |
144144
| Own-fork REP buckets | When escalation triggers its own fork, `escalationChildRepAtFork` equals `escalationRepToFork - floor(forkThreshold / forkBurnDivisor)`. `vaultRepAtFork` preserves ordinary pool REP one-for-one. Creating one selected child does not reduce the post-haircut escalation backing available to another selected child. | [`SecurityPoolForker.forkZoltarWithOwnEscalationGame`](../solidity/contracts/peripherals/SecurityPoolForker.sol), [`SecurityPoolForker.getOwnForkRepBuckets`](../solidity/contracts/peripherals/SecurityPoolForker.sol), [`SecurityPoolForkerBase._initializeOwnForkRepBuckets`](../solidity/contracts/peripherals/SecurityPoolForkerBase.sol) |
145145
| Child-pool deployment window | Child pools are created lazily for selected fork outcomes, but only while the parent pool is `PoolForked` and the eight-week migration window is still open. | [SecurityPoolForkerVaultMigrationBase.sol](../solidity/contracts/peripherals/SecurityPoolForkerVaultMigrationBase.sol), [SecurityPoolUtils.sol](../solidity/contracts/peripherals/SecurityPoolUtils.sol) |
146-
| Matching-question child outcome | When the parent universe forks on the pool's question, the child stores its selected Zoltar outcome index as a fixed result whether the fork used the pool-specific path or a direct Zoltar call. The fixed result applies after continuation and is inherited through later unrelated descendants. `ownFork` changes accounting only. | [SecurityPoolForkerVaultMigrationBase.sol](../solidity/contracts/peripherals/SecurityPoolForkerVaultMigrationBase.sol), [SecurityPoolForker.sol](../solidity/contracts/peripherals/SecurityPoolForker.sol) |
146+
| Matching-question child outcome | When the parent universe forks on the pool's question, the child stores its selected branch as a fixed result. `depositToEscalationGame` rejects new local deposits, and `activateForkMode` rejects every later pool fork transition. See [Child Outcome Resolution](./statoblast-whitepaper.html#child-outcome-resolution) for the collateral and REP-liveness rationale. | [SecurityPool.sol](../solidity/contracts/peripherals/SecurityPool.sol), [SecurityPoolForkerVaultMigrationBase.sol](../solidity/contracts/peripherals/SecurityPoolForkerVaultMigrationBase.sol), [SecurityPoolForker.sol](../solidity/contracts/peripherals/SecurityPoolForker.sol) |
147147
| Unrelated-fork child outcome | A child created by an unrelated fork without an inherited fixed result uses a local escalation result only if that escalation ended before the universe forked; otherwise continuation or later state must produce the outcome. | [SecurityPoolForker.sol](../solidity/contracts/peripherals/SecurityPoolForker.sol) |
148148

149149
## Truth Auction Operations

docs/protocolTerms.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ window.protocolTermDefinitions = Object.freeze({
118118
MIN_SECURITY_BOND_DEBT: 'The smallest security-bond debt amount tracked. Smaller dust amounts are avoided.',
119119
minLiquidationPriceDistanceBps: 'The basis-point distance required beyond the liquidation threshold before execution.',
120120
'non-decision threshold': 'The REP balance level that two or more outcomes must reach for the structural non-decision condition.',
121-
'non-decision': 'A structurally contested escalation state. Whether it authorizes a fork depends on whether it arose locally or was inherited and, for an inherited tie, whether the continuation has a fixed outcome.',
121+
'non-decision': "A structurally contested escalation state. Its local or inherited origin determines the game's canTriggerOwnFork() predicate, but a successful pool fork also requires no inherited fixed outcome. Fixed-outcome pools reject new local escalation deposits before this state can be created.",
122122
NUM_OUTCOMES: 'The Statoblast constant for the three supported outcomes: Invalid, Yes, and No.',
123123
NULLIFIER_DEPTH: 'The depth of the tree used to remember consumed proofs. It prevents the same carried deposit from being used twice.',
124124
'number of ticks': 'The scalar-outcome denominator used to map a scalar answer onto payout numerators.',

0 commit comments

Comments
 (0)