ImmuneFi Bug Report Analysis & Contract Re-Deployment
This explanation will involve a simplified description of how the protocol works, excluding details about origination fees, trading fees, and interest rates, to aid in the understanding of the security flaw.
On October 10th, the GammaSwap development team received a report via ImmuneFi of a potential critical bug in the protocol. After confirmation of the bug, we decided to halt the contracts and enter withdrawal mode only.
All outstanding loan positions were closed and an announcement was made immediately after. Identification of the attack vector required an in depth understanding of the mathematics behind GammaSwap since execution of the attack required exact calculations. However, the deterministic nature of the Ethereum virtual machine made the attack feasible enough that the GammaSwap Labs development team decided to pause the contracts to prevent the possibility of a nefarious actor discovering the attack vector. No funds were lost and no funds from liquidity providers are currently at risk.
In the spirit of full transparency and to further explain the logic of the protocol to help security researchers find more vulnerabilities I will explain in detail the bug found through ImmuneFi. The explanation will involve a simplified description of how the protocol works, excluding details about origination fees, trading fees, and interest rates, to aid in the understanding of the security flaw.
How GammaSwap Liquidity Repayment Works
As previously explained in descriptions of the protocol. GammaSwap works by withdrawing liquidity from an Uniswap V2 style CFMM, turning the writhdrawn liquidity into a loan, and holding the reserve tokens of the withdrawn liquidity as collateral to pay back the liquidity loan on a future date.
In this example a loan is created borrowing 100 liquidity units from the CFMM and using the reserve tokens of the liquidity units as collateral. No additional collateral was deposited to overcollateralize the loan, which is required in the smart contract, to simplify the explanation of the security flaw. However, such a position could be achieved with additional collateral as little as 1.5 units of either token A or token B for maximum leverage. We are also not accounting for potential origination fees when borrowing liquidity.
Since repayment happens at a future date and the borrowed liquidity belongs to a risky asset, the ratio of reserve tokens in the CFMM is likely to be different from the time when liquidity was initially withdrawn. For that reason, repayment of liquidity requires the rebalancing of collateral to the new ratio of the reserve tokens in the CFMM.
The loan from the “Borrowing Liquidity” example is fully repaid here after the price in the CFMM has changed to 2 units of token B. The price of the CFMM changes to 1.84 units of token B due to market impact of the collateral rebalancing transaction. We are assuming the trading fees during rebalancing were zero and the interest rate in the loan was zero to simplify the explanation.
This same logic can be utilized to perform partial repayments of liquidity debt. In this case only the amount of the collateral needed to perform the partial liquidity repayment is used.
In this scenario the loan is twice the size of the previous example, and the liquidity repayment is 50% of the loan (100 liquidity units) therefore the repayment calculations and its effect on the CFMM are the same.
The GammaSwap development team built GammaSwap with the intention of supporting as many ERC20 token implementations as possible. A holdover of 2020 DeFi summer is tokens with transfer fees. Tokens with transfer fees charge a fee when the token is transferred from one address to another. Therefore, when transferring tokens with transfer fees, the amount sent will always be greater than the amount received. Since GammaSwap must transfer the tokens to the CFMM to repay the liquidity debt, a transfer fee on a token might prevent the full repayment of liquidity debt.
However, there’s no way for the GammaSwap contract to know which token has a transfer fee or not, or what is the implementation of the transfer fee. For this reason the GammaSwap team created a transfer fee parameter that would specify an extra amount to transfer per token when repaying liquidity debt to cover the transfer fee of tokens with transfer fees.
A transfer fee parameter of 0.074 tokenA and 0.136 of token B is passed while repaying 100 units of liquidity, to ensure that the CFMM receives 73.64 untis of token A and 135.8 units of token B.
Transfer Fee Vulnerability
Despite several audits by different firms, a vulnerability in the transfer fee parameter of the repayLiquidity function was missed. The deterministic nature of the evm makes it possible to pass a specific number for the transfer fee parameter that makes the amount of one of the tokens held as collateral to send to the CFMM during liquidity repayment equal to the total amount of the token held as collateral while only repaying a portion of the liquidity debt.
Since the collateral is measured as the geometric mean of the collateral tokens, if one of the collateral token amounts is zero, the collateral calculation for the loan is zero. Therefore, the loan becomes uncollateralized if only one of the collateral token amounts becomes zero.
In addition, a change to the repayLiquidity function was made last August that wrote down liquidity debt during liquidity repayment if the collateral calculation was zero. The purpose of this change was to require borrowers that wish to repay their liquidity loans that held bad debt (debt > collateral) but were profitable to only fully repay their liquidity. The repayLiquidity function maximizes the amount of debt that can be repaid with collateral. So if collateral became zero, all liquidity debt that could possibly be repaid was paid. The aim was to ensure that loans were always fully cleared whenever they were repaid and not have to wait for payment through liquidation. The change passed all our fuzzing tests and our main auditor’s audit.
However, this feature combined with the ability to entirely spend the amount of collateral of one of the tokens through the transfer fee parameter, made it possible to force a loan to lose its entire calculated collateral and write down its entire debt, while leaving a substantial amount of the other collateral token in the loan.
The same scenario as the previous example. However, the borrower in this case is attacking the contract by passing transfer fee parameters that will force his collateral calculation to zero and cause a full write down of his liquidity debt while still leaving him with 114.63 units of token B. This same attack could’ve been performed even in tokens without transfer fees.
After the write down of the entire debt, the borrower/attacker would be able to withdraw the entire amount of the other reserve token as his profit.
Feasibility of the Attack
The attack was not found through fuzzing tests because the repaid liquidity function always rebalanced collateral (swapped with the CFMM) and the parameter to send in the attack would have to be accurate up to the last decimal of one of the collateral tokens. For most ERC20 tokens this is around 18 decimals.
In addition, although the right transfer fee parameter can be found through a mathematical formula, which is beyond the scope of this report, most programming languages will have trouble calculating the correct number up to the 18th decimal point without having any loss of precision (rounding issues).
Nevertheless, the right number can ultimately be found through trial and error, as demonstrated by the white hack team that found it. And such process can be automated and optimized to maximize the loss of funds. For this reason, the GammaSwap development team decided to pause GammaSwap until we fixed this issue.
In order to ensure that the liquidity debt is never written down to zero during repayment we removed the liquidity write down logic from the liquidity repayment logic. This means that a loan that has bad debt can only be closed through liquidation, even if it’s a profitable loan.
Nevertheless, merely restricting debt write downs to liquidations does not entirely safeguard collateral from being inadvertently lost during the liquidity repayments. This is because, while liquidity repayment no longer compromises LP fund integrity, the transfer fee parameter could still influence the use of collateral during liquidity repayment. Therefore, to counter this, we’ve also eliminated the transfer fee parameter.
The liquidity rebalancing function is designed to optimize the repayment of liquidity debt using collateral tokens. However, the presence of the transfer fee parameter could reduce the effective amount of collateral tokens for debt repayment, particularly in tokens without transfer fees. Even though, this no longer results in a write down of liquidity debt, allowing the transfer fee parameter could potentially expose the repayLiquidity function to unintended uses and, possibly, unanticipated attack vectors.
This doesn’t mean that tokens with transfer fees are no longer supported by GammaSwap. It means that an attempt to fully repay liquidity using the repayLiquidity function in a pool of tokens with transfer fees will not result in a full repayment of liquidity. A small amount would always remain, approximately equivalent to the amount of the transfer fee. This amount can still be paid through liquidation or repayment with the CFMM’s LP tokens through the repayLiquidityWithLP function.
Given the potential risks of fully supporting tokens with transfer fees and their current limited use in the Ethereum landscape, the GammaSwap development team decided to limit the support of tokens with transfer fees until more research is done.
The GammaSwap team initially planned to use immutable contracts to reduce the risk of hacks and security flaws in future updates. However, given the complexity and innovation of GammaSwap, they recognized the potential for undiscovered security vulnerabilities. This potential increases as complexity and code size increases. To mitigate these risks, the team conducted extensive research over two and a half years, including six audits by three different firms and several informal evaluations of its economic model. These efforts aimed to identify and prepare for all possible risks. Despite this, they acknowledge that while known risks can be accounted for, it is the unknown risks that are more dangerous as they cannot be anticipated or priced in.
“…there are known knowns; there are things we know we know. We also know there are known unknowns; that is to say we know there are some things we do not know. But there are also unknown unknowns — the ones we don’t know we don’t know.”
– Donald Rumsfeld
Therefore, the GammaPool and PositionManager are now upgradeable contracts that can be turned immutable in the future. This means that if a security flaw is found, we will be able to pause, implement a quick patch, upgrade, and resume service until a more robust solution is found for which we would upgrade again. This will allow us to prevent long periods of inactivity such as the one we’ve been experiencing since last October.
The purpose of the upgradability feature is not for the introduction of new functionality to GammaSwap on a future date. It is mainly to account for the ominous “unknown unknowns.” As time goes on and the contracts withstand the test of time, the upgradability feature will be removed in one last upgrade.
The issue we experienced in October is now fully fixed. We had the code re-audited by the white hack team that found the vulnerability.
In addition, we implemented extra checks on parameters to make sure that calculations do not produce numbers that are not compatible with GammaSwap’s design. We also added optimizations to lower gas usage and an improved mathematical library used during rebalancing transactions to handle overflows over the 256 bit limit for numbers in solidity.
The patch has made our protocol more robust and resilient. We have taken a conservative approach — building the original version of GammaSwap as early as mid 2021 and iterating since. We spent 9 months on Testnet with an Alpha and Beta as well as an incentivized Trading Competition. We also did a “soft launch” with conservative LTVs, permissioned pools and no incentives to reduce the risk of insolvency.
We took many of the right steps but in hindsight we could have improved our process by deploying a bounty program on ImmuneFi before launching on Mainnet.
A step we will be taking moving forward with future releases.
We are implementing a liquidity migration contract to easily port over liquidity from our current contracts to the updated contracts.
Once the migration contract is ready, GammaSwap will redeploy the new contracts.
Please stay tuned by following relevant announcements on Discord and X.