Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/mainnet-deployment-addresses.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
{
"id": "deploymentStatusOracle",
"label": "Deployment Status Oracle",
"address": "0xD7549E27fB03aeD7dfb30082f6d8f342DfEAF604"
"address": "0x22D512D74a15b69D43e23B2fC7c899F0495d4525"
},
{
"id": "multicall3",
Expand Down Expand Up @@ -73,7 +73,7 @@
{
"id": "securityPoolFactory",
"label": "Security Pool Factory",
"address": "0x7646559Cccbe18A819b0f6060890D292cD5359E8"
"address": "0x1c157c8CF501aFeBa567514661a191dee26CEd82"
}
],
"derivedContracts": [
Expand Down
127 changes: 82 additions & 45 deletions solidity/contracts/peripherals/SecurityPool.sol

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions solidity/ts/tests/peripherals/escalationMigration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ describe('Peripherals: escalation migration', () => {
functionName: 'withdrawForkedEscalationDeposits',
args: [QuestionOutcome.Yes, [proof]],
}),
/Question not finalized/,
/Question not finalized|Question open/,
)
strictEqualTypeSafe(await getERC20Balance(client, childRepToken, client.account.address), walletBalanceBeforeClaim, 'the honest winner should not receive child REP before the losing vault funds its carried lock')
strictEqualTypeSafe((await getSecurityVault(client, yesSecurityPool.securityPool, client.account.address)).repInEscalationGame, childVaultBeforeClaim.repInEscalationGame, 'the paused continuation should preserve the winner escrow while funding is incomplete')
Expand Down Expand Up @@ -781,7 +781,7 @@ describe('Peripherals: escalation migration', () => {
functionName: 'withdrawForkedEscalationDeposits',
args: [QuestionOutcome.Yes, [attackerProof]],
}),
/Question not finalized/,
/Question not finalized|Question open/,
)
const honestWinnerProof = await createCarryProof(client, securityPoolAddresses.escalationGame, {
expectedOutcome: QuestionOutcome.Yes,
Expand All @@ -802,7 +802,7 @@ describe('Peripherals: escalation migration', () => {
functionName: 'withdrawForkedEscalationDeposits',
args: [QuestionOutcome.Yes, [honestWinnerProof]],
}),
/Question not finalized/,
/Question not finalized|Question open/,
)

await migrateVaultWithUnresolvedEscalation(attackerClient, securityPoolAddresses.securityPool, attackerClient.account.address, QuestionOutcome.Yes)
Expand Down Expand Up @@ -1193,7 +1193,7 @@ describe('Peripherals: escalation migration', () => {

await depositToEscalationGame(client, securityPoolAddresses.securityPool, QuestionOutcome.Yes, reportBond)

await assert.rejects(withdrawFromEscalationGame(client, securityPoolAddresses.securityPool, QuestionOutcome.Yes, [0n]), /Question not finalized/)
await assert.rejects(withdrawFromEscalationGame(client, securityPoolAddresses.securityPool, QuestionOutcome.Yes, [0n]), /Question not finalized|Question open/)
})

test('third parties can permissionlessly settle another vaults resolved escalation deposits', async () => {
Expand Down
3 changes: 3 additions & 0 deletions solidity/ts/tests/peripherals/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
redeemRep,
redeemShares,
sharesToCash,
updateCollateralAmount,
updateVaultFees,
withdrawFromEscalationGame,
} from '../../testsuite/simulator/utils/contracts/securityPool'
Expand Down Expand Up @@ -396,6 +397,7 @@ function usePeripheralsTestFixture() {
redeemRep,
redeemShares,
sharesToCash,
updateCollateralAmount,
updateVaultFees,
withdrawFromEscalationGame,
peripherals_EscalationGame_EscalationGame,
Expand Down Expand Up @@ -676,6 +678,7 @@ export function usePeripheralsForkMigrationFixture() {
'redeemRep',
'redeemShares',
'sharesToCash',
'updateCollateralAmount',
'updateVaultFees',
'withdrawFromEscalationGame',
'peripherals_EscalationGame_EscalationGame',
Expand Down
150 changes: 145 additions & 5 deletions solidity/ts/tests/peripherals/forkMigration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ describe('Peripherals: fork migration', () => {
redeemRep,
redeemShares,
sharesToCash,
updateCollateralAmount,
updateVaultFees,
withdrawFromEscalationGame,
peripherals_EscalationGame_EscalationGame,
Expand Down Expand Up @@ -558,6 +559,143 @@ describe('Peripherals: fork migration', () => {
strictEqualTypeSafe(contractBalance + ethBalanceAfter - ethBalanceBefore, openInterestAmount, 'contract balance + fees should equal initial open interest')
})

test('frequent public collateral updates do not strand extra fee residue', async () => {
const securityPoolAllowance = repDeposit / 4n + 1n
await manipulatePriceOracleAndPerformOperation(client, mockWindow, securityPoolAddresses.priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, client.account.address, securityPoolAllowance)

const openInterestAmount = 100n * 10n ** 18n
const splitUpdateCount = 128n
const endTime = await getQuestionEndDate(client, questionId)
await mockWindow.setTime(endTime - splitUpdateCount - 10n)
await createCompleteSet(client, securityPoolAddresses.securityPool, openInterestAmount)

for (let index = 1n; index <= splitUpdateCount; index++) {
await mockWindow.advanceTime(1n)
await updateCollateralAmount(client, securityPoolAddresses.securityPool)
}
await mockWindow.setTime(endTime + 10000n)
await updateVaultFees(client, securityPoolAddresses.securityPool, client.account.address)

const splitVault = await getSecurityVault(client, securityPoolAddresses.securityPool, client.account.address)
const totalFeesOwed = await getTotalFeesOwedToVaults(client, securityPoolAddresses.securityPool)

assert.ok(totalFeesOwed > 0n, 'repeated public collateral updates should accrue nonzero fees in this setup')
strictEqualTypeSafe(totalFeesOwed, splitVault.unpaidEthFees, 'pool fee accounting should only record fees that the vault index can actually credit')
await redeemFees(client, securityPoolAddresses.securityPool, client.account.address)
const contractBalance = await getETHBalance(client, securityPoolAddresses.securityPool)
const remainingCollateral = await getCompleteSetCollateralAmount(client, securityPoolAddresses.securityPool)
strictEqualTypeSafe(contractBalance, remainingCollateral + (await getTotalFeesOwedToVaults(client, securityPoolAddresses.securityPool)), 'final fee settlement should leave every remaining wei in either collateral or redeemable fees')
})

test('frequent public collateral updates keep multi-vault fee accounting sweepable', async () => {
const firstVaultAllowance = repDeposit / 8n + 1n
await manipulatePriceOracleAndPerformOperation(client, mockWindow, securityPoolAddresses.priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, client.account.address, firstVaultAllowance)

const secondVaultClient = createWriteClient(mockWindow, TEST_ADDRESSES[1], 0)
await approveAndDepositRep(secondVaultClient, repDeposit, questionId)
const secondVaultAllowance = repDeposit / 8n + 3n
await manipulatePriceOracleAndPerformOperation(secondVaultClient, mockWindow, securityPoolAddresses.priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, secondVaultClient.account.address, secondVaultAllowance)

const openInterestAmount = 100n * 10n ** 18n
const splitUpdateCount = 128n
const endTime = await getQuestionEndDate(client, questionId)
await mockWindow.setTime(endTime - splitUpdateCount - 10n)
await createCompleteSet(client, securityPoolAddresses.securityPool, openInterestAmount)

for (let index = 1n; index <= splitUpdateCount; index++) {
await mockWindow.advanceTime(1n)
await updateCollateralAmount(client, securityPoolAddresses.securityPool)
}
await mockWindow.setTime(endTime + 10000n)

await updateVaultFees(client, securityPoolAddresses.securityPool, client.account.address)
await updateVaultFees(secondVaultClient, securityPoolAddresses.securityPool, secondVaultClient.account.address)

const firstVault = await getSecurityVault(client, securityPoolAddresses.securityPool, client.account.address)
const secondVault = await getSecurityVault(client, securityPoolAddresses.securityPool, secondVaultClient.account.address)
const totalFeesOwed = await getTotalFeesOwedToVaults(client, securityPoolAddresses.securityPool)
const totalCreditedFees = firstVault.unpaidEthFees + secondVault.unpaidEthFees

assert.ok(totalCreditedFees > 0n, 'repeated public collateral updates should accrue nonzero fees across both vaults in this setup')
strictEqualTypeSafe(totalFeesOwed, totalCreditedFees, 'pool fee accounting should equal the sum of vault-creditable fees after both vaults sync')

await redeemFees(client, securityPoolAddresses.securityPool, client.account.address)
await redeemFees(secondVaultClient, securityPoolAddresses.securityPool, secondVaultClient.account.address)

strictEqualTypeSafe(await getTotalFeesOwedToVaults(client, securityPoolAddresses.securityPool), 0n, 'pool fee accounting should fully clear once every credited vault fee is redeemed')
})

test('allowance handoff does not pay old fee residue to the new vault', async () => {
const firstVaultAllowance = repDeposit / 4n + 1n
await manipulatePriceOracleAndPerformOperation(client, mockWindow, securityPoolAddresses.priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, client.account.address, firstVaultAllowance)

const openInterestAmount = 100n * 10n ** 18n
const splitUpdateCount = 128n
const endTime = await getQuestionEndDate(client, questionId)
await mockWindow.setTime(endTime - splitUpdateCount - 20n)
await createCompleteSet(client, securityPoolAddresses.securityPool, openInterestAmount)

for (let index = 1n; index <= splitUpdateCount; index++) {
await mockWindow.advanceTime(1n)
await updateCollateralAmount(client, securityPoolAddresses.securityPool)
}

await manipulatePriceOracleAndPerformOperation(client, mockWindow, securityPoolAddresses.priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, client.account.address, 0n)

const secondVaultClient = createWriteClient(mockWindow, TEST_ADDRESSES[1], 0)
await approveAndDepositRep(secondVaultClient, repDeposit, questionId)
const secondVaultAllowance = repDeposit / 4n + 3n
await manipulatePriceOracleAndPerformOperation(secondVaultClient, mockWindow, securityPoolAddresses.priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, secondVaultClient.account.address, secondVaultAllowance)

const collateralBeforeSecondAccrual = await getCompleteSetCollateralAmount(client, securityPoolAddresses.securityPool)
const retentionRate = await getCurrentRetentionRate(client, securityPoolAddresses.securityPool)
const expectedNextSecondDelta = collateralBeforeSecondAccrual - (collateralBeforeSecondAccrual * rpow(retentionRate, 1n, PRICE_PRECISION)) / PRICE_PRECISION

await mockWindow.advanceTime(1n)
await updateVaultFees(secondVaultClient, securityPoolAddresses.securityPool, secondVaultClient.account.address)

const secondVault = await getSecurityVault(secondVaultClient, securityPoolAddresses.securityPool, secondVaultClient.account.address)
assert.ok(secondVault.unpaidEthFees <= expectedNextSecondDelta, 'new allowance holder should not inherit denominator residue that accrued before the handoff')
})

test('zero-allowance gaps do not charge the next vault for idle time', async () => {
const firstVaultAllowance = repDeposit / 4n + 1n
await manipulatePriceOracleAndPerformOperation(client, mockWindow, securityPoolAddresses.priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, client.account.address, firstVaultAllowance)

const openInterestAmount = 100n * 10n ** 18n
const splitUpdateCount = 128n
const endTime = await getQuestionEndDate(client, questionId)
await mockWindow.setTime(endTime - splitUpdateCount - 40n)
await createCompleteSet(client, securityPoolAddresses.securityPool, openInterestAmount)

for (let index = 1n; index <= splitUpdateCount; index++) {
await mockWindow.advanceTime(1n)
await updateCollateralAmount(client, securityPoolAddresses.securityPool)
}

await manipulatePriceOracleAndPerformOperation(client, mockWindow, securityPoolAddresses.priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, client.account.address, 0n)
const collateralAtZeroAllowance = await getCompleteSetCollateralAmount(client, securityPoolAddresses.securityPool)

await mockWindow.advanceTime(30n)
await updateCollateralAmount(client, securityPoolAddresses.securityPool)
strictEqualTypeSafe(await getCompleteSetCollateralAmount(client, securityPoolAddresses.securityPool), collateralAtZeroAllowance, 'collateral should not decay while no vault backs the pool')

const secondVaultClient = createWriteClient(mockWindow, TEST_ADDRESSES[1], 0)
await approveAndDepositRep(secondVaultClient, repDeposit, questionId)
const secondVaultAllowance = repDeposit / 4n + 3n
await manipulatePriceOracleAndPerformOperation(secondVaultClient, mockWindow, securityPoolAddresses.priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, secondVaultClient.account.address, secondVaultAllowance)

const collateralBeforeSecondAccrual = await getCompleteSetCollateralAmount(client, securityPoolAddresses.securityPool)
const retentionRate = await getCurrentRetentionRate(client, securityPoolAddresses.securityPool)
const expectedNextSecondDelta = collateralBeforeSecondAccrual - (collateralBeforeSecondAccrual * rpow(retentionRate, 1n, PRICE_PRECISION)) / PRICE_PRECISION

await mockWindow.advanceTime(1n)
await updateVaultFees(secondVaultClient, securityPoolAddresses.securityPool, secondVaultClient.account.address)

const secondVault = await getSecurityVault(secondVaultClient, securityPoolAddresses.securityPool, secondVaultClient.account.address)
assert.ok(secondVault.unpaidEthFees <= expectedNextSecondDelta, 'new allowance holder should only accrue fees for time after the zero-allowance gap ends')
})

test('redeemCompleteSet exits at the fee-adjusted share exchange rate', async () => {
const securityPoolAllowance = repDeposit / 4n
await manipulatePriceOracleAndPerformOperation(client, mockWindow, securityPoolAddresses.priceOracleManagerAndOperatorQueuer, OperationType.SetSecurityBondsAllowance, client.account.address, securityPoolAllowance)
Expand Down Expand Up @@ -588,9 +726,10 @@ describe('Peripherals: fork migration', () => {
const firstHolderSharesAfterRedeem = await balanceOfShares(firstHolder, securityPoolAddresses.shareToken, genesisUniverse, firstHolder.account.address)
const secondHolderSharesAfterRedeem = await balanceOfShares(secondHolder, securityPoolAddresses.shareToken, genesisUniverse, secondHolder.account.address)
const shareSupplyAfterRedeem = await getShareTokenSupply(client, securityPoolAddresses.securityPool)
const feeDustTolerance = securityPoolAllowance / PRICE_PRECISION

assert.ok(firstHolderPayout > 0n, 'redeeming complete sets should pay ETH to the holder')
strictEqualTypeSafe(collateralAfterRedeem + firstHolderPayout + feeDelta, initialCollateral, 'complete-set redemption should conserve collateral after fee accrual')
approximatelyEqual(collateralAfterRedeem + firstHolderPayout + feeDelta, initialCollateral, feeDustTolerance, 'complete-set redemption should conserve collateral after fee accrual up to bounded fee dust')
strictEqualTypeSafe(shareSupplyAfterRedeem, initialShareSupply - redeemAmount, 'complete-set redemption should reduce share supply by the burned set amount')
strictEqualTypeSafe(firstHolderSharesAfterRedeem[0], firstHolderShares[0] - redeemAmount, 'redeeming should burn the holders invalid-side share')
strictEqualTypeSafe(firstHolderSharesAfterRedeem[1], firstHolderShares[1] - redeemAmount, 'redeeming should burn the holders yes-side share')
Expand Down Expand Up @@ -708,9 +847,10 @@ describe('Peripherals: fork migration', () => {
const feesAfterFirstRedemption = await getTotalFeesOwedToVaults(client, securityPoolAddresses.securityPool)
const firstHolderPayout = (await getETHBalance(client, firstHolder.account.address)) - firstHolderBalanceBeforeRedemption
const feeDelta = feesAfterFirstRedemption - initialFeesOwed
const feeDustTolerance = securityPoolAllowance / PRICE_PRECISION

assert.ok(feeDelta > 0n, 'first redemption should accrue open-interest fees')
strictEqualTypeSafe(collateralAfterFirstRedemption + firstHolderPayout + feeDelta, initialCollateral, 'collateral should shrink by fees and first winning redemption')
approximatelyEqual(collateralAfterFirstRedemption + firstHolderPayout + feeDelta, initialCollateral, feeDustTolerance, 'collateral should shrink by fees and first winning redemption up to bounded fee dust')
strictEqualTypeSafe(await getShareTokenSupply(client, securityPoolAddresses.securityPool), initialShareSupply - firstWinningShares, 'share supply should shrink after first winning redemption')
approximatelyEqual(await sharesToCash(client, securityPoolAddresses.securityPool, secondWinningShares), collateralAfterFirstRedemption, 10n, 'remaining winning shares should not be double counted')

Expand Down Expand Up @@ -1213,7 +1353,7 @@ describe('Peripherals: fork migration', () => {
await triggerOwnGameFork(client, securityPoolAddresses.securityPool)

strictEqualTypeSafe(await getSystemState(client, securityPoolAddresses.securityPool), SystemState.PoolForked, 'parent pool should enter PoolForked after the universe fork is activated')
await assert.rejects(depositRep(client, securityPoolAddresses.securityPool, 1n), /Universe already forked|Question resolved/)
await assert.rejects(depositRep(client, securityPoolAddresses.securityPool, 1n), /Universe forked/)

await migrateRepToZoltar(client, securityPoolAddresses.securityPool, [QuestionOutcome.Yes])
await migrateVault(client, securityPoolAddresses.securityPool, QuestionOutcome.Yes)
Expand Down Expand Up @@ -1710,8 +1850,8 @@ describe('Peripherals: fork migration', () => {
const clientVaultAfterFork = await getSecurityVault(client, securityPoolAddresses.securityPool, client.account.address)
const attackerVaultAfterFork = await getSecurityVault(client, securityPoolAddresses.securityPool, attackerClient.account.address)

await assert.rejects(withdrawFromEscalationGame(client, securityPoolAddresses.securityPool, QuestionOutcome.Yes, [0n]), /Pool not operational/)
await assert.rejects(withdrawFromEscalationGame(attackerClient, securityPoolAddresses.securityPool, QuestionOutcome.No, [0n]), /Pool not operational/)
await assert.rejects(withdrawFromEscalationGame(client, securityPoolAddresses.securityPool, QuestionOutcome.Yes, [0n]), /Pool inactive/)
await assert.rejects(withdrawFromEscalationGame(attackerClient, securityPoolAddresses.securityPool, QuestionOutcome.No, [0n]), /Pool inactive/)

const clientVaultAfterFailedWithdrawal = await getSecurityVault(client, securityPoolAddresses.securityPool, client.account.address)
const attackerVaultAfterFailedWithdrawal = await getSecurityVault(client, securityPoolAddresses.securityPool, attackerClient.account.address)
Expand Down
2 changes: 1 addition & 1 deletion solidity/ts/tests/peripherals/truthAuction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ describe('Peripherals: truth auction', () => {

strictEqualTypeSafe(await getSystemState(client, yesSecurityPool.securityPool), SystemState.ForkMigration, 'child pool should still be in fork migration before the truth-auction window ends')
strictEqualTypeSafe(await getQuestionOutcome(client, yesSecurityPool.securityPool), QuestionOutcome.Yes, 'own-fork child currently reports a finalized outcome before the pool is operational')
await assert.rejects(redeemRep(client, yesSecurityPool.securityPool, client.account.address), /Pool not operational/)
await assert.rejects(redeemRep(client, yesSecurityPool.securityPool, client.account.address), /Pool not operational|Pool inactive/)
})
})

Expand Down
Loading