The Defly wallet app grew out of a fascination with automated market makers (AMM). Most of the current decentralized exchanges (DEXs) are based on this idea. They are extremely elegant in how they can replace centralized order book exchanges with a relatively simple smart contract and a liquidity pool.
In late March we started integrating the HumbleSwap AMM into Defly. We were very excited about this new DEX and pushed hard to have full support in our app. After all, providing a great user experience is one big factor holding DEXs back. This is when we noticed some curious behavior and, upon further investigation, a vulnerability. Now that HumbleSwap was taken offline and all funds were secured, we believe it’s time to shed some light on what has happened.
The root problem turned out to be in HumbleSwap's smart contract. It uses an incorrect formula to compute the number of LP tokens that a user gets when providing liquidity for a pool (the LP tokens describe the user’s share of the liquidity pool). Interestingly, this formula is correct if a user adds equivalent amounts of both assets according to the current exchange rate (exactly how it should be). However, this vulnerability can be exploited if an attacker provides liquidity in an unbalanced way. In this case, the attacker receives too many LP tokens in relation to the new pool ratio (after the token minting). This allows the attacker to later exchange the LP tokens for a profit, particularly when the ratio was moved significantly. We provide a technical deep dive into the vulnerability at the end of this article.
From finding the vulnerability to our first conference call with Reach, the company behind HumbleSwap, it took less than 24 hours. We were able to ping them through a backchannel and reach them on a Saturday afternoon. From our point of view, we understood this as a problem for the entire Algorand ecosystem and wanted it handled as best as possible. We also want HumbleSwap to succeed and elevate Algorand with their services. Reach came through and responded quickly by taking our concerns seriously, notifying their users, and scheduling a relaunch.
Generally speaking, bugs and vulnerabilities happen and need to be managed as best as possible. For anybody moderately familiar with software development this should not come as a surprise. Programming is hard. The field of computer science has large parts dedicated to improving code quality and reducing unexpected behavior. There is a tremendous amount of work going into this. Still, in the end some code will fail under some circumstances and will have to be handled by additional strategies.
This is where bug bounty programs can make a difference. Having seen a fair amount of this class of bugs, we are increasingly in favor of these. HumbleSwap and all other DEXs on Algorand are audited by security professionals and this adds significantly to the security of the Algorand ecosystem. Yet, software that runs crypto can benefit from even more mitigation strategies. One specific challenge is that it uniquely attracts bad actors because of the potential to drain funds. We see two scenarios where bug bounties are helpful. One is to incentivize people who coincidentally find vulnerabilities to report them. The second one is to actively motivate professionals to seek them out. The latter could be understood as an ongoing security audit by expert volunteers.
A question that comes up a lot is how much a critical vulnerability report is worth. In the case of HumbleSwap, it could well be the determining factor between success and failure. Additionally, a worst case scenario (users losing funds, skewed exchange rates) does not stay isolated to one DEX. A spiraling exchange directly affects the entire ecosystem through arbitrage bots. When surveying other major projects (Algofi, Tinyman, Pact) a critical bug report is rewarded with 100k-200k USD. At first this may seem a lot but in the context of potential damage averted this is well within reason.
Defly will not be an exception in regard to bug bounty programs. Having been very close to HumbleSwap's but also Tinyman's relaunch has solidified our plan to offer rewards for vulnerability reports. We are going to launch our own official program once our security audit with Kudelski (which is ongoing) is complete. In the meantime, we want everybody to understand that we honor security-related bug reports and encourage our community not to hesitate when it comes to reporting any unexpected behavior in our app.
For the record, Reach and the HumbleSwap team have been very responsive and their handling of these issues has been exemplary. We are sure they will have a strong rebound with the upcoming relaunch of their DEX. We have reason to believe they will also introduce a bug bounty program and have confirmed a bounty to the Defly team. To all the devs out there, stay vigilant and don't underestimate the potential of small discrepancies to be exploited in this high-stakes field.
Appendix: HumbleSwap Vulnerability
Minting LP tokens in HumbleSwap exposed a vulnerability that allowed attackers to steal users’ funds. In the remainder of this analysis we refer only to HumbleSwaps’ vulnerable smart contract that has been deprecated now. The root problem is that the responsible smart contract applies an incorrect formula to compute the number of LP tokens that a user receives when adding liquidity to a pool. What makes this vulnerability interesting and hard to spot is that for correct user inputs (adding liquidity in a balanced way) this formula is indeed correct. The problem surfaces when an attacker adds liquidity in an unbalanced way. Then the formula returns more LP tokens than the attacker should receive and this can ultimately be exploited.
Minting LP Tokens
The formula that HumbleSwap uses to compute the number of LP tokens is the average between the number of LP tokens received for adding only asset A or only asset B. Mathematically, this formula can be described as follows. We denote by HA and HB the pool’s holdings of asset A and B, respectively. Additionally, we call the number of issued LP tokens ILPT. Minting LP tokens can be seen as a function mint(IA, IB) that takes as input the amounts of asset A and B that the user adds to the pool, denoted IA and IB, and returns the number of minted LP tokens:
Let’s take this formula apart. IA/HA denotes the user’s share of asset A with respect to the holdings HA of asset A and this share is multiplied by the number of issued LP tokens ILPT. The resulting number of LP tokens represents the user’s share of all LP tokens. The same is done for asset B and finally the formula computes the average of these numbers.
Let’s consider an example with the following pool holdings:
Holdings asset A (HA): 8
Holdings asset B (HB): 2
Issued LP tokens (ILPT): 4
The current pool ratio is 4:1, meaning that four tokens of asset A trade for one token of asset B. Users are expected to add liquidity in the same ratio. For example, when a user, let’s call her Alice, adds four tokens of asset A and one token of asset B she obtains two LP tokens since
Notice how the numbers of LP tokens for adding asset A and asset B are both two and therefore the average is again two. This is because Alice added tokens in the appropriate 4:1 ratio. The pool ratio after minting remains unchanged: 12/3 = 4.
Malicious Mints
HumbleSwap is vulnerable if attackers mint LP tokens in an unbalanced way. In the most extreme case, an attacker can add an arbitrary amount of one asset and close to zero of the other asset (actually, the smallest possible amount is one base unit of an asset, e.g., 0.000001 ALGO). We call this unbalanced minting of LP tokens a malicious mint.
The problem is that a malicious mint skews the pool ratio and the number of LP tokens that an attacker receives is computed based on this skewed ratio (and not the pool ratio that existed in the LP before this malicious mint). In other words, an attacker skews the pool ratio in her favor and uses the minted LP tokens to make a profit.
Consider again the same example with HA=8, HB=2, ILPT=4. This time, an attacker, let’s call him Mallory, performs a malicious mint of four tokens of asset A and the smallest possible non-zero amount of asset B (which is 0.000001 assuming that asset B has six decimals):
Mallory got only half the LP tokens in comparison to Alice (1 instead of 2), which looks correct given that Mallory provided only half the liquidity (he provided the same number of tokens of asset A, but no tokens of asset B). This is, however, not correct because the malicious mint has skewed the pool ratio, which is now 12/2.000001 = 5.999997.
Malicious Minting for a Profit
To make a profit, the attacker maliciously mints LP tokens and immediately burns them again (and this process can be repeated multiple times for greater profits). Burning LP tokens exchanges them for equivalent amounts of assets A and B proportional to the number of issued LP tokens ILPT.
Mathematically, burning LP tokens is a function burn(IL) = (OA, OB) that takes the number of LP tokens (IL) as input and returns the number of tokens of asset A and B that the user gets (OA and OB). These numbers are computed as follows:
The ratio IL/ILPT denotes a user’s share of the liquidity pool and multiplying it with HA, respectively, HB returns the number of tokens that the user owns of the respective pool holdings.
We continue the previous example after Mallory has maliciously minted LP tokens. The pool’s state at this point is HA=12, HB2, ILPT5. Mallory burns his one LP token and obtains:
Initially, Mallory started with 4 tokens of asset A and after one malicious mint and burn he now has 2.4 tokens of A and 0.4 tokens of B. If he swaps the 0.4 tokens of B into tokens of asset A at the current exchange rate he receives approximately an additional 2 tokens of asset A, bringing him to a total of 4.4 tokens of A. In other words, Mallory had a 10% ROI since he went from 4 tokens of asset A to 4.4 tokens of asset A with one malicious mint, a burn, and a swap.
Had Mallory been more patient, he could have maliciously minted the 0.4 tokens of asset B that he received earlier and repeated this process. This would have increased his profits.
This attack vector is most profitable if the attacker has a large holding of one asset and is able to skew the pool ratio sufficiently in his favor. The same vulnerability, however, opens another attack vector that allows an attacker to swap without paying fees.
Feeless Swapping
The HumbleSwap vulnerability can be exploited to implement feeless swaps and ultimately steal from liquidity providers. Typically, traders pay a fee for each swap that they perform on a DEX (e.g. 0.3% on HumbleSwap) and this fee is handed over to the liquidity providers (e.g., HumbleSwap retains 0.05% of the fees and gives liquidity providers the remaining 0.25%). The accrued fees are the only direct revenue stream that poolers get from providing liquidity. Were there no fees, liquidity providers would necessarily lose out due to impermanent loss. Therefore, fees are important for the health of an AMM and yet an attacker can exploit the vulnerability to pay no fees.
Feeless swapping can be implemented as repeated malicious minting and burning. Here the attacker exploits that mints and burns require no fee. If an attacker (Mallory) wants to swap X tokens of asset A for tokens of asset B, he proceeds as follows. He maliciously mints all X tokens and burns the resulting LP tokens again. With this, he gets roughly 0.5*X tokens of A and an equivalent amount of asset B back. He then maliciously mints and burns the 0.5*X tokens again, receiving roughly 0.25*X tokens of A and an equivalent amount of asset B. He repeats this process a couple more times and each time the number of tokens of asset A are halved. At some point, when the number of tokens of asset A is small enough, he simply swaps the remainder for asset B. In this attack, Mallory only paid the network fees (except for the last swap) and those are negligible on Algorand.
Comparison to Other AMMs
A simple fix for this vulnerability is to reject all malicious mints that add liquidity in an unbalanced way, but this would lead to many rejected transactions even for benign mints from ordinary users. The problem is that unbalanced mints happen all the time in practice (but not to such an extreme extent that we described here) because from the time a user signs a mint transaction to the time the transaction reaches the blockchain, another user can already have made swaps that slightly changed the pool ratio.
There are two basic solutions to this vulnerability that have been implemented in practice: pool donations and redeems. Both solutions return LP tokens only as if liquidity was provided in a balanced way. The difference is how they deal with the excess amount.
Pool donations simply add the excess amount to the pool’s holding and consider them a donation for which a user gets no LP tokens. This approach is implemented by Uniswap and Tinyman (if you compare Tinyman’s formula to compute the number of LP tokens and HumbleSwap’s you can see that Tinyman uses the min function instead of the average function and this fixes the vulnerability).
Redeems return the excess amount to the user. This approach is implemented by Algofi and Pact through inner transactions.