Skip to content

security pool forker#53

Open
KillariDev wants to merge 8 commits intomainfrom
add-security-pool-forker-and-migrate-escalation-game-and-new-zoltar
Open

security pool forker#53
KillariDev wants to merge 8 commits intomainfrom
add-security-pool-forker-and-migrate-escalation-game-and-new-zoltar

Conversation

@KillariDev
Copy link
Collaborator

  • removes forking code from security pool
  • create security pool forker contract that handles forking and auction
  • integrate to new zoltar
  • integrate to escalation game

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions
Copy link

I've Reviewed the Code

This substantial protocol refactoring introduces critical security vulnerabilities including ETH drainage and double-spending vectors, severe logic errors such as operator precedence bugs and division-by-zero risks, and multiple syntax issues across the Solidity and TypeScript codebase.

⚠️ 39 issues found across 33 files

#1 Missing FinalizedAuction event emission

solidity/contracts/peripherals/Auction.sol L46-L49

The FinalizedAuction event is defined at line 15 but is never emitted in the finalizeAuction function. This prevents off-chain services from tracking auction finalization and accessing the final auction state (recipient, total rep purchased, eth amount). The event should be emitted after the successful ETH transfer with the appropriate parameters.
Tags: bug, architecture
Affected code:

46: 		finalized = true;
47: 		(bool sent, ) = payable(receiver).call{value: address(this).balance}('');
48: 		require(sent, 'Failed to send Ether');
49: 	}

Proposed change:

		finalized = true;
		uint256 ethBalance = address(this).balance;
		(bool sent, ) = payable(receiver).call{value: ethBalance}('');
		require(sent, 'Failed to send Ether');
		emit FinalizedAuction(msg.sender, totalRepPurchased, ethBalance);

#2 Critical ETH refund vulnerability using full contract balance

solidity/contracts/peripherals/PriceOracleManagerAndOperatorQueuer.sol L128-L130

The function requestPriceIfNeededAndQueueOperation refunds excess ETH using address(this).balance, which transfers the entire contract balance to the caller instead of only the specific excess amount sent by the caller in the current transaction. This allows malicious or accidental drainage of all ETH held by the contract, including funds from previous transactions or direct transfers. The refund should calculate the exact excess amount: msg.value if requestPrice was not called, or msg.value - getRequestPriceEthCost() if requestPrice was invoked.
Tags: security, bug
Affected code:

128: 		// send rest of the eth back
129: 		(bool sent, ) = payable(msg.sender).call{ value: address(this).balance }('');
130: 		require(sent, 'Failed to return eth');

Proposed change:

		// send rest of the eth back
		uint256 refundAmount = msg.value;
		if (!isPriceValid() && queuedPendingOperationId == previousQueuedOperationId) {
			refundAmount = msg.value - getRequestPriceEthCost();
		}
		(bool sent, ) = payable(msg.sender).call{ value: refundAmount }('');
		require(sent, 'Failed to return eth');

#3 lockedRepInEscalationGame not decremented on withdrawal

solidity/contracts/peripherals/SecurityPool.sol L321-L325

In the withdrawFromEscalationGame function, when withdrawing deposits from the escalation game, the contract increases the depositor's poolOwnership but never decreases their lockedRepInEscalationGame. This causes the lockedRepInEscalationGame to accumulate indefinitely, leading to an underflow in redeemRep when calculating repAmount (poolOwnershipToRep minus lockedRepInEscalationGame), permanently bricking withdrawals for affected vaults.
Tags: bug, security
Affected code:

321: 		for (uint256 index = 0; index < depositIndexes.length; index++) {
322: 			(address depositor, uint256 amountToWithdraw) = escalationGame.withdrawDeposit(depositIndexes[index]);
323: 			securityVaults[depositor].poolOwnership += repToPoolOwnership(amountToWithdraw);
324: 		}
325: 	}

Proposed change:

        for (uint256 index = 0; index < depositIndexes.length; index++) {
            (address depositor, uint256 amountToWithdraw) = escalationGame.withdrawDeposit(depositIndexes[index]);
            securityVaults[depositor].poolOwnership += repToPoolOwnership(amountToWithdraw);
            securityVaults[depositor].lockedRepInEscalationGame -= amountToWithdraw;
        }

#4 Unrestricted ETH reception allows fund contamination

solidity/contracts/peripherals/SecurityPool.sol L385-L388

The receive() function accepts ETH from any address without validating the sender. This violates the intended security model where only specific contracts (e.g., the truth auction or escalation game) should be able to send ETH to the contract (as noted in the TODO comment). This allows malicious actors to send ETH that could be mistaken for legitimate proceeds, potentially breaking accounting invariants between address(this).balance, completeSetCollateralAmount, and totalFeesOwedToVaults.
Tags: security, bug
Affected code:

385: 	receive() external payable {
386: 		// needed for Truth Auction to send ETH back
387: 		// TODO, add check that its truth auction sending
388: 	}

Proposed change:

    receive() external payable {
        require(msg.sender == address(escalationGame) || msg.sender == securityPoolForker, 'Unauthorized sender');
        // needed for Truth Auction to send ETH back
    }

#5 Inappropriate function naming suggesting malicious behavior

solidity/contracts/peripherals/SecurityPool.sol L373-L375,L377-L380

The function stealAllRep and the error message 'Failed to steal ETH' in migrateEth use terminology suggesting theft and malicious intent. Even if these are intended for emergency migration or admin functions, such naming is inappropriate for production code, creates confusion for auditors and users, and suggests potential backdoors.
Tags: language, security, naming
Affected code:

373: 	function stealAllRep() external onlyForker() {
374: 		repToken.transfer(msg.sender, repToken.balanceOf(address(this)));
375: 	}

Affected code:

377: 	function migrateEth(address payable receiver, uint256 amount) external onlyForker() {
378: 		(bool sent, ) = receiver.call{ value: amount }('');
379: 		require(sent, 'Failed to steal ETH');
380: 	}

#6 Extreme centralization via onlyForker privileges

solidity/contracts/peripherals/SecurityPool.sol L76-L79,L328-L383

The onlyForker modifier grants a single address (securityPoolForker) unrestricted power to modify all critical state variables including systemState, totalSecurityBondAllowance, vault ownership, and complete access to all funds via stealAllRep and migrateEth. This represents a critical centralization risk and potential single point of failure/compromise.
Tags: security, architecture
Affected code:

76: 	modifier onlyForker {
77: 		require(msg.sender == securityPoolForker, 'Only Forker');
78: 		_;
79: 	}

Affected code:

328: 	function setSystemState(SystemState newState) external onlyForker {
329: 		systemState = newState;
330: 	}
331: 
332: 	function setRetentionRate(uint256 newRetention) external onlyForker {
333: 		currentRetentionRate = newRetention;
334: 	}
335: 
336: 	function setVaultOwnership(address vault, uint256 _poolOwnership, uint256 _securityBondAllowance) external onlyForker {
337: 		securityVaults[vault].poolOwnership = _poolOwnership;
338: 		securityVaults[vault].securityBondAllowance = _securityBondAllowance;
339: 	}
340: 
341: 	function setVaultSecurityBondAllowance(address vault, uint256 _securityBondAllowance) external onlyForker {
342: 		securityVaults[vault].securityBondAllowance = _securityBondAllowance;
343: 
344: 	}
345: 	function addToTotalSecurityBondAllowance(uint256 securityBondAllowanceDelta) external onlyForker {
346: 		totalSecurityBondAllowance += securityBondAllowanceDelta;
347: 	}
348: 
349: 	function setPoolOwnershipDenominator(uint256 _poolOwnershipDenominator) external onlyForker {
350: 		poolOwnershipDenominator = _poolOwnershipDenominator;
351: 	}
352: 
353: 	function setVaultPoolOwnership(address vault, uint256 poolOwnership) external onlyForker {
354: 		securityVaults[vault].poolOwnership = poolOwnership;
355: 	}
356: 
357: 	function setVaultFeeIndex(address vault, uint256 newFeeIndex) external onlyForker {
358: 		securityVaults[vault].feeIndex = newFeeIndex;
359: 	}
360: 
361: 	function setShareTokenSupply(uint256 newShareTokenSupply) external onlyForker {
362: 		shareTokenSupply = newShareTokenSupply;
363: 	}
364: 
365: 	function setCompleteSetCollateralAmount(uint256 newCompleteSetCollateralAmount) external onlyForker {
366: 		completeSetCollateralAmount = newCompleteSetCollateralAmount;
367: 	}
368: 
369: 	function setTotalSecurityBondAllowance(uint256 newTotalSecurityBondAllowance) external onlyForker {
370: 		totalSecurityBondAllowance = newTotalSecurityBondAllowance;
371: 	}
372: 
373: 	function stealAllRep() external onlyForker() {
374: 		repToken.transfer(msg.sender, repToken.balanceOf(address(this)));
375: 	}
376: 
377: 	function migrateEth(address payable receiver, uint256 amount) external onlyForker() {
378: 		(bool sent, ) = receiver.call{ value: amount }('');
379: 		require(sent, 'Failed to steal ETH');
380: 	}
381: 	function authorize(ISecurityPool pool) external onlyForker() {
382: 		shareToken.authorize(pool);
383: 	}

#7 Operator precedence bug in startTruthAuction

solidity/contracts/peripherals/SecurityPoolForker.sol L177

Line 177 contains a critical operator precedence error. The expression forkData[parent].repAtFork - forkData[securityPool].migratedRep / SecurityPoolUtils.MAX_AUCTION_VAULT_HAIRCUT_DIVISOR performs division before subtraction due to Solidity operator precedence rules. This results in calculating repAtFork - (migratedRep / divisor) instead of the intended (repAtFork - migratedRep) / divisor or repAtFork - ((repAtFork - migratedRep) / divisor). This causes the contract to attempt to auction significantly more REP than intended (nearly the entire repAtFork amount minus a tiny fraction, rather than the non-migrated remainder minus a small buffer). Since migrated REP has already been transferred to child pools at this stage, this will likely cause the transaction to revert due to insufficient balance or sell incorrect amounts.
Tags: bug, security
Affected code:

177: 			forkData[securityPool].truthAuction.startAuction(ethToBuy, forkData[parent].repAtFork - forkData[securityPool].migratedRep / SecurityPoolUtils.MAX_AUCTION_VAULT_HAIRCUT_DIVISOR);

Proposed change:

		forkData[securityPool].truthAuction.startAuction(ethToBuy, (forkData[parent].repAtFork - forkData[securityPool].migratedRep) / SecurityPoolUtils.MAX_AUCTION_VAULT_HAIRCUT_DIVISOR);

#8 Division by zero in _finalizeTruthAuction

solidity/contracts/peripherals/SecurityPoolForker.sol L191-L193

Line 192 performs a division (repAvailable - repPurchased) without checking if repPurchased equals repAvailable. When the auction purchases all available REP (i.e., totalRepPurchased() == repAtFork), this results in a division by zero, causing the transaction to revert and permanently preventing the auction from being finalized. The condition if (repAvailable > 0) does not protect against this case.
Tags: bug, security
Affected code:

191: 		if (repAvailable > 0) {
192: 			securityPool.setPoolOwnershipDenominator(forkData[securityPool].migratedRep * repAvailable * SecurityPoolUtils.PRICE_PRECISION / (repAvailable - repPurchased));
193: 		}

Proposed change:

		if (repAvailable > repPurchased) {
			securityPool.setPoolOwnershipDenominator(forkData[securityPool].migratedRep * repAvailable * SecurityPoolUtils.PRICE_PRECISION / (repAvailable - repPurchased));
		} else if (repAvailable == repPurchased) {
			securityPool.setPoolOwnershipDenominator(forkData[securityPool].migratedRep * SecurityPoolUtils.PRICE_PRECISION);
		}

#9 Missing view modifier on getMarketOutcome

solidity/contracts/peripherals/SecurityPoolForker.sol L231

The function getMarketOutcome at line 230 reads from state but does not modify it. It should be declared as external view instead of external to allow off-chain calls without broadcasting a transaction and to save gas.
Tags: bug, performance
Affected code:

231: 	function getMarketOutcome(ISecurityPool securityPool) external returns (YesNoMarkets.Outcome outcome){

Proposed change:

	function getMarketOutcome(ISecurityPool securityPool) external view returns (YesNoMarkets.Outcome outcome) {

#10 Incomplete and incorrect outcomes array initialization

solidity/contracts/peripherals/YesNoMarkets.sol L33-L37

The getForkingData function returns a string[4] array intended to represent market outcomes, but only initializes indices 0 and 1 with 'Yes' and 'No' respectively, leaving indices 2 and 3 as empty strings (default values). Furthermore, the Outcome enum defines 4 values (Invalid=0, Yes=1, No=2, None=3), suggesting the array should map each enum value to its corresponding label. Currently, the initialization appears misaligned (setting 'Yes' at index 0 instead of 1, 'No' at index 1 instead of 2) and incomplete (missing 'Invalid' and 'None').
Tags: bug, maintainability
Affected code:

33: 	function getForkingData(uint256 marketId) external view returns (string memory extraInfo, string[4] memory outcomes) {
34: 		extraInfo = markets[marketId].extraInfo;
35: 		outcomes[0] = 'Yes';
36: 		outcomes[1] = 'No';
37: 	}

Proposed change:

	function getForkingData(uint256 marketId) external view returns (string memory extraInfo, string[4] memory outcomes) {
		extraInfo = markets[marketId].extraInfo;
		outcomes[0] = 'Invalid';
		outcomes[1] = 'Yes';
		outcomes[2] = 'No';
		outcomes[3] = 'None';
	}

#11 Unclear and grammatically incorrect comment

solidity/contracts/peripherals/factories/SecurityPoolFactory.sol L64

The comment on line 64 is unclear and grammatically incorrect: 'sharetoken has different salt as sharetoken address does not change in forks'. It should clarify what the ShareToken's salt is different from (presumably the SecurityPool's salt) and use proper grammar. Suggested revision: 'The ShareToken uses a different salt because its address must remain consistent across forks'. Additionally, 'sharetoken' should be capitalized as 'ShareToken' to match the contract name.
Tags: language, readability
Affected code:

64: 		// sharetoken has different salt as sharetoken address does not change in forks

Proposed change:

		// The ShareToken uses a different salt because its address does not change in forks

#12 Inappropriate function name stealAllRep

solidity/contracts/peripherals/interfaces/ISecurityPool.sol L96

The function stealAllRep uses terminology ('steal') that implies unauthorized, malicious, or illegal action. In a production smart contract protocol, function names must clearly describe legitimate protocol operations (e.g., migrateAllRep, transferAllRep, or seizeAllRep if punitive). The current naming is semantically incorrect, unprofessional, and potentially indicates placeholder code or security concerns. It should be renamed to align with the existing migrateEth function and proper naming conventions.
Tags: language, naming, maintainability
Affected code:

96: 	function stealAllRep() external;

Proposed change:

	function migrateAllRep() external;

#13 Inconsistent parameter naming convention in setter functions

solidity/contracts/peripherals/interfaces/ISecurityPool.sol L83,L85,L88

The interface uses inconsistent naming conventions for function parameters. Functions like setRetentionRate (line 82) and setVaultFeeIndex (line 89) consistently use the 'new' prefix (e.g., newRetention, newFeeIndex) when the parameter name matches a state variable name. However, setVaultOwnership (line 83) and setVaultSecurityBondAllowance (line 85) use underscore prefixes (_poolOwnership, _securityBondAllowance), and setVaultPoolOwnership (line 88) uses no prefix at all (poolOwnership). This inconsistency reduces code readability and maintainability. All setter parameters should follow the same 'new' prefix convention used elsewhere in the interface.
Tags: naming, code-style, maintainability
Affected code:

83: 	function setVaultOwnership(address vault, uint256 _poolOwnership, uint256 _securityBondAllowance) external;

Proposed change:

	function setVaultOwnership(address vault, uint256 newPoolOwnership, uint256 newSecurityBondAllowance) external;

Affected code:

85: 	function setVaultSecurityBondAllowance(address vault, uint256 _securityBondAllowance) external;

Proposed change:

	function setVaultSecurityBondAllowance(address vault, uint256 newSecurityBondAllowance) external;

Affected code:

88: 	function setVaultPoolOwnership(address vault, uint256 poolOwnership) external;

Proposed change:

	function setVaultPoolOwnership(address vault, uint256 newPoolOwnership) external;

#14 Syntax error: Extra closing parenthesis in require statements

solidity/contracts/peripherals/tokens/ERC1155.sol L36,L62,L67,L86,L129,L186,L222,L237,L238,L255,L269,L270

Multiple require statements contain extra closing parentheses that will cause compilation to fail. Line 36 has the extra parenthesis before the comma separating the condition and error message. Lines 62, 86, 129, 186, 222, 237, 238, 255, 269, and 270 have extra parentheses at the end of the statement.
Tags: bug
Affected code:

36: 		require(account != address(0), "ERC1155: balance query for the zero address");

Proposed change:

		require(account != address(0), "ERC1155: balance query for the zero address");

Affected code:

62: 		require(accounts.length == ids.length, "ERC1155: accounts and IDs must have same lengths");

Proposed change:

		require(accounts.length == ids.length, "ERC1155: accounts and IDs must have same lengths");

Affected code:

67: 			require(accounts[i] != address(0), "ERC1155: some address in batch balance query is zero");

Proposed change:

			require(accounts[i] != address(0), "ERC1155: some address in batch balance query is zero");

Affected code:

86: 		require(msg.sender != operator, "ERC1155: cannot set approval status for self");

Proposed change:

		require(msg.sender != operator, "ERC1155: cannot set approval status for self");

Affected code:

129: 		require(to != address(0), "ERC1155: target address must be non-zero");

Proposed change:

		require(to != address(0), "ERC1155: target address must be non-zero");

Affected code:

186: 		require(to != address(0), "ERC1155: target address must be non-zero");

Proposed change:

		require(to != address(0), "ERC1155: target address must be non-zero");

Affected code:

222: 		require(to != address(0), "ERC1155: mint to the zero address");

Proposed change:

		require(to != address(0), "ERC1155: mint to the zero address");

Affected code:

237: 		require(to != address(0), "ERC1155: batch mint to the zero address");

Proposed change:

		require(to != address(0), "ERC1155: batch mint to the zero address");

Affected code:

238: 		require(ids.length == values.length, "ERC1155: minted IDs and values must have same lengths");

Proposed change:

		require(ids.length == values.length, "ERC1155: minted IDs and values must have same lengths");

Affected code:

255: 		require(account != address(0), "ERC1155: attempting to burn tokens on zero account");

Proposed change:

		require(account != address(0), "ERC1155: attempting to burn tokens on zero account");

Affected code:

269: 		require(account != address(0), "ERC1155: attempting to burn batch of tokens on zero account");

Proposed change:

		require(account != address(0), "ERC1155: attempting to burn batch of tokens on zero account");

Affected code:

270: 		require(ids.length == values.length, "ERC1155: burnt IDs and values must have same lengths");

Proposed change:

		require(ids.length == values.length, "ERC1155: burnt IDs and values must have same lengths");

#15 Missing empty array check causes permanent token loss

solidity/contracts/peripherals/tokens/ForkedERC1155.sol L22

The migrate function sets the sender's balance for fromId to zero before iterating through the outcomes array. If an empty array is passed, the loop body never executes, resulting in the user's tokens being burned (balance permanently set to zero) without receiving any corresponding child universe tokens. This constitutes a permanent loss of funds.
Tags: bug, security
Affected code:

22: 

Proposed change:

		require(outcomes.length > 0, 'Must specify at least one outcome');

#16 Duplicate outcomes allow double spending and supply inflation

solidity/contracts/peripherals/tokens/ForkedERC1155.sol L27-L28

The function does not validate that elements in the outcomes array are unique. If the same outcome index appears multiple times (e.g., [1, 1]), the loop credits the full fromIdBalance to the same child universe multiple times. This allows users to mint arbitrary amounts of tokens, inflating the supply beyond the intended total. The existing TODO comment on line 27 acknowledges this check is missing but the implementation remains vulnerable.
Tags: bug, security
Affected code:

27: 		// TODO, check that outcomes are unique
28: 		// TODO, do we allow people to migrate later to different universes?

Proposed change:

		// Ensure outcomes are unique to prevent double spending
		uint256 outcomeMask;
		for (uint256 i = 0; i < outcomes.length; i++) {
			uint256 bit = 1 << outcomes[i];
			require((outcomeMask & bit) == 0, 'Duplicate outcome');
			outcomeMask |= bit;
		}

#17 Syntax Error: Mismatched Parentheses in Assembly

solidity/contracts/peripherals/tokens/TokenId.sol L11

In the getTokenId function's assembly block, the expression contains an extra closing parenthesis after the bitmask literal. The code shows ...FFFFFFFFFFFFFF))), (three closing parentheses) which incorrectly closes the or operation before the second argument and(_outcome, 0xFF) is provided. This results in a parenthesis mismatch (4 opens vs 5 closes) that will cause a compilation failure. The correct syntax requires only two closing parentheses after the mask to close the and and shl operations respectively.
Tags: bug, syntax-error
Affected code:

11: 			_tokenId := or(shl(8, and(_universeId, 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)), and(_outcome, 0xFF))

Proposed change:

			_tokenId := or(shl(8, and(_universeId, 0x00FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)), and(_outcome, 0xFF))

#18 Incorrect camelCase in function names

solidity/ts/tests/testPeripherals.ts L17,L18,L81,L216,L265

Two function names violate camelCase conventions: getnonDecisionThreshold should be getNonDecisionThreshold (capitalize 'N' after 'get'), and getZoltarforkThreshold should be getZoltarForkThreshold (capitalize 'F' in Fork). These appear in imports and usage throughout the file. Additionally, several lines contain extra spaces.
Tags: naming, language, code-style
Affected code:

17: import { getEscalationGameDeposits, getMarketResolution, getnonDecisionThreshold, getStartBond } from '../testsuite/simulator/utils/contracts/escalationGame.js'

Proposed change:

import { getEscalationGameDeposits, getMarketResolution, getNonDecisionThreshold, getStartBond } from '../testsuite/simulator/utils/contracts/escalationGame.js'

Affected code:

18: import { ensureZoltarDeployed, forkUniverse, getRepTokenAddress, getTotalTheoreticalSupply, getUniverseForkData, getZoltarAddress, getZoltarforkThreshold  } from '../testsuite/simulator/utils/contracts/zoltar.js'

Proposed change:

import { ensureZoltarDeployed, forkUniverse, getRepTokenAddress, getTotalTheoreticalSupply, getUniverseForkData, getZoltarAddress, getZoltarForkThreshold } from '../testsuite/simulator/utils/contracts/zoltar.js'

Affected code:

81: 		assert.ok(await getnonDecisionThreshold(client, securityPoolAddresses.escalationGame) > 10n * reportBond, 'fork treshold need to be big enough')

Proposed change:

assert.ok(await getNonDecisionThreshold(client, securityPoolAddresses.escalationGame) > 10n * reportBond, 'fork treshold need to be big enough')

Affected code:

216: 		const zoltarforkThreshold  = await getZoltarforkThreshold (client, genesisUniverse)

Proposed change:

const zoltarForkThreshold = await getZoltarForkThreshold(client, genesisUniverse)

Affected code:

265: 		const zoltarforkThreshold  = await getZoltarforkThreshold (client, genesisUniverse)

Proposed change:

const zoltarForkThreshold = await getZoltarForkThreshold(client, genesisUniverse)

#19 Spelling and grammar errors in test messages

solidity/ts/tests/testPeripherals.ts L81,L88

Test assertion messages contain typographical and grammatical errors. Line 81: 'treshold' should be 'threshold' and 'need' should be 'needs'. Line 88: 'cumulator' should be 'cumulative amount'.
Tags: language
Affected code:

81: 		assert.ok(await getnonDecisionThreshold(client, securityPoolAddresses.escalationGame) > 10n * reportBond, 'fork treshold need to be big enough')

Proposed change:

assert.ok(await getnonDecisionThreshold(client, securityPoolAddresses.escalationGame) > 10n * reportBond, 'fork threshold needs to be big enough')

Affected code:

88: 		strictEqualTypeSafe(yesDeposits[0].amount, reportBond, 'amount should be report bond')

Proposed change:

strictEqualTypeSafe(yesDeposits[0].cumulativeAmount, reportBond, 'cumulative amount should be report bond')

#20 Unit mismatch in liquidator balance assertion

solidity/ts/tests/testPeripherals.ts L140

Line 140 divides repDepositShare by PRICE_PRECISION but compares it to repDeposit + (repDeposit * 10n) which is not divided by PRICE_PRECISION. This compares values in different units (normalized value vs wei value), causing the assertion to fail. Should use strictEqual18Decimal to compare raw 18-decimal values, or divide the right side by PRICE_PRECISION.
Tags: bug
Affected code:

140: 		strictEqualTypeSafe(liquidatorVault.repDepositShare / PRICE_PRECISION, repDeposit+(repDeposit * 10n), 'liquidator should have all the rep in the pool')

Proposed change:

strictEqual18Decimal(liquidatorVault.repDepositShare, repDeposit + (repDeposit * 10n), 'liquidator should have all the rep in the pool')

#21 Incorrect cache invalidation time calculation

solidity/ts/testsuite/simulator/EthereumClientService.ts L46

The timestamp comparison at line 46 multiplies timestamp.getTime() (milliseconds) by 1000, resulting in a value 1000x larger than Date.now(). This causes the difference to always be negative, so the cache invalidation condition always evaluates to false and expired blocks are never cleared from cache. The calculation should compare milliseconds to milliseconds.
Tags: bug, logic, performance
Affected code:

46: 		if ((Date.now() - this.cachedBlock.timestamp.getTime() * 1000) > TIME_BETWEEN_BLOCKS * MAX_BLOCK_CACHE) return undefined

Proposed change:

		if ((Date.now() - this.cachedBlock.timestamp.getTime()) > TIME_BETWEEN_BLOCKS * MAX_BLOCK_CACHE * 1000) return undefined

#22 Missing request abort controller in getBlock branches

solidity/ts/testsuite/simulator/EthereumClientService.ts L134,L147

The jsonRpcRequest calls at lines 134 and 147 (when fullObjects is false) omit the requestAbortController parameter, preventing request cancellation for these specific code paths. This is inconsistent with the other branches in these methods which correctly pass the abort controller.
Tags: bug, maintainability
Affected code:

134: 		if (fullObjects === false) return EthereumBlockHeaderWithTransactionHashes.parse(await this.requestHandler.jsonRpcRequest({ method: 'eth_getBlockByNumber', params: [blockTag, false] }))

Proposed change:

		if (fullObjects === false) return EthereumBlockHeaderWithTransactionHashes.parse(await this.requestHandler.jsonRpcRequest({ method: 'eth_getBlockByNumber', params: [blockTag, false] }, requestAbortController))

Affected code:

147: 			return EthereumBlockHeaderWithTransactionHashes.parse(await this.requestHandler.jsonRpcRequest({ method: 'eth_getBlockByHash', params: [blockHash, false] }))

Proposed change:

			return EthereumBlockHeaderWithTransactionHashes.parse(await this.requestHandler.jsonRpcRequest({ method: 'eth_getBlockByHash', params: [blockHash, false] }, requestAbortController))

#23 Missing request abort controller in ethSimulateV1

solidity/ts/testsuite/simulator/EthereumClientService.ts L194

The eth_simulateV1 method accepts a requestAbortController parameter but fails to pass it to the underlying jsonRpcRequest call at line 194, preventing callers from aborting these potentially long-running simulation requests.
Tags: bug, maintainability
Affected code:

194: 		const unvalidatedResult = await this.requestHandler.jsonRpcRequest(call)

Proposed change:

		const unvalidatedResult = await this.requestHandler.jsonRpcRequest(call, requestAbortController)

#24 Typo in comment: 'transatons'

solidity/ts/testsuite/simulator/SimulationModeEthereumClientService.ts L18

The word 'transactions' is misspelled as 'transatons' in the comment on line 18. Per guidelines, language issues are treated as critical defects.
Tags: language
Affected code:

18: const MOCK_SIMULATION_PRIVATE_KEY = 0x2n // key used to sign simulated transatons

Proposed change:

const MOCK_SIMULATION_PRIVATE_KEY = 0x2n // key used to sign simulated transactions

#25 Typo in error message: 'transction'

solidity/ts/testsuite/simulator/SimulationModeEthereumClientService.ts L214

The word 'transaction' is misspelled as 'transction' in the error message on line 214. Error messages are user-facing strings and must be correct.
Tags: language
Affected code:

214: 		if (lastTransaction === undefined) throw new Error('last transction did not exist')

Proposed change:

		if (lastTransaction === undefined) throw new Error('last transaction did not exist')

#26 Missing await for async ethereumClientService.call

solidity/ts/testsuite/simulator/SimulationModeEthereumClientService.ts L617

Line 617 calls ethereumClientService.call() which returns a Promise, but the result is not awaited before being passed to EthereumData.parse(). This causes the parser to receive a Promise object instead of the actual data, leading to incorrect behavior or runtime errors.
Tags: bug
Affected code:

617: 			return { result: EthereumData.parse(ethereumClientService.call(params, 'finalized', requestAbortController)) }

Proposed change:

			return { result: EthereumData.parse(await ethereumClientService.call(params, 'finalized', requestAbortController)) }

#27 Incorrect type assertion casting uint256 to number

solidity/ts/testsuite/simulator/SimulationModeEthereumClientService.ts L381

Line 381 decodes a uint256 balance value (which can exceed Number.MAX_SAFE_INTEGER) and casts it to 'number' before converting to BigInt. This can cause precision loss for large balances. The viem library returns bigint for uint256 types, so the cast should be to bigint or omitted entirely.
Tags: bug, security
Affected code:

381: 	const parsed = decodeFunctionResult({ abi: BALANCE_ABI, functionName: 'getBalance', data: dataStringWith0xStart(lastResult.returnData) }) as number

Proposed change:

	const parsed = decodeFunctionResult({ abi: BALANCE_ABI, functionName: 'getBalance', data: dataStringWith0xStart(lastResult.returnData) }) as bigint

#28 BigInt constructor crashes on checksummed addresses

solidity/ts/testsuite/simulator/utils/contracts/deployPeripherals.ts L190-L194

Line 190 uses BigInt(parent) to check if the parent address is zero. The BigInt constructor in JavaScript throws a SyntaxError when given hex strings containing mixed-case letters (e.g., checksummed Ethereum addresses like '0xAbC...'). Since the parent parameter accepts any 0x${string} and may receive checksummed addresses from viem utilities or other sources, this code will crash at runtime when processing valid checksummed addresses. Use a direct comparison against zeroAddress instead.
Tags: bug, compatibility
Affected code:

190: 		truthAuction: BigInt(parent) == 0n ? zeroAddress : getCreate2Address({
191: 			bytecode: `0x${ peripherals_Auction_Auction.evm.bytecode.object }`,
192: 			from: infraContracts.auctionFactory,
193: 			salt: securityPoolSaltWithMsgSender
194: 		}),

Proposed change:

		truthAuction: parent === zeroAddress ? zeroAddress : getCreate2Address({
			bytecode: `0x${ peripherals_Auction_Auction.evm.bytecode.object }`,
			from: infraContracts.auctionFactory,
			salt: securityPoolSaltWithMsgSender
		}),

#29 Incorrect use of locale-sensitive toLocaleLowerCase for hex normalization

solidity/ts/testsuite/simulator/utils/contracts/deployPeripherals.ts L16

Line 16 uses toLocaleLowerCase() to normalize a hex address string for bytecode replacement. This is risky because toLocaleLowerCase() is locale-sensitive (e.g., in Turkish locale, 'I' becomes 'ı'). While Ethereum addresses currently only contain 0-9 and A-F, using locale-sensitive methods for programmatic string transformation is an anti-pattern that could lead to subtle bugs if the character set changes or if the code is reused elsewhere. Use toLowerCase() instead.
Tags: anti-pattern, maintainability
Affected code:

16: 	const replaceLib = (bytecode: string, hash: string, replaceWithAddress: `0x${ string }`) => bytecode.replaceAll(`__$${ hash }$__`, replaceWithAddress.slice(2).toLocaleLowerCase())

Proposed change:

	const replaceLib = (bytecode: string, hash: string, replaceWithAddress: `0x${ string }`) => bytecode.replaceAll(`__$${ hash }$__`, replaceWithAddress.slice(2).toLowerCase())

#30 Missing universe identifier in Escalation Game deployment name

solidity/ts/testsuite/simulator/utils/contracts/deployments.ts L42-L44

The Escalation Game deployment entry is added to the universe-specific deployments array (getDeploymentsForUniverse) but lacks the universe identifier in its deployment name, unlike all other universe-specific contracts (RepV2, PriceOracleManagerAndOperatorQueuer, ETH SecurityPool, ShareToken, Truth Auction). Since a unique escalation game address is generated for each universe (passed via the escalationGame parameter from universe-specific address generation), omitting the universe name will cause naming collisions and make it impossible to distinguish between escalation games of different universes in the deployment list.
Tags: bug, naming, consistency
Affected code:

42: 		abi: peripherals_EscalationGame_EscalationGame.abi,
43: 		deploymentName: `Escalation Game`,
44: 		address: escalationGame

Proposed change:

	}, {
		abi: peripherals_EscalationGame_EscalationGame.abi,
		deploymentName: `Escalation Game ${ getUniverseName(universeId) }`,
		address: escalationGame

#31 Typo in parameter name: seucurityPoolAddress

solidity/ts/testsuite/simulator/utils/contracts/peripherals.ts L245,L252

The parameter seucurityPoolAddress in the balanceOfSharesInCash function contains a typographical error. It should be securityPoolAddress to match the terminology used throughout the codebase (as seen in the import from './securityPool.js'). This typo reduces code readability and may cause confusion when referencing the variable.
Tags: naming, language
Affected code:

245: export const balanceOfSharesInCash = async (client: ReadClient, seucurityPoolAddress: `0x${ string }`, shareTokenAddress: `0x${ string }`, universeId: bigint, account: `0x${ string }`) => {

Proposed change:

	shareTokenAddress: `0x${ string }`, universeId: bigint, account: `0x${ string }`) => {

Affected code:

252: 	return await shareArrayToCash(client, seucurityPoolAddress, array)

Proposed change:

	return await shareArrayToCash(client, securityPoolAddress, array)

#32 Missing bigint-to-number conversion in getOpenOracleReportMeta

solidity/ts/testsuite/simulator/utils/contracts/peripherals.ts L184-L197

The function getOpenOracleReportMeta returns a ReportMeta interface where several fields (settlementTime, feePercentage, protocolFee, multiplier, disputeDelay) are typed as number. However, the contract likely returns these as uint256/bigint (consistent with the getOpenOracleExtraData function which explicitly converts bigints to numbers). Returning unconverted bigints violates the type contract and may cause runtime errors (e.g., JSON serialization failures or arithmetic issues) in consuming code. The values should be converted using Number() similar to lines 92-93 in getOpenOracleExtraData.
Tags: bug, maintainability
Affected code:

184: 	return {
185: 		exactToken1Report,
186: 		escalationHalt,
187: 		fee,
188: 		settlerReward,
189: 		token1,
190: 		settlementTime,
191: 		token2,
192: 		timeType,
193: 		feePercentage,
194: 		protocolFee,
195: 		multiplier,
196: 		disputeDelay
197: 	}

Proposed change:

	return {
		exactToken1Report,
		escalationHalt,
		fee,
		settlerReward,
		token1,
		settlementTime: Number(settlementTime),
		token2,
		timeType,
		feePercentage: Number(feePercentage),
		protocolFee: Number(protocolFee),
		multiplier: Number(multiplier),
		disputeDelay: Number(disputeDelay)
	}

#33 Incorrect WETH balance assertion assumes zero initial balance

solidity/ts/testsuite/simulator/utils/contracts/peripheralsTestUtils.ts L63-L65

The assertion on line 65 checks that the WETH balance equals amount2, assuming the account starts with zero WETH. This will fail if the account already holds WETH or if the function is invoked multiple times in a test sequence (accumulating balance). Should record the balance before wrapping and verify the delta.
Tags: bug
Affected code:

63: 	await wrapWeth(client, amount2)
64: 	const wethBalance = await getERC20Balance(client, WETH_ADDRESS, client.account.address)
65: 	assert.strictEqual(wethBalance, amount2, 'Did not wrap weth')

Proposed change:

	const wethBalanceBefore = await getERC20Balance(client, WETH_ADDRESS, client.account.address)
	await wrapWeth(client, amount2)
	const wethBalance = await getERC20Balance(client, WETH_ADDRESS, client.account.address)
	assert.strictEqual(wethBalance, wethBalanceBefore + amount2, 'Did not wrap WETH')

#34 Parameter incorrectly passed as transaction value instead of argument

solidity/ts/testsuite/simulator/utils/contracts/securityPool.ts L33-L41

In createCompleteSet, the parameter completeSetsToCreate is passed as value (ETH) instead of in args. Based on the naming convention (consistent with completeSetsToRedeem which is passed as an argument) and the parameter name suggesting a quantity rather than an ETH amount, this appears to be a bug. The function should pass the parameter in args array to match the pattern used in redeemCompleteSet and other similar functions.
Tags: bug
Affected code:

33: export const createCompleteSet = async (client: WriteClient, securityPoolAddress: `0x${ string }`, completeSetsToCreate: bigint) => {
34: 	return await client.writeContract({
35: 		abi: peripherals_SecurityPool_SecurityPool.abi,
36: 		functionName: 'createCompleteSet',
37: 		address: securityPoolAddress,
38: 		args: [],
39: 		value: completeSetsToCreate,
40: 	})
41: }

Proposed change:

export const createCompleteSet = async (client: WriteClient, securityPoolAddress: `0x${ string }`, completeSetsToCreate: bigint) => {
	return await client.writeContract({
		abi: peripherals_SecurityPool_SecurityPool.abi,
		functionName: 'createCompleteSet',
		address: securityPoolAddress,
		args: [completeSetsToCreate],
	})
}

#35 Typo in function name: getSecurityPoolsEscalationGame

solidity/ts/testsuite/simulator/utils/contracts/securityPool.ts L98

Function name uses plural 'Pools' instead of singular 'Pool', which is inconsistent with other functions in the file (getSecurityVault, getPoolOwnershipDenominator, etc.) and the fact that the function operates on a single security pool address.
Tags: naming, language
Affected code:

98: export const getSecurityPoolsEscalationGame = async (client: ReadClient, securityPoolAddress: `0x${ string }`) => {

Proposed change:

export const getSecurityPoolEscalationGame = async (client: ReadClient, securityPoolAddress: `0x${ string }`) => {

#36 Incorrect stateMutability in inline ABI for getMarketOutcome

solidity/ts/testsuite/simulator/utils/contracts/securityPoolForker.ts L81

The inline ABI fragment for getMarketOutcome specifies 'stateMutability': 'nonpayable' (line 81), which indicates a function that can modify state. However, this function is being called via client.readContract (line 72), which is intended for view/pure functions. This mismatch suggests the function should likely be marked as 'view' or 'pure'. Nonpayable functions invoked via eth_call (readContract) will not persist state changes, and if the function actually requires state transitions or has side effects, this call pattern is incorrect. If it is genuinely a getter function, the ABI metadata is wrong.
Tags: bug
Affected code:

81: 			"stateMutability": "nonpayable",

Proposed change:

		"stateMutability": "view",

#37 objectEntries type assertion fails for objects with numeric keys

solidity/ts/testsuite/simulator/utils/typescript.ts L38

The type assertion T[keyof T & string] evaluates to never when T has numeric keys (e.g., {0: string, 1: number} or arrays), because keyof T returns number literals like 0 | 1, and number & string is never. This causes the return type to be [string, never][] instead of the actual value types, leading to compile-time type unsoundness where valid entries are incorrectly typed.
Tags: bug
Affected code:

38: export const objectEntries = <T extends object>(obj: T) => Object.entries(obj) as [string, T[keyof T & string]][]

Proposed change:

export const objectEntries = <T extends object>(obj: T) => Object.entries(obj) as [string, T[keyof T]][]

#38 Incorrect regex pattern for named capture group in jsonParse

solidity/ts/testsuite/simulator/utils/utilities.ts L31

The regex pattern (:<hex>[a-fA-F0-9])+ on line 31 is syntactically incorrect for a named capture group. It will literal match the string (:<hex> followed by hex characters instead of capturing hex digits into a group named 'hex'. The correct syntax should be (?<hex>[a-fA-F0-9]+). As a result, the bytesMatch.groups object will never contain the expected 'hex' property, causing the Uint8Array parsing logic to fail silently.
Tags: bug, logic
Affected code:

31: 		const bytesMatch = /^b'(:<hex>[a-fA-F0-9])+'$/.exec(value)

Proposed change:

		const bytesMatch = /^b'(?<hex>[a-fA-F0-9]+)'$/.exec(value)

#39 Incorrect contract existence check logic

solidity/ts/testsuite/simulator/utils/utilities.ts L225

The contractExists function checks for !== undefined to determine if a contract exists. However, viem's getCode (or getBytecode) returns '0x' for addresses with no deployed bytecode, not undefined. This causes the function to incorrectly return true for non-existent contracts. It should check for !== '0x' (and optionally !== undefined for defensive programming).
Tags: bug, logic
Affected code:

225: export const contractExists = async (client: ReadClient, contract: `0x${ string }`) => await client.getCode({ address: contract }) !== undefined

Proposed change:

export const contractExists = async (client: ReadClient, contract: `0x${ string }`) => await client.getCode({ address: contract }) !== '0x'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant