ctfwriteup.com
Search…
⌃K

# Math

## Token sale

### Code Audit

pragma solidity ^0.4.21;
contract TokenSaleChallenge {
uint256 constant PRICE_PER_TOKEN = 1 ether;
function TokenSaleChallenge(address _player) public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
}
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);
}
}
The constant `PRICE_PER_TOKEN` is 1 ether, which is represented by `10**18` wei in Solidity. This is a really large number.
In the function `buy()`:
We know that integer overflow/underflow is a thing for old versions prior to Solidity 0.8. Since `PRICE_PER_TOKEN` is huge and we have control over `numTokens`, we can pick a suitable `numTokens` and make `numTokens * PRICE_PER_TOKEN` overflow.
How large `numTokens` is supposed to be? Let's do the math:
// INT_MAX = 2**256 - 1 = 115792089237316195423570985008687907853269984665640564039457584007913129639935
// numTokens will multiply with 10**18, so take out the last 18 digits: 115792089237316195423570985008687907853269984665640564039457
// To overflow this thing after multiplication, add 1: 115792089237316195423570985008687907853269984665640564039458
// After multiplication, the product is 115792089237316195423570985008687907853269984665640564039458000000000000000000
// After overflow this becomes 415992086870360064, which is msg.value
// This value is slightly less than 0.5 ether and it is suitable to solve this challenge

### Solution

1. 1.
Copy and paste the challenge contract into Remix and interact with it via "At Address".
2. 2.
Call `buy(115792089237316195423570985008687907853269984665640564039458)` and send 415992086870360064 wei as `msg.value`.
3. 3.
Call `sell(1)`.
4. 4.
Call `isComplete()` to verify if the challenge was successfully solved.

## Token whale

### Code Audit

pragma solidity ^0.4.21;
contract TokenWhaleChallenge {
uint256 public totalSupply;
string public name = "Simple ERC20 Token";
string public symbol = "SET";
uint8 public decimals = 18;
player = _player;
totalSupply = 1000;
balanceOf[player] = 1000;
}
function isComplete() public view returns (bool) {
return balanceOf[player] >= 1000000;
}
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);
}
function approve(address spender, uint256 value) public {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
}
require(balanceOf[from] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
require(allowance[from][msg.sender] >= value);
allowance[from][msg.sender] -= value;
_transfer(to, value);
}
}
The `transferFrom(from, to, value)` function calls `_transfer(to, value)`:
function _transfer(address to, uint256 value) internal {
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
}
This implementation is wrong. In fact, it deducts balance from `msg.sender` instead of `from`. Moreover, `balanceOf[msg.sender] -= value` has underflow problem.

## Solution

Here is the attack plan:
• Initially we have `balanceOf[player] = 1000`.
• Create a proxy account in Metamask, call it `backup`.
• `player` calls `transfer(backup, 510)`. Here "510" can be any number greater than 500. If we transfer 510 tokens to `backup`, we now have 490 in `player` and 510 in `backup`.
• `backup` calls `approve(player, 500)`. This sets the allowance and prepares for `transferFrom()`.
• `player` calls `transferFrom(backup, backup, 500)`. In this step, `_transfer(backup, 500)` is being called. Since `_transfer()` deducts balance from `msg.sender` instead of `from`, the `player`'s account will be deducted 500. Recall that `player`'s balance is 490 at this moment, so `balanceOf[msg.sender]` is going to underflow to a huge number. That is, the `player` account has a huge balance now.
• Call `isComplete()` to verify if the challenge was successfully solved.

## Retirement fund

### Code Audit

pragma solidity ^0.4.21;
contract RetirementFundChallenge {
uint256 startBalance;
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) {
}
function withdraw() public {
require(msg.sender == owner);
if (now < expiration) {
// early withdrawal incurs a 10% penalty
} else {
}
}
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
}
}
The `collectPenalty()` function has underflow problem:
collectPenalty() underflow
Here `startBalance` is 1 ether, and the developer assumed that `address(this).balance <= 1`. This assumption is clearly false since an attacker can call `selfdestruct()` in a proxy contract to forcefully send ether to the challenge contract. This will make `address(this).balance > startBalance`, so `startBalance - address(this).balance < 0` and it causes underflow. `withdrawn` will be a huge positive number.

### Solution

Write a selfdestruct contract and deploy it with `msg.value == 1 wei`:
pragma solidity ^0.4.21;
interface IRetirementFundChallenge {
function collectPenalty() external;
function isComplete() external view returns (bool);
}
contract SelfDestruct {
function SelfDestruct(address _challenge) public payable {
require(msg.value == 1);
selfdestruct(_challenge);
}
}
After that, copy and paste the challenge contract into Remix and interact with it via "At Address". Call `collectPenalty()`. This step must be done manually since `collectPenalty()` checks `require(msg.sender == beneficiary)` and we have `beneficiary = player` in the constructor. Call `isComplete()` to verify that the challenge was solved successfully.

## Mapping

### Code Audit

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];
}
}
`uint256[] map` is a dynamic array stored in storage:
In the case of a dynamic array, the reserved slot p contains the length of the array as a uint256, and the array data itself is located sequentially at the address keccak256(p).
Understanding Ethereum Smart Contract Storage
Understanding Ethereum Smart Contract Storage
In our case, the storage layout is:
slot 0: isComplete
slot 1: map.length
// ...
slot keccak(1): map[0]
slot keccak(1) + 1: map[1]
slot keccak(1) + 2: map[2]
slot keccak(1) + 3: map[3]
slot keccak(1) + 4: map[4]
// ...
Recall that the max storage slot is `2**256 - 1` and the next slot after that is just slot 0 because of the overflow.

### Solution

Let's do a bit of math:
• `map[0]` is at slot `keccak(1)`.
• Max storage is at slot `2**256`.
• We need to overwrite slot `2**256 - keccak(1)`, which is just slot 0.
• `keccak(1) == 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6`
• `2**256 - 1 == 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`
• `2**256 - keccak(1) = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 + 1`
Compuet `2**256 - keccak(1)` in Python:
>>> 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 + 1
35707666377435648211887908874984608119992236509074197713628505308453184860938
Call `set(35707666377435648211887908874984608119992236509074197713628505308453184860938,1)` to overwrite `isComplete`.

## Donation

### Code Audit

pragma solidity ^0.4.21;
contract DonationChallenge {
struct Donation {
uint256 timestamp;
uint256 etherAmount;
}
Donation[] public donations;
function DonationChallenge() public payable {
require(msg.value == 1 ether);
owner = msg.sender;
}
function isComplete() public view returns (bool) {
}
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);
}
}
Note that when the struct `donation` is declared, its location is not defined (`memory` or `storage`). In this case, Solidity picks `storage` by default. Since `donation` is uninitialized, it will be pointing to slot 0 and slot 2 (this struct has two entries, each entry is 32-byte long). Specifically, `donation.timestamp` points to slot 0 and `donation.etherAmount` points to slot 1.
Understanding Ethereum Smart Contract Storage
Understanding Ethereum Smart Contract Storage

### Solution

Let's anaylze the storage of this contract:
• slot 0: since `Donation[] public donations` is a dynamic array, its length is stored in this slot.
• slot 1: `address public owner` is stored here.
When the following code snippet is executed:
Donation donation;
donation.timestamp = now;
donation.etherAmount = etherAmount;
Since `donation` is uninitialized, `donation.timestamp` overwrites slot 0 and `donation.etherAmount` overwrites slot 1. That is, we can overwrite `owner` with `donation.etherAmount`.
Now our task is choosing a suitable `donation.etherAmount`. This value is uint256, but we want it to represent an address. Note that there is another bug in the `donate()` function:
donate()
Since `ether` is just an alias of `10**18`, this code is equivalent to:
uint256 scale = 10**18 * 1 * 10**18;
require(msg.value == etherAmount / (10**18 * 1 * 10**18))
In this way, `msg.value` will be a small number, definitely less than 1 ether. Here you can convert your Metamask wallet address to uint256, compute `msg.value == <converted_address> / 10**36` and call `donate(<converted_address>)` together with the `msg.value` you just computed. Once `owner` is overwritten, call `withdraw()`.