Octane Security Sinks a Three-Pointer for ScorePlay

ScorePlay is a sports prediction platform that leverages AI for personalized strategies, oracles for result resolution, and blockchain for payouts. Decentralized prediction platforms have an inherent edge over traditional sites, with provably fair resolution and trustless payouts, but maintaining this edge in the eyes of customers requires serious security, transparent odds, and provably fair resolutions.

Get new posts & updates straight to your inbox
By subscribing you agree to with our Privacy Policy.
Thank you for subscribing!
Oops! Something went wrong while submitting the form.
Screenshot of the UI of the Octane platform
Analyze your code

Octane Security Sinks a Three-Pointer for ScorePlay

ScorePlay is a sports prediction platform that leverages AI for personalized strategies, oracles for result resolution, and blockchain for payouts. Decentralized prediction platforms have an inherent edge over traditional sites, with provably fair resolution and trustless payouts, but maintaining this edge in the eyes of customers requires serious security, transparent odds, and provably fair resolutions.

Attack Surface

With real money on the line and a complex combination of technologies, ScorePlay’s potential attack surface is large. Even a small bug in its Solidity code could throw off the intended odds, trap user funds, or let the house take an unintended cut.

As part of a comprehensive approach to smart contract security, ScorePlay integrated Octane’s AI-powered offensive security engine into its development pipeline. Octane uncovered one critical vulnerability and provided two best practice recommendations.

One of these findings was missed by an industry-leading manual audit firm, underscoring the value of Octane’s deterministic code review engine. It systematically applies every detection pattern across the entire codebase so that any recognized vulnerability is always flagged. No coffee breaks, no blind spots.

Octane not only found these issues but auto-generated fixes for each, making it easy for ScorePlay’s developers to integrate the patches and secure the platform’s contracts.

Summary of Findings

#1: Incorrect State Update Check – (Severity: Critical)

Vector: ScorePlay’s payout logic contained an accounting flaw. When resolving a prediction market (resolveCondition), the EnhancedSportsPrediction contract would calculate the house’s fee (a percentage of the total pool) and add it to a cumulative totalFeesCollected. But due to a logic error in certain scenarios, it would add this fee twice.

Specifically, the code added the house cut once unconditionally, and then again depending on whether there were any winners. If no one predicted the winning outcome, it then added the entire pool, so the house received totalPool + houseCut. This sums to more than 100% of the pot. If there were winners, it added the house’s fee a second time on top of the first.

Essentially, the protocol’s math was giving the house an unintended extra share of the winnings. Picture a dealer at a casino who skims two rakes from the poker pot instead of one. The house ends up pocketing double the fees, while the players are stiffed on their payouts.

Impact: This financial bug corrupts the contract’s accounting in favor of the house (the contract owner). The owner could withdraw significantly more fees than they actually earned, draining the pot of funds that should have been reserved for future payouts. While early claimants would receive their full payout, those who claimed later might find the pool underfunded, leading to partial or failed payouts. If unaddressed, this flaw also poses an inadvertent compliance risk: the platform could be seen as defrauding users by taking more fees than disclosed upfront.

Fix: Octane’s automated fix simplified the fee accounting logic to ensure the house’s cut is only applied one time per outcome resolution. Now, totalFeesCollected is updated only in the conditional branch that applies: if no one wins, the house takes the entire pool, if there are winners, the house takes only the calculated houseCut. The unconditional totalFeesCollected += houseCut; line was eliminated.

#2: Unbounded userOutcomes Loop in claimRefund() – (Best Practice)

Vector: The claimRefund() function iterates through userOutcomes[msg.sender] without bounds or batching. This means the function could stall if a user had placed predictions on an excessive number of outcomes (that failed to resolve or were later voided, entitling them to a refund). As the code attempts to loop through all of a user’s recorded outcomes in one go, it risks running out of gas and reverting the transaction. This is an edge-case scenario, but one worth flagging.

Impact: A determined attacker could knowingly predict hundreds of outcomes. Then, when they go to claim a refund, the sheer length of the loop would exhaust the block gas limit, resulting in a perpetual failure. In short, one user’s heavy predictions could grind the refund logic to a halt. This would freeze not only their own funds but would also strain the entire platform as the refund contract repeatedly tries and fails to execute the bloated refund.

This all-or-nothing loop also means a single problematic outcome (e.g. a token transfer failure or a storage inconsistency) would abort the whole refund process. Users would quickly lose trust if word spreads that refunds are unclaimable due to a system error.

We can compare this to a perfectly solvent bank that only allows withdrawals during a brief 10:00am-11:00am window – and each customer’s teller must count every single check they’ve ever deposited before handing over any cash. If you’ve deposited more than a handful of checks, the teller simply never finishes counting before the window closes. Rumors spread that “the bank can’t pay,” and customers panic, sparking a bank run even though the vault is full of cash. The only real issue was that the withdrawal process was too inefficient.

Fix: Octane recommended refactoring claimRefund() to process refunds in manageable batches instead of one giant loop. The patched function now accepts start and end indices, so refunds can be claimed over multiple transactions. For example, a user with 100 outcomes could refund 20 at a time over 5 transactions. The code now checks that the range is valid: start must be less than end, and both must fall within the bounds of the userOutcomes array. It only loops from start to end, accumulating the refundable amount in each batch. 

Crucially, the contract no longer deletes or clears the userOutcomes list until all outcomes have been processed in the final batch. If a batch doesn’t cover the entire list, the remaining outcomes stay for subsequent calls. This ensures no outcome is skipped or double-refunded. In effect, the new design splits the refund workload across transactions, preventing any single call from running out of gas and ensuring users can always reclaim what's theirs, no matter how many predictions they’ve placed.

#3: Unbounded winningOutcome in resolveCondition() – (Best Practice)

Vector: In resolveCondition(uint256 winningOutcome, ...), the contract checked timing (ensuring that the prediction period had elapsed) and that the condition wasn’t already resolved, but it did not check that winningOutcome lay between the condition’s minOutcome and maxOutcome. An oracle could call resolveCondition(matchId, 9999) even if the valid outcomes were, say, 0-10. The absence of a range check meant the code would proceed to set condition.winningOutcome = 9999 and mark the condition resolved, without any immediate error being thrown.

Impact: This vulnerability could be abused to invalidate an entire market’s predictions. If a rogue (or mistaken) oracle resolves a condition with an outcome that no one predicted (because it wasn’t a valid choice), the contract would find that “no one won.” According to the logic, if winningPool == 0 (no winning predictions), the house takes the whole totalPool as fees. Thus, the attacker could force a “house win” scenario at will, stealing the pot from rightful winners. Even if not exploited maliciously, this bug could also occur accidentally due to oracle error, in which case all users who predicted legitimate outcomes would be unjustly burned – they’d lose their stakes despite an actual winner existing, simply because the winning outcome was recorded incorrectly.

This is like the NBA commissioner declaring the Miami Heat the winner of the Western Conference Finals, even though they play in the Eastern Conference and never actually competed in the series. Every fan who predicted the Nuggets vs Timberwolves final loses their money, and the bookie keeps the entire pot. By allowing an outcome that was never in play, the system creates a scenario where the house always wins, unfairly nullifying all real predictions.

Fix: The fix was straightforward: validate the outcome ID before using it. Octane’s suggested patch added a guard right after the existing time and status checks: the winningOutcome must be >= condition.minOutcome and <= condition.maxOutcome. If not, the function reverts with an InvalidOutcome error. This ensures that only legitimate outcome values can resolve a prediction. The rest of the function remained unchanged. Once the check passes, resolution proceeds normally, distributing rewards or fees as intended. 

Octane closed the loophole: now no outcome can be declared unless it was explicitly part of the original game. ScorePlay users can rest assured that a prediction will never be decided on “phantom outcomes” and that if they pick a winning option, the contract can’t cheat them (either inadvertently or maliciously) by misreporting the result. It’s an unlikely scenario, but one worth protecting against.

Stack the Deck Securely in Your Favor

Using Octane, ScorePlay easily identified and fixed multiple issues in its smart contracts before they could impact the platform or its users. By simulating smart but malicious adversaries 24/7, Octane catches bugs that traditional audits can (and do) miss.

In each case, Octane not only flagged the problem but provided a precise and effective patch, enabling ScorePlay’s team to remediate within hours. The end result is a fair, functional platform where users’ funds and winnings are safe from both malicious exploits and unintentional errors.

“Just like the matches they predict, ScorePlay users expect the rules of the platform to be enforced exactly as written. Ours just happen to be written in code. Octane’s AI scans every commit, so we spend less time chasing down bugs and more time delivering results.”
— João ‘JohnDoeBlocks’, Tech Cofounder at ScorePlay

Whether you’re launching a prediction market, a lending protocol, or any novel form of dApp, playing offense on security is a smart bet.

Book a demo today and stay a step ahead of attackers.

Faq

Contents