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
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"dependencies": {
"@preact/signals": "2.0.1",
"@zoltar/shared": "file:shared",
"tevm": "1.0.0-next.149",
"viem": "2.38.3"
},
Expand Down Expand Up @@ -55,6 +56,7 @@
"ui:vendor": "cd ui && bun ./build/vendor.mts",
"gas-costs": "cd solidity && bun run gas-costs",
"test": "bun run ensure-contract-artifacts && bun run check:shared-dependencies && bun run tsc && bun test --parallel=4 --timeout 300000",
"test:integration:mainnet": "RUN_MAINNET_INTEGRATION_TESTS=1 bun test --timeout 300000 ui/ts/tests/uniswapQuoter.integration.test.ts",
"test:auction-fuzz": "bun run ensure-contract-artifacts && bun run check:shared-dependencies && bun run tsc && bun test --timeout 300000 ./solidity/ts/fuzz/auctionTickMath.fuzz.ts",
"coverage:ui": "bun run ensure-shared-build && bun run check:shared-dependencies && bun test --isolate --coverage --coverage-reporter=lcov --coverage-reporter=text --coverage-dir=coverage/ui ui/ts/tests",
"coverage:contracts:ts": "bun run ensure-contract-artifacts && bun test --coverage --coverage-reporter=lcov --coverage-reporter=text --coverage-dir=coverage/contracts-ts solidity/ts/tests",
Expand Down
8 changes: 6 additions & 2 deletions shared/ts/addressDerivation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ function getShareTokenSalt(questionId: bigint, securityMultiplier: bigint) {
return keccak256(encodeAbiParameters([{ type: 'uint256' }, { type: 'uint256' }], [securityMultiplier, questionId]))
}

export function getCallerScopedSalt(caller: Address, salt: Hex) {
return keccak256(encodeAbiParameters([{ type: 'address' }, { type: 'bytes32' }], [caller, salt]))
}

function getSecurityPoolDeployerAddress(securityPoolFactory: Address) {
return getCreateAddress({
from: securityPoolFactory,
Expand Down Expand Up @@ -89,7 +93,7 @@ export function createSecurityPoolAddressHelper(config: SecurityPoolAddressConfi
const getSecurityPoolAddresses = (parent: Address, universeId: bigint, questionId: bigint, securityMultiplier: bigint) => {
const infraContracts = config.getInfraContracts()
const securityPoolSalt = getSecurityPoolSalt(parent, universeId, questionId, securityMultiplier)
const securityPoolSaltWithMsgSender = keccak256(encodeAbiParameters([{ type: 'address' }, { type: 'bytes32' }], [infraContracts.securityPoolFactory, securityPoolSalt]))
const securityPoolSaltWithMsgSender = getCallerScopedSalt(infraContracts.securityPoolFactory, securityPoolSalt)

const repToken = config.getRepTokenAddress(universeId)
const priceOracleManagerAndOperatorQueuer = getCreate2Address({
Expand All @@ -108,7 +112,7 @@ export function createSecurityPoolAddressHelper(config: SecurityPoolAddressConfi
: getCreate2Address({
bytecode: config.getTruthAuctionInitCode(infraContracts.securityPoolForker),
from: infraContracts.uniformPriceDualCapBatchAuctionFactory,
salt: securityPoolSalt,
salt: securityPoolSaltWithMsgSender,
})
const securityPool = getCreate2Address({
bytecode: config.getSecurityPoolInitCode({
Expand Down
2 changes: 2 additions & 0 deletions solidity/contracts/peripherals/SecurityPoolForker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,8 @@ contract SecurityPoolForker is SecurityPoolForkerVaultMigrationBase {
uint256 repToFork = repBalanceAfter - repBalanceBefore;
if (repToFork > 0) IERC20(address(rep)).safeTransfer(address(migrationProxy), repToFork);
migrationProxy.forkUniverse(securityPool.questionId());
uint256 leftoverProxyRep = rep.balanceOf(address(migrationProxy));
if (leftoverProxyRep > 0) migrationProxy.lockRep(leftoverProxyRep);
uint256 forkTime = zoltar.getForkTime(securityPool.universeId());
require(forkTime > 0, 'e7');
_snapshotEscalationAtFork(data, escalationGame, forkTime);
Expand Down
39 changes: 28 additions & 11 deletions solidity/contracts/peripherals/SecurityPoolOracleCoordinator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -208,11 +208,10 @@ contract SecurityPoolOracleCoordinator {
require(!securityPool.isEscalationResolved(), 'question already resolved');
stagedOperationCounter++;
uint256 operationId = stagedOperationCounter;
// Capture the target vault state at queue time on purpose.
// Liquidations are intentionally valued against the vault state that existed when the
// caller requested the oracle-backed operation, so the target cannot escape by
// depositing REP or reducing allowance after the request is staged but before the
// oracle report settles.
// Capture the target vault state at queue time. Liquidation may still execute if
// the target deposits more REP after staging, but allowance changes or ownership
// decreases make the snapshot stale. Stale operations are consumed and must be
// restaged against current state.
// Liquidation should value the vault's full collateral claim. That means using the
// pool's total REP balance here rather than only the currently withdrawable balance.
(uint256 snapshotTargetOwnership, uint256 snapshotTargetAllowance, , ) = securityPool.securityVaults(
Expand Down Expand Up @@ -268,13 +267,24 @@ contract SecurityPoolOracleCoordinator {
StagedOperation memory stagedOperation = stagedOperations[operationId];
require(stagedOperation.initiatorVault != address(0), 'no such operation');
require(isPriceValid(), 'price is not valid to execute');
require(
block.timestamp <= stagedOperation.queuedAt + settlementTime + stagedOperation.validForSeconds,
'staged operation expired'
);
_consumeActiveStagedOperation(operationId);
stagedOperations[operationId].initiatorVault = address(0);
if (block.timestamp > stagedOperation.queuedAt + settlementTime + stagedOperation.validForSeconds) {
_consumeStagedOperation(operationId);
emit ExecutedStagedOperation(operationId, stagedOperation.operation, false, 'staged operation expired');
return;
}
if (stagedOperation.operation == OperationType.Liquidation) {
(uint256 currentTargetOwnership, uint256 currentTargetAllowance, , ) = securityPool.securityVaults(
stagedOperation.targetVault
);
if (
currentTargetOwnership < stagedOperation.snapshotTargetOwnership ||
currentTargetAllowance != stagedOperation.snapshotTargetAllowance
) {
_consumeStagedOperation(operationId);
emit ExecutedStagedOperation(operationId, stagedOperation.operation, false, 'stale liquidation');
return;
}
_consumeStagedOperation(operationId);
try
securityPool.performLiquidation(
stagedOperation.initiatorVault,
Expand All @@ -295,6 +305,7 @@ contract SecurityPoolOracleCoordinator {
emit ExecutedStagedOperation(operationId, stagedOperation.operation, false, 'Unknown error');
}
} else if (stagedOperation.operation == OperationType.WithdrawRep) {
_consumeStagedOperation(operationId);
try securityPool.performWithdrawRep(stagedOperation.initiatorVault, stagedOperation.amount) {
emit ExecutedStagedOperation(operationId, stagedOperation.operation, true, '');
} catch Error(string memory reason) {
Expand All @@ -305,6 +316,7 @@ contract SecurityPoolOracleCoordinator {
emit ExecutedStagedOperation(operationId, stagedOperation.operation, false, 'Unknown error');
}
} else {
_consumeStagedOperation(operationId);
try securityPool.performSetSecurityBondsAllowance(stagedOperation.initiatorVault, stagedOperation.amount) {
emit ExecutedStagedOperation(operationId, stagedOperation.operation, true, '');
} catch Error(string memory reason) {
Expand All @@ -317,6 +329,11 @@ contract SecurityPoolOracleCoordinator {
}
}

function _consumeStagedOperation(uint256 operationId) private {
_consumeActiveStagedOperation(operationId);
stagedOperations[operationId].initiatorVault = address(0);
}

function getPendingOperationSlot() public view returns (StagedOperation memory) {
return stagedOperations[pendingOperationSlotId];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ contract UniformPriceDualCapBatchAuctionFactory {
address owner,
bytes32 salt
) external returns (UniformPriceDualCapBatchAuction) {
return new UniformPriceDualCapBatchAuction{ salt: salt }(owner);
return new UniformPriceDualCapBatchAuction{ salt: keccak256(abi.encode(msg.sender, salt)) }(owner);
}
}
20 changes: 10 additions & 10 deletions solidity/ts/tests/auction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,7 @@ describe('Auction', () => {

const tick = tickForPrice(PRICE_PRECISION)
const bidAmount = 1n * 10n ** 18n
await assert.rejects(async () => await submitBid(client, auctionAddress, tick, bidAmount), 'Auction ended')
await assert.rejects(async () => await submitBid(client, auctionAddress, tick, bidAmount), /auction ended/)
})
})

Expand All @@ -843,7 +843,7 @@ describe('Auction', () => {
const minBid = await getMinBidSize(client, auctionAddress)
strictEqualTypeSafe(minBid, 1n, 'minBidSize should be 1')

await assert.rejects(async () => await submitBid(client, auctionAddress, 0n, 0n), 'invalid')
await assert.rejects(async () => await submitBid(client, auctionAddress, 0n, 0n), /bid too small/)

await submitBid(client, auctionAddress, 0n, 1n)
})
Expand All @@ -868,15 +868,15 @@ describe('Auction', () => {

const freshAddress = getUniformPriceDualCapBatchAuctionAddress(addressString(TEST_ADDRESSES[3]))
await deployUniformPriceDualCapBatchAuction(client, addressString(TEST_ADDRESSES[3]))
await assert.rejects(async () => await submitBid(client, freshAddress, tick, bidAmount), 'invalid')
await assert.rejects(async () => await submitBid(client, freshAddress, tick, bidAmount), /not started/)

await startAuction(client, auctionAddress, ethRaiseCap, maxRepBeingSold)
await submitBid(client, auctionAddress, tick, ethRaiseCap)
await mockWindow.advanceTime(AUCTION_TIME + 1n)
await finalize(client, auctionAddress)
strictEqualTypeSafe(await isFinalized(client, auctionAddress), true, 'auction should be finalized before post-finalization assertions')

await assert.rejects(async () => await submitBid(client, auctionAddress, tick, bidAmount), 'finalized')
await assert.rejects(async () => await submitBid(client, auctionAddress, tick, bidAmount), /finalized/)
})

test('withdrawBids reverts before finalize', async () => {
Expand All @@ -887,7 +887,7 @@ describe('Auction', () => {
await startAuction(client, auctionAddress, ethRaiseCap, maxRepBeingSold)
await submitBid(client, auctionAddress, tick, 1n * 10n ** 18n)

await assert.rejects(async () => await withdrawBids(client, auctionAddress, client.account.address, [{ tick, bidIndex: 0n }]), 'not finalized')
await assert.rejects(async () => await withdrawBids(client, auctionAddress, client.account.address, [{ tick, bidIndex: 0n }]), /not finalized/)
})
})

Expand Down Expand Up @@ -1283,7 +1283,7 @@ describe('Auction', () => {
const post = await getETHBalance(client, refundClient.account.address)
approximatelyEqual(post - pre, c.refundBidder === 'alice' ? c.aliceAmount : c.bobAmount, DEFAULT_TOLERANCE, 'refund amount')
} else {
await assert.rejects(async () => await refundLosingBids(refundClient, auctionAddress, [{ tick: refundTick, bidIndex: 0n }]), 'cannot withdraw binding bid')
await assert.rejects(async () => await refundLosingBids(refundClient, auctionAddress, [{ tick: refundTick, bidIndex: 0n }]), /cannot withdraw binding bid/)
}

if (c.checkClearingUnchanged) {
Expand All @@ -1301,11 +1301,11 @@ describe('Auction', () => {
const maxRepBeingSold = 10n * 10n ** 18n

const attacker = createTestClient(1)
await assert.rejects(async () => await startAuction(attacker, auctionAddress, ethRaiseCap, maxRepBeingSold), 'only owner')
await assert.rejects(async () => await startAuction(attacker, auctionAddress, ethRaiseCap, maxRepBeingSold), /only owner can start/)

await startAuction(client, auctionAddress, ethRaiseCap, maxRepBeingSold)

await assert.rejects(async () => await startAuction(client, auctionAddress, ethRaiseCap, maxRepBeingSold), 'already started')
await assert.rejects(async () => await startAuction(client, auctionAddress, ethRaiseCap, maxRepBeingSold), /already started/)
})
})

Expand Down Expand Up @@ -1498,10 +1498,10 @@ describe('Auction', () => {
strictEqualTypeSafe(await isFinalized(client, auctionAddress), true)

// 1) Non-owner (alice) cannot withdraw her losing bid -> revert with "Only owner can call"
await assert.rejects(async () => await withdrawBids(alice, auctionAddress, alice.account.address, [{ tick: losingTick, bidIndex: 0n }]), 'Only owner can call')
await assert.rejects(async () => await withdrawBids(alice, auctionAddress, alice.account.address, [{ tick: losingTick, bidIndex: 0n }]), /Only owner can call/)

// 2) Non-owner (bob) cannot withdraw his winning bid -> also revert
await assert.rejects(async () => await withdrawBids(bob, auctionAddress, bob.account.address, [{ tick: winningTick, bidIndex: 0n }]), 'Only owner can call')
await assert.rejects(async () => await withdrawBids(bob, auctionAddress, bob.account.address, [{ tick: winningTick, bidIndex: 0n }]), /Only owner can call/)

// 3) Owner withdraws for alice (losing) -> full ETH refund
const aliceBalanceBefore = await getETHBalance(client, alice.account.address)
Expand Down
4 changes: 2 additions & 2 deletions solidity/ts/tests/peripherals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ describe('Peripherals Contract Test Suite', () => {
await finalizeQuestionAsYesWithoutFork()

const walletRepBeforeRedeem = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), client.account.address)
await assert.rejects(redeemRep(client, securityPoolAddresses.securityPool, client.account.address), 'redeemRep should stay blocked until escalation deposits are settled')
await assert.rejects(redeemRep(client, securityPoolAddresses.securityPool, client.account.address), /settle locks first/)

await withdrawFromEscalationGame(client, securityPoolAddresses.securityPool, QuestionOutcome.Yes, [0n])
const vaultAfterSettlement = await getSecurityVault(client, securityPoolAddresses.securityPool, client.account.address)
Expand Down Expand Up @@ -3588,7 +3588,7 @@ describe('Peripherals Contract Test Suite', () => {

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), 'redeemRep should remain blocked until the child pool is operational')
await assert.rejects(redeemRep(client, yesSecurityPool.securityPool, client.account.address), /not operational/)
})

// - TODO test that users can claim their stuff (shares+rep) even if zoltar forks after question ends
Expand Down
Loading
Loading