top of page

No More Bets - How Ctrl+F led to breaking Polymarket's polling markets

The crafts of security auditing and bounty hunting are deeply interwoven. Very often a novel exploit idea discovered during auditing is replicated at scale to find ways it impacts live projects. Conversely, every successful bounty hunt concludes with new ideas packed in one's audit inventory, to be used when the moment presents itself.


Today we will showcase a bug in Polymarket found by a simple text search of an issue from one of our audits. We'll dissect the technical details and share the full disclosure process.


Intro to Conditional Tokens


Gnosis developed back in 2019 a library to support prediction markets - the Conditional Token Framework (CTF). The core idea is being able to create a complex tree of tokens, where each token node signifies some subset of choices. In the betting stage, users deposit collateral, mint a "full" token layer (representing all options), then trade the sub-tokens in external markets to arrive at their final position. In the redemption stage, an agreed-upon oracle assigns value to each betting outcome. This allows users to redeem an appropriate amount of collateral based on their bet. This level of superficial understanding is enough for now, more info can be found in the docs.



Audit of Butter Conditional Funding Markets


In January, we reviewed Butter's implementation which uses the CTF. Even before the audit, they addressed an issue which can be seen below:



The prepareCondition() call is crucial in CTF - it creates a new condition identified by (oracle, question, answerCount) tuple on the contract. After the call, users can call splitPosition() to get tokens of all outcomes of that particular condition.


An important detail is that prepareCondition() can only be called once on a specific condition:

The arguments are hashed to create the conditionId, and it is checked that the payoutNumerators is zero before setting it to an array of outcome size.


This behavior is a pretty serious footgun, because the protocol is permissionless and anyone can call prepareCondition() with any arguments. In other words, if a project uses conditional tokens by calling prepareCondition() without guaranteeing the condition doesn't exist, it can be attacked by preparing the condition directly. Then in the contract code path, the call will forever revert and lose the associated functionality.


Looking back to the changed lines in Butter - the added check in green addresses the issue by making sure the array is empty. The getOutcomeSlotCount() is just an accessor for the array length:


Finding Polymarket's CtfAdapter


Whenever we come across a bug class or functionality that's easy to get wrong, we consider whether it is easy to search across bug bounties. Here, it's as simple as looking for prepareCondition() in the assets folder and seeing what isn't wrapped correctly. Pretty quickly we find the UmaCtfAdapter contract by Polymarket.


As its name suggests, the contract mostly wraps the logic of integrating with the CTF. When Polymarket comes up with a new poll, an admin account calls initialize() passing the question details and parameters. The function saves parameters to storage and prepares the condition unsafely:


As explained, this entire functionality can be disrupted by observing the mempool for upcoming polls and frontrunning the condition creation. Since a question can't be initialized, it can't be settled through resolve(), because the storage is not set up. So, an attacker continuously blocking initialize() calls can in practice force no new questions to be available on Polymarket.


Note that the underlying blockchain here is Polygon, which has a public mempool and low gas fees, making it an ideal target for frontrunning.


The Disclosure


Since at this point our account was still doing time on Immunefi's suspended list (after the max approve == user error scandal), we've contacted Polymarket directly and were looking forward to a positive response.


The initial discussion was around securing a fair payout range. Their Immunefi program lists very few impacts in scope like theft or freeze of funds in-scope. Our impact of "long-term DoS of questions" doesn't fit, so we wanted to make sure we would be fairly rewarded for our time.


We've shared as much information as we could without giving away the exact details, but they didn't agree to commit to a range without seeing the report. We decided to proceed in good faith and shared the finding. To our surprise, they responded that the issue was already reported on Immunefi 3 months ago and they decided not to fix it.


we paid a $500 good faith bounty on this given it was 1. known already and 2. has prevention methods (polygon fastlane) and 3. didn't meet any criteria. 

We asked for a submission ID to confirm it is a duplicate, a standard practice in bounty platforms like Immunefi. They refused to provide it. We explained our rationale on why it is important to be fixed, and that counting on a centralized private mempool solution is not the way to go for a company that respects itself. Our detailed breakdown can be found here. They were very defensive about their commitment to security and dismissed our take.


The chat can be viewed below. To date we did not receive any further response.



Our take is that the team abused the fact it is not in the impact table to avoid a respectable payout, yet offered a $500 good will bounty to prevent the original reporter from going public. Also any future dups through Immunefi will be under the disclosure embargo.

Had they performed a proper fix by redeploying the UmaCtfAdapter, the case for the hacker to be properly rewarded a five-figure bounty would be too strong, so they opted for no-fix. Of course, that's very cynical since through the knowledge acquired from the report, they are able to prepare through use of private mempools, scripting, and even deploying a fixed CtfAdapter. This way they avoid the guaranteed delay they would have suffered had they only found out about the issue during an attack. They've also failed to document the issue as a known risk, simply hoping no one ever notices outside Immunefi. For those reasons, we view Polymarket's handling as shady and below the baseline expected from good will projects.



Bug Variant Analysis


The issue takes us back to the Permit frontrunning bug we disclosed over a year ago. The concept of disrupting function calls by executing a specific state changing sub-call directly is really powerful, and this is just one more example of that. To be fair, Gnosis certainly did not do a good enough job safeguarding developers from the risk. At a base level, there are no warnings around the issue nor any safe example in the documentation. Furthermore, we'd assert that any function call which has to be wrapped by a try/catch or additional checks, is indicative of bad design. It would have been far safer to simply return from the function if the condition already exists.




That's all for today! Follow us to be informed about more upcoming adventures in the Audit-Bug Bounty continuum.



About TrustSec


TrustSec is a world-renowned boutique auditing and bug bounty firm, founded by famed white-hat hacker Trust in 2022. Since then, we have published over 100 reports for a wide range of clients and received bug bounties from more than 40 projects. Composed exclusively of top competitive bounty hunters, TrustSec employs unique, fully collaborative methodologies to secure even the most complex codebases. Visit our website to learn why top names like Optimism, zkSync, OlympusDAO, The Graph, BadgerDAO, Reserve Protocol, and many others trust us as their security partner.



 
 
bottom of page