Percent Finance Incident Post-Mortem
Percent Finance is a community-owned fork of Compound, which uses Chainlink as its price oracle. These two key differences meant that Percent could innovate quickly, and carve its own path, while of course standing on the shoulders of the above two giants. Percent already succeeded in adding 15 tokens to its money market, all securely using Chainlink prices, and there are plans for more. Percent community members also did research into interest rate models, with the results of these labours being a plan to use 6 different models across its 15 current assets.
Taking a step back, Percent Finance was created by the anon developer @PercentFinance. Their goal was always to gradually hand over control of the website to its community, thus making it truly community-owned. This happened throughout October, with multi-sig members, that were voted in by and from the community, taking the lead in adding the extra token markets and creating the interest rate models. Finally, @PercentFinance announced their departure to the multi-sig community members on the 3rd of November, expecting for them to announce it along with a product roadmap and developer hires that were being worked on to the wider community.
The results of those discussions between the stepping down lead dev and multi-signers were due to be announced to the community (including @PercentFinance’s departure) in a detailed article on November 4. One key topic was the need for a Solidity developer to take over as soon as possible as lead dev. In the interim, 3 of the multi-sig members had exposure to Sol and could carry out operations.
On the morning of Nov 4, we executed the 15 transactions to swap interest rate models for the 15 assets. These transactions were deemed innocuous enough to be handled by the interim developer (myself, @vfat). There were very minor changes to these contracts compared to the old one, the main thing was adjusting the interest rate parameters. The rates are provided by these contracts using their `getSupplyRate` and `getBorrowRate` functions, which return a single uint value. They were based off the JumpRateModelV2 contract, listed here Deployed contract addresses · percent-finance/percent-dev Wiki (github.com)
Unfortunately there was a major incorrect assumption by myself, that all 15 tokens used the same interest rate model, as it was the only one listed on our Deployed Contracts page. I was personally familiar with the latest 7 as I had taken part in deploying them, but I had not double checked the previous 8. What was actually the case was that the contracts for pUSDC, pETH and pWBTC, used pre-existing interest rate contracts by Compound, as @PercentFinance had deemed that Compound’s interest rate parameters would be sufficient for these assets. Because these were old contracts, they were not listed in the “Deployed Contracts” Wiki (not being newly deployed).
The problem was that these old interest rate contracts have different signatures for `getSupplyRate` and `getBorrowRate`. They return 2 uint values, the first one being an error code. So, after the swap, they were unable to call these functions on the new interest rate contracts, as the signatures do not match. Making the problem worse, these functions are checked before every interaction with these contracts (supplying, borrowing, redeeming, repaying, etc). They are also checked before changing the interest rate contract again. So, because the current interest rate contract does not work, it is impossible to change to a new one.
This meant that these 3 contracts were no longer usable, and the user funds in them were permanently locked. These amounted to: 446,813 USDC, 28 wBTC and 313 ETH. I attempted to contact both Circle and BitGo to see about possibly retrieving the USDC and wBTC, but both avenues were dead-ends. I was not able to get an official response from either, however I was made aware that Circle does not offer this functionality, at least currently, although they might do in the future. They only respond to requests from law enforcement. BitGo on the other hand only offers this functionality for funds send to the wBTC contract, but no other contracts.
The missing wBTC and ETH had not been borrowed for the most part, so the rest of the markets did not depend on them. USDC however provided a bigger challenge. After accounting for wash lending (people who maximized PCT farming by leveraging their supply/borrow), there was a total of approximately $1.6M supplied and $1.15M borrowed. The chief borrower was one person who had deposited 148 YFI as collateral. When the incident occurred, the price of 1 YFI was $8600.
So, besides users losing the 446k USDC, we had to get the market operational again, so that the YFI depositor could repay their USDC loan and withdraw their YFI collateral, and the USDC depositors could then withdraw their $1.15M USDC that was not lost. A plan was formed over the next couple of days to replace the broken contracts with new ones, by applying an on-chain haircut: 27% for USDC (as there was $1.15M still owed and $445k missing), 95% for ETH and 100% for WBTC.
As we did not have a professional Solidity dev, this task fell on the multi-sig members with Solidity exposure. We had to make sure to get this right of course, so it had to be done carefully and tested thoroughly before deploying. We managed to do this using Hardhat, in the end deploying 6 new contracts and executing the market replacement functions, all of which worked correctly. Being inexperienced, this process took us 16 days, and was completed on November 20.
Some of the USDC suppliers had taken a hedge against their collateral by borrowing YFI, which was available as the YFI depositor couldn’t repay their loan and withdraw their collateral. This was possible due to the multi-sig’s decision not to freeze borrows, which seemed like the fairest option at the time.
This was done while YFI was hovering around the $10k mark. The users borrowed 72 YFI in total. The YFI depositor was able to deposit DAI to use as collateral and withdraw the remainder of the YFI, which left the YFI market at 72 supplied 72 borrowed. 65 of these belong to user 0x3E, 6 to user 0x35, 0.70 to user 0xda and 0.48 to user 0x08.
In these 16 days YFI shot up in price nearly 200%.
This meant that 3 users who had borrowed YFI (0xF9, 0xD9 and 0xA4), were now exceedingly in profit against their deposited USDC. Conversely, the YFI suppliers, who were originally not affected by the missing USDC, were now taking substantial losses. In addition, all the other USDC depositors are now facing a 100% loss on their capital as opposed to 27%.
The current situation is that the total user losses are $1.9M, circa $1.1M of which is to the broken smart contracts, and $800k to the 3 users who borrowed YFI.
This concludes the analysis of the incident, we are now on to the task of planning our next steps. The idea behind Percent is rock solid, and there is no reason it can’t continue, albeit with much better practices in play. There is on-going discussion in the Discord #path-forward channel, as well as the Percent Governance Forum.
I will propose that at minimum everyone is compensated when we do our re-launch, either through the issuance of new tokens, or through future earnings, or both. This includes the profitable accounts, if they were to repay their profits. This would of course be put to a vote among Percent holders, but I am certain it will be approved.
I would like to get everyone in the above table, including the accounts that are in profit, to reach out to me or one of the other multi-sig members via our Discord Channel, our Twitter account, or email us at email@example.com and discuss their particular situation.
The blame for the incident lies squarely on myself, so I would like to extend my personal apology to all the affected users, and my promise that I will strive to recoup as much of everyone’s losses as possible.
Vasilis Fatouros (@vfat, vfat.eth) and the multi-sig team