From 947e993dc0653329372b3e1ddd0634131fbafe57 Mon Sep 17 00:00:00 2001 From: KillariDev Date: Fri, 13 Feb 2026 14:03:43 +0200 Subject: [PATCH 1/5] zoltar --- solidity/contracts/Constants.sol | 2 +- solidity/contracts/ReputationToken.sol | 28 +- solidity/ts/tests/test.ts | 255 +++++------------- .../simulator/utils/contracts/zoltar.ts | 137 ++++++++++ 4 files changed, 227 insertions(+), 195 deletions(-) create mode 100644 solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts diff --git a/solidity/contracts/Constants.sol b/solidity/contracts/Constants.sol index 46d9fb3..9f1cbb0 100644 --- a/solidity/contracts/Constants.sol +++ b/solidity/contracts/Constants.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNICENSE +// SPDX-License-Identifier: Unlicense pragma solidity 0.8.33; library Constants { diff --git a/solidity/contracts/ReputationToken.sol b/solidity/contracts/ReputationToken.sol index 2a0dd87..7e392c0 100644 --- a/solidity/contracts/ReputationToken.sol +++ b/solidity/contracts/ReputationToken.sol @@ -1,23 +1,39 @@ -// SPDX-License-Identifier: UNICENSE +// SPDX-License-Identifier: Unlicense pragma solidity 0.8.33; import './ERC20.sol'; contract ReputationToken is ERC20 { - + uint256 private totalTheoreticalSupply; address public immutable zoltar; + event Mint(address account, uint256 value); + event Burn(address account, uint256 value); + + modifier isZoltar { + require(msg.sender == zoltar, 'Not zoltar'); + _; + } constructor(address _zoltar) ERC20('Reputation', 'REP') { zoltar = _zoltar; } - function mint(address account, uint256 value) external { - require(msg.sender == zoltar, "Not zoltar"); + function setMaxTheoreticalSupply(uint256 _totalTheoreticalSupply) external isZoltar { + totalTheoreticalSupply = _totalTheoreticalSupply; + } + + function mint(address account, uint256 value) external isZoltar { _mint(account, value); + emit Mint(account, value); } - function burn(address account, uint256 value) external { - require(msg.sender == zoltar, "Not zoltar"); + function burn(address account, uint256 value) external isZoltar { _burn(account, value); + totalTheoreticalSupply -= value; + emit Burn(account, value); + } + + function getTotalTheoreticalSupply() external view returns (uint256) { + return totalTheoreticalSupply; } } diff --git a/solidity/ts/tests/test.ts b/solidity/ts/tests/test.ts index 9937d1c..68e552d 100644 --- a/solidity/ts/tests/test.ts +++ b/solidity/ts/tests/test.ts @@ -1,21 +1,23 @@ import { describe, beforeEach, test } from 'node:test' import { getMockedEthSimulateWindowEthereum, MockWindowEthereum } from '../testsuite/simulator/MockWindowEthereum.js' import { createWriteClient } from '../testsuite/simulator/utils/viem.js' -import { BURN_ADDRESS, DAY, GENESIS_REPUTATION_TOKEN, REP_BOND, TEST_ADDRESSES } from '../testsuite/simulator/utils/constants.js' -import { approveToken, createQuestion, ensureZoltarDeployed, getERC20Balance, getQuestionData, getZoltarAddress, getUniverseData, initialTokenBalance, isZoltarDeployed, setupTestAccounts, reportOutcome, isFinalized, finalizeQuestion, getWinningOutcome, dispute, splitRep, splitStakedRep } from '../testsuite/simulator/utils/utilities.js' +import { GENESIS_REPUTATION_TOKEN, TEST_ADDRESSES } from '../testsuite/simulator/utils/constants.js' +import { approveToken, setupTestAccounts, getERC20Balance, getChildUniverseId, contractExists } from '../testsuite/simulator/utils/utilities.js' import assert from 'node:assert' import { addressString } from '../testsuite/simulator/utils/bigint.js' -import { QuestionOutcome } from '../testsuite/simulator/types/types.js' +import { areEqualArrays } from '../testsuite/simulator/utils/typed-arrays.js' +import { createTransactionExplainer } from '../testsuite/simulator/utils/transactionExplainer.js' +import { getDeployments } from '../testsuite/simulator/utils/contracts/deployments.js' +import { ensureZoltarDeployed, forkerClaimRep, forkUniverse, getRepTokenAddress, getTotalTheoreticalSupply, getUniverseData, getUniverseForkData, getZoltarAddress, isZoltarDeployed, splitRep } from '../testsuite/simulator/utils/contracts/zoltar.js' describe('Contract Test Suite', () => { - let mockWindow: MockWindowEthereum - let curentTimestamp: bigint + const genesisUniverse = 0n beforeEach(async () => { mockWindow = getMockedEthSimulateWindowEthereum() + mockWindow.setAfterTransactionSendCallBack(createTransactionExplainer(getDeployments(genesisUniverse, 0n, 0n))) await setupTestAccounts(mockWindow) - curentTimestamp = await mockWindow.getTime() }) test('canDeployContract', async () => { @@ -28,196 +30,73 @@ describe('Contract Test Suite', () => { assert.strictEqual(genesisUniverseData.reputationToken.toLowerCase(), addressString(GENESIS_REPUTATION_TOKEN), 'Genesis universe not recognized or not initialized properly') }) - test('canCreateQuestion', async () => { - const client = createWriteClient(mockWindow, TEST_ADDRESSES[0], 0) - await ensureZoltarDeployed(client) - const zoltar = getZoltarAddress() - const genesisUniverse = 0n - - await approveToken(client, addressString(GENESIS_REPUTATION_TOKEN), zoltar) - - const repBalance = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), client.account.address) - assert.strictEqual(repBalance, initialTokenBalance, "REP not initially minted") - - const endTime = curentTimestamp + DAY - await createQuestion(client, genesisUniverse, endTime, "test") - - const questionId = 1n - const questionData = await getQuestionData(client, questionId) - - assert.strictEqual(questionData.endTime, endTime, 'Question endTime not as expected') - assert.strictEqual(questionData.originUniverse, genesisUniverse, 'Question origin universe not as expected') - assert.strictEqual(questionData.designatedReporter.toLowerCase(), client.account.address, 'Question designated reporter not as expected') - assert.strictEqual(questionData.extraInfo, "test", 'Question extraInfo not as expected') - }) - - test('canResolveQuestion', async () => { - const client = createWriteClient(mockWindow, TEST_ADDRESSES[0], 0) - await ensureZoltarDeployed(client) - const zoltar = getZoltarAddress() - const genesisUniverse = 0n - - await approveToken(client, addressString(GENESIS_REPUTATION_TOKEN), zoltar) - - const endTime = curentTimestamp + DAY - await createQuestion(client, genesisUniverse, endTime, "test") - - const questionId = 1n - const winningOutcome = QuestionOutcome.Yes - - // We can't report until the question has reached its end time - await assert.rejects(reportOutcome(client, genesisUniverse, questionId, winningOutcome)) - - await mockWindow.advanceTime(DAY) - - await reportOutcome(client, genesisUniverse, questionId, winningOutcome) - - const isFInalized = await isFinalized(client, genesisUniverse, questionId) - assert.ok(!isFInalized, "Question incorrectly recognized as finalized") - await assert.rejects(finalizeQuestion(client, genesisUniverse, questionId)) - - await mockWindow.advanceTime(DAY + 1n) - - const isFinalizedNow = await isFinalized(client, genesisUniverse, questionId) - assert.ok(isFinalizedNow, "Question not recognized as finalized") - - const repBalanceBeforeReturn = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), client.account.address) - await finalizeQuestion(client, genesisUniverse, questionId) - const repBalanceAfterReturn = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), client.account.address) - assert.strictEqual(repBalanceAfterReturn, repBalanceBeforeReturn + REP_BOND, "REP bond not returned") - - const questionOutcome = await getWinningOutcome(client, genesisUniverse, questionId) - assert.strictEqual(questionOutcome, winningOutcome, "Winning outcome not as expected") - }) - - test('canInitialReport', async () => { - const client = createWriteClient(mockWindow, TEST_ADDRESSES[0], 0) - const otherClient = createWriteClient(mockWindow, TEST_ADDRESSES[1], 0) - await ensureZoltarDeployed(client) - const zoltar = getZoltarAddress() - const genesisUniverse = 0n - - await approveToken(client, addressString(GENESIS_REPUTATION_TOKEN), zoltar) - - const endTime = curentTimestamp + DAY - await createQuestion(client, genesisUniverse, endTime, "test") - - const questionId = 1n - const winningOutcome = QuestionOutcome.Yes - - await mockWindow.advanceTime(DAY) - - // We can't report as a non designated reporter until their designated reporting period is over - await assert.rejects(reportOutcome(otherClient, genesisUniverse, questionId, winningOutcome)) - - await mockWindow.advanceTime(DAY * 3n + 1n) - - await reportOutcome(otherClient, genesisUniverse, questionId, winningOutcome) - - // We still need to wait for the question to go without a dispute for the dispute period before it is finalized - const isFInalized = await isFinalized(client, genesisUniverse, questionId) - assert.ok(!isFInalized, "Question incorrectly recognized as finalized") - await assert.rejects(finalizeQuestion(client, genesisUniverse, questionId)) - - await mockWindow.advanceTime(DAY + 1n) - - const isFinalizedNow = await isFinalized(client, genesisUniverse, questionId) - assert.ok(isFinalizedNow, "Question not recognized as finalized") - - // The REP bond can now be returned to the initial reporter - const repBalanceBeforeReturn = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), otherClient.account.address) - await finalizeQuestion(client, genesisUniverse, questionId) - const repBalanceAfterReturn = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), otherClient.account.address) - assert.strictEqual(repBalanceAfterReturn, repBalanceBeforeReturn + REP_BOND, "REP bond not returned") - }) - test('canForkQuestion', async () => { const client = createWriteClient(mockWindow, TEST_ADDRESSES[0], 0) const client2 = createWriteClient(mockWindow, TEST_ADDRESSES[1], 0) await ensureZoltarDeployed(client) const zoltar = getZoltarAddress() - const genesisUniverse = 0n + const marketText = 'test market' + const outcomes = ['Outcome 1', 'Outcome 2', 'Outcome 3', 'Outcome 4'] as const await approveToken(client2, addressString(GENESIS_REPUTATION_TOKEN), zoltar) await approveToken(client, addressString(GENESIS_REPUTATION_TOKEN), zoltar) - const endTime = curentTimestamp + DAY - await createQuestion(client, genesisUniverse, endTime, "test") - - const questionId = 1n - - // We'll create a second question and buy complete sets with both users as well - await createQuestion(client, genesisUniverse, endTime, "test 2") - - const questionId2 = 2n - - await mockWindow.advanceTime(DAY) - - const initialOutcome = QuestionOutcome.Yes - await reportOutcome(client, genesisUniverse, questionId, initialOutcome) - - // We'll also report on the second question - await reportOutcome(client, genesisUniverse, questionId2, initialOutcome) - - const disputeOutcome = QuestionOutcome.No - await dispute(client2, genesisUniverse, questionId, disputeOutcome) - - // Three child universe now exist - const invalidUniverseId = 1n - const yesUniverseId = 2n - const noUniverseId = 3n - const invalidUniverseData = await getUniverseData(client, invalidUniverseId) - const yesUniverseData = await getUniverseData(client, yesUniverseId) - const noUniverseData = await getUniverseData(client, noUniverseId) - const invalidREPToken = invalidUniverseData.reputationToken - const yesREPToken = yesUniverseData.reputationToken - const noREPToken = noUniverseData.reputationToken - - assert.notEqual(invalidREPToken, addressString(0n), 'invalid universe not recognized or not initialized properly') - assert.notEqual(yesREPToken, addressString(0n), 'yes universe not recognized or not initialized properly') - assert.notEqual(noREPToken, addressString(0n), 'no universe not recognized or not initialized properly') - - //The client balances of REP staked in the escalation game have migrated to the respective universe REP - const client1YesREPBalance = await getERC20Balance(client, yesREPToken, client.account.address) - const client2NoREPBalance = await getERC20Balance(client2, noREPToken, client2.account.address) - assert.strictEqual(client1YesREPBalance, REP_BOND, "REP bond not migrated during fork") - assert.strictEqual(client2NoREPBalance, REP_BOND * 2n, "Dispute bond not migrated during fork") - - // The forking question is resolved to each respective outcome in the child universes - const invalidUniverseWinningOutcome = await getWinningOutcome(client, invalidUniverseId, questionId) - const yesUniverseWinningOutcome = await getWinningOutcome(client, yesUniverseId, questionId) - const noUniverseWinningOutcome = await getWinningOutcome(client, noUniverseId, questionId) - - assert.strictEqual(invalidUniverseWinningOutcome, QuestionOutcome.Invalid, "Invalid universe forking question outcome not as expected") - assert.strictEqual(yesUniverseWinningOutcome, QuestionOutcome.Yes, "Yes universe forking question outcome not as expected") - assert.strictEqual(noUniverseWinningOutcome, QuestionOutcome.No, "No universe forking question outcome not as expected") - - const disputeBond = REP_BOND * 2n - - // migrate unstaked REP from client 1 - const client1REPBalance = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), client.account.address) - const repBurned = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), addressString(1n)) - await splitRep(client, genesisUniverse) - - const repBurnedAfterMigration = await getERC20Balance(client, addressString(GENESIS_REPUTATION_TOKEN), addressString(BURN_ADDRESS)) - assert.strictEqual(repBurnedAfterMigration, repBurned + client1REPBalance + REP_BOND + disputeBond, "REP not sent to burn address during migration") - - // TODO check balance of REP in child universes - const client1InvalidREPBalanceAfterMigrate = await getERC20Balance(client, invalidREPToken, client.account.address) - const client1YesREPBalanceAfterMigrate = await getERC20Balance(client, yesREPToken, client.account.address) - const client1NoREPBalanceAfterMigrate = await getERC20Balance(client, noREPToken, client.account.address) - assert.strictEqual(client1InvalidREPBalanceAfterMigrate, client1REPBalance, "REP not migrated to invalid as expected") - assert.strictEqual(client1YesREPBalanceAfterMigrate, client1REPBalance + REP_BOND, "REP not migrated to yes as expected") - assert.strictEqual(client1NoREPBalanceAfterMigrate, client1REPBalance, "REP not migrated to no as expected") - - // We can migrate the REP staked in the other question as well - await splitStakedRep(client, genesisUniverse, questionId2) - - const client1InvalidREPBalanceAfterStakedMigrate = await getERC20Balance(client, invalidREPToken, client.account.address) - const client1YesREPBalanceAfterStakedMigrate = await getERC20Balance(client, yesREPToken, client.account.address) - const client1NoREPBalanceAfterStakedMigrate = await getERC20Balance(client, noREPToken, client.account.address) - assert.strictEqual(client1InvalidREPBalanceAfterStakedMigrate, client1REPBalance + REP_BOND, "staked REP not migrated to invalid as expected") - assert.strictEqual(client1YesREPBalanceAfterStakedMigrate, client1REPBalance + REP_BOND * 2n, "staked REP not migrated to yes as expected") - assert.strictEqual(client1NoREPBalanceAfterStakedMigrate, client1REPBalance + REP_BOND, "staked REP not migrated to no as expected") + const preForkUniverseData = await getUniverseData(client, genesisUniverse) + const genesisRepToken = getRepTokenAddress(genesisUniverse) + const totalTheoreticalSupply = await getTotalTheoreticalSupply(client, genesisRepToken) + assert.strictEqual(preForkUniverseData.forkTime, 0n, 'Universe was forked already') + assert.strictEqual(preForkUniverseData.parentUniverseId, 0n, 'Universe had parent') + assert.strictEqual(preForkUniverseData.forkingOutcomeIndex, 0n, 'Universe has forking outcome index') + assert.strictEqual(preForkUniverseData.reputationToken, genesisRepToken, 'Universe had parent') + const priorRepbalance = await getERC20Balance(client, genesisRepToken, client.account.address) + + // do fork + await forkUniverse(client, genesisUniverse, marketText, outcomes) + const afterforkBalance = await getERC20Balance(client, genesisRepToken, client.account.address) + assert.strictEqual(afterforkBalance + totalTheoreticalSupply/20n, priorRepbalance, 'balance mismatch') + const universeData = await getUniverseData(client, genesisUniverse) + assert.ok(universeData.forkTime > 0, 'Universe was supposed to be forked') + assert.strictEqual(universeData.parentUniverseId, 0n, 'Universe had parent') + assert.strictEqual(universeData.forkingOutcomeIndex, 0n, 'Universe has forking outcome index') + assert.strictEqual(universeData.reputationToken, genesisRepToken, 'Wrong rep token') + const universeForkData = await getUniverseForkData(client, genesisUniverse) + assert.strictEqual(universeForkData.forkedBy, client.account.address, 'We should have been the forker') + const forkerDeposit = totalTheoreticalSupply / 20n - totalTheoreticalSupply / 20n / 5n // 5% of supply minus 20% burn + assert.strictEqual(universeForkData.forkerRepDeposit, forkerDeposit, 'wrong deposit amount') + assert.strictEqual(universeForkData.forkingQuestionExtraInfo, marketText, 'Market text did not match') + assert.ok(areEqualArrays([...universeForkData.categories], [...outcomes]), 'Outcomes did not match') + assert.strictEqual(await getERC20Balance(client, genesisRepToken, zoltar), forkerDeposit, 'forkers deposit should be in zoltar') + + // forker claim balance + const outcomeIndices = [0n, 1n, 3n] + await forkerClaimRep(client, genesisUniverse, outcomeIndices) + assert.strictEqual(await getERC20Balance(client, genesisRepToken, zoltar), 0n, 'forkers deposit should burned') + const universeForkDataAfterClaim = await getUniverseForkData(client, genesisUniverse) + assert.strictEqual(universeForkDataAfterClaim.forkerRepDeposit, 0n, 'deposit is gone') + for (const indice of outcomeIndices) { + const indiceUniverse = getChildUniverseId(genesisUniverse, indice) + const repForIndice = getRepTokenAddress(indiceUniverse) + assert.ok(await contractExists(client, repForIndice), `rep token for indice ${ indice } exist`); + const ourBalance = await getERC20Balance(client, repForIndice, client.account.address) + assert.strictEqual(ourBalance, forkerDeposit) + } + + // split rest of the rep + const splitOutcomeIndixes = [0n, 1n, 2n] + const priorBalances = await Promise.all(splitOutcomeIndixes.map(async (indice) => { + const indiceUniverse = getChildUniverseId(genesisUniverse, indice) + const repForIndice = getRepTokenAddress(indiceUniverse) + return await contractExists(client, repForIndice) ? await getERC20Balance(client, repForIndice, client.account.address) : 0n + })) + const priorSplitBalance = await getERC20Balance(client, genesisRepToken, client.account.address) + await splitRep(client, genesisUniverse, splitOutcomeIndixes) + assert.strictEqual(await getERC20Balance(client, genesisRepToken, client.account.address), 0n, 'splitters rep should be gone') + for (const [index, indice] of splitOutcomeIndixes.entries()) { + const indiceUniverse = getChildUniverseId(genesisUniverse, indice) + const repForIndice = getRepTokenAddress(indiceUniverse) + assert.ok(await contractExists(client, repForIndice), `rep token for indice ${ indice } exist`); + const ourBalance = await getERC20Balance(client, repForIndice, client.account.address) + assert.strictEqual(ourBalance, priorSplitBalance + priorBalances[index], 'after split balance mismatch') + } }) }) diff --git a/solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts b/solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts new file mode 100644 index 0000000..9720b4d --- /dev/null +++ b/solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts @@ -0,0 +1,137 @@ +import { mainnet } from "viem/chains" +import { ReputationToken_ReputationToken, Zoltar_Zoltar } from "../../../../types/contractArtifact.js" +import { ReadClient, WriteClient } from "../viem.js" +import { GENESIS_REPUTATION_TOKEN, PROXY_DEPLOYER_ADDRESS } from "../constants.js" +import { encodeDeployData, getAddress, getContractAddress, getCreate2Address, keccak256, numberToBytes } from "viem" +import { addressString, bytes32String } from "../bigint.js" +import { ensureProxyDeployerDeployed } from "../utilities.js" + +export function getZoltarAddress() { + const bytecode: `0x${ string }` = `0x${ Zoltar_Zoltar.evm.bytecode.object }` + return getContractAddress({ bytecode, from: addressString(PROXY_DEPLOYER_ADDRESS), opcode: 'CREATE2', salt: numberToBytes(0) }) +} + +export const isZoltarDeployed = async (client: ReadClient) => { + const expectedDeployedBytecode: `0x${ string }` = `0x${ Zoltar_Zoltar.evm.deployedBytecode.object }` + const address = getZoltarAddress() + const deployedBytecode = await client.getCode({ address }) + return deployedBytecode === expectedDeployedBytecode +} + +export const deployZoltarTransaction = () => { + const bytecode: `0x${ string }` = `0x${ Zoltar_Zoltar.evm.bytecode.object }` + return { to: addressString(PROXY_DEPLOYER_ADDRESS), data: bytecode } as const +} + +export const ensureZoltarDeployed = async (client: WriteClient) => { + await ensureProxyDeployerDeployed(client) + if (await isZoltarDeployed(client)) return + const hash = await client.sendTransaction(deployZoltarTransaction()) + await client.waitForTransactionReceipt({ hash }) +} + +export const getUniverseData = async (client: ReadClient, universeId: bigint) => { + const universeData = await client.readContract({ + abi: Zoltar_Zoltar.abi, + functionName: 'universes', + address: getZoltarAddress(), + args: [universeId] + }) + const [forkTime, reputationToken, parentUniverseId, forkingOutcomeIndex] = universeData + return { forkTime, reputationToken, parentUniverseId, forkingOutcomeIndex: BigInt(forkingOutcomeIndex) } +} + +export const getUniverseForkData = async (client: ReadClient, universeId: bigint) => { + const universeForkData = await client.readContract({ + abi: Zoltar_Zoltar.abi, + functionName: 'universeForkData', + address: getZoltarAddress(), + args: [universeId] + }) + const categories = await client.readContract({ + abi: Zoltar_Zoltar.abi, + functionName: 'getForkingQuestionCategories', + address: getZoltarAddress(), + args: [universeId] + }) + const [forkingQuestionExtraInfo, forkedBy, forkerRepDeposit] = universeForkData + return { forkingQuestionExtraInfo, forkedBy, forkerRepDeposit, categories } +} + +export const forkUniverse = async (client: WriteClient, universeId: bigint, extraInfo: string, questionCategories: readonly [string, string, string, string]) => { + return await client.writeContract({ + chain: mainnet, + abi: Zoltar_Zoltar.abi, + functionName: 'forkUniverse', + address: getZoltarAddress(), + args: [universeId, extraInfo, questionCategories] + }) +} + +export const splitRep = async (client: WriteClient, universeId: bigint, outcomeIndexes: bigint[]) => { + return await client.writeContract({ + abi: Zoltar_Zoltar.abi, + functionName: 'splitRep', + address: getZoltarAddress(), + args: [universeId, outcomeIndexes.map((index) => Number(index))] + }) +} + +export const deployChild = async (client: WriteClient, universeId: bigint, outcomeIndex: bigint) => { + return await client.writeContract({ + chain: mainnet, + abi: Zoltar_Zoltar.abi, + functionName: 'deployChild', + address: getZoltarAddress(), + args: [universeId, Number(outcomeIndex)] + }) +} + +export const getOutcomeName = async (client: ReadClient, universeId: bigint) => { + return await client.readContract({ + abi: Zoltar_Zoltar.abi, + functionName: 'getOutcomeName', + address: getZoltarAddress(), + args: [universeId] + }) +} + +export async function getTotalTheoreticalSupply(client: ReadClient, repToken: `0x${ string }`) { + return await client.readContract({ + abi: ReputationToken_ReputationToken.abi, + functionName: 'getTotalTheoreticalSupply', + address: repToken, + args: [] + }) +} + +export const forkerClaimRep = async (client: WriteClient, universeId: bigint, outcomeIndices: bigint[]) => { + return await client.writeContract({ + chain: mainnet, + abi: Zoltar_Zoltar.abi, + functionName: 'forkerClaimRep', + address: getZoltarAddress(), + args: [universeId, outcomeIndices.map((x) => Number(x))] + }) +} + +export const getZoltarForkTreshold = async (client: ReadClient, universeId: bigint) => { + return await client.readContract({ + abi: Zoltar_Zoltar.abi, + functionName: 'getForkTreshold', + address: getZoltarAddress(), + args: [universeId] + }) +} + + +export function getRepTokenAddress(universeId: bigint): `0x${ string }` { + if (universeId === 0n) return getAddress(addressString(GENESIS_REPUTATION_TOKEN)) + const initCode = encodeDeployData({ + abi: ReputationToken_ReputationToken.abi, + bytecode: `0x${ ReputationToken_ReputationToken.evm.bytecode.object }`, + args: [getZoltarAddress()] + }) + return getCreate2Address({ from: getZoltarAddress(), salt: bytes32String(universeId), bytecodeHash: keccak256(initCode) }) +} + From df6c0fe1b5ca30ad5639616d689576de9a8954d0 Mon Sep 17 00:00:00 2001 From: KillariDev Date: Fri, 13 Feb 2026 14:21:14 +0200 Subject: [PATCH 2/5] fix issues found --- solidity/ts/tests/test.ts | 12 ++++++------ .../testsuite/simulator/utils/contracts/zoltar.ts | 13 ------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/solidity/ts/tests/test.ts b/solidity/ts/tests/test.ts index 68e552d..f957600 100644 --- a/solidity/ts/tests/test.ts +++ b/solidity/ts/tests/test.ts @@ -47,7 +47,7 @@ describe('Contract Test Suite', () => { assert.strictEqual(preForkUniverseData.forkTime, 0n, 'Universe was forked already') assert.strictEqual(preForkUniverseData.parentUniverseId, 0n, 'Universe had parent') assert.strictEqual(preForkUniverseData.forkingOutcomeIndex, 0n, 'Universe has forking outcome index') - assert.strictEqual(preForkUniverseData.reputationToken, genesisRepToken, 'Universe had parent') + assert.strictEqual(preForkUniverseData.reputationToken, genesisRepToken, 'Universe reputation token mismatch') const priorRepbalance = await getERC20Balance(client, genesisRepToken, client.account.address) // do fork @@ -70,7 +70,7 @@ describe('Contract Test Suite', () => { // forker claim balance const outcomeIndices = [0n, 1n, 3n] await forkerClaimRep(client, genesisUniverse, outcomeIndices) - assert.strictEqual(await getERC20Balance(client, genesisRepToken, zoltar), 0n, 'forkers deposit should burned') + assert.strictEqual(await getERC20Balance(client, genesisRepToken, zoltar), 0n, 'forkers deposit should be burned') const universeForkDataAfterClaim = await getUniverseForkData(client, genesisUniverse) assert.strictEqual(universeForkDataAfterClaim.forkerRepDeposit, 0n, 'deposit is gone') for (const indice of outcomeIndices) { @@ -82,16 +82,16 @@ describe('Contract Test Suite', () => { } // split rest of the rep - const splitOutcomeIndixes = [0n, 1n, 2n] - const priorBalances = await Promise.all(splitOutcomeIndixes.map(async (indice) => { + const splitOutcomeIndices = [0n, 1n, 2n] + const priorBalances = await Promise.all(splitOutcomeIndices.map(async (indice) => { const indiceUniverse = getChildUniverseId(genesisUniverse, indice) const repForIndice = getRepTokenAddress(indiceUniverse) return await contractExists(client, repForIndice) ? await getERC20Balance(client, repForIndice, client.account.address) : 0n })) const priorSplitBalance = await getERC20Balance(client, genesisRepToken, client.account.address) - await splitRep(client, genesisUniverse, splitOutcomeIndixes) + await splitRep(client, genesisUniverse, splitOutcomeIndices) assert.strictEqual(await getERC20Balance(client, genesisRepToken, client.account.address), 0n, 'splitters rep should be gone') - for (const [index, indice] of splitOutcomeIndixes.entries()) { + for (const [index, indice] of splitOutcomeIndices.entries()) { const indiceUniverse = getChildUniverseId(genesisUniverse, indice) const repForIndice = getRepTokenAddress(indiceUniverse) assert.ok(await contractExists(client, repForIndice), `rep token for indice ${ indice } exist`); diff --git a/solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts b/solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts index 9720b4d..b76553a 100644 --- a/solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts +++ b/solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts @@ -60,7 +60,6 @@ export const getUniverseForkData = async (client: ReadClient, universeId: bigint export const forkUniverse = async (client: WriteClient, universeId: bigint, extraInfo: string, questionCategories: readonly [string, string, string, string]) => { return await client.writeContract({ - chain: mainnet, abi: Zoltar_Zoltar.abi, functionName: 'forkUniverse', address: getZoltarAddress(), @@ -79,7 +78,6 @@ export const splitRep = async (client: WriteClient, universeId: bigint, outcomeI export const deployChild = async (client: WriteClient, universeId: bigint, outcomeIndex: bigint) => { return await client.writeContract({ - chain: mainnet, abi: Zoltar_Zoltar.abi, functionName: 'deployChild', address: getZoltarAddress(), @@ -107,7 +105,6 @@ export async function getTotalTheoreticalSupply(client: ReadClient, repToken: `0 export const forkerClaimRep = async (client: WriteClient, universeId: bigint, outcomeIndices: bigint[]) => { return await client.writeContract({ - chain: mainnet, abi: Zoltar_Zoltar.abi, functionName: 'forkerClaimRep', address: getZoltarAddress(), @@ -115,16 +112,6 @@ export const forkerClaimRep = async (client: WriteClient, universeId: bigint, ou }) } -export const getZoltarForkTreshold = async (client: ReadClient, universeId: bigint) => { - return await client.readContract({ - abi: Zoltar_Zoltar.abi, - functionName: 'getForkTreshold', - address: getZoltarAddress(), - args: [universeId] - }) -} - - export function getRepTokenAddress(universeId: bigint): `0x${ string }` { if (universeId === 0n) return getAddress(addressString(GENESIS_REPUTATION_TOKEN)) const initCode = encodeDeployData({ From 9fa7f05795df572fa3f6042564a34ad3dff07767 Mon Sep 17 00:00:00 2001 From: KillariDev Date: Mon, 23 Feb 2026 15:57:33 +0200 Subject: [PATCH 3/5] fix according --- solidity/contracts/Zoltar.sol | 265 +++++++----------- solidity/ts/tests/test.ts | 38 +-- .../simulator/utils/contracts/zoltar.ts | 5 +- .../simulator/utils/deployPeripherals.ts | 2 - .../testsuite/simulator/utils/deployments.ts | 2 +- .../ts/testsuite/simulator/utils/utilities.ts | 2 +- 6 files changed, 121 insertions(+), 193 deletions(-) diff --git a/solidity/contracts/Zoltar.sol b/solidity/contracts/Zoltar.sol index ca5b4fe..2d6ed8b 100644 --- a/solidity/contracts/Zoltar.sol +++ b/solidity/contracts/Zoltar.sol @@ -1,207 +1,138 @@ -// SPDX-License-Identifier: UNICENSE +// SPDX-License-Identifier: Unlicense pragma solidity 0.8.33; import './Constants.sol'; import './ReputationToken.sol'; -contract Zoltar { +uint256 constant FORK_TRESHOLD_DIVISOR = 20; // TODO, revisit, 5% of total supply atm +uint256 constant FORK_BURN_DIVISOR = 5; // TODO, revisit, 20% of fork treshold +contract Zoltar { struct Universe { - ReputationToken reputationToken; - uint56 forkingQuestion; uint256 forkTime; + ReputationToken reputationToken; + uint248 parentUniverseId; + uint8 forkingOutcomeIndex; } - mapping(uint192 => Universe) public universes; - - struct QuestionData { - uint64 endTime; - uint192 originUniverse; - address designatedReporter; - string extraInfo; - } - - struct QuestionResolutionData { - address initialReporter; - Outcome outcome; - uint64 reportTime; - bool finalized; - } + mapping(uint248 => Universe) public universes; - enum Outcome { - Invalid, - Yes, - No, - None + struct UniverseForkData { + string forkingQuestionExtraInfo; + address forkedBy; + uint256 forkerRepDeposit; + string[4] forkingQuestionCategories; } - mapping(uint56 => QuestionData) public questions; - - // UniverseId => QuestionId => Data - mapping(uint192 => mapping(uint56 => QuestionResolutionData)) questionResolutions; - - uint56 questionIdCounter = 0; - - // TODO: Revist what behavior the bond should be - uint256 constant public REP_BOND = 1 ether; + mapping(uint248 => UniverseForkData) public universeForkData; - uint256 constant public DESIGNATED_REPORTING_TIME = 3 days; - uint256 constant public DISPUTE_PERIOD = 1 days; + event UniverseForked(address forker, uint248 universeId, string extraInfo, string[4] questionCategories); + event DeployChild(address deployer, uint248 universeId, uint8 outcomeIndex, uint248 childUniverseId, ReputationToken childReputationToken); + event SplitRep(uint248 universeId, uint256 amount, address migrator, address recipient, uint8[] outcomeIndices); + event ForkerClaimRep(address forker, uint248 universeId, uint8[] outcomeIndices, uint256 forkerRepDeposit); - constructor() { - universes[0] = Universe( - ReputationToken(Constants.GENESIS_REPUTATION_TOKEN), - 0, - 0 - ); - } - - function isQuestionLegit(uint192 _universeId, uint56 _questionId) public view returns (bool) { - QuestionData memory questionData = questions[_questionId]; - require(questionData.endTime != 0, "Question is not valid"); - - if (questionData.originUniverse == _universeId) return true; - - Universe memory universeData = universes[_universeId]; - require(address(universeData.reputationToken) != address(0), "Universe is not valid"); - - do { - _universeId >>= 2; - // If a parent didn't fork this wouldn't be a valid universe - Universe memory curUniverseData = universes[_universeId]; - if (curUniverseData.forkTime == 0) return false; - - // A resolved question cannot have children, as a question in a forked universe does not get resolved there - QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId]; - if (questionResolutionDataIsFinalized(questionResolutionData)) return false; - - // If other checks passed and the ids are equal its a legitimate child. If this never gets reached it isn't. - if (questionData.originUniverse == _universeId) return true; - } while (_universeId > 0); - - return false; - } - - function createQuestion(uint192 _universeId, uint64 _endTime, address _designatedReporterAddress, string memory _extraInfo) public returns (uint56 _questionId) { - Universe memory universe = universes[_universeId]; - require(universe.forkingQuestion == 0, "Universe is forked"); - universe.reputationToken.transferFrom(msg.sender, address(this), REP_BOND); - _questionId = ++questionIdCounter; - questions[_questionId] = QuestionData( - _endTime, - _universeId, - _designatedReporterAddress, - _extraInfo - ); - } - - function reportOutcome(uint192 _universeId, uint56 _questionId, Outcome _outcome) external { - Universe memory universe = universes[_universeId]; - require(universe.forkingQuestion == 0, "Universe is forked"); - QuestionData memory questionData = questions[_questionId]; - QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId]; - require(questionResolutionData.reportTime == 0, "Question already has a report"); - require(_outcome != Outcome.None, "Invalid outcome"); - require(block.timestamp > questionData.endTime, "Question has not ended"); - require(msg.sender == questionData.designatedReporter || block.timestamp > questionData.endTime + DESIGNATED_REPORTING_TIME, "Reporter must be designated reporter"); - - questionResolutions[_universeId][_questionId].initialReporter = msg.sender; - questionResolutions[_universeId][_questionId].outcome = _outcome; - questionResolutions[_universeId][_questionId].reportTime = uint64(block.timestamp); - } - - function finalizeQuestion(uint192 _universeId, uint56 _questionId) external returns (Outcome) { - Universe memory universe = universes[_universeId]; - QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId]; - if (!questionResolutionData.finalized) { - require(questionResolutionDataIsFinalized(questionResolutionData), "Cannot withdraw REP bond before finalized"); - questionResolutionData.finalized = true; - questionResolutions[_universeId][_questionId] = questionResolutionData; - universe.reputationToken.transfer(questionResolutionData.initialReporter, REP_BOND); - } - return questionResolutionData.outcome; + function getForkTime(uint248 universeId) external view returns (uint256) { + Universe memory universe = universes[universeId]; + return universe.forkTime; } - function splitStakedRep(uint192 _universeId, uint56 _questionId) external { - QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId]; - require(questionResolutionData.reportTime != 0, "No REP staked in this question"); - require(!questionResolutionDataIsFinalized(questionResolutionData), "Cannot migrate REP from finalized question"); - - splitRepInternal(_universeId, REP_BOND, address(this), questionResolutionData.initialReporter, Outcome.None); + function getForkingQuestionCategories(uint248 universeId) external view returns (string[4] memory) { + return universeForkData[universeId].forkingQuestionCategories; } - function isFinalized(uint192 _universeId, uint56 _questionId) external view returns (bool) { - QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId]; - if (questionResolutionData.finalized) return true; - return questionResolutionDataIsFinalized(questionResolutionData); + function getRepToken(uint248 universeId) external view returns (ReputationToken) { + Universe memory universe = universes[universeId]; + return universe.reputationToken; } - - function questionResolutionDataIsFinalized(QuestionResolutionData memory questionResolutionData) internal view returns (bool) { - return questionResolutionData.reportTime != 0 && block.timestamp > questionResolutionData.reportTime + DISPUTE_PERIOD; + function getForkedBy(uint248 universeId) external view returns (address) { + UniverseForkData memory forkData = universeForkData[universeId]; + return forkData.forkedBy; } - - function getWinningOutcome(uint192 _universeId, uint56 _questionId) public view returns (Outcome) { - QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId]; - require(questionResolutionDataIsFinalized(questionResolutionData), "Question is not finalized"); - - return questionResolutionData.outcome; + function getForkerDeposit(uint248 universeId) external view returns (uint256) { + UniverseForkData memory forkData = universeForkData[universeId]; + return forkData.forkerRepDeposit; } - // TODO: Currently escalation game is a single dispute. Likely will be more complex. - function dispute(uint192 _universeId, uint56 _questionId, Outcome _outcome) external { - Universe memory universe = universes[_universeId]; - require(universe.forkingQuestion == 0, "Universe is forked"); - QuestionResolutionData memory questionResolutionData = questionResolutions[_universeId][_questionId]; - require(_outcome != questionResolutionData.outcome, "Dispute must be for a different outcome than the currently winning one"); - require(block.timestamp < questionResolutionData.reportTime + DISPUTE_PERIOD, "Question not in dispute window"); - require(_outcome != Outcome.None, "Invalid outcome"); - - uint256 disputeStake = REP_BOND * 2; - - for (uint8 i = 1; i < Constants.NUM_OUTCOMES + 1; i++) { - uint192 childUniverseId = (_universeId << 2) + i; - universes[childUniverseId] = Universe(new ReputationToken{ salt: bytes32(uint256(childUniverseId)) }(address(this)), 0, 0); - - questionResolutions[childUniverseId][_questionId].reportTime = 1; - questionResolutions[childUniverseId][_questionId].outcome = Outcome(i - 1); - questionResolutions[childUniverseId][_questionId].finalized = true; - } + constructor() { + universes[0] = Universe(0, ReputationToken(Constants.GENESIS_REPUTATION_TOKEN), 0, 0); + } - universe.forkingQuestion = _questionId; - universe.forkTime = block.timestamp; - universes[_universeId] = universe; + function getForkTreshold(uint248 universeId) public view returns (uint256) { + Universe memory universe = universes[universeId]; + return universe.reputationToken.getTotalTheoreticalSupply() / FORK_TRESHOLD_DIVISOR; + } - splitRepInternal(_universeId, REP_BOND, questionResolutionData.initialReporter, questionResolutionData.initialReporter, questionResolutionData.outcome); - splitRepInternal(_universeId, disputeStake, msg.sender, msg.sender, _outcome); + function forkUniverse(uint248 universeId, string memory _extraInfo, string[4] memory _questionCategories) public { + Universe memory universe = universes[universeId]; + require(universe.forkTime == 0, 'Universe has forked already'); + require(_questionCategories.length >= 1, 'need atleast one category on top of invalid'); + universes[universeId].forkTime = block.timestamp; + uint256 forkTreshold = getForkTreshold(universeId); + universeForkData[universeId] = UniverseForkData(_extraInfo, msg.sender, forkTreshold - forkTreshold / FORK_BURN_DIVISOR, _questionCategories); + universes[universeId].reputationToken.transferFrom(msg.sender, address(this), forkTreshold); + burnRep(universes[universeId].reputationToken, address(this), forkTreshold / FORK_BURN_DIVISOR); // burn 20% + emit UniverseForked(msg.sender, universeId, _extraInfo, _questionCategories); } - function splitRep(uint192 universeId) public { + function splitRep(uint248 universeId, uint8[] memory outcomeIndexes) public { uint256 amount = universes[universeId].reputationToken.balanceOf(msg.sender); - splitRepInternal(universeId, amount, msg.sender, msg.sender, Outcome.None); + splitRepInternal(universeId, amount, msg.sender, msg.sender, outcomeIndexes); } - // singleOutcome will only credit the provided outcome if it is a valid outcome, else all child universe REP will be minted - function splitRepInternal(uint192 universeId, uint256 amount, address migrator, address recipient, Outcome singleOutcome) private { - Universe memory universe = universes[universeId]; - require(universe.forkTime != 0, "Universe has not forked"); - + function burnRep(ReputationToken reputationToken, address migrator, uint256 amount) private { // Genesis is using REPv2 which we cannot actually burn - if (universeId == 0) { + if (address(reputationToken) == Constants.GENESIS_REPUTATION_TOKEN) { if (migrator == address(this)) { - universe.reputationToken.transfer(Constants.BURN_ADDRESS, amount); + reputationToken.transfer(Constants.BURN_ADDRESS, amount); } else { - universe.reputationToken.transferFrom(migrator, Constants.BURN_ADDRESS, amount); + reputationToken.transferFrom(migrator, Constants.BURN_ADDRESS, amount); } } else { - ReputationToken(address(universe.reputationToken)).burn(migrator, amount); + ReputationToken(address(reputationToken)).burn(migrator, amount); } + } - for (uint8 i = 1; i < Constants.NUM_OUTCOMES + 1; i++) { - if (singleOutcome != Outcome.None && i != uint8(singleOutcome) + 1) continue; - uint192 childUniverseId = (universeId << 2) + i; - Universe memory childUniverse = universes[childUniverseId]; - ReputationToken(address(childUniverse.reputationToken)).mint(recipient, amount); - } + function getOutcomeName(uint248 universeId) external view returns (string memory) { + if (universeId == 0) return 'Genesis'; + Universe memory universe = universes[universeId]; + if (universe.forkingOutcomeIndex == 0) return 'Invalid'; + return universeForkData[universe.parentUniverseId].forkingQuestionCategories[universe.forkingOutcomeIndex - 1]; + } + + function getChildUniverseId(uint248 universeId, uint8 outcomeIndex) public pure returns (uint248) { + return uint248(uint256(keccak256(abi.encode(universeId, outcomeIndex)))); + } + + function deployChild(uint248 universeId, uint8 outcomeIndex) public { + Universe memory universe = universes[universeId]; + require(universe.forkTime != 0, 'Universe has not forked'); + uint248 childUniverseId = getChildUniverseId(universeId, outcomeIndex); + ReputationToken childReputationToken = new ReputationToken{ salt: bytes32(uint256(childUniverseId)) }(address(this)); + childReputationToken.setMaxTheoreticalSupply(universe.reputationToken.getTotalTheoreticalSupply()); + universes[childUniverseId] = Universe(0, childReputationToken, universeId, outcomeIndex); + emit DeployChild(msg.sender, universeId, outcomeIndex, childUniverseId, childReputationToken); + } + function forkerClaimRep(uint248 universeId, uint8[] memory outcomeIndices) public { + UniverseForkData memory data = universeForkData[universeId]; + require(data.forkedBy == msg.sender, 'only forker can claim'); + universeForkData[universeId].forkerRepDeposit = 0; + emit ForkerClaimRep(msg.sender, universeId, outcomeIndices, data.forkerRepDeposit); + splitRepInternal(universeId, data.forkerRepDeposit, address(this), data.forkedBy, outcomeIndices); + } + + function splitRepInternal(uint248 universeId, uint256 amount, address migrator, address recipient, uint8[] memory outcomeIndices) private { + Universe memory universe = universes[universeId]; + require(universe.forkTime != 0, 'Universe has not forked'); + emit SplitRep(universeId, amount, migrator, recipient, outcomeIndices); + burnRep(universe.reputationToken, migrator, amount); + for (uint8 i = 0; i < outcomeIndices.length; i++) { + require(i == 0 || outcomeIndices[i] > outcomeIndices[i - 1], 'outcomes are not sorted'); // force sorting to avoid duplicate indices + require(outcomeIndices[i] < universeForkData[universeId].forkingQuestionCategories.length + 1, 'outcome index overflow'); + uint248 childUniverseId = getChildUniverseId(universeId, outcomeIndices[i]); + if (address(universes[childUniverseId].reputationToken) == address(0x0)) deployChild(universeId, outcomeIndices[i]); + universes[childUniverseId].reputationToken.mint(recipient, amount); + } } } + diff --git a/solidity/ts/tests/test.ts b/solidity/ts/tests/test.ts index f957600..9985d96 100644 --- a/solidity/ts/tests/test.ts +++ b/solidity/ts/tests/test.ts @@ -27,7 +27,7 @@ describe('Contract Test Suite', () => { assert.ok(isDeployed, `Not Deployed!`) const genesisUniverseData = await getUniverseData(client, 0n) - assert.strictEqual(genesisUniverseData.reputationToken.toLowerCase(), addressString(GENESIS_REPUTATION_TOKEN), 'Genesis universe not recognized or not initialized properly') + assert.strictEqual(genesisUniverseData.reputationToken, GENESIS_REPUTATION_TOKEN, 'Genesis universe not recognized or not initialized properly') }) test('canForkQuestion', async () => { @@ -68,34 +68,34 @@ describe('Contract Test Suite', () => { assert.strictEqual(await getERC20Balance(client, genesisRepToken, zoltar), forkerDeposit, 'forkers deposit should be in zoltar') // forker claim balance - const outcomeIndices = [0n, 1n, 3n] - await forkerClaimRep(client, genesisUniverse, outcomeIndices) + const outcomeIndexes = [0n, 1n, 3n] + await forkerClaimRep(client, genesisUniverse, outcomeIndexes) assert.strictEqual(await getERC20Balance(client, genesisRepToken, zoltar), 0n, 'forkers deposit should be burned') const universeForkDataAfterClaim = await getUniverseForkData(client, genesisUniverse) assert.strictEqual(universeForkDataAfterClaim.forkerRepDeposit, 0n, 'deposit is gone') - for (const indice of outcomeIndices) { - const indiceUniverse = getChildUniverseId(genesisUniverse, indice) - const repForIndice = getRepTokenAddress(indiceUniverse) - assert.ok(await contractExists(client, repForIndice), `rep token for indice ${ indice } exist`); - const ourBalance = await getERC20Balance(client, repForIndice, client.account.address) + for (const index of outcomeIndexes) { + const indexUniverse = getChildUniverseId(genesisUniverse, index) + const repForIndex = getRepTokenAddress(indexUniverse) + assert.ok(await contractExists(client, repForIndex), `rep token for index ${ index } exist`); + const ourBalance = await getERC20Balance(client, repForIndex, client.account.address) assert.strictEqual(ourBalance, forkerDeposit) } // split rest of the rep - const splitOutcomeIndices = [0n, 1n, 2n] - const priorBalances = await Promise.all(splitOutcomeIndices.map(async (indice) => { - const indiceUniverse = getChildUniverseId(genesisUniverse, indice) - const repForIndice = getRepTokenAddress(indiceUniverse) - return await contractExists(client, repForIndice) ? await getERC20Balance(client, repForIndice, client.account.address) : 0n + const splitOutcomeIndexes = [0n, 1n, 2n] + const priorBalances = await Promise.all(splitOutcomeIndexes.map(async (index) => { + const indiceUniverse = getChildUniverseId(genesisUniverse, index) + const repForIndex = getRepTokenAddress(indiceUniverse) + return await contractExists(client, repForIndex) ? await getERC20Balance(client, repForIndex, client.account.address) : 0n })) const priorSplitBalance = await getERC20Balance(client, genesisRepToken, client.account.address) - await splitRep(client, genesisUniverse, splitOutcomeIndices) + await splitRep(client, genesisUniverse, splitOutcomeIndexes) assert.strictEqual(await getERC20Balance(client, genesisRepToken, client.account.address), 0n, 'splitters rep should be gone') - for (const [index, indice] of splitOutcomeIndices.entries()) { - const indiceUniverse = getChildUniverseId(genesisUniverse, indice) - const repForIndice = getRepTokenAddress(indiceUniverse) - assert.ok(await contractExists(client, repForIndice), `rep token for indice ${ indice } exist`); - const ourBalance = await getERC20Balance(client, repForIndice, client.account.address) + for (const [index, outcomeIndex] of splitOutcomeIndexes.entries()) { + const indiceUniverse = getChildUniverseId(genesisUniverse, outcomeIndex) + const repForIndex = getRepTokenAddress(indiceUniverse) + assert.ok(await contractExists(client, repForIndex), `rep token for index ${ outcomeIndex } exist`); + const ourBalance = await getERC20Balance(client, repForIndex, client.account.address) assert.strictEqual(ourBalance, priorSplitBalance + priorBalances[index], 'after split balance mismatch') } }) diff --git a/solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts b/solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts index b76553a..8e7822f 100644 --- a/solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts +++ b/solidity/ts/testsuite/simulator/utils/contracts/zoltar.ts @@ -1,4 +1,3 @@ -import { mainnet } from "viem/chains" import { ReputationToken_ReputationToken, Zoltar_Zoltar } from "../../../../types/contractArtifact.js" import { ReadClient, WriteClient } from "../viem.js" import { GENESIS_REPUTATION_TOKEN, PROXY_DEPLOYER_ADDRESS } from "../constants.js" @@ -103,12 +102,12 @@ export async function getTotalTheoreticalSupply(client: ReadClient, repToken: `0 }) } -export const forkerClaimRep = async (client: WriteClient, universeId: bigint, outcomeIndices: bigint[]) => { +export const forkerClaimRep = async (client: WriteClient, universeId: bigint, outcomeindexes: bigint[]) => { return await client.writeContract({ abi: Zoltar_Zoltar.abi, functionName: 'forkerClaimRep', address: getZoltarAddress(), - args: [universeId, outcomeIndices.map((x) => Number(x))] + args: [universeId, outcomeindexes.map((x) => Number(x))] }) } diff --git a/solidity/ts/testsuite/simulator/utils/deployPeripherals.ts b/solidity/ts/testsuite/simulator/utils/deployPeripherals.ts index a7a9f53..f70cc41 100644 --- a/solidity/ts/testsuite/simulator/utils/deployPeripherals.ts +++ b/solidity/ts/testsuite/simulator/utils/deployPeripherals.ts @@ -4,7 +4,6 @@ import { WriteClient } from './viem.js' import { PROXY_DEPLOYER_ADDRESS } from './constants.js' import { addressString } from './bigint.js' import { contractExists, getRepTokenAddress, getZoltarAddress } from './utilities.js' -import { mainnet } from 'viem/chains' import { peripherals_Auction_Auction, peripherals_factories_AuctionFactory_AuctionFactory, peripherals_factories_PriceOracleManagerAndOperatorQueuerFactory_PriceOracleManagerAndOperatorQueuerFactory, peripherals_factories_SecurityPoolFactory_SecurityPoolFactory, peripherals_factories_ShareTokenFactory_ShareTokenFactory, peripherals_openOracle_OpenOracle_OpenOracle, peripherals_PriceOracleManagerAndOperatorQueuer_PriceOracleManagerAndOperatorQueuer, peripherals_SecurityPool_SecurityPool, peripherals_SecurityPoolUtils_SecurityPoolUtils, peripherals_tokens_ShareToken_ShareToken, Zoltar_Zoltar } from '../../../types/contractArtifact.js' export function getSecurityPoolUtilsAddress() { @@ -163,7 +162,6 @@ export const getSecurityPoolAddresses = (parent: `0x${ string }`, universeId: bi export const deployOriginSecurityPool = async (client: WriteClient, universeId: bigint, questionId: bigint, securityMultiplier: bigint, startingRetentionRate: bigint, startingRepEthPrice: bigint, completeSetCollateralAmount: bigint) => { const infraAddresses = getInfraContractAddresses() return await client.writeContract({ - chain: mainnet, abi: peripherals_factories_SecurityPoolFactory_SecurityPoolFactory.abi, functionName: 'deployOriginSecurityPool', address: infraAddresses.securityPoolFactory, diff --git a/solidity/ts/testsuite/simulator/utils/deployments.ts b/solidity/ts/testsuite/simulator/utils/deployments.ts index 2e8c761..0364eef 100644 --- a/solidity/ts/testsuite/simulator/utils/deployments.ts +++ b/solidity/ts/testsuite/simulator/utils/deployments.ts @@ -48,7 +48,7 @@ export const getDeployments = (genesisUniverse: bigint, questionId: bigint, secu const infraAddresses = getInfraContractAddresses() const originAddresses = getSecurityPoolAddresses(zeroAddress, genesisUniverse, questionId, securityMultiplier) - const oucomes = [QuestionOutcome.Invalid, QuestionOutcome.No, QuestionOutcome.Yes] + const oucomes = [0n, 1n, 2n] const getChildAddresses = (parentSecurityPoolAddress: `0x${ string }`, parentUniverseId: bigint): Deployment[] => { return oucomes.flatMap((outcome) => { diff --git a/solidity/ts/testsuite/simulator/utils/utilities.ts b/solidity/ts/testsuite/simulator/utils/utilities.ts index e359afc..f45c599 100644 --- a/solidity/ts/testsuite/simulator/utils/utilities.ts +++ b/solidity/ts/testsuite/simulator/utils/utilities.ts @@ -365,7 +365,7 @@ export const getReportBond = async (client: ReadClient) => { }) } -export function getChildUniverseId(parentUniverseId: bigint, outcome: QuestionOutcome): bigint { +export function getChildUniverseId(parentUniverseId: bigint, outcome: bigint | QuestionOutcome): bigint { return (parentUniverseId << 2n) + BigInt(outcome) + 1n } From 1a40b7a51f2f4dc8ab46f01ea2ef77be18fa7365 Mon Sep 17 00:00:00 2001 From: KillariDev Date: Mon, 23 Feb 2026 16:07:38 +0200 Subject: [PATCH 4/5] fixes --- solidity/contracts/Zoltar.sol | 24 ++++++++++++------------ solidity/ts/tests/test.ts | 16 ++++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/solidity/contracts/Zoltar.sol b/solidity/contracts/Zoltar.sol index 2d6ed8b..24c28cc 100644 --- a/solidity/contracts/Zoltar.sol +++ b/solidity/contracts/Zoltar.sol @@ -28,8 +28,8 @@ contract Zoltar { event UniverseForked(address forker, uint248 universeId, string extraInfo, string[4] questionCategories); event DeployChild(address deployer, uint248 universeId, uint8 outcomeIndex, uint248 childUniverseId, ReputationToken childReputationToken); - event SplitRep(uint248 universeId, uint256 amount, address migrator, address recipient, uint8[] outcomeIndices); - event ForkerClaimRep(address forker, uint248 universeId, uint8[] outcomeIndices, uint256 forkerRepDeposit); + event SplitRep(uint248 universeId, uint256 amount, address migrator, address recipient, uint8[] outcomeIndexes); + event ForkerClaimRep(address forker, uint248 universeId, uint8[] outcomeIndexes, uint256 forkerRepDeposit); function getForkTime(uint248 universeId) external view returns (uint256) { Universe memory universe = universes[universeId]; @@ -113,24 +113,24 @@ contract Zoltar { emit DeployChild(msg.sender, universeId, outcomeIndex, childUniverseId, childReputationToken); } - function forkerClaimRep(uint248 universeId, uint8[] memory outcomeIndices) public { + function forkerClaimRep(uint248 universeId, uint8[] memory outcomeIndexes) public { UniverseForkData memory data = universeForkData[universeId]; require(data.forkedBy == msg.sender, 'only forker can claim'); universeForkData[universeId].forkerRepDeposit = 0; - emit ForkerClaimRep(msg.sender, universeId, outcomeIndices, data.forkerRepDeposit); - splitRepInternal(universeId, data.forkerRepDeposit, address(this), data.forkedBy, outcomeIndices); + emit ForkerClaimRep(msg.sender, universeId, outcomeIndexes, data.forkerRepDeposit); + splitRepInternal(universeId, data.forkerRepDeposit, address(this), data.forkedBy, outcomeIndexes); } - function splitRepInternal(uint248 universeId, uint256 amount, address migrator, address recipient, uint8[] memory outcomeIndices) private { + function splitRepInternal(uint248 universeId, uint256 amount, address migrator, address recipient, uint8[] memory outcomeIndexes) private { Universe memory universe = universes[universeId]; require(universe.forkTime != 0, 'Universe has not forked'); - emit SplitRep(universeId, amount, migrator, recipient, outcomeIndices); + emit SplitRep(universeId, amount, migrator, recipient, outcomeIndexes); burnRep(universe.reputationToken, migrator, amount); - for (uint8 i = 0; i < outcomeIndices.length; i++) { - require(i == 0 || outcomeIndices[i] > outcomeIndices[i - 1], 'outcomes are not sorted'); // force sorting to avoid duplicate indices - require(outcomeIndices[i] < universeForkData[universeId].forkingQuestionCategories.length + 1, 'outcome index overflow'); - uint248 childUniverseId = getChildUniverseId(universeId, outcomeIndices[i]); - if (address(universes[childUniverseId].reputationToken) == address(0x0)) deployChild(universeId, outcomeIndices[i]); + for (uint8 i = 0; i < outcomeIndexes.length; i++) { + require(i == 0 || outcomeIndexes[i] > outcomeIndexes[i - 1], 'outcomes are not sorted'); // force sorting to avoid duplicate indices + require(outcomeIndexes[i] < universeForkData[universeId].forkingQuestionCategories.length + 1, 'outcome index overflow'); + uint248 childUniverseId = getChildUniverseId(universeId, outcomeIndexes[i]); + if (address(universes[childUniverseId].reputationToken) == address(0x0)) deployChild(universeId, outcomeIndexes[i]); universes[childUniverseId].reputationToken.mint(recipient, amount); } } diff --git a/solidity/ts/tests/test.ts b/solidity/ts/tests/test.ts index 9985d96..feedcce 100644 --- a/solidity/ts/tests/test.ts +++ b/solidity/ts/tests/test.ts @@ -52,8 +52,8 @@ describe('Contract Test Suite', () => { // do fork await forkUniverse(client, genesisUniverse, marketText, outcomes) - const afterforkBalance = await getERC20Balance(client, genesisRepToken, client.account.address) - assert.strictEqual(afterforkBalance + totalTheoreticalSupply/20n, priorRepbalance, 'balance mismatch') + const afterForkBalance = await getERC20Balance(client, genesisRepToken, client.account.address) + assert.strictEqual(afterForkBalance + totalTheoreticalSupply/20n, priorRepbalance, 'balance mismatch') const universeData = await getUniverseData(client, genesisUniverse) assert.ok(universeData.forkTime > 0, 'Universe was supposed to be forked') assert.strictEqual(universeData.parentUniverseId, 0n, 'Universe had parent') @@ -76,7 +76,7 @@ describe('Contract Test Suite', () => { for (const index of outcomeIndexes) { const indexUniverse = getChildUniverseId(genesisUniverse, index) const repForIndex = getRepTokenAddress(indexUniverse) - assert.ok(await contractExists(client, repForIndex), `rep token for index ${ index } exist`); + assert.ok(await contractExists(client, repForIndex), `rep token for index ${ index } exists`); const ourBalance = await getERC20Balance(client, repForIndex, client.account.address) assert.strictEqual(ourBalance, forkerDeposit) } @@ -84,17 +84,17 @@ describe('Contract Test Suite', () => { // split rest of the rep const splitOutcomeIndexes = [0n, 1n, 2n] const priorBalances = await Promise.all(splitOutcomeIndexes.map(async (index) => { - const indiceUniverse = getChildUniverseId(genesisUniverse, index) - const repForIndex = getRepTokenAddress(indiceUniverse) + const indexUniverse = getChildUniverseId(genesisUniverse, index) + const repForIndex = getRepTokenAddress(indexUniverse) return await contractExists(client, repForIndex) ? await getERC20Balance(client, repForIndex, client.account.address) : 0n })) const priorSplitBalance = await getERC20Balance(client, genesisRepToken, client.account.address) await splitRep(client, genesisUniverse, splitOutcomeIndexes) assert.strictEqual(await getERC20Balance(client, genesisRepToken, client.account.address), 0n, 'splitters rep should be gone') for (const [index, outcomeIndex] of splitOutcomeIndexes.entries()) { - const indiceUniverse = getChildUniverseId(genesisUniverse, outcomeIndex) - const repForIndex = getRepTokenAddress(indiceUniverse) - assert.ok(await contractExists(client, repForIndex), `rep token for index ${ outcomeIndex } exist`); + const indexUniverse = getChildUniverseId(genesisUniverse, outcomeIndex) + const repForIndex = getRepTokenAddress(indexUniverse) + assert.ok(await contractExists(client, repForIndex), `rep token for index ${ outcomeIndex } exists`); const ourBalance = await getERC20Balance(client, repForIndex, client.account.address) assert.strictEqual(ourBalance, priorSplitBalance + priorBalances[index], 'after split balance mismatch') } From 6df447f22f1fa5ed7d3547807f3c6295214ea1b6 Mon Sep 17 00:00:00 2001 From: KillariDev Date: Mon, 23 Feb 2026 16:10:18 +0200 Subject: [PATCH 5/5] typos --- solidity/contracts/Zoltar.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/solidity/contracts/Zoltar.sol b/solidity/contracts/Zoltar.sol index 24c28cc..b01ffb6 100644 --- a/solidity/contracts/Zoltar.sol +++ b/solidity/contracts/Zoltar.sol @@ -4,8 +4,8 @@ pragma solidity 0.8.33; import './Constants.sol'; import './ReputationToken.sol'; -uint256 constant FORK_TRESHOLD_DIVISOR = 20; // TODO, revisit, 5% of total supply atm -uint256 constant FORK_BURN_DIVISOR = 5; // TODO, revisit, 20% of fork treshold +uint256 constant FORK_THRESHOLD_DIVISOR = 20; // TODO, revisit, 5% of total supply atm +uint256 constant FORK_BURN_DIVISOR = 5; // TODO, revisit, 20% of fork threshold contract Zoltar { struct Universe { @@ -57,9 +57,9 @@ contract Zoltar { universes[0] = Universe(0, ReputationToken(Constants.GENESIS_REPUTATION_TOKEN), 0, 0); } - function getForkTreshold(uint248 universeId) public view returns (uint256) { + function getForkThreshold(uint248 universeId) public view returns (uint256) { Universe memory universe = universes[universeId]; - return universe.reputationToken.getTotalTheoreticalSupply() / FORK_TRESHOLD_DIVISOR; + return universe.reputationToken.getTotalTheoreticalSupply() / FORK_THRESHOLD_DIVISOR; } function forkUniverse(uint248 universeId, string memory _extraInfo, string[4] memory _questionCategories) public { @@ -67,10 +67,10 @@ contract Zoltar { require(universe.forkTime == 0, 'Universe has forked already'); require(_questionCategories.length >= 1, 'need atleast one category on top of invalid'); universes[universeId].forkTime = block.timestamp; - uint256 forkTreshold = getForkTreshold(universeId); - universeForkData[universeId] = UniverseForkData(_extraInfo, msg.sender, forkTreshold - forkTreshold / FORK_BURN_DIVISOR, _questionCategories); - universes[universeId].reputationToken.transferFrom(msg.sender, address(this), forkTreshold); - burnRep(universes[universeId].reputationToken, address(this), forkTreshold / FORK_BURN_DIVISOR); // burn 20% + uint256 forkThreshold = getForkThreshold(universeId); + universeForkData[universeId] = UniverseForkData(_extraInfo, msg.sender, forkThreshold - forkThreshold / FORK_BURN_DIVISOR, _questionCategories); + universes[universeId].reputationToken.transferFrom(msg.sender, address(this), forkThreshold); + burnRep(universes[universeId].reputationToken, address(this), forkThreshold / FORK_BURN_DIVISOR); // burn 20% emit UniverseForked(msg.sender, universeId, _extraInfo, _questionCategories); }