etherscan.io/address/0xf42c…
34 Million USD gone. Just like that. Locked in the contract forever.
A lot of people put light on the grieving which locked processRefunds() for a bit, that was the first exploit.
Luckily that was unlocked, but funds are still locked forever. How?
🧵 1/
2/ Let's take a look at an overview.
People took bids. It stored their data. Then, they were eligible for refunds.
By calling processRefunds(), a loop is made according to a refundProgress counter which then does the refund.
3/ This was the cause of the more-so-well-known exploit of a griever contract that can call the bid function (because they did not disable contract calling) which as a fallback that fails.
In short, someone could have bid and broke the processRefunds() by bidding from a contract
4/ This was exploited and done. An example had been posted on GitHub as proof of concept.
Also another person sent some on-chain messages to prove that this was the case, and to invest more in contract auditing.
etherscan.io/tx/0x3ded3a94e…
5/ Essentially what this does is:
Bid with a malicious contract that fails on fallback when receives ETH.
This makes the function fail, and stops the "chain" of refunds from continuing.
6/ If you decompile the bytecode, they have a trigger on a fallback to either enable the stuck or disable it.
require (uint8) is probably a trigger to enable or disable the failing of receiving ETH.
Nice touch! That means it can be unstuck.
7/ As for a solidity readable, it was probably something similar to this.
If block, fail the receivable fallback. Otherwise, let it succeed.
Demonstration was written by @notchefbob which tried to notify the team of the issue.
@notchefbob 8/ Thus refunds AND withdrawals were stuck in a limbo, under the power of a single contract. Such power.
No refunds were able to be made, and in addition to that, the claimProjectFunds() logic of the owner required that all refunds were made first, before they can withdraw.
@notchefbob 9/ Funds were getting stuck and refunds were failing.
That would wrench any project owner's gut.
@notchefbob 10/ With the gods in favor, the anonymous contract deployer had unstuck the contract and processRefunds() worked again. He even sent a little message to let people know it was a demonstration. Invest in devs. Invest in security.
etherscan.io/tx/0x2f667bb69…
@notchefbob 11/ Crisis averted!.... Or so they thought...
Now, this next part is truly painful.
@notchefbob 12/ Process Refunds started working again and people were getting their ETH back. However, there was a second exploit in the code.
Refunds work. Emergency withdrawals work.
etherscan.io/tx/0xd62f044cf…
However, the team will never be able to withdraw their ETH. Ever.
@notchefbob 13/ As a developer this is gut wrenching to even type...
A require of refundProgress >= totalBids was made
The assumption is that all refunds has to be processed first before withdrawing. It makes sense in their logic, and crisis was averted.
However...
@notchefbob 14/ We take a look at the _bid() function which takes in two arguments:
uint8 amount
uint256 value
@notchefbob 15/ Bids are stored in an index and that index is linked to the user. This is to store their bidding data for refunds.
There are a total of 5495 items for auction thus index "should" increase accordingly, based on their withdraw logic.
@notchefbob 16/ However, taking in an argument of
uint8 amount
but always incrementing the index by 1 (++) is the devil here.
After the mint-out, the bidIndex only went up to 3669, this is because of multi-mints in a single TX.
@notchefbob 17/ In processRefunds() code, there is a require statement that
requires _refundProgress < _bidIndex
this essentially means that _refundProgress can never be above 3669.
@notchefbob 18/ Looking back to claimProjectFunds(), there is a require statement as well.
require(refundProgress >= totalBids).
This means as long as totalBids is higher than refundProgress, project cannot withdraw their funds.
@notchefbob 19/ However, if you take a look at the value of totalBids...
It is 5495.
3669 will never be higher than 5495, which means this function is stuck. Forever.
@notchefbob 20/ A summary
Exploit 1: processRefunds() able to get stuck
Exploit 2: bids count did not increment correctly with mint amount
Exploit 3: withdraw requires bids count to increment correctly
Final Caveat: funds stuck forever.
@notchefbob 21/ I would like to make some ending remarks but it's hard to find the words.
Devs, and Artists, run the NFT space.
I would suggest to never skimp out of them.
Good devs know and will demand their worth.
Invest in audits. Invest in security.
@notchefbob 22/ I would never wish this upon anyone.
It is truly gut wrenching and I am really sad to see this happen.
Share this Scrolly Tale with your friends.
A Scrolly Tale is a new way to read Twitter threads with a more visually immersive experience.
Discover more beautiful Scrolly Tales like this.