- Published on
Capture the Ether (RareSkills Repo) - Token Bank - Solution
- Authors
- Name
- Marco Besier, Ph.D.
- @marcobesier
Capture the Ether (RareSkills Repo) - Token Bank - Solution
Contract
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
interface ITokenReceiver {
function tokenFallback(address from, uint256 value, bytes memory 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);
constructor() {
balanceOf[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function isContract(address _addr) private view returns (bool is_contract) {
uint256 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 memory 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;
address public player;
constructor(address _player) {
token = new SimpleERC223Token();
player = _player;
// 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 addcontract(address _contract) public {
balanceOf[_contract] = 500000 * 10 ** 18;
}
function isComplete() public view returns (bool) {
return token.balanceOf(address(this)) == 0;
}
function tokenFallback(address from, uint256 value, bytes memory data) 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));
unchecked {
balanceOf[msg.sender] -= amount;
}
}
}
Solution
I'm not sure whether this design is intentional, but the RareSkills challenge differs from the original Capture the Ether challenge. More specifically, the RareSkills version contains an additional function, addcontract
, that can be used to drain the bank easily. The most convenient way to follow along is Remix.
After deployment, we have token.balanceOf(address(this)) == 10**6 * 10**18
, where address(this)
refers to the bank's address. To complete the challenge, we must reduce this balance to 0.
However, achieving that is straightforward by using the following sequence of actions:
- Deploy
TokenBankChallenge
specifying the deployer's address as the_player
. - Call
withdraw
from the deployer's account, specifying the amount to be500_000_000_000_000_000_000_000
. - Call
addcontract
, specifying the deployer's address for the_contract
parameter. - Call
withdraw
from the deployer's account, specifying the amount to be500_000_000_000_000_000_000_000
.
Performing the above steps solves the challenge.
NOTE: The original Capture the Ether challenge does not contain the addcontract
function. However, notice that withdraw
doesn't follow the checks-effects-interactions pattern and is, therefore, vulnerable to reentrancy. (Note that transfer
calls tokenFallback
on the receiver.) A detailed solution for this original case is explained in Christoph Michel's Capture the Ether blog post.