diff --git a/docs/mainnet-deployment-addresses.json b/docs/mainnet-deployment-addresses.json index 1c446b8c..dfe8dc86 100644 --- a/docs/mainnet-deployment-addresses.json +++ b/docs/mainnet-deployment-addresses.json @@ -13,7 +13,7 @@ { "id": "deploymentStatusOracle", "label": "Deployment Status Oracle", - "address": "0x9f60ca8EfCc044EB5CFF8f71d46fC292e4b1A886" + "address": "0xD7549E27fB03aeD7dfb30082f6d8f342DfEAF604" }, { "id": "multicall3", @@ -43,44 +43,44 @@ { "id": "zoltarQuestionData", "label": "ZoltarQuestionData", - "address": "0x3fDE26F6C206DDc4991087FCeB5f13EC9f6F3E94" + "address": "0x0B44b5428ed177B279455Ae6245C8aD46702725B" }, { "id": "zoltar", "label": "Zoltar", - "address": "0x99B04b933512F95711Db02A163742C8468C6D0b5" + "address": "0x216326652dd28905917baC191A43a8f5237c16c6" }, { "id": "shareTokenFactory", "label": "ShareTokenFactory", - "address": "0x9e3adB2eFB22B4C8471B3A439127C9c1e42DA197" + "address": "0x2255ae657cA45A75F9aa7dF453A901B1F1E7f9Ca" }, { "id": "priceOracleManagerAndOperatorQueuerFactory", "label": "OpenOracle Price Coordinator Factory", - "address": "0x4318D853ccFCa15ab973621ea4BBE1Aa6A628C93" + "address": "0x20CeeD3987AEeaFB24B83C7281C1C52135BbA9BA" }, { "id": "securityPoolForker", "label": "Security Pool Forker", - "address": "0x12DDac4Eaa2854eBF38D5e6cd116538003D6f608" + "address": "0x55AD363204C991d0bb37494372F3821f2B5681De" }, { "id": "escalationGameFactory", "label": "Escalation Game Factory", - "address": "0x0e15497C658F9530C85931A02e9E3f6047F9e62F" + "address": "0x04AbAd5b45864673586C0b19c611D8e96B60266d" }, { "id": "securityPoolFactory", "label": "Security Pool Factory", - "address": "0x5b7F5A06B3eBf67eE8f5FF03718a05904F700052" + "address": "0x7646559Cccbe18A819b0f6060890D292cD5359E8" } ], "derivedContracts": [ { "id": "escalationGameProofVerifier", "label": "Escalation Game Proof Verifier", - "address": "0x77a2BfF40a775fA2d05a6574926c66E00a369E82" + "address": "0x1A899c1D329ad210CC6DD88ED068410a007D51De" } ] } diff --git a/shared/ts/testing/scalarOutcomeParityFixtures.ts b/shared/ts/testing/scalarOutcomeParityFixtures.ts index fc034f29..c7ab0de1 100644 --- a/shared/ts/testing/scalarOutcomeParityFixtures.ts +++ b/shared/ts/testing/scalarOutcomeParityFixtures.ts @@ -40,6 +40,7 @@ export const SCALAR_PARITY_DECIMAL_BASE = 10n ** SCALAR_PARITY_DECIMALS export const SCALAR_PARITY_PART_BIT_LENGTH = 120n export const SCALAR_PARITY_TOTAL_BITS = 256n export const SCALAR_PARITY_PART_MASK = (1n << SCALAR_PARITY_PART_BIT_LENGTH) - 1n +export const SCALAR_PARITY_RESERVED_BITS_MASK = ((1n << 15n) - 1n) << 240n export const SCALAR_PARITY_UINT120_MAX = SCALAR_PARITY_PART_MASK export const SCALAR_PARITY_INT256_MIN = -(1n << 255n) export const SCALAR_PARITY_INT256_MAX = (1n << 255n) - 1n @@ -214,6 +215,7 @@ export function formatScalarParityLabel(question: ScalarParityQuestion, tickInde export function describeScalarParityOutcomeIndex(question: ScalarParityQuestion, outcomeIndex: bigint): ScalarParityOutcomeIndexDescriptor { if (question.numTicks <= 0n) return { kind: 'malformed' } + if ((outcomeIndex & SCALAR_PARITY_RESERVED_BITS_MASK) !== 0n) return { kind: 'malformed' } const { invalid, firstPart, secondPart } = splitScalarParityOutcomeIndex(outcomeIndex) if (invalid) return firstPart === 0n && secondPart === 0n ? { kind: 'invalid' } : { kind: 'malformed' } if (firstPart + secondPart !== question.numTicks) return { kind: 'malformed' } diff --git a/solidity/contracts/ZoltarQuestionData.sol b/solidity/contracts/ZoltarQuestionData.sol index c4f57ce1..bc3692a0 100644 --- a/solidity/contracts/ZoltarQuestionData.sol +++ b/solidity/contracts/ZoltarQuestionData.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.35; import { ScalarOutcomes } from './ScalarOutcomes.sol'; contract ZoltarQuestionData { + uint256 private constant SCALAR_RESERVED_BITS_MASK = ((uint256(1) << 15) - 1) << 240; + struct QuestionData { string title; string description; @@ -98,6 +100,10 @@ contract ZoltarQuestionData { secondPart = uint120(value & ((1 << 120) - 1)); } + function hasNonZeroScalarReservedBits(uint256 answer) public pure returns (bool) { + return answer & SCALAR_RESERVED_BITS_MASK != 0; + } + function getOutcomeLabels( uint256 questionId, uint256 startIndex, @@ -121,6 +127,7 @@ contract ZoltarQuestionData { function isMalformedAnswerOption(uint256 questionId, uint256 answer) external view returns (bool) { if (outcomeLabels[questionId].length == 0) { // scalar + if (hasNonZeroScalarReservedBits(answer)) return true; (bool invalid, uint120 firstPart, uint120 secondPart) = splitUint256IntoTwoWithInvalid(answer); if (invalid) { if (firstPart == 0 && secondPart == 0) return false; @@ -141,6 +148,7 @@ contract ZoltarQuestionData { function getAnswerOptionName(uint256 questionId, uint256 answer) external view returns (string memory) { if (outcomeLabels[questionId].length == 0) { // scalar + if (hasNonZeroScalarReservedBits(answer)) return 'Malformed'; (bool invalid, uint120 firstPart, uint120 secondPart) = splitUint256IntoTwoWithInvalid(answer); if (invalid) { if (firstPart == 0 && secondPart == 0) return 'Invalid'; diff --git a/solidity/ts/tests/questionData.test.ts b/solidity/ts/tests/questionData.test.ts index b2cbd547..65ab1ba9 100644 --- a/solidity/ts/tests/questionData.test.ts +++ b/solidity/ts/tests/questionData.test.ts @@ -30,6 +30,7 @@ import type { ScalarParityQuestion } from '@zoltar/shared/testing/scalarOutcomeP const MAX_UINT256 = 2n ** 256n - 1n const SCALAR_ENCODING_FUZZ_SAMPLE_COUNT = 12 const SCALAR_ENCODING_FUZZ_STATE_MASK = (1n << 128n) - 1n +const SCALAR_RESERVED_BITS_MASK = ((1n << 15n) - 1n) << 240n type ScalarEncodingFuzzSample = { firstPart: bigint @@ -63,6 +64,10 @@ function getScalarEncodingFuzzSamples(question: ScalarParityQuestion, seed: bigi return samples } +function withScalarReservedBits(answer: bigint, reservedBits = 1n) { + return answer | ((reservedBits << 240n) & SCALAR_RESERVED_BITS_MASK) +} + setDefaultTimeout(TEST_TIMEOUT_MS) describe('Question Data', () => { @@ -304,6 +309,35 @@ describe('Question Data', () => { } }) + test('scalar answers with non-zero reserved bits are malformed even when the payload is otherwise canonical', async () => { + const testScalarQuestion = { + title: 'scalar reserved bits', + description: 'scalar reserved bits', + startTime: (await mockWindow.getTime()) + 100000n, + endTime: (await mockWindow.getTime()) + 200000n, + numTicks: 1000n, + displayValueMin: 0n, + displayValueMax: 1000n, + answerUnit: 'unit', + } + await createQuestion(client, testScalarQuestion, []) + const questionId = getQuestionId(testScalarQuestion, []) + + const canonicalScalarAnswer = combineUint256FromTwoWithInvalid(false, 600n, 400n) + const aliasedScalarAnswer = withScalarReservedBits(canonicalScalarAnswer, 0x1234n) + const canonicalInvalidAnswer = combineUint256FromTwoWithInvalid(true, 0n, 0n) + const aliasedInvalidAnswer = withScalarReservedBits(canonicalInvalidAnswer, 0x7fffn) + const canonicalScalarLabel = await getAnswerOptionName(client, questionId, canonicalScalarAnswer) + + assert.strictEqual(await isMalformedAnswerOption(client, questionId, canonicalScalarAnswer), false, 'canonical scalar answer should remain valid') + assert.notStrictEqual(canonicalScalarLabel, 'Malformed', 'canonical scalar answer should keep a non-malformed label') + assert.strictEqual(await isMalformedAnswerOption(client, questionId, aliasedScalarAnswer), true, 'scalar alias with reserved bits should be malformed') + assert.strictEqual(await getAnswerOptionName(client, questionId, aliasedScalarAnswer), 'Malformed', 'scalar alias with reserved bits should render as malformed') + assert.strictEqual(await isMalformedAnswerOption(client, questionId, aliasedInvalidAnswer), true, 'invalid alias with reserved bits should be malformed') + assert.strictEqual(await getAnswerOptionName(client, questionId, aliasedInvalidAnswer), 'Malformed', 'invalid alias with reserved bits should render as malformed') + assert.strictEqual(await getAnswerOptionName(client, questionId, canonicalInvalidAnswer), 'Invalid', 'canonical invalid answer should remain available') + }) + test('accepts scalar questions at the uint120 encoding boundary', async () => { const maxScalarNumTicks = (1n << 120n) - 1n const testScalarQuestion = { diff --git a/solidity/ts/tests/zoltar.test.ts b/solidity/ts/tests/zoltar.test.ts index 9f9c69c7..a9f85667 100644 --- a/solidity/ts/tests/zoltar.test.ts +++ b/solidity/ts/tests/zoltar.test.ts @@ -34,6 +34,11 @@ import { formatScalarOutcomeLabel, getScalarOutcomeIndex } from '../testsuite/si // Forker deposit fractions: deposit is 5% of total supply (1/20), and 20% of that deposit is burned (1/5 of deposit) const FORKER_DEPOSIT_FRACTION = 20n const MAX_UINT256 = 2n ** 256n - 1n +const SCALAR_RESERVED_BITS_MASK = ((1n << 15n) - 1n) << 240n + +function withScalarReservedBits(answer: bigint, reservedBits = 1n) { + return answer | ((reservedBits << 240n) & SCALAR_RESERVED_BITS_MASK) +} function formatStorageSlot(slot: bigint) { return `0x${slot.toString(16).padStart(64, '0')}` @@ -518,6 +523,16 @@ describe('Contract Test Suite', () => { const malformedChildUniverseId = getChildUniverseId(genesisUniverse, malformedScalarOutcomeIndex) await assert.rejects(deployChild(client, genesisUniverse, malformedScalarOutcomeIndex), /Malformed/) assert.ok(!(await contractExists(client, getRepTokenAddress(malformedChildUniverseId))), 'malformed scalar child universe should not be deployed') + + const canonicalScalarOutcomeIndex = getScalarOutcomeIndex(scalarQuestionData, 5n) + const aliasedScalarOutcomeIndex = withScalarReservedBits(canonicalScalarOutcomeIndex, 0x4567n) + const aliasedChildUniverseId = getChildUniverseId(genesisUniverse, aliasedScalarOutcomeIndex) + assert.ok(aliasedChildUniverseId !== getChildUniverseId(genesisUniverse, canonicalScalarOutcomeIndex), 'reserved-bit alias should still hash to a distinct child id before validation') + await assert.rejects(deployChild(client, genesisUniverse, aliasedScalarOutcomeIndex), /Malformed/) + assert.ok(!(await contractExists(client, getRepTokenAddress(aliasedChildUniverseId))), 'reserved-bit scalar alias should not deploy a child universe') + + const migrationBalance = await getMigrationRepBalance(client, genesisUniverse, client.account.address) + await assert.rejects(splitMigrationRep(client, genesisUniverse, migrationBalance, [aliasedScalarOutcomeIndex]), /Malformed/) }) test('getDeployedChildUniverses pages deployed child universes', async () => { diff --git a/ui/ts/lib/scalarOutcome.ts b/ui/ts/lib/scalarOutcome.ts index b53bfe96..e3427314 100644 --- a/ui/ts/lib/scalarOutcome.ts +++ b/ui/ts/lib/scalarOutcome.ts @@ -19,6 +19,7 @@ const SCALAR_DECIMAL_BASE = 10n ** SCALAR_DECIMALS const SCALAR_PART_BIT_LENGTH = 120n const SCALAR_TOTAL_BITS = 256n const SCALAR_PART_MASK = (1n << SCALAR_PART_BIT_LENGTH) - 1n +const SCALAR_RESERVED_BITS_MASK = ((1n << 15n) - 1n) << 240n type ScalarOutcomeIndexDescriptor = | { @@ -115,6 +116,7 @@ export function formatScalarOutcomeLabel(question: ScalarQuestionDetails, tickIn export function getScalarOutcomeIndexDescriptor(question: ScalarQuestionDetails, outcomeIndex: bigint): ScalarOutcomeIndexDescriptor { if (question.numTicks <= 0n) return { kind: 'malformed' } + if ((outcomeIndex & SCALAR_RESERVED_BITS_MASK) !== 0n) return { kind: 'malformed' } const { invalid, firstPart, secondPart } = splitScalarOutcomeIndex(outcomeIndex) if (invalid) return firstPart === 0n && secondPart === 0n ? { kind: 'invalid' } : { kind: 'malformed' } diff --git a/ui/ts/tests/scalarOutcome.test.ts b/ui/ts/tests/scalarOutcome.test.ts index e3f26adf..3b3afe3d 100644 --- a/ui/ts/tests/scalarOutcome.test.ts +++ b/ui/ts/tests/scalarOutcome.test.ts @@ -10,6 +10,11 @@ const scalarQuestion = { displayValueMin: 0n, numTicks: 10n, } +const SCALAR_RESERVED_BITS_MASK = ((1n << 15n) - 1n) << 240n + +function withScalarReservedBits(answer: bigint, reservedBits = 1n) { + return answer | ((reservedBits << 240n) & SCALAR_RESERVED_BITS_MASK) +} void describe('scalar outcome helpers', () => { void test('rejects zero tick scalar questions', () => { @@ -53,6 +58,17 @@ void describe('scalar outcome helpers', () => { expect(() => formatScalarOutcomeIndexLabel(scalarQuestion, 5n)).toThrow('Scalar outcome index is malformed') }) + void test('treats reserved-bit scalar aliases as malformed', () => { + const canonicalOutcomeIndex = getScalarOutcomeIndex(scalarQuestion, 4n) + const aliasedOutcomeIndex = withScalarReservedBits(canonicalOutcomeIndex, 0x1234n) + const aliasedInvalidOutcomeIndex = withScalarReservedBits(0n, 0x7fffn) + + expect(getScalarOutcomeIndexDescriptor(scalarQuestion, aliasedOutcomeIndex)).toEqual({ kind: 'malformed' }) + expect(getScalarOutcomeIndexDescriptor(scalarQuestion, aliasedInvalidOutcomeIndex)).toEqual({ kind: 'malformed' }) + expect(isValidScalarOutcomeIndex(scalarQuestion, aliasedOutcomeIndex)).toBe(false) + expect(() => formatScalarOutcomeIndexLabel(scalarQuestion, aliasedOutcomeIndex)).toThrow('Scalar outcome index is malformed') + }) + void test('rejects scalar inputs that do not divide into whole ticks', () => { expect(() => parseScalarFormInputs({ scalarMin: '1', scalarMax: '10', scalarIncrement: '0.4' })).toThrow('Scalar min, max, and increment do not produce a whole number of ticks') })