People don't understand NFTs (or tokens)
When speaking to people about non-fungible tokens (NFTs) (or regular “fungible” tokens) on the Ethereum blockchain, I find they generally have a mental visualisation in some way that these items are almost like collectible gold coins - they “move around”, albeit in a digital sense, and they’re all made alike. However, this is not the case, and understanding that this isn’t the case is paramount when trying to understand DeFi projects and protocols, wallets, UX, and security.
In Ethereum (or to be specific, the EVM - Ethereum Virtual Machine), there are only three core things: Ether (the “currency”), transactions, and contracts. Blocks are just signed bundles of transactions, and everything else (tokens, protocols, etc.) are nothing more than contracts 1
# So, what really are tokens / NFTs?
Tokens / NFTs are just standard interfaces for contracts to conform to.
This is the most important thing to understand. They are standards, and as such, can be implemented any which way. Specifically, tokens and NFTs are usually the ERC20 and ERC721 standards respectively. These standards are what allow you to trade any token you want on Uniswap, or any NFT on OpenSea.
What this ultimately means is, even if the token contract appears to be a regular ERC20 token, unless the code is verified and audited, you can’t be sure what it’s doing.
If we look at the (trimmed down) code for an ERC20 fungible token:
#... event Transfer: sender: indexed(address) receiver: indexed(address) value: uint256 # ... balanceOf: public(HashMap[address, uint256]) # ... @external def transfer(_to : address, _value : uint256) -> bool: """ @dev Transfer token for a specified address @param _to The address to transfer to. @param _value The amount to be transferred. """ # NOTE: vyper does not allow underflows # so the following subtraction would revert on insufficient balance self.balanceOf[msg.sender] -= _value self.balanceOf[_to] += _value log Transfer(msg.sender, _to, _value) return True
All tokens are just this - a map of Ethereum address <> balance, and some functions to manipulate that in various ways (which is actually quite similar to how the banking system works - transfers are just subtracting balance for one account, and increasing it in another).
Now let’s take a look at ERC721:
# ... event Transfer: sender: indexed(address) receiver: indexed(address) tokenId: indexed(uint256) # ... # @dev Mapping from NFT ID to the address that owns it. idToOwner: HashMap[uint256, address] # @dev Mapping from NFT ID to approved address. idToApprovals: HashMap[uint256, address] # @dev Mapping from owner address to count of his tokens. ownerToNFTokenCount: HashMap[address, uint256] # @dev Mapping from owner address to mapping of operator addresses. ownerToOperators: HashMap[address, HashMap[address, bool]] # ... @view @external def ownerOf(_tokenId: uint256) -> address: """ @dev Returns the address of the owner of the NFT. Throws if `_tokenId` is not a valid NFT. @param _tokenId The identifier for an NFT. """ owner: address = self.idToOwner[_tokenId] # Throws if `_tokenId` is not a valid NFT assert owner != ZERO_ADDRESS return owner @internal def _transferFrom(_from: address, _to: address, _tokenId: uint256, _sender: address): """ @dev Execute transfer of a NFT. Throws unless `msg.sender` is the current owner, an authorized operator, or the approved address for this NFT. (NOTE: `msg.sender` not allowed in private function so pass `_sender`.) Throws if `_to` is the zero address. Throws if `_from` is not the current owner. Throws if `_tokenId` is not a valid NFT. """ # Check requirements assert self._isApprovedOrOwner(_sender, _tokenId) # Throws if `_to` is the zero address assert _to != ZERO_ADDRESS # Clear approval. Throws if `_from` is not the current owner self._clearApproval(_from, _tokenId) # Remove NFT. Throws if `_tokenId` is not a valid NFT self._removeTokenFrom(_from, _tokenId) # Add NFT self._addTokenTo(_to, _tokenId) # Log the transfer log Transfer(_from, _to, _tokenId)
The same thing - just a map of “token ID” (the number, like #8159, that you see on OpenSea for each item) to the address of the owner.
# Poisonous transfer functions
Let’s take an example of why this matters. There is an excellent demonstration
by Nathan Worsley titled
Salmonella: Wrecking sandwich traders for fun and profit.
While the details are interesting, what really matters is this: 68 ETH 2
taken from a bot that was attempting to front-run transactions for profit,
because the bot wasn’t built to understand this principle. All Nathan had to do
was add a few extra lines of code to the
transfer function of the contract.
# So how does this affect me, and how can I protect myself?
This means that any interaction with a token or NFT could be potentially dangerous to your funds.
As for protecting yourself, unfortunately, this is hard. There are two options:
- Read the code for every token you interact with yourself
- Rely on audits / community trust
Generally, if you didn’t already know all of the above, you’re probably better off with (2). Only interact with projects that you trust, make sure you’re always on the correct website, and be very cautious of signing transactions, in the same way most people are nowadays generally suspicious of cold calls/emails/texts asking you for detailed information, or to click a link.
Ethereum is a dark forest. Stay safe out there.
This raises the interesting discussion of self-custody - are non-crypto-native-software-engineering-professional people capable of it, and if they aren’t (they aren’t!), what is the point of crypto/web3?
It’s a discussion probably best saved for another time, but suffice it to say, it doesn’t render blockchain technology pointless. If people cannot self-custody, they can trust another party to custody for them - this is, essentially, a bank. Unlike the banks of the traditional system, however, if you:
- Are persona-non-grata to the banks for any reason
- Live in a country that the US government has decided shouldn’t be allowed access to the Western financial system
- Have a desire to keep your funds available and irrepressible, such as for example, being a political opponent under an authoritarian regime
You can use self-custody to maintain your fundamental human right to transact with others. This has value far beyond what the mainstream narrative is often willing to admit.
Quick aside: smart contracts
First of all, it pays to understand what a “smart contract” is.
A smart contract is just a piece of code which can be deployed to the blockchain.
For example, here’s a rudimentary registry contract to mimick ENS:
# This language is called Vyper # You may have heard of Solidity - this is an alternative to that registry: HashMap[Bytes, address] @external def register(name: Bytes, owner: address): assert self.registry[name] == ZERO_ADDRESS # check name has not been set yet. self.registry[name] = owner @view @external def lookup(name: Bytes) -> address: return self.registry[name]
You can then interact with this code by sending transactions from your wallet. ↩︎