The MEME "Rug Pull"
One thing I observed with particular interest last year was the MEME "Rug Pull" NFT auction, set up by @DirSchmidt and @iamsimonwan. The idea: There are 12 NFTs and hidden inside one of them is a Bitcoin. None of the NFT owners know who among them is in possession of the Bitcoin, but each has the ability to look inside their own art piece to see if the Bitcoin is there. If they do so, what they found will be available on-chain for everyone to see. And of course, if they find the Bitcoin, the mystery is now ruined for all the other owners as well - the rug pull.
An intriguing concept. I'm a big fan of using the possibilities provided by NFTs to add an interactive dimension to digital art, a kind of collective performance if you will. It's worth watching the video introducing the concept behind the rug pull, which expresses these ideas quite well.
The auction took place about about a month ago. So what happened? No one looked into their NFT, not until four days days ago, when RStudios acquires NFT 73 on OpenSea for $13,000, finds the Bitcoin, then sells it on straight-away for 1.7 ETH (around $1200). A lucky guess? Not really.
I was curious how MEME built this from the beginning, since it isn't really possibly to hide information on the blockchain (you may remember Rarible's easily defeated early attempts at "unlockable" content). Now doubly so. Let's have a closer look.
- 0xe6df9 - the
Vault: A contract, created by MEME, which holds the 1 BTC prize, and knows who the winner is. The source code is not published.
- 0x21e2f: The account of
RStudios, our winner/attacker.
- 0xb0261 -
Exploit1: A self-destructing contract, created by
- 0x35740 -
Exploit2: Another self-destructing contract, created by
Exploit1. MEME credits this address with the rekt by.
It's basically all over in one transaction - in block 11531666.
This is where
RStudios sends the NFT they bought to the
Exploit1 smart contract they had prepared [^1].
Exploit1 then immediately proceeds to
create a child-contract,
Exploit2, and transfers the NFT to that.
Exploit2, now being the owner of the NFT, asks the
Vault to reveal if the Bitcoin
is hidden inside. The vault confirms that it is, and hands the 1 BTC to
Exploit2 sends both the Bitcoin and the NFT back to
RStudios goes on to pretty much right away sell the BTC for ETH
on Uniswap and asks both constructs to delete themselves, apparently trying to hide their traces.
safeTransferFrom is called on the MEME ERC721 contract which issues the NFT tokens. This is the transaction entry point, and everything else flows from there.
[^2]: The easiest way to see this is in the
Logs tab on Etherscan.
You can also follow the execution on Tenderly.
How they did it
This is all very suspicious - enough to cause us to suspect that
RStudios knew perfectly well that the NFT they were buying was the one hiding the Bitcoin.
Since there are no secrets on the blockchain, maybe they were able to look inside the
Vault contract and figure out what the winning NFT was?
Not quite - the actual attack is a lot more interesting.
Since we don't have access to any of the contract source codes, we have to read disassembled, reconstructed code - not my strong suit, so if an actual expert wants to add to this, let me know. I don't understand it 100%, but this is what I figure:
The idea was to have the vault select one of the NFTs at random as the Bitcoin holder. But they didn't just want to write the number of that NFT number into the contract, which likely would have been pretty easy to find.
Also, since the blockchain is about as good at random numbers as it is at hiding stuff (you can't really do things at random), developers tend to try to use block hashes as a source of randomness. Unfortunately, only the 256 most recent block hashes can be accessed in smart contracts.
For this reason, the winning NFT was, I think, not actually fixed. The vault contract probably picked a new random value whenever the old one was no longer available (due to the block hash it was supposed to be based on not being available anymore). In other words, it decided if you are the winner the moment you try to look.
The contract the attacker deployed actually has this loop (source):
idx = 1
while idx <= 50:
if sha3(block.hash(block.number - 70), addr(_321), block.timestamp) % 12 != 3:
idx = idx + 1
Vault contract calculates the random number something like this (source):
sha3(block.hash(someBlockNumber - 70), someSender, someBlockTimestamp)
My conclusion: The attacker waited until the
Vault had to generate a new random number, at which point the vault
someBlockTimestamp to the current block being mined, something they have
access to in their
Exploit1 smart contract. They now have to get
someSender to the right value - a value that
will cause whatever NFT they have to win. I don't think the actual NFT they bought matters, the winner is being
Vault is going to use the address of the sender here,
Exploit1 will loop until it finds an address
thata is right, then creates
Exploit2 at that address, and start the redeem process from there. It can now be sure
that whatever it's NFT is, the pseudo-randomness code of the
Vault will declare it the winner.
With bitcoin at $27,000, the attacker walks away with a profit of $15,200. Not too bad for what I assume where some nights of coding. And maybe a literal rug-pull by a hacker is a perfect conclusion to this experiment.
An interesting post script here is that the subsequent buyer of the NFT, who got it for a dirty cheap 1.7 ETH, clearly thought that no artistic ideals are lofty enough to not take a chance at a $30,000 pay-off, and immediately tried to look into the NFT, only to find it now empty. His is the lone visible reveal transaction on the vault contract.