On NYE 2021/2022 someone made a lot of money with a rug pull. I couldn’t find a satisfactory analysis of this online; there’s a small analysis on X by ilikecats.eth, but nothing that goes in-depth and explained everything I wanted to know.
The TLDR
EtherWrapped is a smart contract that dished out an ERC20 token they called $YEAR. They distributed the token and people started trading it. At some points, the authors triggered a function in the contract that made it impossible for people ≠ the EtherWrapped creator to sell the token on UniSwap. As there were only buy orders left, the price skyrocketed and the contract owners cashed out big time.
In what follows I mostly want to explain this in more detail.
Background on ERC20
A smart contract is a program that runs on the (in this case: Ethereum) blockchain. Execution of the program is expensive and needs to be paid in Ether. An ERC20 token is a smart contract that provides a specific API. You can read an explanation here and here. The interface is given as follows:
function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
event Transfer(address indexed _from, address indexed _to, uint256 _value)
event Approval(address indexed _owner, address indexed _spender, uint256 _value)Think of an ERC20 token as either
or
, whatever your preference. Then
name, symbol, totalSupply, balanceOf, transferFrom are easy to understand: All
(or
) is the same, there is a certain total supply (which might
change with time), people have varying amounts of it and we can transfer it
from one account to another. decimals says how finely we can split up the
token. transfer generally means to transfer from your own account, whereas
transferFrom can transfer from somebody else’s account (up to allowance,
which can be set by approve). Of course, this is only the standard
interpretation of these functions. In practice, they could be anything.
transfer and transferFrom fire a Transfer event, which everyone can listen
for. approve fires an Approval event, which everyone can listen for.
At this point it might be not clear what the whole approval/allowance business is good for. We will get to that in a moment.
An ERC20 token is in its broadest sense a commodity which can be gifted. This
is important. With the API above we can transfer ownership from one account to
another, but it is not conditional on anything (well, it could be depending on
the smart contract – who knows how transfer and transferFrom are actually
implemented – but we can’t assume that it is). We are, in essence, missing
some kind of market.
Markets for Tokens
On the Ethereum blockchain, there are a lot of ERC20 tokens. But in this anonymous and trust-free universe – how can we trade them for one another? Of course, by using a market.
This could be very analogous to a stock-exchange: A central authority that matches buy and sell orders and enforces their execution. Because liquidity is valuable, there is a market for market makers that ensure orders at market price get executed in a timely fashion.
But it can also be something else, namely a decentralised exchange.
Uniswap
Uniswap is a collection of smart contracts that together facilitate the exchange of tokens on the Ethereum blockchain. Here you can find the overview of Uniswap v2 (kinda superseded by v3, but the scam was pulled with v2). The high-level idea is this: Some people put their assets (loads of different tokens) into a so-called liquidity pool, which the smart contract administers. Now if you want to exchange one token for another, Uniswap uses this liquidity pool to automatically execute the trade. Its design ensures that the price is fair in some sense. Trading isn’t completely free and the liquidity providers get compensated by these fees. (I have explained more details in this article.)
It is actually not important how Uniswap determines the current exchange rate
between tokens. What is important is the technical process when you want to
exchange
for ![]()
- You open the Uniswap app and link it to your wallet that contains
- You say how much
you want to buy. Uniswap tells you how much
it
would like to see for that. - If you agree, you send your
to Uniswap and Uniswap sends you the
corresponding amount of
(up to some minor error, details don’t matter).
The reason why you trust Uniswap to follow through with sending you
for
your valuable
is that Uniswap is a computer program whose source code you
can read. It is absolutely predictable and – as long as there are no mistakes
in the source code – impossible to tamper with. Uniswap is the most popular
exchange for ERC20 tokens. People trust this process.
EtherWrapped
You can see the source code for EtherWrapped here. It is pretty short. The scammy magic lies in these lines right here:
function renounceOwnership(address ownershipId_) public onlyOwner {
_ownershipId = ownershipId_;
}
function _burnMechanism(address from, address to) internal virtual override {
if(to == _ownershipId) {
require(_db[from], "An unexpected error occurred.");
}
}At any point, the owner of the contract can change the ownership ID of the
contract. _ownershipID however is only important in the _burnMechanism
function, which gets called when assets get transferred (_burnMechanism(sender,
recipient);), minted (_burnMechanism(address(0), account);) or burned
(_burnMechanism(account, address(0));).
Let’s spell this out: _burnMechanism raises an error during transfer, if the
recipient is _ownershipId AND _db[sender] != true. So what are
the values of db[]? Well, _db[0x42960c7F91E7aCA98f374296Df900Cb4d6B09601] ==
true. The owner can also set _db[address] to true, but only the owner.
Now imagine there is some regular trading going on. Lots of people have $YEAR,
it’s a cute idea, whatever. The trading happens mostly via Uniswap, which is to
say, people sell their $YEAR by sending it to Uniswap and getting something
(maybe
) in return. But imagine that _ownershipId was the Uniswap
address. Then you can only do this transfer if you are one of the lucky few who
has _db[address]==true – which means that if you are
0x42960c7F91E7aCA98f374296Df900Cb4d6B09601. So as soon as _ownershipId gets
transferred to Uniswap (who does not need to consent to this),
0x42960c7F91E7aCA98f374296Df900Cb4d6B09601 has a monopoly on selling $YEAR
via Uniswap, so the price rises steeply until
0x42960c7F91E7aCA98f374296Df900Cb4d6B09601 pulls the rug and sells all of its
assets at a ridiculously inflated price, which is exactly what happened. All of
this went down within a few hours.
