From 507e901f27eb98dc5674fb7a77d2dbce2615430b Mon Sep 17 00:00:00 2001 From: KillariDev Date: Fri, 13 Feb 2026 14:09:05 +0200 Subject: [PATCH 1/4] add escalation game with not enough tests --- package.json | 3 +- .../contracts/peripherals/EscalationGame.sol | 211 ++++++++++++++++++ .../factories/EscalationGameFactory.sol | 14 ++ solidity/package.json | 3 +- solidity/ts/tests/testEscalationGame.ts | 42 ++++ .../utils/contracts/escalationGame.ts | 96 ++++++++ 6 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 solidity/contracts/peripherals/EscalationGame.sol create mode 100644 solidity/contracts/peripherals/factories/EscalationGameFactory.sol create mode 100644 solidity/ts/tests/testEscalationGame.ts create mode 100644 solidity/ts/testsuite/simulator/utils/contracts/escalationGame.ts diff --git a/package.json b/package.json index 44c3dac..eb5e502 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "setup-contracts": "cd solidity && npm run setup", "compile-contracts": "cd solidity && npm run compile-contracts", "test": "cd solidity && npm run test", - "test-peripherals": "cd solidity && npm run compile-contracts && npm run test-peripherals" + "test-peripherals": "cd solidity && npm run compile-contracts && npm run test-peripherals", + "test-escalationGame": "cd solidity && npm run compile-contracts && npm run test-escalationGame" } } diff --git a/solidity/contracts/peripherals/EscalationGame.sol b/solidity/contracts/peripherals/EscalationGame.sol new file mode 100644 index 0000000..ea86f1f --- /dev/null +++ b/solidity/contracts/peripherals/EscalationGame.sol @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.33; + +import { ReputationToken } from '../ReputationToken.sol'; +import { Zoltar } from '../Zoltar.sol'; +import { ISecurityPool } from './interfaces/ISecurityPool.sol'; +import { YesNoMarkets } from './YesNoMarkets.sol'; + +struct Deposit { + address depositor; + uint256 amount; + uint256 cumulativeAmount; +} + +uint256 constant escalationTimeLength = 4233600; // 7 weeks +uint256 constant SCALE = 1e6; +contract EscalationGame { + uint256 public startingTime; + uint256[3] public balances; // outcome -> amount + mapping(uint8 => Deposit[]) public deposits; // make a fixed array with dynamic + ISecurityPool public securityPool; + uint256 public nonDecisionTreshold; + uint256 public startBond; + address public owner; + uint256 public nonDecisionTimestamp; + + event GameStarted(uint256 startingTime, uint256 startBond, uint256 forkTreshold); + event DepositOnOutcome(address depositor, YesNoMarkets.Outcome outcome, uint256 amount, uint256 depositIndex, uint256 cumulativeAmount); + event WithdrawDeposit(address depositor, YesNoMarkets.Outcome winner, uint256 amountToWithdraw, uint256 depositIndex); + event ClaimDeposit(uint256 amountToWithdraw, uint256 burnAmount); + + constructor(ISecurityPool _securityPool) { + securityPool = _securityPool; + owner = msg.sender; + } + + function start(uint256 _startBond, uint256 _nonDecisionTreshold) public { + require(owner == msg.sender, 'only owner can start'); + require(startingTime == 0, 'already started'); + startingTime = block.timestamp + 3 days; + nonDecisionTreshold = _nonDecisionTreshold; + startBond = _startBond; + emit GameStarted(startingTime, nonDecisionTreshold, startBond); + } + + function getBalances() public view returns (uint256[3] memory) { + return [balances[0], balances[1], balances[2]]; + } + + // TODO, verify that this is never bigger than forkThreshold and is always increasing or constant in terms of timeSinceStart + // approx for: attrition cost = start deposit * (fork treshold / start deposit) ^ (time since start / time limit) + function compute5TermTaylorSeriesAttritionCostApproximation(uint256 startDeposit, uint256 forkThreshold, uint256 timeSinceStart) public pure returns (uint256) { + require(timeSinceStart <= escalationTimeLength, 'Invalid time'); + uint256 ratio = forkThreshold * SCALE / startDeposit; + require(ratio > SCALE, 'ratio must be > 1'); // since startDeposit < forkThreshold + uint256 z = (ratio - SCALE) * SCALE / (ratio + SCALE); + uint256 z2 = z * z / SCALE; + uint256 lnRatio = 2 * (z + z2 * z / (3 * SCALE) + z2 * z2 * z / (5 * SCALE)); + + uint256 tLnX = timeSinceStart * lnRatio / escalationTimeLength; + // Compute series: 1 + t*ln(x) + (t*ln(x))^2/2! + ... + (t*ln(x))^5/5! + uint256 series = SCALE; + uint256 term = tLnX; + // 1 + series += term; + // 2 + term = term * tLnX / (2 * SCALE); + series += term; + // 3 + term = term * tLnX / (3 * SCALE); + series += term; + // 4 + term = term * tLnX / (4 * SCALE); + series += term; + // 5 + term = term * tLnX / (5 * SCALE); + series += term; + return startDeposit * series / SCALE; + } + + // todo investigate this function more for errors. This can result in weird errors where you fork just before/after escalation game end + function computeTimeSinceStartFromAttritionCost(uint256 startDeposit, uint256 forkThreshold, uint256 attritionCost) public view returns (uint256) { + uint256 low = 0; + uint256 high = nonDecisionTreshold; + if (attritionCost <= startDeposit) return 0; + uint256 maxCost = nonDecisionTreshold; + if (attritionCost >= maxCost) return nonDecisionTreshold; + + // binary search + for (uint256 iteration = 0; iteration < 64; iteration++) { + uint256 midTime = (low + high) / 2; + + uint256 midCost = compute5TermTaylorSeriesAttritionCostApproximation(startDeposit, forkThreshold, midTime); + + if (midCost == attritionCost) return midTime; + if (midCost < attritionCost) { + low = midTime + 1; + } else { + high = midTime - 1; + } + } + return (low + high) / 2; + } + + function getEscalationGameEndDate() public view returns (uint256 endTime) { + if (nonDecisionTimestamp > 0) return nonDecisionTimestamp; + return startingTime + computeTimeSinceStartFromAttritionCost(startBond, nonDecisionTreshold, getBindingCapital()); + } + + function totalCost() public view returns (uint256) { + if (startingTime >= block.timestamp) return 0; + uint256 timeFromStart = block.timestamp - startingTime; + if (timeFromStart >= escalationTimeLength) return nonDecisionTreshold; + return compute5TermTaylorSeriesAttritionCostApproximation(startBond, nonDecisionTreshold, timeFromStart); + } + + function getMarketResolution() public view returns (YesNoMarkets.Outcome outcome){ + uint256 currentTotalCost = totalCost(); + uint8 invalidOver = balances[0] >= currentTotalCost ? 1 : 0; + uint8 yesOver = balances[1] >= currentTotalCost ? 1 : 0; + uint8 noOver = balances[2] >= currentTotalCost ? 1 : 0; + if (invalidOver + yesOver + noOver >= 2) return YesNoMarkets.Outcome.None; // if two or more outcomes are over the total cost, the game is still going + // the game has ended to timeout + if (balances[0] > balances[1] && balances[0] > balances[2]) return YesNoMarkets.Outcome.Invalid; + if (balances[1] > balances[0] && balances[1] > balances[2]) return YesNoMarkets.Outcome.Yes; + return YesNoMarkets.Outcome.No; + } + + function hasReachedNonDecision() public view returns (bool) { + uint8 invalidOver = balances[0] >= nonDecisionTreshold ? 1 : 0; + uint8 yesOver = balances[1] >= nonDecisionTreshold ? 1 : 0; + uint8 noOver = balances[2] >= nonDecisionTreshold ? 1 : 0; + if (invalidOver + yesOver + noOver >= 2) return true; + return false; + } + + function getBindingCapital() public view returns (uint256) { + if ((balances[0] >= balances[1] && balances[0] <= balances[2]) || (balances[0] >= balances[2] && balances[0] <= balances[1])) { + return balances[0]; + } else if ((balances[1] >= balances[0] && balances[1] <= balances[2]) || (balances[1] >= balances[2] && balances[1] <= balances[0])) { + return balances[1]; + } + return balances[2]; + } + + // deposits on market outcome, returns how much user actually ended depositing + function depositOnOutcome(address depositor, YesNoMarkets.Outcome outcome, uint256 amount) public returns (uint256 depositAmount) { + require(nonDecisionTimestamp == 0, 'System has already reached a non-decision'); + require(msg.sender == address(securityPool), 'Only Security Pool can deposit'); + require(getMarketResolution() == YesNoMarkets.Outcome.None, 'System has already timeouted'); + require(balances[uint256(outcome)] < nonDecisionTreshold, 'Already full'); + require(amount >= startBond, 'all amounts need to be bigger or equal to start deposit'); // checks that we get start bond and spam protection + Deposit memory deposit; + deposit.depositor = depositor; + balances[uint256(outcome)] += amount; + if (balances[uint256(outcome)] > nonDecisionTreshold) { + depositAmount = amount - (balances[uint256(outcome)] - nonDecisionTreshold); + balances[uint256(outcome)] = nonDecisionTreshold; + } else { + depositAmount = amount; + } + deposit.amount = depositAmount; + deposit.cumulativeAmount = balances[uint256(outcome)]; + deposits[uint8(outcome)].push(deposit); + emit DepositOnOutcome(depositor, outcome, deposit.amount, deposits[uint8(outcome)].length - 1, deposit.cumulativeAmount); + if (hasReachedNonDecision()) { + nonDecisionTimestamp = block.timestamp; + } + } + + // todo, this should be calculated against to actual fork treshold, not the one set at the start. The actual can be lower than the games treshold but never above + function claimDepositForWinning(uint256 depositIndex, YesNoMarkets.Outcome outcome) public returns (address depositor, uint256 amountToWithdraw) { + require(msg.sender == address(securityPool) || msg.sender == address(securityPool.securityPoolForker()), 'Only Security Pool can withdraw'); + Deposit memory deposit = deposits[uint8(outcome)][depositIndex]; + deposits[uint8(outcome)][depositIndex].amount = 0; + depositor = deposit.depositor; + uint256 maxWithdrawableBalance = getBindingCapital(); + if (deposit.cumulativeAmount > maxWithdrawableBalance) { + amountToWithdraw = deposit.amount; + emit ClaimDeposit(amountToWithdraw, 0); + } else if (deposit.cumulativeAmount + deposit.amount > maxWithdrawableBalance) { + uint256 excess = (deposit.cumulativeAmount + deposit.amount - maxWithdrawableBalance); + uint256 burnAmount = excess * 2 / 5; + amountToWithdraw = (deposit.amount - excess) + excess * 2 - burnAmount; + emit ClaimDeposit(amountToWithdraw, burnAmount); + } else { + uint256 burnAmount = (deposit.amount * 2) / 5; + amountToWithdraw = deposit.amount * 2 - burnAmount; + emit ClaimDeposit(amountToWithdraw, burnAmount); + } + } + + // todo, allow withdrawing after someones elses fork as well (game is canceled) + function withdrawDeposit(uint256 depositIndex) public returns (address depositor, uint256 amountToWithdraw) { + require(msg.sender == address(securityPool), 'Only Security Pool can withdraw'); + require(nonDecisionTimestamp == 0, 'System has reached non-decision'); + // if system hasnt forked, check outcome is winning + YesNoMarkets.Outcome marketResolution = getMarketResolution(); + (depositor,amountToWithdraw) = claimDepositForWinning(depositIndex, marketResolution); + emit WithdrawDeposit(depositor, marketResolution, amountToWithdraw, depositIndex); + } + + // todo, for the UI, we probably want to retrive multiple outcomes at once + function getDepositsByOutcome(YesNoMarkets.Outcome outcome, uint256 startIndex, uint256 numberOfEntries) external view returns (Deposit[] memory returnDeposits) { + returnDeposits = new Deposit[](numberOfEntries); + uint256 iterateUntil = startIndex + numberOfEntries > deposits[uint8(outcome)].length ? deposits[uint8(outcome)].length : startIndex + numberOfEntries; + for (uint256 i = startIndex; i < iterateUntil; i++) { + returnDeposits[i] = deposits[uint8(outcome)][i]; + } + } +} diff --git a/solidity/contracts/peripherals/factories/EscalationGameFactory.sol b/solidity/contracts/peripherals/factories/EscalationGameFactory.sol new file mode 100644 index 0000000..8ad6801 --- /dev/null +++ b/solidity/contracts/peripherals/factories/EscalationGameFactory.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Unlicense +pragma solidity 0.8.33; + +import { ISecurityPool } from '../interfaces/ISecurityPool.sol'; +import { EscalationGame } from '../EscalationGame.sol'; + +contract EscalationGameFactory { + function deployEscalationGame(uint256 startBond, uint256 _nonDecisionTreshold) external returns (EscalationGame) { + ISecurityPool securityPool = ISecurityPool(payable(msg.sender)); + EscalationGame game = new EscalationGame{ salt: bytes32(uint256(0x0)) }(securityPool); + game.start(startBond, _nonDecisionTreshold); + return game; + } +} diff --git a/solidity/package.json b/solidity/package.json index 2e1eb46..5f8fb27 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -6,7 +6,8 @@ "setup": "npm ci --ignore-scripts && npm run compile-contracts", "compile-contracts": "tsc --project tsconfig-compile.json && node ./js/compile.js", "test": "npx tsc && node --test", - "test-peripherals": "npx tsc && node --test ./js/tests/testPeripherals.js" + "test-peripherals": "npx tsc && node --test ./js/tests/testPeripherals.js", + "test-escalationGame": "npx tsc && node --test ./js/tests/testEscalationGame.js" }, "keywords": [], "author": "", diff --git a/solidity/ts/tests/testEscalationGame.ts b/solidity/ts/tests/testEscalationGame.ts new file mode 100644 index 0000000..8309694 --- /dev/null +++ b/solidity/ts/tests/testEscalationGame.ts @@ -0,0 +1,42 @@ +import test, { beforeEach, describe } from 'node:test' +import { getMockedEthSimulateWindowEthereum, MockWindowEthereum } from '../testsuite/simulator/MockWindowEthereum.js' +import { createWriteClient, WriteClient } from '../testsuite/simulator/utils/viem.js' +import { TEST_ADDRESSES } from '../testsuite/simulator/utils/constants.js' +import { contractExists, setupTestAccounts } from '../testsuite/simulator/utils/utilities.js' +import { QuestionOutcome } from '../testsuite/simulator/types/types.js' +import assert from 'node:assert' +import { deployEscalationGame, depositOnOutcome, getBalances, getStartingTime } from '../testsuite/simulator/utils/contracts/escalationGame.js' +import { ensureZoltarDeployed } from '../testsuite/simulator/utils/contracts/zoltar.js' +import { ensureInfraDeployed } from '../testsuite/simulator/utils/contracts/deployPeripherals.js' + +describe('Escalation Game Test Suite', () => { + let mockWindow: MockWindowEthereum + + let client: WriteClient + const reportBond = 1n * 10n ** 18n + const nonDecisionTreshold = 1000n * 10n ** 18n + beforeEach(async () => { + mockWindow = getMockedEthSimulateWindowEthereum() + client = createWriteClient(mockWindow, TEST_ADDRESSES[0], 0) + await setupTestAccounts(mockWindow) + await ensureZoltarDeployed(client) + await ensureInfraDeployed(client) + }) + + test('can start a game', async () => { + const escalationGame = await deployEscalationGame(client, reportBond, nonDecisionTreshold) + assert.ok(await contractExists(client, escalationGame), 'game was deployed') + const outcomeBalances = await getBalances(client, escalationGame) + assert.strictEqual(outcomeBalances.yes, 0n, 'yes stake') + assert.strictEqual(outcomeBalances.no, 0n, 'no stake') + assert.strictEqual(outcomeBalances.invalid, 0n, 'invalid stake') + + const startingTime = await getStartingTime(client, escalationGame) + assert.strictEqual(startingTime !== 0n, true, 'game was started') + await depositOnOutcome(client, escalationGame, client.account.address, QuestionOutcome.No, reportBond) + const outcomeBalancesAfterDeposit = await getBalances(client, escalationGame) + assert.strictEqual(outcomeBalancesAfterDeposit.yes, 0n, 'yes stake') + assert.strictEqual(outcomeBalancesAfterDeposit.no, reportBond, 'no stake') + assert.strictEqual(outcomeBalancesAfterDeposit.invalid, 0n, 'invalid stake') + }) +}) diff --git a/solidity/ts/testsuite/simulator/utils/contracts/escalationGame.ts b/solidity/ts/testsuite/simulator/utils/contracts/escalationGame.ts new file mode 100644 index 0000000..866b0f0 --- /dev/null +++ b/solidity/ts/testsuite/simulator/utils/contracts/escalationGame.ts @@ -0,0 +1,96 @@ +import { encodeDeployData, getCreate2Address, numberToBytes } from 'viem' +import { peripherals_EscalationGame_EscalationGame, peripherals_factories_EscalationGameFactory_EscalationGameFactory } from '../../../../types/contractArtifact.js' +import { AccountAddress, QuestionOutcome } from '../../types/types.js' +import { ReadClient, WriteClient } from '../viem.js' +import { getInfraContractAddresses } from './deployPeripherals.js' + +export const getNonDecisionTreshold = async (client: ReadClient, escalationGame: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_EscalationGame_EscalationGame.abi, + functionName: 'nonDecisionTreshold', + address: escalationGame, + args: [], + }) +} + +export const getMarketResolution = async (client: ReadClient, escalationGame: `0x${ string }`) => { + return (await client.readContract({ + abi: peripherals_EscalationGame_EscalationGame.abi, + functionName: 'getMarketResolution', + address: escalationGame, + args: [], + })) as QuestionOutcome +} + +export const getStartBond = async (client: ReadClient, escalationGame: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_EscalationGame_EscalationGame.abi, + functionName: 'startBond', + address: escalationGame, + args: [], + }) +} + +export const getEscalationGameDeposits = async (readClient: ReadClient, escalationGame: AccountAddress, outcome: QuestionOutcome) => { + let currentIndex = 0n + const numberOfEntries = 30n + let pages: { depositIndex: bigint, depositor: `0x${ string }`, amount: bigint, cumulativeAmount: bigint}[] = [] + do { + const newDeposits = (await readClient.readContract({ + abi: peripherals_EscalationGame_EscalationGame.abi, + functionName: 'getDepositsByOutcome', + address: escalationGame, + args: [outcome, currentIndex, numberOfEntries] + })).map((deposit, index) => ({ ...deposit, depositIndex: currentIndex + BigInt(index) })).filter((deposit) => BigInt(deposit.depositor) !== 0x0n) + pages.push(...newDeposits) + if (BigInt(newDeposits.length) !== numberOfEntries) break + currentIndex += numberOfEntries + } while(true) + return pages +} + +export const deployEscalationGame = async (writeClient: WriteClient, startBond: bigint, nonDecisionTreshold: bigint) => { + await writeClient.writeContract({ + abi: peripherals_factories_EscalationGameFactory_EscalationGameFactory.abi, + functionName: 'deployEscalationGame', + address: getInfraContractAddresses().escalationGameFactory, + args: [startBond, nonDecisionTreshold], + }) + return getCreate2Address({ + bytecode: encodeDeployData({ + abi: peripherals_EscalationGame_EscalationGame.abi, + bytecode: `0x${ peripherals_EscalationGame_EscalationGame.evm.bytecode.object }`, + args: [ writeClient.account.address ] + }), + from: getInfraContractAddresses().escalationGameFactory, + salt: numberToBytes(0) + }) +} + +export const getBalances = async (client: ReadClient, escalationGame: `0x${ string }`) => { + const [invalid, yes, no] = await client.readContract({ + abi: peripherals_EscalationGame_EscalationGame.abi, + functionName: 'getBalances', + address: escalationGame, + args: [], + }) + return { invalid, yes, no } +} + +export const getStartingTime = async (client: ReadClient, escalationGame: `0x${ string }`) => { + return await client.readContract({ + abi: peripherals_EscalationGame_EscalationGame.abi, + functionName: 'startingTime', + address: escalationGame, + args: [], + }) +} + +export const depositOnOutcome = async (writeClient: WriteClient, escalationGame: `0x${ string }`, depositor: `0x${ string }`, outcome: QuestionOutcome, amount: bigint) => { + await writeClient.writeContract({ + abi: peripherals_EscalationGame_EscalationGame.abi, + functionName: 'depositOnOutcome', + address: escalationGame, + args: [depositor, outcome, amount], + }) +} From 024a0ad9765197f9f755ad689c1a10d4eefe4924 Mon Sep 17 00:00:00 2001 From: KillariDev Date: Fri, 13 Feb 2026 14:26:44 +0200 Subject: [PATCH 2/4] fix according to comments --- .../contracts/peripherals/EscalationGame.sol | 38 +++++++++---------- .../factories/EscalationGameFactory.sol | 4 +- solidity/ts/tests/testEscalationGame.ts | 4 +- .../utils/contracts/escalationGame.ts | 8 ++-- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/solidity/contracts/peripherals/EscalationGame.sol b/solidity/contracts/peripherals/EscalationGame.sol index ea86f1f..80252ff 100644 --- a/solidity/contracts/peripherals/EscalationGame.sol +++ b/solidity/contracts/peripherals/EscalationGame.sol @@ -19,7 +19,7 @@ contract EscalationGame { uint256[3] public balances; // outcome -> amount mapping(uint8 => Deposit[]) public deposits; // make a fixed array with dynamic ISecurityPool public securityPool; - uint256 public nonDecisionTreshold; + uint256 public nonDecisionThreshold; uint256 public startBond; address public owner; uint256 public nonDecisionTimestamp; @@ -34,13 +34,13 @@ contract EscalationGame { owner = msg.sender; } - function start(uint256 _startBond, uint256 _nonDecisionTreshold) public { + function start(uint256 _startBond, uint256 _nonDecisionThreshold) public { require(owner == msg.sender, 'only owner can start'); require(startingTime == 0, 'already started'); startingTime = block.timestamp + 3 days; - nonDecisionTreshold = _nonDecisionTreshold; + nonDecisionThreshold = _nonDecisionThreshold; startBond = _startBond; - emit GameStarted(startingTime, nonDecisionTreshold, startBond); + emit GameStarted(startingTime, startBond, nonDecisionThreshold); } function getBalances() public view returns (uint256[3] memory) { @@ -81,10 +81,10 @@ contract EscalationGame { // todo investigate this function more for errors. This can result in weird errors where you fork just before/after escalation game end function computeTimeSinceStartFromAttritionCost(uint256 startDeposit, uint256 forkThreshold, uint256 attritionCost) public view returns (uint256) { uint256 low = 0; - uint256 high = nonDecisionTreshold; + uint256 high = escalationTimeLength; if (attritionCost <= startDeposit) return 0; - uint256 maxCost = nonDecisionTreshold; - if (attritionCost >= maxCost) return nonDecisionTreshold; + uint256 maxCost = nonDecisionThreshold; + if (attritionCost >= maxCost) return escalationTimeLength; // binary search for (uint256 iteration = 0; iteration < 64; iteration++) { @@ -104,14 +104,14 @@ contract EscalationGame { function getEscalationGameEndDate() public view returns (uint256 endTime) { if (nonDecisionTimestamp > 0) return nonDecisionTimestamp; - return startingTime + computeTimeSinceStartFromAttritionCost(startBond, nonDecisionTreshold, getBindingCapital()); + return startingTime + computeTimeSinceStartFromAttritionCost(startBond, nonDecisionThreshold, getBindingCapital()); } function totalCost() public view returns (uint256) { if (startingTime >= block.timestamp) return 0; uint256 timeFromStart = block.timestamp - startingTime; - if (timeFromStart >= escalationTimeLength) return nonDecisionTreshold; - return compute5TermTaylorSeriesAttritionCostApproximation(startBond, nonDecisionTreshold, timeFromStart); + if (timeFromStart >= escalationTimeLength) return nonDecisionThreshold; + return compute5TermTaylorSeriesAttritionCostApproximation(startBond, nonDecisionThreshold, timeFromStart); } function getMarketResolution() public view returns (YesNoMarkets.Outcome outcome){ @@ -120,16 +120,16 @@ contract EscalationGame { uint8 yesOver = balances[1] >= currentTotalCost ? 1 : 0; uint8 noOver = balances[2] >= currentTotalCost ? 1 : 0; if (invalidOver + yesOver + noOver >= 2) return YesNoMarkets.Outcome.None; // if two or more outcomes are over the total cost, the game is still going - // the game has ended to timeout + // the game has ended due to timeout if (balances[0] > balances[1] && balances[0] > balances[2]) return YesNoMarkets.Outcome.Invalid; if (balances[1] > balances[0] && balances[1] > balances[2]) return YesNoMarkets.Outcome.Yes; return YesNoMarkets.Outcome.No; } function hasReachedNonDecision() public view returns (bool) { - uint8 invalidOver = balances[0] >= nonDecisionTreshold ? 1 : 0; - uint8 yesOver = balances[1] >= nonDecisionTreshold ? 1 : 0; - uint8 noOver = balances[2] >= nonDecisionTreshold ? 1 : 0; + uint8 invalidOver = balances[0] >= nonDecisionThreshold ? 1 : 0; + uint8 yesOver = balances[1] >= nonDecisionThreshold ? 1 : 0; + uint8 noOver = balances[2] >= nonDecisionThreshold ? 1 : 0; if (invalidOver + yesOver + noOver >= 2) return true; return false; } @@ -148,14 +148,14 @@ contract EscalationGame { require(nonDecisionTimestamp == 0, 'System has already reached a non-decision'); require(msg.sender == address(securityPool), 'Only Security Pool can deposit'); require(getMarketResolution() == YesNoMarkets.Outcome.None, 'System has already timeouted'); - require(balances[uint256(outcome)] < nonDecisionTreshold, 'Already full'); + require(balances[uint256(outcome)] < nonDecisionThreshold, 'Already full'); require(amount >= startBond, 'all amounts need to be bigger or equal to start deposit'); // checks that we get start bond and spam protection Deposit memory deposit; deposit.depositor = depositor; balances[uint256(outcome)] += amount; - if (balances[uint256(outcome)] > nonDecisionTreshold) { - depositAmount = amount - (balances[uint256(outcome)] - nonDecisionTreshold); - balances[uint256(outcome)] = nonDecisionTreshold; + if (balances[uint256(outcome)] > nonDecisionThreshold) { + depositAmount = amount - (balances[uint256(outcome)] - nonDecisionThreshold); + balances[uint256(outcome)] = nonDecisionThreshold; } else { depositAmount = amount; } @@ -205,7 +205,7 @@ contract EscalationGame { returnDeposits = new Deposit[](numberOfEntries); uint256 iterateUntil = startIndex + numberOfEntries > deposits[uint8(outcome)].length ? deposits[uint8(outcome)].length : startIndex + numberOfEntries; for (uint256 i = startIndex; i < iterateUntil; i++) { - returnDeposits[i] = deposits[uint8(outcome)][i]; + returnDeposits[i - startIndex] = deposits[uint8(outcome)][i]; } } } diff --git a/solidity/contracts/peripherals/factories/EscalationGameFactory.sol b/solidity/contracts/peripherals/factories/EscalationGameFactory.sol index 8ad6801..66a4a96 100644 --- a/solidity/contracts/peripherals/factories/EscalationGameFactory.sol +++ b/solidity/contracts/peripherals/factories/EscalationGameFactory.sol @@ -5,10 +5,10 @@ import { ISecurityPool } from '../interfaces/ISecurityPool.sol'; import { EscalationGame } from '../EscalationGame.sol'; contract EscalationGameFactory { - function deployEscalationGame(uint256 startBond, uint256 _nonDecisionTreshold) external returns (EscalationGame) { + function deployEscalationGame(uint256 startBond, uint256 _nonDecisionThreshold) external returns (EscalationGame) { ISecurityPool securityPool = ISecurityPool(payable(msg.sender)); EscalationGame game = new EscalationGame{ salt: bytes32(uint256(0x0)) }(securityPool); - game.start(startBond, _nonDecisionTreshold); + game.start(startBond, _nonDecisionThreshold); return game; } } diff --git a/solidity/ts/tests/testEscalationGame.ts b/solidity/ts/tests/testEscalationGame.ts index 8309694..ff1759f 100644 --- a/solidity/ts/tests/testEscalationGame.ts +++ b/solidity/ts/tests/testEscalationGame.ts @@ -14,7 +14,7 @@ describe('Escalation Game Test Suite', () => { let client: WriteClient const reportBond = 1n * 10n ** 18n - const nonDecisionTreshold = 1000n * 10n ** 18n + const nonDecisionThreshold = 1000n * 10n ** 18n beforeEach(async () => { mockWindow = getMockedEthSimulateWindowEthereum() client = createWriteClient(mockWindow, TEST_ADDRESSES[0], 0) @@ -24,7 +24,7 @@ describe('Escalation Game Test Suite', () => { }) test('can start a game', async () => { - const escalationGame = await deployEscalationGame(client, reportBond, nonDecisionTreshold) + const escalationGame = await deployEscalationGame(client, reportBond, nonDecisionThreshold) assert.ok(await contractExists(client, escalationGame), 'game was deployed') const outcomeBalances = await getBalances(client, escalationGame) assert.strictEqual(outcomeBalances.yes, 0n, 'yes stake') diff --git a/solidity/ts/testsuite/simulator/utils/contracts/escalationGame.ts b/solidity/ts/testsuite/simulator/utils/contracts/escalationGame.ts index 866b0f0..ddb934d 100644 --- a/solidity/ts/testsuite/simulator/utils/contracts/escalationGame.ts +++ b/solidity/ts/testsuite/simulator/utils/contracts/escalationGame.ts @@ -4,10 +4,10 @@ import { AccountAddress, QuestionOutcome } from '../../types/types.js' import { ReadClient, WriteClient } from '../viem.js' import { getInfraContractAddresses } from './deployPeripherals.js' -export const getNonDecisionTreshold = async (client: ReadClient, escalationGame: `0x${ string }`) => { +export const getNonDecisionThreshold = async (client: ReadClient, escalationGame: `0x${ string }`) => { return await client.readContract({ abi: peripherals_EscalationGame_EscalationGame.abi, - functionName: 'nonDecisionTreshold', + functionName: 'nonDecisionThreshold', address: escalationGame, args: [], }) @@ -49,12 +49,12 @@ export const getEscalationGameDeposits = async (readClient: ReadClient, escalati return pages } -export const deployEscalationGame = async (writeClient: WriteClient, startBond: bigint, nonDecisionTreshold: bigint) => { +export const deployEscalationGame = async (writeClient: WriteClient, startBond: bigint, nonDecisionThreshold: bigint) => { await writeClient.writeContract({ abi: peripherals_factories_EscalationGameFactory_EscalationGameFactory.abi, functionName: 'deployEscalationGame', address: getInfraContractAddresses().escalationGameFactory, - args: [startBond, nonDecisionTreshold], + args: [startBond, nonDecisionThreshold], }) return getCreate2Address({ bytecode: encodeDeployData({ From c25bbd1880b40cf449f4555289264d49eb0b4548 Mon Sep 17 00:00:00 2001 From: KillariDev Date: Fri, 13 Feb 2026 14:59:26 +0200 Subject: [PATCH 3/4] fix according to comments --- .../contracts/peripherals/EscalationGame.sol | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/solidity/contracts/peripherals/EscalationGame.sol b/solidity/contracts/peripherals/EscalationGame.sol index 80252ff..05a1cd2 100644 --- a/solidity/contracts/peripherals/EscalationGame.sol +++ b/solidity/contracts/peripherals/EscalationGame.sol @@ -24,7 +24,7 @@ contract EscalationGame { address public owner; uint256 public nonDecisionTimestamp; - event GameStarted(uint256 startingTime, uint256 startBond, uint256 forkTreshold); + event GameStarted(uint256 startingTime, uint256 startBond, uint256 nonDecisionThreshold); event DepositOnOutcome(address depositor, YesNoMarkets.Outcome outcome, uint256 amount, uint256 depositIndex, uint256 cumulativeAmount); event WithdrawDeposit(address depositor, YesNoMarkets.Outcome winner, uint256 amountToWithdraw, uint256 depositIndex); event ClaimDeposit(uint256 amountToWithdraw, uint256 burnAmount); @@ -37,6 +37,8 @@ contract EscalationGame { function start(uint256 _startBond, uint256 _nonDecisionThreshold) public { require(owner == msg.sender, 'only owner can start'); require(startingTime == 0, 'already started'); + require(_nonDecisionThreshold > _startBond, 'threshold must exceed start bond'); + require(_startBond > 0, 'start bond must be positive'); startingTime = block.timestamp + 3 days; nonDecisionThreshold = _nonDecisionThreshold; startBond = _startBond; @@ -47,12 +49,12 @@ contract EscalationGame { return [balances[0], balances[1], balances[2]]; } - // TODO, verify that this is never bigger than forkThreshold and is always increasing or constant in terms of timeSinceStart - // approx for: attrition cost = start deposit * (fork treshold / start deposit) ^ (time since start / time limit) - function compute5TermTaylorSeriesAttritionCostApproximation(uint256 startDeposit, uint256 forkThreshold, uint256 timeSinceStart) public pure returns (uint256) { + // TODO, verify that this is never bigger than nonDecisionThreshold and is always increasing or constant in terms of timeSinceStart + // approx for: attrition cost = start deposit * (nonDecisionThreshold / start deposit) ^ (time since start / time limit) + function compute5TermTaylorSeriesAttritionCostApproximation(uint256 timeSinceStart) public pure returns (uint256) { require(timeSinceStart <= escalationTimeLength, 'Invalid time'); - uint256 ratio = forkThreshold * SCALE / startDeposit; - require(ratio > SCALE, 'ratio must be > 1'); // since startDeposit < forkThreshold + uint256 ratio = nonDecisionThreshold * SCALE / startBond; + require(ratio > SCALE, 'ratio must be > 1'); // since startBond < nonDecisionThreshold uint256 z = (ratio - SCALE) * SCALE / (ratio + SCALE); uint256 z2 = z * z / SCALE; uint256 lnRatio = 2 * (z + z2 * z / (3 * SCALE) + z2 * z2 * z / (5 * SCALE)); @@ -75,14 +77,14 @@ contract EscalationGame { // 5 term = term * tLnX / (5 * SCALE); series += term; - return startDeposit * series / SCALE; + return startBond * series / SCALE; } // todo investigate this function more for errors. This can result in weird errors where you fork just before/after escalation game end - function computeTimeSinceStartFromAttritionCost(uint256 startDeposit, uint256 forkThreshold, uint256 attritionCost) public view returns (uint256) { + function computeTimeSinceStartFromAttritionCost(uint256 attritionCost) public view returns (uint256) { uint256 low = 0; uint256 high = escalationTimeLength; - if (attritionCost <= startDeposit) return 0; + if (attritionCost <= startBond) return 0; uint256 maxCost = nonDecisionThreshold; if (attritionCost >= maxCost) return escalationTimeLength; @@ -90,7 +92,7 @@ contract EscalationGame { for (uint256 iteration = 0; iteration < 64; iteration++) { uint256 midTime = (low + high) / 2; - uint256 midCost = compute5TermTaylorSeriesAttritionCostApproximation(startDeposit, forkThreshold, midTime); + uint256 midCost = compute5TermTaylorSeriesAttritionCostApproximation(startBond, midTime); if (midCost == attritionCost) return midTime; if (midCost < attritionCost) { @@ -104,14 +106,14 @@ contract EscalationGame { function getEscalationGameEndDate() public view returns (uint256 endTime) { if (nonDecisionTimestamp > 0) return nonDecisionTimestamp; - return startingTime + computeTimeSinceStartFromAttritionCost(startBond, nonDecisionThreshold, getBindingCapital()); + return startingTime + computeTimeSinceStartFromAttritionCost(startBond, getBindingCapital()); } function totalCost() public view returns (uint256) { if (startingTime >= block.timestamp) return 0; uint256 timeFromStart = block.timestamp - startingTime; if (timeFromStart >= escalationTimeLength) return nonDecisionThreshold; - return compute5TermTaylorSeriesAttritionCostApproximation(startBond, nonDecisionThreshold, timeFromStart); + return compute5TermTaylorSeriesAttritionCostApproximation(startBond, timeFromStart); } function getMarketResolution() public view returns (YesNoMarkets.Outcome outcome){ @@ -121,6 +123,7 @@ contract EscalationGame { uint8 noOver = balances[2] >= currentTotalCost ? 1 : 0; if (invalidOver + yesOver + noOver >= 2) return YesNoMarkets.Outcome.None; // if two or more outcomes are over the total cost, the game is still going // the game has ended due to timeout + // TODO, doesn't handle ties well logically. We could avoid it by checking if tie is happening before deposit and break it there if (balances[0] > balances[1] && balances[0] > balances[2]) return YesNoMarkets.Outcome.Invalid; if (balances[1] > balances[0] && balances[1] > balances[2]) return YesNoMarkets.Outcome.Yes; return YesNoMarkets.Outcome.No; @@ -147,7 +150,7 @@ contract EscalationGame { function depositOnOutcome(address depositor, YesNoMarkets.Outcome outcome, uint256 amount) public returns (uint256 depositAmount) { require(nonDecisionTimestamp == 0, 'System has already reached a non-decision'); require(msg.sender == address(securityPool), 'Only Security Pool can deposit'); - require(getMarketResolution() == YesNoMarkets.Outcome.None, 'System has already timeouted'); + require(getMarketResolution() == YesNoMarkets.Outcome.None, 'System has already timed out'); require(balances[uint256(outcome)] < nonDecisionThreshold, 'Already full'); require(amount >= startBond, 'all amounts need to be bigger or equal to start deposit'); // checks that we get start bond and spam protection Deposit memory deposit; @@ -168,7 +171,7 @@ contract EscalationGame { } } - // todo, this should be calculated against to actual fork treshold, not the one set at the start. The actual can be lower than the games treshold but never above + // todo, this should be calculated against to actual nonDecisionThreshold, not the one set at the start. The actual can be lower than the games treshold but never above function claimDepositForWinning(uint256 depositIndex, YesNoMarkets.Outcome outcome) public returns (address depositor, uint256 amountToWithdraw) { require(msg.sender == address(securityPool) || msg.sender == address(securityPool.securityPoolForker()), 'Only Security Pool can withdraw'); Deposit memory deposit = deposits[uint8(outcome)][depositIndex]; @@ -200,7 +203,7 @@ contract EscalationGame { emit WithdrawDeposit(depositor, marketResolution, amountToWithdraw, depositIndex); } - // todo, for the UI, we probably want to retrive multiple outcomes at once + // todo, for the UI, we probably want to retrieve multiple outcomes at once function getDepositsByOutcome(YesNoMarkets.Outcome outcome, uint256 startIndex, uint256 numberOfEntries) external view returns (Deposit[] memory returnDeposits) { returnDeposits = new Deposit[](numberOfEntries); uint256 iterateUntil = startIndex + numberOfEntries > deposits[uint8(outcome)].length ? deposits[uint8(outcome)].length : startIndex + numberOfEntries; From 2d263d93be6ca5209f54a8451cee4c2550b07161 Mon Sep 17 00:00:00 2001 From: KillariDev Date: Fri, 13 Feb 2026 15:07:28 +0200 Subject: [PATCH 4/4] fix according to comments --- solidity/contracts/peripherals/EscalationGame.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/solidity/contracts/peripherals/EscalationGame.sol b/solidity/contracts/peripherals/EscalationGame.sol index 05a1cd2..ba998bc 100644 --- a/solidity/contracts/peripherals/EscalationGame.sol +++ b/solidity/contracts/peripherals/EscalationGame.sol @@ -51,7 +51,7 @@ contract EscalationGame { // TODO, verify that this is never bigger than nonDecisionThreshold and is always increasing or constant in terms of timeSinceStart // approx for: attrition cost = start deposit * (nonDecisionThreshold / start deposit) ^ (time since start / time limit) - function compute5TermTaylorSeriesAttritionCostApproximation(uint256 timeSinceStart) public pure returns (uint256) { + function compute5TermTaylorSeriesAttritionCostApproximation(uint256 timeSinceStart) public view returns (uint256) { require(timeSinceStart <= escalationTimeLength, 'Invalid time'); uint256 ratio = nonDecisionThreshold * SCALE / startBond; require(ratio > SCALE, 'ratio must be > 1'); // since startBond < nonDecisionThreshold @@ -92,7 +92,7 @@ contract EscalationGame { for (uint256 iteration = 0; iteration < 64; iteration++) { uint256 midTime = (low + high) / 2; - uint256 midCost = compute5TermTaylorSeriesAttritionCostApproximation(startBond, midTime); + uint256 midCost = compute5TermTaylorSeriesAttritionCostApproximation(midTime); if (midCost == attritionCost) return midTime; if (midCost < attritionCost) { @@ -106,14 +106,14 @@ contract EscalationGame { function getEscalationGameEndDate() public view returns (uint256 endTime) { if (nonDecisionTimestamp > 0) return nonDecisionTimestamp; - return startingTime + computeTimeSinceStartFromAttritionCost(startBond, getBindingCapital()); + return startingTime + computeTimeSinceStartFromAttritionCost(getBindingCapital()); } function totalCost() public view returns (uint256) { if (startingTime >= block.timestamp) return 0; uint256 timeFromStart = block.timestamp - startingTime; if (timeFromStart >= escalationTimeLength) return nonDecisionThreshold; - return compute5TermTaylorSeriesAttritionCostApproximation(startBond, timeFromStart); + return compute5TermTaylorSeriesAttritionCostApproximation(timeFromStart); } function getMarketResolution() public view returns (YesNoMarkets.Outcome outcome){