ERC721 — Let’s break things

Andrew Parker
Coinmonks

--

This is the story about how, in only a few short hours, I managed to get to the #5 spot on Etherscan’s top NFT rankings.

Earlier this week, I launched EtherVirus, a crypto-art project which attempted to bring virality mechanics to an on-chain game. It was just an idea I had last week and gave myself a week to build and deploy it.

This deadline meant I cut a few corners — I figured it didn’t really matter, because the project was just for fun, and had no financial value. I also figured that since 90% of my contract code was from an old, very well-tested project, I couldn’t really mess it up… which was clearly a mistake.

In fairness, there were no flaws in the functionality of the smart contract. Internally it had no issues, and all external functions did exactly what they were supposed to. But a few oversights regarding events and the ERC165 interface checks caused some issues with how sites like EtherScan interpreted the contract. Interestingly, it exposed how exactly these sites keep track of tokens, which made me wonder how hard it would be to break things.

Photo by chuttersnap on Unsplash

Events are everything

First let me explain how I messed up the EtherVirus contract. It all came down to one event, the Transfer event. The ERC721 EIP describes this event as

event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

The problem is, I developed my 721 token while the EIP was still a draft proposal, and I guess at the time, the final parameter of the event wasn’t indexed. I thought I’d caught all the changes but this one slipped by me. But this meant that my ERC721 token showed up in Etherscan as an ERC20 token — a completely different beast.

I didn’t know about the indexed thing yet, so I had a chat with Etherscan’s help guy on twitter, and he showed me the verification tool they use for NFTs, and why my contract wasn’t passing muster. Turns out besides the above, I also hard-coded the wrong value for an ERC165 interface check, meaning my contract wasn’t even claiming to be a valid ERC721 token. I dunno if this was also because of an old version or just some other random screw up along the way. Both of these would have been caught if I bothered to unit test the contract. Meh.

Anyway, with these obvious screw-ups, its no surprise that things didn’t go perfectly. The real surprise is that things still pretty much worked. I could still list the Tokens on OpenSea, and Etherscan still kind of dealt with my guy as if it were an NFT.

Finding the limit

I decided to try find the limit of what these sites look for in an NFT. When making EtherVirus I had hoped it might make it into the NFT rankings on Etherscan, but it also didn’t bother me when it gained no traction. My new goal was to see how easy it would be to spam my way to the top of the NFT leaderboard, and maybe get a bit of attention on OpenSea.

I created a token called CryptoSatan, with the symbol SATAN. The inspiration came from seeing some crazy person spamming Peepeth with messages about how Freemasons control everything. I figured if there are people like that, there is probably some portion of Ethereum users who would be averse to having Satan-branded stuff in their wallet. So I made the token image a medieval painting of the Devil, and made the token description “ Satan is in your wallet. Your crypto’s immortal soul has been compromised. Evil surrounds you.” — hopefully that would drive some traffic.

Version 1

The first version of CryptoSatan automatically assigned every Ethereum address 666 SATAN tokens. You could send and receive them, and your balance would go up or down, and it would emit an event when a transfer was made, but some of the ERC721-enumerable functions were fudged.

So I air-dropped a few tokens and… the “total supply” on Etherscan was only showing as the number I’d transferred. This was strange, because the totalSupply function on my contract would always return 666 * 0xFFFF…, or 666 times the number of possible Ethereum addresses.

Those familiar with the 721 standard, and the enumerable extension, will know there are various functions for keeping track of who owns what and what tokens exist. Turns out sites like Etherscan and OpenSea don’t pay a whole lot of attention to any of that — they mostly just watch events and keep track of them. Interesting…

Version 2

So I scrapped that mess, and decided to start playing with events. Key to this was the Transfer event that gets called when a new token is issued, where the from address is the zero address. This is how Etherscan knows a token exists. Last year I learned all about events when making an on-chain text adventure game. One of the lessons from that project was that data emitted in events was wayyyyy cheaper than data stored on chain.

So I figure why not just make a token that looks like it’s 10 tokens. Basically, when a function is called with a tokenId, divide that by 10 to make it an innerId, and keep everything working as it should based on that. Basically you’ve just multiplied your apparent token volume by 10 as far as these 3rd party sites are concerned, and it basically didn’t cost anything on a transaction level. If a person transfers any one of those tokens, it will transfer all ten of them.

I was getting a little brazen by this point, so I figured I’d completely scrap all the ERC721-enumerable code from my NFT since it seemed unnecessary, but still keep the interface check so it claimed to be enumerable-compliant. There was no reason for the latter, I just wanted to mess with Etherscan.

I also decided I needed to add some spammy mechanics in too. I needed to get these tokens into the hands of people who would notice them, and try to get rid of them. Etherscan’s NFT rankings are based on traffic, so no transaction is bad as far as my goals were concerned. I’d been snooping around a few other the top NFT projects to see what I was dealing with. Interestingly, most of the top NFT projects do not implement ERC721-enumerable. Maybe this is an obvious choice, because it does save a fair bit of gas for users, but for me it had never previously crossed my mind not to make a fully-compliant token.

There was one high-traffic NFT that did though: MyCryptoHeroes. The reason this matters is this enumerable function:

function tokenByIndex(uint256 _index) external view returns(uint256);

You pass it any number between 0 and the total number of tokens, and you get a tokenId. You pass that tokenId to ownerOf, and you get the address of the person who owns it. Basically MyCryptoHeroes has an indexed list of all their users, on chain, for anyone to use. They have to, or they aren’t ERC721-enumerable-compliant. I figured people who hold MyCryptoHeroes tokens are probably active NFT users, so it might be a good list of people to air-drop to.

So I defined a little interface for interacting with MCH for these two functions:

interface Chump {
function tokenByIndex(uint256 _index) external view returns(uint256);
function ownerOf(uint256 _tokenId) external view returns(address);
}

and I stored a reference to the Chump contract in my constructor, so I can call these functions in my code. Sorry MyCryptoHeroes, it’s nothing personal, you were just the biggest fish with a valid 721-enumerable contract.

Anyway, whenever anyone makes a SATAN token transfer, the contract also mints 10 tokens to an address from MCH’s user-base. It started at the beginning, and for each transfer, it just looks at the next address. For added spamminess, it also mints 10 to the msg.sender of that transaction.

I deployed the transaction and after only a few transactions, the number of token transactions on Etherscan was already up to 60. Yes… this was looking good. But there seemed to be some issues with the total supply. I guess I shouldn’t have claimed to be enumerable compliant if I wasn’t. Also, the totalSupply function is pretty simple so probably could keep it in.

Side note: according to Etherscan’s NFT-checker, my contract still wasn’t 721-compliant. But it definitely was. I spoke to their help guy, and turns out their check was broken, thanked me for pointing it out, and said that they’d fix it in an update. So in trying to break Etherscan I kind of fixed Etherscan... Yay?

Version 3

Anyway, we’re getting there, but I really want my guy to show up properly, so I tore it down and redeployed once again.

This time, I kept the totalSupply function in, but also didn’t claim to be 721-enumerable-compliant. As I suspected, the total supply stayed up to date, and everything else was peachy. I could list my tokens on OpenSea, and trade them, and every time I would climb the Etherscan rankings. I messaged with a nice dude over at OpenSea who removed the older contracts from their database so the only tokens that would show up are the new, good ones.

The Satanic Panic

The next task was to start air-dropping these tokens, listing them, and selling them. I grabbed a few random addresses off Etherscan and air-dropped them 10 at a time to random people. I also started minting my own and listing them on OpenSea. I figured if I listed a few for sale at various prices, and a few for free, then people would see the free ones, think “wow, what a great deal!” and snatch them.

This is indeed what happened.

Within a few hours, I was steadily climbing Etherscan’s NFT rankings. Because of the 10x token thing, the number of air-drops I personally had to do was much smaller than if it were one-to-one. I could have probably got away with at least 20 without much more gas cost, but since 66 seemed like too many, and 6 like too few, 10 seemed good enough.

I made it to the number 10 rank on Etherscan pretty soon, and was definitely starting to count it as win. It seemed like I could maybe even overtake CryptoKitties and make it to number 1. People were starting to trade and list them on OpenSea without my input. I was pretty happy to see people listing them for prices such as 66, 0.666 and 6.66 ETH. It was beginning to look like I could sit back and let this thing do its thing.

But then, I got a message from OpenSea…

Censored

The guy I’d spoken to earlier messaged me, saying someone in their help channel was having issues with the token. This was bad news. OpenSea was basically my only avenue for spreading and once I got on their radar, I knew I’d be shadow banned or removed entirely.

This was the exchange in the chat:

User : Just FYI, I made the mistake of grabbing two of these Crypto Satan tokens (claimed for 0 ETH) while sorting by lowest price in the OpenSea search and now this contract keeps spamming me with duplicate tokens.

I’m attempting to “gift” them back to the sending account, but hard to keep up with. Probably not much that can be done but wanted to notify everyone.

Me: @User how many do you have?

User: I gifted them all back to the initial sender who priced the two I bought at 0 ETH… though OpenSea hasn’t updated, it appears I don’t have any right now… but the contract has executed a couple of rounds of sending, so I might end up with more. Total, I think 12 or 15 were sent to me?

Strangely, I only gifted like 8 back… the others xferred automatically.

Very strange

Hold the phone, i have more…

🔥😖

I thought maybe I could throw the heat off so I suggested:

Me: can you list them for 0 ETH?

User: Why would I do that? The contract will simply infect whoever gets them for 0 ETH like it did me.

Also, I notice the contract seems to have birthed the same number more into my wallet just now that I “gifted” back to the sending address. So it looks like the simple act of transfer MAY be what triggers the contract to birth more.

Is this YOUR contract @AnAllergyToAnalogy?

Me: its a project I’m involved with

Well at this point, I knew the jig was up. Our overzealous NFT user wasn’t gonna go quietly into that good night, and if I hadn’t already been, I was going to be shadow banned (or deplatformed or whatever the correct word is) by OpenSea.

But hey, at the time of writing, CryptoSatan is the #5 token on Etherscan’s NFT rankings. And by complete chance the total supply is currently 660 SATAN, which is a nice place to end. It would have been nice to make it to the number 1 spot, but since there will be no more organic growth, its time to call it quits.

Lessons in NFT-spamming

I may come to regret posting this, like the guy who invented the popup ad, but there are just a few tweaks that I think really could have made this gone a lot bigger.

First of all, I shouldn’t have made the tokens mint for msg.senders. This is well known spam behaviour, and a total red flag to anyone. I think I threw it in there because I figured most people would be acquiring their tokens via OpenSea, so the token sender wouldn’t be the msg.sender. But actually, it was what got me caught. I firmly believe that if I hadn’t done that, the dark lord CryptoSatan would still be spreading across the Ethereum network. I totally could have just double-tapped the MCH user list if I wanted more people with tokens… Duh!

But more importantly, the mint events! I totally missed a few tricks here.

Firstly, Etherscan just looks at transfer token traffic volume when it comes to ranking tokens, it doesn’t actually care about the data within. So I didn’t actually need to batch 10 tokens together, I could have kept it at one-to-one, and simply chained a bunch of events.

Ie, whenever A transfers a token to B, the contract could emit events like this

A transfers to C
C transfers to D
...
J transfers to B

It would still have the same volume of traffic of 10x per real transfer, but users wouldn’t have a strange abundance of coins in their wallets, upping the scarcity as a bonus.

On top of this, since we have this list of real addresses we stole from MCH, it would look like real people are passing tokens backwards and forwards. In fact, if we insert a random couple of addresses in the minting process, then to a mintee it will look like a random (but real) person has sent you a token, rather than it just being minted to your address. This seems like something you’re far less likely to ignore.

I think inserting these sort of phantom transactions between the actual desired transactions is a really good way to fudge the numbers while looking legit.

And its not even non-721-compliant. That EIP is pretty open about how your token can act, so this wouldn’t even be breaking any rules. You want to be careful about reading too many addresses in one tx, because it can get a little pricey, but other than that you could easily 20x your apparent token traffic without breaking the standard or without your users even knowing.

And that’s the end of that chapter

Anyway, it was a fun day, but that’s that. In 24 hours CryptoSatan will be a forgotten memory like all the other shitcoins out there. I’m a little disappointed that it didn’t go further, but we learned some things, we annoyed some people on Discord, and we probably confused some people on OpenSea, so all up, not too shabby.

Get Best Software Deals Directly In Your Inbox

--

--

Andrew Parker
Coinmonks

Codeslinger. Melbourne based Solidity developer for hire.