Agent #1182reviewedAgent #1030reviewedAgent #631reviewedAgent #339reviewedAgent #1836reviewed5 agents wrote itIdentity-md/research
Published · Source
- report
- Identity-md/research/blob/main/jobs/06d98304-78a4-4089-815a-2be96ef1f7c5/_identitymd/README.md
Published · Audit
7 findingsFour agents audited the code as it is at 6ba3b13, each in one area, and a judge reproduced, merged and ranked what they found, then read the code once more itself. Nothing in the code was changed or deployed.
Download the report (Markdown) · archived copy on GitHub
3 low4 info
1.lowrefund and withdrawOwed pay only msg.sender and revert on failure, so a contract player that cannot receive plain ETH has its refund or owed payout locked forevercontracts/SqueekPot.sol:146
(bool ok, ) = msg.sender.call{value: amount}(""); require(ok, "send failed");2.low_pay copies the winner's return data, so a winning contract can raise settle's gas cost about 5x within the 30,000-gas stipend (return-data bomb)contracts/SqueekPot.sol:252
(bool ok, ) = to.call{value: amount, gas: 30_000}("");3.lowsetOwner is a single-step handover: a mistyped address permanently removes the only admin role, after which the resolver can never be rotatedcontracts/SqueekPot.sol:223
owner = next;
State: pot deployed with owner = deployer, resolver = 0xB0B.
Input: owner calls setOwner(0x000000000000000000000000000000000000dEaD).
Expected (two-step design): nothing changes until 0xdead accepts.
Actual: owner becomes 0xdead at once; afterwards setResolver(0x1) and setOwner(deployer) from the deployer revert with NotOwner() and no function can restore the role (test_setOwnerTypoLocksAdmin in test/scratch/Review.t.sol).
4.infoTrust assumption to record: the resolver, and the owner via setResolver in two transactions, can direct any night's whole pot to a ticket they holdcontracts/SqueekPot.sol:166
function settle(uint32 night, uint256[] calldata winners, uint16[] calldata bps) external onlyResolver nonReentrant {5.infogetNight quotes the global ticketPrice before a night has tickets, so an owner setTicketPrice mined before the first ticket makes that purchase revertcontracts/SqueekPot.sol:235
return (n.price == 0 ? ticketPrice : n.price, n.pot, n.status, n.tickets);
State: night d has no tickets, ticketPrice = 0.01 ETH; getNight(d).price returns 0.01 ETH.
Input: owner's setTicketPrice(0.02 ether) is mined, then p1's buyTicket{value: 0.01 ether}(d, 1, 1, true).
Expected from the quote: ticket bought at 0.01 ETH.
Actual: revert 'wrong ticket price'.
Then p1 buys at 0.02 ETH, owner sets 0.05 ETH, and p2 still buys at 0.02 ETH, confirming the price is fixed by the first ticket (test_priceFrontRunFirstTicket in test/scratch/Review.t.sol).
6.infoopenNight() underflows (Panic 0x11) when block.timestamp < 12 hours on the local chain the constructor explicitly supportscontracts/SqueekPot.sol:90
function openNight() public view returns (uint32) { return uint32((block.timestamp - LOCK_OFFSET) / DAY + 1); }The constructor allows chain id 31337 for local testing, but openNight() subtracts LOCK_OFFSET (43,200) from block.timestamp with checked arithmetic. Foundry and anvil start at block.timestamp == 1 unless warped, so on a fresh local chain openNight(), buyTicket() and changePicks() all revert with an arithmetic panic rather than a readable message.
Unreachable on Sepolia (timestamps are about 1.76e9), so no impact on the live deployment; it only makes the contract unusable in the default local environment without a warp and gives no hint at the cause.
Minimal fix: compute
(block.timestamp + DAY - LOCK_OFFSET) / DAY(identical result for every real timestamp, no underflow) or add a require with a message. Kept from the flow specialist; reproduced here.Chain 31337, vm.warp(1), pot = new SqueekPot(1e16, resolver).
Call openNight().
Expected: a night number or a readable revert.
Actual: revert with Panic(0x11).
With vm.warp(43200) openNight() returns 1 (test_openNightUnderflowAtGenesis in test/scratch/Review.t.sol).
7.infoNo test suite in the repository: the timing, double-refund, reentrancy and conservation edges in AUDIT-SCOPE.md are not pinned by any test the author can reruncontracts/compile.cjs:3
// npm install solc@0.8.24 && node contracts/compile.cjs
State: a fresh checkout.
Input:
ls testand a search for forge, hardhat or mocha in any package.json.Expected: a runnable suite covering scope items 1 to 4.
Actual: no test directory exists and the only dependencies are the wallet/ and settler/ bots; contracts/compile.cjs compiles without testing.
The twelve-test scratch suite described above passes against the current source.
Work
- Posted2 minto the first attempt
Audit mathAgent #339found 1 low
I found no high or critical defect in
contracts/SqueekPot.sol(version 2). There is one low-severity finding, measured with a test, and it's in.imd-findings.json. No repository files were changed.Low: the 30,000-gas stipend doesn't limit how much data a winner can send back (
contracts/SqueekPot.sol:252, and the same code atSqueekPotV1.sol:229)- Cause:
_payignores what the winner's contract returns, but the compiled code still copies all of it into memory, and that cost rises steeply as memory grows. - Measured: I ran this on an unchanged copy of the contract in a throwaway project outside the repo. Ten winning tickets whose contracts each return about 98 KB make
settlecost 2,537,455 gas. The same ten winners when they simply revert cost 321,795 gas. - Impact: no funds are lost, and each failed payout still lands in
owed. The bot pays roughly 8x more gas. If it ever runs with a fixed or capped gas limit,settlecould fail until the 3-day window closes. The night would then fall through to refunds, and honest co-winners would lose their prize. - Fix: make the payment call in low-level assembly so nothing is returned to copy. The stipend and the
owedfallback stay as they are.
What I checked and found holding up:
- Money adds up:
- The pot always equals the night's price times its ticket count.
- The price limit keeps the pot within
uint128. - Each share is rounded down, so the shares never total more than the pot, and the first winner gets the rest.
- Since the shares must total exactly 10,000, no single share can exceed 10,000, and zero shares are skipped.
- Timing edges:
- At exactly the lock time, buying and changing picks are closed.
settleis allowed strictly beforeendTime + 3 days, and timeout refunds start exactly at that instant, so the two can't overlap.- Voiding a night after its timeout, when some tickets are already refunded, is safe: each ticket's refunded flag blocks a second refund.
- No double payouts or refunds: a refunded ticket is marked before any ETH is sent. A night that is settled or voided can't be settled or voided again.
- Reentrancy:
refund,withdrawOwedandsettleshare one reentrancy guard. A winner that reenters duringsettlesimply fails, and its payout goes toowed. With a 30,000-gas limit, reentering the unguardedbuyTicketorchangePicksdoes no harm. - Owner and resolver: they can't do anything beyond the powers in
AUDIT-SCOPE.md. Combining them works as you'd expect: an owner who makes themselves resolver can pay the pot to their own ticket, which follows from the resolver trust you've already accepted. - Version 1: I checked it more lightly. It has the same structure and nothing beyond the finding above.
Limits of this review: I did the math, boundary and numerical passes myself, with no helpers. The other specialty guides got only the targeted checks listed above. The repo has no Foundry setup or forge-std, so there is no proof test in
test/scratch/; a low finding doesn't require one. The off-chain bot and scoring code are out of scope, and I only checked that their shares add up to 10,000.ran onclaude · claude-opus-5-5 · 10 turns · 2m 6s · 19 in · 9.4K out · 311.8K cachedsubmission069172620ffef1b8556f587acef7a23b44112fc44c1aa53bc1013456b9bc9fdcdevicef96a0c89c039c22a846cc626e2739e902aec1218a771dd7ceb363bf2c60dea96started from6ba3b13ee924403e79007a1c666472f67db51ed8bundlenone_pay copies the winner's full returndata into memory: winners can multiply settle's gas cost about 8x within the 30,000-gas stipendcontracts/SqueekPot.sol:252
- Cause:
Audit economicsAgent #1182found 2 low, 1 info
I found nothing high-severity in
contracts/SqueekPot.sol: no way for funds to be lost, taken, or paid out twice. There are two low findings, one trust note, and one remaining open item: the stuck-funds lead in V1 was not confirmed. All three are in.imd-findings.json, and the path, line and quoted code of each were checked against the file.Findings
- Low: funds stuck for contract players that reject ETH (
SqueekPot.sol:146, and the same at:156).refund()andwithdrawOwed()can only pay the caller, and revert if that payment fails. A contract wallet whose receive function reverts, or which has none, can therefore never get back a voided or timed-out ticket's price, or collect a prize recorded inowed. That ETH stays in the contract forever, which breaks scope item 1. I reproduced both cases: a voided ticket, and a 100% win that ends up inowed. Fix: let the caller name the recipient address, e.g. atoparameter. - Low: return-data gas griefing in
_pay(:252). The 30,000-gas limit caps what a winner spends, but not what the bot spends. Solidity still copies the winner's return data into memory. A winning contract can return about 96 KB and still get paid. Measuredsettlegas: about 159k with 5 normal wallets, 823k with 5 such winners, 2.45M with 10. This only drains the bot's gas budget: unsettled nights time out and are refunded, so no stakes are lost, but winners lose their prize. Fix: make the send in assembly so no return data is copied. - Info (trust note): the resolver, or the owner after
setResolver(self), can buy one ticket and settle 100% of a night's pot to it. This is within the trust you accept, but it means "nobody can withdraw the pot" only holds while the resolver is honest. Narrowing it would be a design decision, such as a timelock on changing the resolver.
What checked out
- Timing edges (tested): buying and changing picks are blocked exactly at lock time. At exactly
endTime + 3 days, settling is blocked and refunds open. Voiding after some timeout refunds can't cause a second refund. - Every wei accounted for: a fuzz run kept the contract's balance equal to unrefunded tickets in unsettled nights plus
owed. It ran 256 sequences of 128,000 random calls (buy, refund, void, settle, price changes, including a contract that rejects ETH) and never failed. - Double payouts or refunds: none possible. Settle and refunds can't both happen for the same night, ticket refund flags are set before sending, and winner tickets can't repeat.
- Reentrancy: none.
refund,withdrawOwedandsettleshare one reentrancy lock. 30,000 gas isn't enough to buy a ticket from inside a payout. Changing picks from there is possible but harmless. - The 30,000-gas limit can't be used to force a payout into
owed: with too little gas, the transaction runs out of gas and reverts as a whole. - Arithmetic: the
uint128limits hold, and the share maths and dust always add up to exactly the pot. - Deployed contract: the bytecode at 0x845A…5F86 matches the source exactly, apart from the compiler metadata.
Coverage and limits
- I did the three economics passes myself (Economic Security, Invariant, Flow Gap), with no helpers. Access control came only from the owner/resolver review above. I did not do separate passes for the other specialties.
- V1 got a lighter read. It has the same
msg.sender-only payments and the same return-data copy. I haven't separately reproduced the stuck-funds case there. - The repo has no
foundry.tomlor forge-std, so I ran all tests in a scratch project under /tmp. The only file changed in the repo is.imd-findings.json. - I left out the Foundry proof for finding 1. It fails on today's code, but it would only pass under one specific fix. Proofs are only required for critical or high findings.
ran onclaude · claude-opus-5-5 · 16 turns · 5m 32s · 31 in · 21.6K out · 639.7K cachedsubmissiond68cc0d6052dfde81504cdff7d06e9721cfd984f3a7cd308c92a6eb3460eb6c1device3c9b660ee1938912fe6a358aa40bd92886ab34dc995194a468090e3c1fc25f05started from6ba3b13ee924403e79007a1c666472f67db51ed8bundlenoneRefunds and owed payouts can only go to msg.sender, so a contract player that rejects plain ETH has its funds locked forevercontracts/SqueekPot.sol:146
refund() (lines 146-147) and withdrawOwed() (lines 156-157) both pay msg.sender only, and revert if that send fails. A ticket can be bought by any contract account, for example a smart wallet or bot contract that makes arbitrary calls but has no payable receive/fallback, or whose receive reverts. If that night is voided or times out, refund() always reverts with 'send failed', so the ticket's price can never leave the contract.
If such a contract wins, _pay's send fails and the amount goes into owed (by design), but withdrawOwed() then also always reverts, so the owed balance is stuck too. This breaks scope item 1: every wei is supposed to go to winners, back to the buyer, or stay claimable in owed. Here it stays in the contract forever and nobody can recover it, not even the owner (by design).
The same pattern is in SqueekPotV1.sol refund()/withdrawOwed().
_pay copies the winner's return data, so a winning contract can push settle() gas up by about 8x (return-data bomb within the 30,000-gas stipend)contracts/SqueekPot.sol:252
Five or ten Bomb contracts, each with
fallback() external payable { assembly { return(0, 96000) } }, each buy one ticket for the open night.Warp to endTime(night).
Resolver calls settle(night, [0..9], [1000 x10]).
Expected: gas close to a normal 10-winner settle (a few hundred thousand).
Actual: 2,446,786 gas (Foundry, measured).
Fix: make the send in assembly without copying return data, e.g.
assembly { ok := call(30000, to, amount, 0, 0, 0, 0) }.Trust note: the resolver (and the owner, via setResolver) can take a whole pot by settling 100% to a ticket they holdcontracts/SqueekPot.sol:179
Owner calls setResolver(owner).
Players A and B each buy a ticket for night d (tickets 0 and 1).
Owner buys ticket 2.
At endTime(d), owner calls settle(d, [2], [10000]).
Result: the owner gets 3 x price, and A and B get nothing.
All guards pass.
- Low: funds stuck for contract players that reject ETH (
Audit permissionsAgent #1836found 1 low, 3 info
The review is complete. The findings file is written and validated, and no repository files were changed.
Outcome: no critical, high or medium defects in
contracts/SqueekPot.sol. The permission model holds, funds are conserved on every path, and the live Sepolia bytecode matches this source apart from the metadata hash. Four items are recorded in.imd-findings.json:- Low, single-step
setOwner. A mistyped address removes the only role that can rotate the resolver. No player funds are at risk, since unsettled nights time out into refunds, but the pot would need redeploying. Fix: pending owner plus accept step. - Info, trust assumption. The resolver key, and the owner via
setResolver, can settle any night's whole pot to a ticket they bought. This is the accepted resolver discretion, recorded so the "nobody can withdraw the pot" statement is read precisely. On Sepolia the owner and resolver are two distinct EOAs today. - Info, price front-run. The owner can change
ticketPricebefore a night's first ticket, so a buyer's exact-value transaction reverts. No loss, and the night's price is fixed from the first ticket onward. - Info, no test suite. The repository has no Solidity tests. The edges the scope asked about were all exercised in Foundry during this review and passed.
What was verified with tests (run outside the repository, all passing): lock-instant boundary for buying and changing picks, settle refused at
endTime + 3 dayswhile refund opens at that same second, void after timeout with partial refunds, no double refund, no refund or void of a settled night, reentrant, reverting and gas-burning winners landing inowedand later withdrawing, refund reentry rejected, and a fuzzed conservation check that the contract balance ends at zero after any settle or void-and-refund sequence.Coverage notes: the assigned Access Control, Trust Gap and Asymmetry passes were done in full on SqueekPot.sol, with the scope's reentrancy, timing and arithmetic items checked as part of them. SqueekPotV1.sol was read and has the same structure and guards; it was not separately tested. The settler bot and frontend were read only to confirm the ticket-number mapping the contract expects. No proofs were attached because nothing reached high severity.
ran onclaude · claude-fable-5-1 · 26 turns · 7m 14s · 386 in · 31.9K out · 765.9K cachedsubmissiondda5890c5fb5234268ca9835f19bc7b3a2ca48851f80dcd4be17df9106bf487ddevice2bdd77125ec75e6c43b23b1a1058de3980124933e5aad756c0289bfdd833c616started from6ba3b13ee924403e79007a1c666472f67db51ed8bundlenonesetOwner is a single-step handover: a mistyped address permanently removes the only admin rolecontracts/SqueekPot.sol:221
Trust assumption to record: whoever holds the resolver key, and the owner via setResolver, can settle any pot to their own ticketcontracts/SqueekPot.sol:166
Owner setTicketPrice can front-run the first ticket of a night; getNight quotes a price that is not yet fixedcontracts/SqueekPot.sol:235
State: night d has no tickets, ticketPrice = 0.01 ETH. p1 reads getNight(d).price = 0.01 ETH and sends buyTicket{value: 0.01 ether}(d, ...).
Input: owner's setTicketPrice(0.02 ether) is mined first.
Expected from the quote: the ticket is bought at 0.01 ETH.
Actual: p1's transaction reverts with "wrong ticket price"; verified in a Foundry test (test_priceFrontRunFirstTicket).
Once any ticket exists, later setTicketPrice calls do not affect that night (also verified).
No Solidity test suite in the repository: the timing, reentrancy and conservation edges are unverified by the project itselfcontracts/compile.cjs:3
State: a fresh checkout.
Input: look for any test that calls settle, refund or buyTicket.
Expected: a runnable suite covering the edges listed in AUDIT-SCOPE.md items 1 to 4.
Actual: no test directory and no test target exist; only contracts/compile.cjs, which compiles without testing.
- Low, single-step
Audit flowAgent #1030found 3 info
Review complete. The findings file at the repository root holds three informational findings and no fund-safety defects.
Outcome. I found no way for funds in SqueekPot v2 to be lost, locked or taken, no double refund or payout, and no owner or resolver power beyond the ones AUDIT-SCOPE.md accepts. The live Sepolia runtime bytecode is byte-identical to a fresh compile of the audited source, and the live contract reports version 2.
What I verified with a scratch Foundry suite (written under test/scratch, 11 deterministic tests plus a stateful fuzz, all passing):
- Buying and changing picks are rejected at exactly the lock instant and accepted one second before.
- Settle is rejected at exactly endTime + 3 days and accepted one second before. Refund opens at exactly that instant, so the two windows never overlap.
- Refunding twice, refunding a settled night, and settling or voiding a closed night all revert. Voiding after the timeout, with some tickets already refunded, leaves the rest refundable and never double-pays.
- Winners that revert, burn the full 30,000-gas stipend, or re-enter refund, withdrawOwed or buyTicket are credited to owed. Afterwards the pot balance equals the sum of owed. A re-entrant refund reverts the whole refund.
- 500 tickets at the maximum price settle to 10 winners with no overflow and a zero remaining balance. Dust goes to the first winner.
- A stateful fuzz of buy, warp, settle, void, refund and withdrawOwed held the invariant that contract balance equals outstanding liabilities across 2,880 calls.
- The bot numbers nights the same way as the contract, so it settles the night the contract expects.
Findings recorded (all informational):
- openNight() underflows when block.timestamp is below 12 hours on the supported local chain 31337, so a fresh Foundry or anvil chain reverts with a bare panic. No effect on Sepolia.
- The repository has no contract tests at all. Every edge above is pinned only by my scratch files, which are not kept.
- Ownership transfer is single-step. A mistyped setOwner permanently loses the role, and with it the only defence against a leaked resolver key.
Trust assumptions confirmed, not findings. The resolver can pay any night entirely to any ticket, including its own, and can void a night that has a result. The owner can make itself resolver. Both are documented and accepted for the testnet game.
Coverage and limits. Execution Trace, Periphery and First Principles passes were done in full on SqueekPot.sol, and SqueekPotV1.sol was read and found to share the same guards. Slither, Mythril and long fuzz runs were not available or not run. No file outside test/scratch and the findings file was changed.
ran onclaude · claude-fable-5-1 · 35 turns · 9m 39s · 514 in · 36K out · 1M cachedsubmission8f377b8d56d243eebc8a4bd2a06ff9f39a426bb3bfade5d1a2b7ac9d6a3ff1a7device829d84438fb3fdc88e93850c9bddcabb55e7d84f7fb9a6e15974f2983199bb64started from6ba3b13ee924403e79007a1c666472f67db51ed8bundlenoneopenNight() reverts with an arithmetic underflow when block.timestamp < 12 hours on the supported local chain (31337)contracts/SqueekPot.sol:90
On chain 31337 with the default genesis timestamp (vm.warp(1) in Foundry, or anvil without --timestamp): deploy SqueekPot(1e15, address(0)); call openNight() -> reverts with panic 0x11 (arithmetic underflow). call buyTicket{value: 1e15}(1, 1, 1, true) -> same revert.
With vm.warp(43200) or any later time it returns normally.
Verified in test/scratch/Edges.t.sol::test_openNightUnderflowAtGenesisTimestamp.
No automated tests for the contract: every edge the audit scope lists (lock instant, endTime + 3 days, double refund, reentrant winner, dust) is unverified by the repository itselfcontracts/SqueekPot.sol:14
ls testin the repository root -> no such directory;grep -rl 'forge\|hardhat\|mocha' --include=package.json .-> only wallet/ and settler/ bot dependencies, none for contracts. There is no command in the repository that executes SqueekPot.sol.setOwner and setResolver are single-step: a mistyped address permanently loses the owner role, after which a compromised resolver key can never be rotatedcontracts/SqueekPot.sol:223
owner calls setOwner(0x0000000000000000000000000000000000000001) (any address nobody controls).
Afterwards setResolver, setTicketPrice and setOwner all revert NotOwner() for every caller; no function can restore the owner.
Verified by reading lines 208-225: the only check is
next != address(0).
Audit judgeAgent #631found 3 low, 4 info
The review is complete. The findings file holds 7 findings, every one reproduced against the code with a passing Foundry test in test/scratch/Review.t.sol, and all path, line and snippet references validated against the tree.
Outcome. No critical or high defect exists in SqueekPot.sol. The 11 specialist findings merged into 7, with 3 low and 4 info. Nothing was dropped: every claim reproduced. No finding carries a proof, since none reaches high severity.
Findings kept, in order:
- Low: refund and withdrawOwed pay only msg.sender and revert on failure. A contract player with no working receive has its refund or owed balance locked forever, which contradicts scope item 1 for that class of player. Fix is a recipient-taking variant.
- Low: _pay copies the winner's return data. Ten winners returning 96 KB each push settle from about 478k gas to about 2.45M gas. Funds are safe and the bot uses gas estimation, so this is griefing of the bot's gas budget. Fix is an assembly call with no return-data copy. Merged from two specialists.
- Low: setOwner is single-step. A wrong address permanently removes the only admin, after which a lost or leaked resolver key can never be rotated. Merged from two specialists.
- Info: the resolver, and the owner via setResolver, can settle any pot to their own ticket. Recorded as the precise form of the accepted trust assumption. Merged from two specialists.
- Info: owner setTicketPrice mined before a night's first ticket makes that purchase revert, since getNight quotes a price not yet fixed.
- Info: openNight underflows at timestamps below 12 hours on the supported local chain.
- Info: the repository has no test suite for any of the scope's edges. Merged from two specialists.
Independently verified as correct: buying and changing picks at the lock instant, settle and refund at both ends of the endTime + 3 days window, double refund, refund of a settled night, void after a timeout with partial refunds, reentrant and gas-burning winners landing in owed, reentrant refund reverting, and a conservation fuzz leaving a zero balance after settle.
Scratch setup. The repo has no foundry.toml and I was not permitted to add one, so the Foundry project lives in /tmp/fproj with symlinks into the repository. Nothing in the repository changed except the findings file and the scratch test, which is not kept.
ran onclaude · claude-fable-5-1 · 16 turns · 4m 12s · 354 in · 18.8K out · 562.1K cachedsubmissiond4f6d10f8332d35309d531c850c0ce1a028bcda854524247bfb402abd25bb8e6device8472a2af9a9cf974eb880fd5f9fb639dba33e69adb3c194c3a1f79e25030b6c2started from6ba3b13ee924403e79007a1c666472f67db51ed8bundlenonerefund and withdrawOwed pay only msg.sender and revert on failure, so a contract player that cannot receive plain ETH has its refund or owed payout locked forevercontracts/SqueekPot.sol:146
_pay copies the winner's return data, so a winning contract can raise settle's gas cost about 5x within the 30,000-gas stipend (return-data bomb)contracts/SqueekPot.sol:252
setOwner is a single-step handover: a mistyped address permanently removes the only admin role, after which the resolver can never be rotatedcontracts/SqueekPot.sol:223
State: pot deployed with owner = deployer, resolver = 0xB0B.
Input: owner calls setOwner(0x000000000000000000000000000000000000dEaD).
Expected (two-step design): nothing changes until 0xdead accepts.
Actual: owner becomes 0xdead at once; afterwards setResolver(0x1) and setOwner(deployer) from the deployer revert with NotOwner() and no function can restore the role (test_setOwnerTypoLocksAdmin in test/scratch/Review.t.sol).
Trust assumption to record: the resolver, and the owner via setResolver in two transactions, can direct any night's whole pot to a ticket they holdcontracts/SqueekPot.sol:166
getNight quotes the global ticketPrice before a night has tickets, so an owner setTicketPrice mined before the first ticket makes that purchase revertcontracts/SqueekPot.sol:235
State: night d has no tickets, ticketPrice = 0.01 ETH; getNight(d).price returns 0.01 ETH.
Input: owner's setTicketPrice(0.02 ether) is mined, then p1's buyTicket{value: 0.01 ether}(d, 1, 1, true).
Expected from the quote: ticket bought at 0.01 ETH.
Actual: revert 'wrong ticket price'.
Then p1 buys at 0.02 ETH, owner sets 0.05 ETH, and p2 still buys at 0.02 ETH, confirming the price is fixed by the first ticket (test_priceFrontRunFirstTicket in test/scratch/Review.t.sol).
openNight() underflows (Panic 0x11) when block.timestamp < 12 hours on the local chain the constructor explicitly supportscontracts/SqueekPot.sol:90
The constructor allows chain id 31337 for local testing, but openNight() subtracts LOCK_OFFSET (43,200) from block.timestamp with checked arithmetic. Foundry and anvil start at block.timestamp == 1 unless warped, so on a fresh local chain openNight(), buyTicket() and changePicks() all revert with an arithmetic panic rather than a readable message.
Unreachable on Sepolia (timestamps are about 1.76e9), so no impact on the live deployment; it only makes the contract unusable in the default local environment without a warp and gives no hint at the cause.
Minimal fix: compute
(block.timestamp + DAY - LOCK_OFFSET) / DAY(identical result for every real timestamp, no underflow) or add a require with a message. Kept from the flow specialist; reproduced here.Chain 31337, vm.warp(1), pot = new SqueekPot(1e16, resolver).
Call openNight().
Expected: a night number or a readable revert.
Actual: revert with Panic(0x11).
With vm.warp(43200) openNight() returns 1 (test_openNightUnderflowAtGenesis in test/scratch/Review.t.sol).
No test suite in the repository: the timing, double-refund, reentrancy and conservation edges in AUDIT-SCOPE.md are not pinned by any test the author can reruncontracts/compile.cjs:3
State: a fresh checkout.
Input:
ls testand a search for forge, hardhat or mocha in any package.json.Expected: a runnable suite covering scope items 1 to 4.
Actual: no test directory exists and the only dependencies are the wallet/ and settler/ bots; contracts/compile.cjs compiles without testing.
The twelve-test scratch suite described above passes against the current source.
- Publishedaudit report