Skip to content

Commit 5f80329

Browse files
committed
add Proof of Weak Hands Coin
1 parent 03d895d commit 5f80329

File tree

7 files changed

+497
-3
lines changed

7 files changed

+497
-3
lines changed

ProofofWeakHandsCoin/Analysis.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Analysis of the Proof of Weak Hands Coin ponzi hack
2+
4chan decided to create a crypto ponzi scheme, which was advertised as such, and obviously it worked well. In only three days it had over 1,000 ETH, but an underflow vulnerability allowed someone to withdraw 866 ETH from the ponzi.
3+
4+
## Addresses involved
5+
The Ponzi contract: [0xa7ca36f7273d4d38fc2aec5a454c497f86728a7a](https://etherscan.io/address/0xa7ca36f7273d4d38fc2aec5a454c497f86728a7a#code)
6+
The approve tx: [0xc836c7c6ac8135cf1df3da5754cfa9959d327e2ec3748f83124089dfe621a98b](https://etherscan.io/tx/0xc836c7c6ac8135cf1df3da5754cfa9959d327e2ec3748f83124089dfe621a98b)
7+
The transferFrom tx: [0x233107922bed72a4ea7c75a83ecf58dae4b744384e2b3feacd28903a17b864e0](https://etherscan.io/tx/0x233107922bed72a4ea7c75a83ecf58dae4b744384e2b3feacd28903a17b864e0)
8+
The withdraw tx: [0x496c0411f52978dfd7953b7e6965465977162bfaf7b88c0c78fcdc97cd395d62](https://etherscan.io/tx/0x496c0411f52978dfd7953b7e6965465977162bfaf7b88c0c78fcdc97cd395d62)
9+
10+
The attacker address: [0xB9cd700b8A16069Bf77edEdC71c3284780422774](https://etherscan.io/address/0xB9cd700b8A16069Bf77edEdC71c3284780422774)
11+
The attacker 2nd address: [0x945C84b2FdD331ed3E8e7865E830626e6CeFAB94](https://etherscan.io/address/0x945C84b2FdD331ed3E8e7865E830626e6CeFAB94)
12+
13+
## The vulnerability: Underflow
14+
15+
There is two issues in this contract: an underflow vulnerability and a poor internal logic that can potentialy lead to this underflow.
16+
The underflow vulnerability of the contract is in the `sell()` function:
17+
```solidity
18+
function sell(uint256 amount) internal {
19+
var numEthers = getEtherForTokens(amount);
20+
// remove tokens
21+
totalSupply -= amount;
22+
balanceOfOld[msg.sender] -= amount; //** possible underflow if amount is bigger that the balance
23+
24+
// fix payouts and put the ethers in payout
25+
var payoutDiff = (int256)(
26+
earningsPerShare * amount + (numEthers * PRECISION)
27+
);
28+
payouts[msg.sender] -= payoutDiff;
29+
totalPayouts -= payoutDiff;
30+
}
31+
```
32+
`sell()` is called when a user call `tranfer()` with `_to` equal to the contract address. There is in fact a check to ensure that when a user wants to sell his tokens, he has that amount of tokens. But since `transferTokens(_from, _to, _value)` allows a user to transfer tokens from a different address (who has previously approved the user to do so), the check is done for that `_from` address and if that address indeed has those tokens, the check pass. But when `sell()` decrease the balance, it does so to the `msg.sender` address instead of the `_from` so if the user (`msg.sender`) has less tokens that the amount passed in `_value`, an underflow will occur. The error is that `_from` is not passed to `sell()` as an argument.
33+
34+
```solidity
35+
function transfer(address _to, uint256 _value) public {
36+
transferTokens(msg.sender, _to, _value);
37+
}
38+
39+
function transferTokens(
40+
address _from,
41+
address _to,
42+
uint256 _value
43+
) internal {
44+
if (balanceOfOld[_from] < _value) revert(); //** underflow check
45+
if (_to == address(this)) {
46+
sell(_value); //** sell doesn't know who _from is
47+
} else {
48+
int256 payoutDiff = (int256)(earningsPerShare * _value);
49+
balanceOfOld[_from] -= _value;
50+
balanceOfOld[_to] += _value;
51+
payouts[_from] -= payoutDiff;
52+
payouts[_to] += payoutDiff;
53+
}
54+
Transfer(_from, _to, _value);
55+
}
56+
57+
58+
```
59+
60+
61+
## The exploit
62+
63+
The attacker will use an underflow to increase his token balance to the maximum amount and will then use that balance to exit the Ponzi with a lot of ETH.
64+
65+
He first used a second account (0x945) to buy some tokens and to approv his first account (0xb9cd) to transfer some of his tokens:
66+
67+
![approve transaction](img/approve.png "approve transaction")
68+
69+
Now that he can transfer tokens on behalf of his other account, he called `transferFrom(_from_=0x945, _to=PonziContract, _value=1)`:
70+
71+
![transferFrom transaction](img/transferFrom.png "transferFrom transaction")
72+
73+
This check the allowance and then call `transferToken()`:
74+
```solidity
75+
function transferFrom(
76+
address _from,
77+
address _to,
78+
uint256 _value
79+
) public {
80+
var _allowance = allowance[_from][msg.sender];
81+
if (_allowance < _value) revert();
82+
allowance[_from][msg.sender] = _allowance - _value;
83+
transferTokens(_from, _to, _value);
84+
}
85+
function transferTokens(
86+
address _from,
87+
address _to,
88+
uint256 _value
89+
) internal {
90+
if (balanceOfOld[_from] < _value) revert();
91+
if (_to == address(this)) {
92+
sell(_value); //** _from is not passed as parameter
93+
} else {
94+
int256 payoutDiff = (int256)(earningsPerShare * _value);
95+
balanceOfOld[_from] -= _value;
96+
balanceOfOld[_to] += _value;
97+
payouts[_from] -= payoutDiff;
98+
payouts[_to] += payoutDiff;
99+
}
100+
Transfer(_from, _to, _value);
101+
}
102+
```
103+
`transferToken()` check that the balance of `_from` is high enough to transfer `_value` of tokens (it is) and since `_to` is the address of the contract, call `sell()`:
104+
```solidity
105+
function sell(uint256 amount) internal {
106+
var numEthers = getEtherForTokens(amount);
107+
// remove tokens
108+
totalSupply -= amount;
109+
balanceOfOld[msg.sender] -= amount; //** possible underflow if amount is bigger that the balance
110+
111+
// fix payouts and put the ethers in payout
112+
var payoutDiff = (int256)(
113+
earningsPerShare * amount + (numEthers * PRECISION)
114+
);
115+
payouts[msg.sender] -= payoutDiff;
116+
totalPayouts -= payoutDiff;
117+
}
118+
```
119+
But since `sell()` doesn't receive the `_from` parameter (0x945), it assumes that it is the `msg.sender` (0xb9cd), which doesn't have any token, so `balanceOfOld[msg.sender] -= amount` underflow and is now equal to the maximum amount of `uint256`.
120+
121+
He then sold some of his tokens and called `withdraw()` which allowed him to exit the Ponzi with over 866 ETH:
122+
![transfer transaction](img/transfer.png "transfer transaction")
123+
124+
![withdraw transaction](img/withdraw.png "withdraw transaction")
125+

0 commit comments

Comments
 (0)