Ethernaut —Naught Coin (ERC20) Exploitation
Background
For the past week and a half, I have been doing my best to familiarize myself with Ethereum Smart Contracts and their implementation using Solidity. With that in mind, up until this point I had only played around with developing, deploying, then interacting with basic Solidity contracts I created on my local testnet. With this in mind, instead of creating my own vulnerable contracts based on some of the known attacks that Consensys has documented here, I looked around for Capture The Flag style challenges for Ethereum contracts. That’s when I stumbled on Zeppelin’s set of Ethernaut challenges, and decided to play around with the problems they created instead.
Baby’s First ERC20 Token!
After digging into the challenges a bit, I decided to start working on the Naught Coin challenge. This challenge involves identifying a vulnerability within an implementation of an ERC20 token. This implementation makes it so that the player receives an entire supply of tokens upon creation of the contract. The one catch to this is, the player must wait 10 years in order to transfer or use any of the tokens — Or that is at least what it should require. In order to complete this challenge, the player must successfully drain all of the tokens from the contract creator into another account without waiting 10 years.
Getting Started
Conceptual Prerequisites
Based on Zeppelin’s difficulty rating, this challenge is a 5/6 in difficulty. In my opinion, this is because a solid understanding of Solidity contract inheritance and variable states (specifically mappings) is required. Outside of this, understanding how a basic ERC20 token works is also required (this is later touched on).
Testing Environment
The Ethernaut set of challenges have already been set up for the player’s use on the Ropsten test network, and can be interacted with via the player’s browser console with a JavaScript interface.
Despite this, I decided to instead use a browser-local test environment to avoid having to deal with setting up a Ropsten account for testing. To do this, I used the browser version of Remix, which can be found here.
Preparing NaughtCoin For Remix (Browser Version)
The original source code for the NaughtCoin challenge has been pasted below, however, changes must be made for this to properly work within the browser version of Remix. If using MetaMask with Ropsten with the given instance provided by Ethernaut, this can safely be ignored, and all other steps of this challenge can be performed as-is.
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';
contract NaughtCoin is StandardToken {
string public constant name = 'NaughtCoin';
string public constant symbol = '0x0';
uint public constant decimals = 18;
uint public timeLock = now + 10 years;
uint public INITIAL_SUPPLY = 1000000 * (10 ** decimals);
address public player;
function NaughtCoin(address _player) public {
player = _player;
totalSupply_ = INITIAL_SUPPLY;
balances[player] = INITIAL_SUPPLY;
Transfer(0x0, player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value) lockTokens public returns(bool) {
super.transfer(_to, _value);
}
// Prevent the initial owner from transferring tokens until the timelock has passed
modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
if (now < timeLock) {
_;
}
} else {
_;
}
}
}
For testing in the browser-based Remix, the following change must be made:
import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';
Must be changed to:
import 'https://github.com/OpenZeppelin/zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';
Making this change will allow imports to properly occur all the way up the chain of dependencies on compilation, which Remix will then import the source code of into the Remix editor side panel:
Initial Analysis Of NaughtCoin
To begin analysis of this challenge, I began by carefuly enumerating the NaughtCoin
implementation as was provided in the Ethernaut challenge. To do this, I broke up the approach into logical progressions and detailed them below.
Imports
When I first started the challenge, the first thing that jumped out to me was the line:
contract NaughtCoin is StandardToken
If I wanted to understand what NaughtCoin was, I would need to identify what StandardToken
was first, since NaughtCoin
was derived from it. Thus, looking at the import
ed contract, I decided to start walking down the chain of imports and implementations.
import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';
Tracing from StandardToken
, the following imports and implementations were identified:
import "./BasicToken.sol";
import "./ERC20.sol";contract StandardToken is ERC20, BasicToken
Thus, BasicToken
and ERC20
are imported, then inherited from in StandardToken
‘s implementation.
Continuing from BasicToken
, the following imports and implementations were identified:
import "./ERC20Basic.sol";
import "../../math/SafeMath.sol";contract BasicToken is ERC20Basic
From this definition of the BasicToken
contract, BasicToken
inherits from the imported ERC20Basic
, and makes use of the SafeMath
library. For brevity, the SafeMath
library is skipped over, as it is not relevant to include in this trace. Finally, ERC20Basic
does not require any other contracts to derive from.
Jumping back up to StandardToken
‘s implementation, ERC20
is also inherited from. Tracing this import, the following imports and implementations were identified:
import "./ERC20Basic.sol";contract ERC20 is ERC20Basic
Similar to BasicToken
, ERC20
also inherits from ERC20Basic
, and does not require any further tracing because of the shared inheritance.
For convenient reference I have created a basic inheritance flow chart below.
NaughtCoin Contract Variables
Moving past imports and inheritance, definition of variables within NaughtCoin
was my next focus. The definitions within NaughtCoin
(but not including all inherited definitions) are as follows:
string public constant name = 'NaughtCoin';
string public constant symbol = '0x0';
uint public constant decimals = 18;
uint public timeLock = now + 10 years;
uint public INITIAL_SUPPLY = 1000000 * (10 ** decimals);
address public player;
Of particular importance to us are the timeLock
and player
variables. The timeLock
definition provides the 10 year period that the challenge describes as being required before trading of tokens, and the player
variable is the address to the player who receives all of the INITIAL_SUPPLY
tokens.
NaughtCoin Constructor
Relevant NaughtCoin constructor code has been included below for reference:
function NaughtCoin(address _player) public {
player = _player;
totalSupply_ = INITIAL_SUPPLY;
balances[player] = INITIAL_SUPPLY;
Transfer(0x0, player, INITIAL_SUPPLY);
}
When the NaughtCoin
contract is deployed, an address for player
is required and subsequently stored. This is nearly equivalent to the standard usage of owner
within most contracts, and is important for the operations involving the transfer of tokens(which we will be exploiting later).
The totalSupply
is also stored and initialized to the value of INITIAL_SUPPLY
. Though, the more interesting assignment that occurs during initialization is balances[player]
. This assignment creates an entry in the balances
mapping for the address of player
with a value of INITIAL_SUPPLY
. At initialization, the player
is the only entry that is present and has a balance. Thus, to get NaughtCoin, other addresses must receive a transfer from the player
who initialized the NaughtCoin
contract. But, once again, referring back to the challenge description, this should not be possible until after a time period of 10 years since initialization.
NaughtCoin lockTokens
Modifier
Relevant lockTokens
modifier source code has been included below for reference:
modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
if (now < timeLock) {
_;
}
} else {
_;
}
}
In this challenge, the lockTokens
modifier is what initially appeared to me as the most important, mostly because it was applied to the transfer
function, which allows a particular sender to transfer tokens from one address to another within the scope of the balances
.
It’s important for this portion of analysis to understand that a modifier (official docs here) is something that encapsulates the call to a particular function. If you want to perform actions before or after arbitrary function calls, a modifier is the way to invoke those actions. A simple way of reading modifiers is to consider the _
as the function body (think source code of the function: function lol() public { /* everything in here */ }
).
When first analyzing lockTokens
, it appears that if you are the msg.sender
(the person performing a call), and you are also the player who initiated NaughtCoin
, you will continue execution within the first branch of the if statement:
if (msg.sender == player) {
require(now > timeLock);
if (now < timeLock) {
_;
}
}
This is where a require
block checks to make sure that 10 years has passed since timeLock
. If it hasn’t, a revert
will occur (resetting the modified states of the contract and raising an error). The if statement then checks to see if now
(the current date/time) is less than timeLock
, and if true, will execute the function body.
Based on our analysis, we have identified that we are unable to pass the msg.sender == player
check if we are performing a call from an account other than player
. Furthermore, we are never able to achieve function execution with the require
and if
statements inside this block, since they contradict each other. Because of this, the transfer
function is rendered pretty much useless, since we are never able to achieve execution from the player
call scope. Thus, we are unable to use the transfer
function to successfully move tokens to another addresses balance.
NaughtCoin transfer Function
Relevant transfer
modifier source code has been included below for reference:
function transfer(address _to, uint256 _value) lockTokens public returns(bool) {
super.transfer(_to, _value);
}
Last but not least, the transfer
function. This function merely calls to StandardToken
‘s implementation of the transfer
function using super
, since NaughtCoin
inherits from StandardToken
.
More importantly however, the lockTokens
modifier is applied to the transfer
function. Thus, based on our previous analysis of the lockTokens
modifier, we have determined we will never be able to execute super.transfer(...)
, since we are never able to bypass the timeLock
checks.
Conclusion
Based on our findings thus far, we have identified that the transfer
function is completely off limits! However, that leaves us with nothing custom to analyze. Instead, we are now forced to look into the implementation of StandardToken
for further leads on how to drain the player
‘s balance!
Looking Beyond NaughtCoin — Analysis of the Zeppelin ERC20 Function Implementations
Previously when performing the analysis on the imports and inheritance of NaughtCoin
, we identified the chain of imports and inheritance. This is important, because it actually exposes a significant amount of extra functionality in NaughtCoin
. In particular, we are looking to identify functions that allow us to modify balances
before the 10 year time lock, and assign what is in balances[player]
to a balances[notPlayer]
of our choosing.
The ERC20 Interface Specification
In order for a token to be compatible with the ERC20 specification, it must have implementations for the following interface.
// ERC Token Standard #20 Interface// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20-token-standard.mdcontract ERC20Interface {function totalSupply() public constant returns (uint);function balanceOf(address tokenOwner) public constant returns (uint balance);function allowance(address tokenOwner, address spender) public constant returns (uint remaining);function transfer(address to, uint tokens) public returns (bool success);function approve(address spender, uint tokens) public returns (bool success);function transferFrom(address from, address to, uint tokens) public returns (bool success);event Transfer(address indexed from, address indexed to, uint tokens);event Approval(address indexed tokenOwner, address indexed spender, uint tokens);}
This specification is one of the most popular, which makes finding further documentation pretty easy. However, I pasted the specification interface since this will be rather useful to reference when walking through this challenge solution.
Identifying Interesting Functions
To begin identifying where I should look first, I found that just looking at the ERC20 interface gave me a few methods I would want to look further into. In particular, the first function that jumped out to me was the transferFrom
function. We had identified that NaughtCoin
‘s transfer
implementation was rendered useless by the lockTokens
modifier, but NaughtCoin
did not implement the transferFrom
function, which means that the StandardToken
implementation of transferFrom
is being called.
Having deduced this, the question of what transferFrom
actually does is the next question to tackle on our journey to draining some coins.
Analyzing the implementation of transferFrom
Relevant transferFrom
function source code, found within StandardToken.sol
has been included below for reference:
function transferFrom(
address _from,
address _to,
uint256 _value
)
public
returns (bool)
{
require(_to != address(0));
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emit Transfer(_from, _to, _value);
return true;
}
Upon reading the source code of the transferFrom
function, it appears that it allows a third party to transfer a designated amount of tokens from one account to another account. This is derived from reading the requirements for transfer (first three lines of the function body).
In the first require
, the _to
function argument is compared to the address(0)
value. Doing a bit of research regarding the address(0)
value, it was discovered that address(0)
is sometimes used as an address to burn tokens. More importantly, it is an address that nobody can control. Thus, this require
checks to make sure that someone cannot just burn tokens arbitrarily (which would have worked for us, since we just want to drain our tokens out of balances[player]
). Next, the _value
passed to the function is checked to make sure that the balance of _from
is greater. This prevents anyone from withdrawing more tokens than are available. Finally, the last require
is checking to make sure that the _value
passed is less than, or equal to, the allowed
amount of tokens that the msg.sender
is allowed to manage on behalf of _from
. The rest of this function body just contains the logic for appropriately transferring tokens from the balances[_from]
to the balances[_to]
.
The important takeaway from this function is that we have found a new important variable in this contract: the allowed
mapping. It seems that based on the third require
we are able to use transferFrom
if we have an entry of player
as the _from
(e.g. allowed[player]
), a sub entry of _to
in allowed
(e.g. allowed[player]
), and any other address than address(0)
as the _to
.
Based on the findings in transferFrom
, we now know that if we want to be able to modify balances
through the transferFrom
function we need to first have an entry for player
in allowed
, which appears to define an account that is managed, and a corresponding address under this entry in allowed
, which appears to be an account that is allowed
to manage the top level account, up to a certain amount of tokens defined by the return of allowed[player][managerAccount]
.
Searching For Allowance (Approval)
Relevant approve
function source code, found within StandardToken.sol
has been included below for reference:
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}
Now that we have identified the constraints we need in order to execute a transfer using transferFrom
, we need to massage the constraints into the allowed
mapping. Referring back to the ERC20 interface specification, there is a function named approve
, which accepts an address for the spender
, and a uint tokens
.
Similar to the transferFrom
function, StandardToken
also appears to provide the implementation for the approve
function. Within this function, it builds the exact allowed
hierarchy that we need to have permissions to use transferFrom
, as long as the msg.sender
is the player
, and the spender
is the account we wish to give tokens to.
Putting It All Together — Exploiting NaughtCoin
With the information we have now, we should have all the calls we need to be able to successfully use the transferFrom
. The exploit chain has been detailed as follows.
Initially, the player
is the owner
of the tokens/contract, and the contract is initialized with balances[owner]=INITIAL_SUPPLY
. As such, our challenge is to drain it into an account we can control — the exfil_address
. To do this, we have identified that we have the ability to call the transferFrom
function to bypass the lockTokens
modifier, and transfer a certain amount of tokens from one account to another. The one catch is the requirement that the account performing the transfer is allowed
to perform the operations, with the particular amount of tokens. Thus, we go ahead and call the approve
function as the player
( owner
), with the exfil_address
as the approved spender, with a total amount of tokens allowed to be spent as INITIAL_SUPPLY
. Once approved, this will allow exfil_address
to transfer the INITIAL_SUPPLY
of tokens from player
( owner
) to the exfil_address
. Thus, we have successfully drained the player
of tokens before the timeLock
period of 10 years.
The calls detailed in the exploit can be performed trivially through the browser version Remix through the use of the in-memory execution and the 5 default accounts. Only two accounts are required — One for the deployment of the NaughtCoin contract and the subsequent approve
call (which will populate the existing contracts in the sidebar), and the other for calling the transferFrom
as the exfil_account
. Once the contract is deployed, the call graph pictured above can be followed exactly within Remix to achieve successful execution of the vulnerability.
Conclusion
This challenge was a wild ride of learning how to debug Solidity contracts, and understanding how execution scopes modify the underlying logic of a contract. It is also an extremely thorough method of learning the ERC20 specification and implementation, since you must understand each major function in order to exploit NaughtCoin
.