Ante v0.5 Zellic Audit Walkthrough
As a team trying to increase trust in a trustless space, Ante strongly believes in conducting regular internal and 3rd-party code audits as a part of any protocol’s comprehensive security stance. With quality audit slots massively backlogged earlier this year, though, we released our own internal audit of Ante v0.5 in late March. Well, you no longer have to take our word that Ante is SAFU — we’re excited to finally announce the results of our audit by Zellic!
About Zellic
Zellic was founded by members of the #1 ranked competitive hacking (CTF) team in ZA WARUDO. They gained significant attention when they teamed up with samczsun in investigating the Wormhole hack and are regularly called upon to investigate ongoing hacks.
Findings
The audit was conducted on the ante-v0–core repo, commit fdd0d8d68a5697415cde511aa5dc98c469871bb7
. 5 findings were reported, with 1 critical severity, 1 high severity, 1 medium severity, and 2 low severity.
Finding 1: Ability to force tests to fail with gas limit (Critical)
One quirk of the EVM is that by default, only up to 63/64 of the remaining gas can be allocated to an external call. This was implemented in EIP114 in order to reduce the attack surface area for call stack depth related exploits; however, Solidity also contains a quirk where try/catch reverts before the last 1/64 of gas is consumed. This means that there could be a scenario in which gas is manually set to an amount:
- low enough that the external call to
checkTestPasses
on the Ante Test reverts due to lack of gas - high enough that the overall
checkTest
call to the Ante Pool is still able to complete with the 1/64 remaining gas
An Ante Pool where this condition can be satisfied can generate a false test failure, incorrectly allocating staker funds to challengers.
Risk Analysis
Before you get too alarmed, all funds currently staked in Ante are SAFU!
To check if any of the currently deployed Ante Pools are at risk of being exploited, we checked current gas usage for each test’s checkTestPasses
call vs. the gas required to complete the outer _checkTestNoRevert
and checkTest
calls. Basically, if the cost of checkTestPasses
exceeds the cost of the remaining computations by over 63x, the exploit can be triggered.
Based on our gas analysis, the largest ratio reached is ~15x, with most tests falling in the 0.2–2x range. This means that even the most computationally expensive Ante Tests currently deployed have a safety factor of over 4x. Based on this, we believe that user funds in Ante v0.5 are not currently at risk from this exploit.
That being said, we have some advice for any Ante Test writers in v0.5:
- Keep it simple— this exploit becomes feasible when
checkTestPasses
is gas-intensive, so keeping the actual test logic simple makes it highly unlikely to exploit. - Check your tests— the minimum amount of gas used after the external call is somewhere around ~110,000 units. If the cost of
checkTestPasses
exceeds 63x that amount, or ~6,930,000 units, the exploit can be triggered. We’re adding a simple script to the community test repo that makes it easy for test writers to check gas usage for v0.5 Ante Tests.
The Fix
We evaluated a number of approaches for fixing this attack vector and decided that in Ante v0.6, we plan on implementing magic return values from the Ante Test in a way that allows the Ante Pool to distinguish between an intentional value and a revert due to out-of-gas error. This does mean that v0.5 Ante Tests remain vulnerable to this exploit, but as mentioned, we will be providing test writers with a tool to simulate gas usage of their tests to minimize the chance of an exploitable Ante Test being deployed.
Finding 2: Number of challengers is constrained by block gas limit (High)
When an Ante Test is checked and found to fail, the Ante Pool calculates the amount of challenger capital eligible for payout in an internal function _calculateChallengerEligibility
.
Notably, this was implemented with a loop through the array of challengers, meaning that the amount of gas used scales linearly with the number of challengers — basically a O(n) computation.
Since the gas in a single block is limited, if the number of challengers gets too high, you could perform a block stuffing attack and basically cause the checkTest call to fail. The result: an attacker can prevent an Ante Test from failing.
Attack scenarios:
- A staker realizes that a test is going to fail and uses this attack to prevent the
checkTest
call from triggering test failure until they have unstaked their funds - A challenger could prevent a test from failing while additional users continue staking the test, adding to the potential payout challengers receive once the test fails
This is a previously known issue as mentioned in our Docs and specifically excluded from the Ante bug bounty. However, Zellic was able to help quantify the feasibility of this attack vector. Zellic’s analysis found that the number of challengers required to hit the gas limit would be somewhere on the scale of ~3,000 challengers, costing a potential attacker roughly $60k USD in challenged funds and gas fees at the time of the audit.
The fix:
We instituted a maximum number of challengers that can challenge each Ante Pool. While it seems unlikely that any test would hit 800+ challengers in the near term, we are also paying particular attention in future versions of the protocol to performing calculations in constant time vs. linear time when possible.
The fix is implemented and is live in the v0.5 contracts on Avalanche and all subsequent chains (v0.5 Ante Tests deployed on Ethereum remain vulnerable to this block stuffing attack).
Finding 3: Bypassable minimum challenger stake (Low)
When challenging an Ante Test, we enforce a minimum challenger stake (0.01 ETH) to discourage malicious challengers from putting a trivial challenge stake in each pool in order to run generalized front-running bots.
However, we did not check that when a challenger removes partial challenged funds, the remaining funds still exceed the minimum challenger stake. Thus, a challenger could bypass the minimum challenger stake by staking the minimum amount, then withdrawing everything except 1 base unit (0.00000001 ETH).
This by itself is a fairly inconsequential bug, but when combined with Finding 2, it reduces the cost of a block stuffing attack on Ante to the previously stated $60k range.
The fix
We added a check on unstake that checks that the amount remaining after unstake is greater than the minimum challenge; otherwise, the entire challenged amount must be removed.
The fix is implemented and is live in the v0.5 contracts on Avalanche and all subsequent chains (v0.5 Ante Tests deployed on Ethereum still exhibit this bug).
Finding 4: Reentrant checkTest allows pool draining (Medium)
Because checkTest
does not prohibit reentrancy, an attacker could write a malicious Ante Test that makes an external call to an attacker contract. This could enable an attacker to call checkTest
on a failing test multiple times, claiming the 5% first verifier bounty multiple times and draining the Ante Pool. To be exploitable, an Ante Test would need to:
- be able to return
false
without reverting - not have a
view
orpure
checkTestPasses
function - call a function on the tested contract that then makes an external call to an attacker-controlled contract
No live Ante Tests satisfy these requirements and so no user funds are currently at risk for this particular attack.
The fix
We implemented ReentrancyGuard on AntePool and added the nonReentrant
modifier to all relevant functions in AntePool to close off this attack vector.
A brief aside on our philosophical approach to the possibility of malicious Ante Tests: the Ante team strongly believes that community-driven test writing is essential to reaching code coverage at scale. Where we can (e.g. implementing the nonReentrant fix) we try to implement checks/safeguards, but as an open-source protocol, we also can’t entirely prevent people from writing bad or even malicious Ante Tests. We do believe that Ante Tests’ simple nature makes it easier to understand and spot malicious tests. We are also exploring various ways to rate the quality of Ante Tests (e.g. beyond the minimum bar of “isn’t trivially true” and “works roughly as intended”, tests that are resistant to flash loan manipulation and minimize external attack vectors would be considered higher quality) in a decentralized and scalable way.
Finding 5: Late challengers receive no payout (Low)
Challengers that stake funds within 12 blocks of test failure are not eligible for payouts. Because AntePool only tracks the block number of the most recent challenge, this produces one minor edge case where if a long-time challenger happens to add additional funds within 12 blocks of a test failure, they will receive no payout, even for their older challenge.
The 12-block window was implemented in order to discourage generalized front-running (e.g. scenario where someone monitors for checkTest and is able to “steal” the 5% first verifier bounty)
We decided that the benefits of keeping the 12-block delay outweigh the low risk of a late challenger.
Conclusion
Code security is a never-ending process, and getting trusted 3rd-party audits should be a part of every responsible protocol’s security toolbox. The Ante team would like to thank the Zellic team for being super-responsive to work with and for their thorough auditing. You can read their full report here.
Also, we have some cool new stuff in the works, including Ante v0.6! Follow us on Twitter and join the Discord to stay on top of the latest Ante news. We are looking forward to continuing to work with such a high quality partner in Zellic!