
Capture the Funds Contest Continues!
Building on Success of the 1st Real-World DeFi Security Challenge
Our first DeFi hacking challenge — Capture The Funds, Certora’s Next-gen CTF — ran in late 2025 and, building on that success, we are announcing a relaunch of the contest with a twist: Capture the Funds - Endless CTF!
Capture the Funds aimed to give experienced security researchers a realistic environment to explore, exploit, and showcase deep technical skills. Instead of hunting for isolated bugs, contestants tried to steal value from a DeFi ecosystem we prepared for them. This gets closer to real-world security analysis, where the outcome is not just what is the bug but how clean is your exploit, how far can you push it, and how does one weakness cascade through the ecosystem?
With the help of our co-sponsors, the Ethereum Foundation, Coinbase, Aave, and Lido Finance, the four-week contest was a success, yielding high praise from the participants and some lessons learned that we’re excited to build upon. Read to the end for info about today’s re-launch of the challenge.
Security CTFs are great educational resources and ways to attract talent that thrives on tough, hands-on problems. We felt we were in a strong position to contribute something new here, given our experience across web3 security.
From the start, we had a goal: avoid a linear set of disconnected challenges. Instead, we wanted to build a coherent DeFi “world” that ethical hackers could explore freely, choosing their own path through protocols and attack surfaces. We wanted an open-world feel where instead of just following breadcrumbs in the code, you have to reason about a live system, forming hypotheses, and validating them through active exploitation.
We also wanted the scoring to reflect the medium. Rather than a binary checklist of bugs, scoring would use Ethereum’s native currency, naturally capturing degrees of success. Two contestants might find the same weakness, but rake in very different amounts of ETH depending on the efficiency of their exploits and how they chain actions together.
Lastly, we wanted realism. Most vulnerabilities were inspired by patterns we’ve seen in real audits, alongside a couple based on EVM and Solidity quirks that we think are worth knowing. The goal was not to be tricky or arcane for its own sake, but to be realistic, instructive, and engaging.
Orchestrating such a competition posed several technical and conceptual challenges:
We answered the first question gradually over the course of more than 6 months. Much of the time was spent tweaking the ecosystem such that both the protocols and the vulnerabilities would make sense. This means that any mechanism presented in a protocol must have an internal justification, and that all protocols must be interconnected in some meaningful way (e.g., the Auction protocol invests locked assets in the Lending protocol). Furthermore, in many CTFs (not pointing any fingers), the protocols tend to contain idiosyncrasies that serve no purpose other than allowing some vulnerability to exist. While such protocols surely have educational value, we instead used our considerable protocol audit experience to go beyond contrived examples. We chose protocols and mechanisms that you might encounter in the wild (where the REAL vulnerabilities are lurking in the dark).
Creating a mechanism to accept and verify submissions was mainly a design challenge. Unlike regular CTFs, a submission is not a “flag” string, but a logical sequence of operations from a given initial state, which is much harder to verify. A straightforward solution would be to create a separate blockchain for each participant, accessible by a web UI. While that would have some advantages (e.g., easy to track scoring and participation), it would have been too complex and computationally expensive. Instead, we decided to go with a composite approach: each participant runs a local instance of the blockchain and submits a “replay” file. We provide a local NodeJS server that makes interacting with the local blockchain easy. Every time the user tests an attacking contract, it is written down in a replay file, executed on the local blockchain, and they can see the effect on their ETH balance. Once the user is pleased with their local heist, they can submit the replay file to the CTF server, which spins up an ad hoc instance of the blockchain in the initial state, replays the attack, and scores the submission accordingly.
Finally, it was important from the start that participants enjoy the experience, as web3 security researchers reportedly prefer fun [citation needed]. The problem is that exploitation is almost always the opposite of fun. So, we devised many features of the local NodeJS server web UI to ease the pain: a convenient template for the attack contract, a Hardhat console and event logs, a visualization of the state of each protocol, and an exploration mode for testing exploit steps using freely minted assets instead of flashloans.
The competition lasted four weeks whereas most CTFs last just a few days. The first couple of days were hectic — an unintended vulnerability was discovered in one of the protocols, breaking some of the CTF internal logic. Luckily, we caught it early enough that the ramifications were minor. One week later, we found that some submissions exploited one of our bugs in an unintended way, making it more profitable than we anticipated. Since it was a nice exploitation, we decided to keep it as is. These two incidents have taught us an important lesson: It’s really hard to write secure code, especially if you make it insecure on purpose.
The rest of the competition went smoothly. It was interesting to watch the number of submissions increase dramatically in the last few days, hinting that many people delayed submitting to avoid giving valuable information to their competitors (i.e., that a certain score is achievable). The last 48 hours saw an onslaught of submissions, which caused drastic changes in the leaderboard. Just 30 minutes before the end of the competition, the winning solution overtook the previous leader by more than 100 points. More about the winners later.
The following write-up is intentionally incomplete: it does not include full end-to-end exploits for every issue in the challenge set, and some protocol sections are only covered at the level of root-cause analysis and attack direction. Look at it as a guide for constructing the exploits rather than a set of turnkey solutions.
Spoiler alert! If you want to find these vulnerabilities for yourself, skip to the Winning section below.
In Lottery, buying a ticket yields a random base prize and an optional “skill bonus” that requires solving , a standard number-theory task (see, for example, Alpertron’s QUADMOD).
The real red flag is the unnecessary boilerplate around the skill challenges: Dozens of public entrypoints named solveMulmodXXXXX, together with “audit fix” comments hinting that earlier versions contained backdoors. Lottery is deployed together with a LotteryExtension, and any call that does not match a function in the main contract is forwarded to the extension via fallback and delegatecall. Users are therefore lured to call the high-paying solveMulmodXXXXX functions in the extension.
What the “auditors” missed is that Solidity dispatch is purely selector-based: the selector is only 4 bytes (the first 4 bytes of keccak256("<signature>")), and the main contract resolves selectors before fallback is considered. Because the function names vary over a large space (the 5-digit suffix gives candidates), it is feasible to find collisions (e.g., Birthday Attack). When an extension function’s selector collides with a selector of any function implemented directly in Lottery, the call never reaches the extension—it is routed to the main contract instead.
This is exactly what happens to the most lucrative “skill” functions. For example, solveMulmod99781, solveMulmod99715, solveMulmod99437, and solveMulmod98752 in LotteryExtension all collide with function selectors in the main Lottery contract. A user who tries to solve one of these top challenges is silently dispatched to a different solveMulmod verifier in Lottery (with different parameters), so even a correct solution for the intended equation fails verification and the skill bonus is lost. In effect, the highest rewarding extension puzzles are unreachable; the best reachable high-reward calls are solveMulmod93740, solveMulmod90174, and solveMulmod89443.
In Auction, users can auction off NFTs (ERC721s) either via a regular (English) auction or a Dutch auction. Bidders do not pay directly from their wallets. Instead, they first deposit the chosen ERC20 into the AuctionVault and receive an AuctionToken, which represents a pro-rata claim on that vault balance (and is the unit that gets locked/unlocked during bidding).
The core bug is a type confusion enabled by ABI overlap between ERC20 and ERC721. Not only do both expose transferFrom(address,address,uint256) (interpreting the uint256 as an amount vs. a tokenId), they also both expose approve(address,uint256) (again, allowance amount vs. tokenId approval). Auction relies on interface types (IERC20 vs. IERC721) but does not enforce that the supplied token contract actually implements the intended standard. As a result, an attacker can pass an ERC721 where the protocol expects an ERC20 (or vice versa) and still satisfy the protocol’s approval and transfer flows, while changing the meaning of the uint256 argument from amount to tokeId (or vice versa).
In the first direction (ERC721 treated as ERC20), the attacker registers an NFT contract as the “underlying” asset for a new AuctionToken and then uses depositERC20/withdrawERC20. Under the hood, the protocol calls transferFrom() on the “ERC20,” so depositing/withdrawing actually moves NFTs whose tokenId equals the provided amount. Because the vault must have approved the AuctionManager for the relevant tokenId, this becomes particularly exploitable for NFTs placed in the vault via Dutch auctions, where the manager is approved at auction creation. The attacker can then withdraw specific tokenIds directly through the ERC20 withdrawal path.
In the second direction (ERC20 treated as ERC721), the attacker creates an “NFT auction” where the nftContract is actually an ERC20 token contract. The createAuction path transfers tokenId units of that ERC20 into the vault (since transferFrom exists), but no corresponding AuctionTokens are minted. So, the vault’s reported underlying asset increases without increasing AuctionToken supply. Because AuctionToken balances are scaled against the vault’s total underlying, this temporarily inflates the value of existing AuctionTokens. An attacker who holds AuctionTokens can withdraw more underlying during the inflation window, and later reclaim the injected ERC20 by letting the auction settle (back to themselves), leaving the vault net-drained.
The Exchange protocol lets users swap between ERC20 tokens through an unlock flow: during the unlocked window, the caller can pull out any assets held by the vault (via sendTo) and run arbitrary logic, as long as by the end of the call all per-token balances are “settled” back to zero. This effectively enables fee-free flash liquidity: you can borrow assets inside unlock, do whatever you want with them, and repay before the transient context closes.
The vulnerability is in SafeCast.toInt256(uint256 value). The routine is meant to reject values that do not fit in a signed int256, but it mistakenly allows value = 2^255 (i.e., INT_MIN when interpreted as int256) instead of reverting. As a consequence, calling sendTo with amount = 2^255 does not record a huge positive debt as intended; it records a huge negative delta (a credit/surplus) because the cast returns -2^255. Ordinarily, this call would still fail because sendTo immediately attempts token.transfer(to, amount), and transferring 2^255 tokens is impossible in practice.
The key is that one of the ERC20s, NISC, has a gas-saving optimization: transfer short-circuits when from == to (i.e., a self-transfer returns success without doing balance checks or state updates). By choosing to = address(ExchangeVault) the attacker can execute sendTo(NISC, address(this), 2^255) to mint an enormous “credit” delta without any real transfer occurring. They then use that credit to withdraw the entire real NISC balance from the vault with a normal sendTo(NISC, attacker, vaultBalance), and finally restore the delta to exactly zero by performing one more self-transfer for the remaining difference (2^255 - vaultBalance). Because all three calls occur inside a single unlock, the settlement check passes, and the attacker walks away with all NISC held by the ExchangeVault.
In LendingPool, flashloans are routed through a privileged FlashLoaner contract and are intended to be costly (10% fee). The intended invariant is simple: FlashLoaner.flashloan() snapshots its token balance at entry, transfers amount to the receiver, runs the receiver callback, and then checks that its post-callback balance is at least initialBalance + fee. It then repays the pools it drew liquidity from and treats any remaining surplus as the fee revenue.
The bug is that this accounting is balance-based and flashloan() is re-enterable. During a nested flashloan, the FlashLoaner balance already contains the principal from outer calls; because the repayment condition does not track per-loan liabilities, that outer principal is indistinguishable from “repayment surplus,” and is effectively counted as the fee needed for the inner call (and vice versa while unwinding). In other words, in recursive calls the protocol implicitly uses outstanding principal from other active frames to satisfy the initialBalance + fee checks.
The exploit is to recursively call flashloan() with geometrically increasing amounts (roughly scaling by , (~11× for a 10% fee), and only repay once at the deepest level. As the call stack unwinds, each frame passes its repayment check because the contract still holds principals from other frames, so the “fee” is paid using temporarily borrowed funds rather than the attacker’s own capital. The attacker ends up with (nearly) fee-free flash liquidity at the protocol’s expense.
In Community Insurance, liquidity providers earn rewards for providing coverage against bad debt in the Lending protocol. Rewards are tracked by an external RewardDistributor, and the insurance shares (an ERC20-like token) call into the distributor on every share transfer/mint/burn to “settle” rewards for the relevant parties using their pre-transfer free-share balances (and the current total free supply).
The critical design decision is that these reward updates are performed via an external call wrapped in try/catch with an empty catch block, so that a malfunctioning distributor does not block normal share operations. This turns reward accounting into a best-effort side effect: if the call fails, the share transfer still succeeds, but the distributor’s global index (lastUpdateTime, rewardPerTokenStored) and/or the user’s checkpoint (userRewardPerTokenPaid) are not updated.
A malicious user can force this failure reliably by gas griefing: submitting the transfer/mint/burn transaction with just enough gas that the external updateReward call receives too little gas under EIP-150’s 63/64 rule of gas, causing an out-of-gas error in the callee while the caller continues and the catch {} swallows the error. Once the attacker can selectively skip reward updates, they can desynchronize the distributor’s time/index accounting from share movements and later trigger an update/claim under attacker-chosen conditions (e.g., after manipulating free supply), extracting rewards inconsistent with the intended “continuous, pro-rata over time” model.
As contestants raced to the finish line to improve their ranking on the contest leaderboard, the scoring system performed as designed. The greatest gray hats rose above the great & the good. All their attacks were commendable and the best 3 were even rewarded with cash prizes. Two of those winners have generously written about their work, which you may want to study carefully: Bill (1st place) tells how he won in this article and shares his winning solution, and SpicyMeatball (3rd place) explains each of his exploits and the full attack in his solution repo.
Now, let’s see who can do even better.
Announcing the launch of Capture the Funds - Endless CTF!
To encourage more in the web3 security space to learn the lessons in Capture the Funds, we are taking submissions again with a $1000 prize for anyone who beats the target score by at least 25 ETH (since we’re looking for innovations beyond gas optimization). When that happens, the bar is raised and the prize is available again. And on it goes!
Be sure to explore & attack the latest code linked on the contest website. It is the original contest’s DeFi ecosystem, but … some vulnerabilities have been patched, others remain to be discovered, and there is a new leaderboard seeded with the Certora target score based on the exploits published so far. Read the latest Prizes & Eligibility rules and join our Discord server to discuss in the #capture-the-funds channel.
Our first Capture the Funds contest delivered a realistic and engaging DeFi security challenge. By crafting an interconnected set of protocols with vulnerabilities inspired by real-world audits, we moved beyond isolated bug hunting to promote deeper security analysis.
With the launch of the Capture the Funds - Endless CTF, we look forward to seeing more researchers explore and contribute to the collective knowledge of Web3 security. So, we invite you to learn the exploits of the best and try to push even further because, just like the pursuit of securing DeFi, … the challenge is endless.