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
18 changes: 9 additions & 9 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": "0x9f60ca8EfCc044EB5CFF8f71d46fC292e4b1A886"
"address": "0xD7549E27fB03aeD7dfb30082f6d8f342DfEAF604"
},
{
"id": "multicall3",
Expand Down Expand Up @@ -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"
}
]
}
2 changes: 2 additions & 0 deletions shared/ts/testing/scalarOutcomeParityFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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' }
Expand Down
8 changes: 8 additions & 0 deletions solidity/contracts/ZoltarQuestionData.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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';
Expand Down
34 changes: 34 additions & 0 deletions solidity/ts/tests/questionData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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 = {
Expand Down
15 changes: 15 additions & 0 deletions solidity/ts/tests/zoltar.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')}`
Expand Down Expand Up @@ -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 () => {
Expand Down
2 changes: 2 additions & 0 deletions ui/ts/lib/scalarOutcome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
| {
Expand Down Expand Up @@ -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' }
Expand Down
16 changes: 16 additions & 0 deletions ui/ts/tests/scalarOutcome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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')
})
Expand Down