(https://ethernaut.openzeppelin.com/level/0x9b261b23cE149422DE75907C6ac0C30cEc4e652A)
這一關雖然標記的難度為5/10,不過需要蠻多的知識點才有辦法過關;題目的目標為要進入gate並變成entrant:
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
因此,只要我們能成功呼叫enter這個函數並執行成功,就過關了,但是呢,這個函數有3個modifier我們必須要滿足才能執行這個函數,分別來看看這三個modifier吧。
gateOne:
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
需要msg.sender 不等於 tx.origin,這個不難,了解tx.origin是整個呼叫鏈的最原始呼叫者的address,而msg.sender則是函數的直接呼叫者的address,因此用remix寫一隻合約來呼叫enter即可(tx.origin=自己電子錢包的位址、msg.sender=所寫合約部署後的address)。
gateTwo:
modifier gateTwo() {
require(gasleft().mod(8191) == 0);
_;
}
這一題筆者卡關了一些時間,後來上網找到了可用remix的debugger功能。首先需要知道gasleft()這個solidity內建函數,它會回傳剩餘的gas量,而這題要剩餘的gas量除以8191後等於0,因此可行的解法式就是我們給出的gas量= 執行gateTwo前用掉的gas量 + 剩餘的gas量(等於 8191的倍數即可),程式的寫法如下: address.enter{gas: 給出的gas量)(); 問題在於如何得到執行gateTwo前用掉的gas量呢?這時就可以利用remix的debugger功能,我們先隨便給出一個數字然後執行,用debugger看看剩餘多少gas,就可以計算出用了多少gas了! (使用debugger找出相關的step,會發現在其Stack中會出現 1fff( 即8191),而相關步驟中在筆者的例子裡也會反覆出現142e0,也就是gasleft() return的值,82656,參考下圖)
gateThree:
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(tx.origin), "GatekeeperOne: invalid gateThree part three");
_;
}
這一段我們需要熟悉solidity的casting,可先參考這裡,節錄對我們解題最主要的部份:
uint32 a = 0x12345678; uint16 b = uint16(a); // b = 0x5678
從上可以理解,若a=0x00005678 ,則 uint16(a) = uint32(a),(也就是0x5678 = 0x00005678),程式上可用位元遮罩的方式來達成,也就是用 a & 0x0000ffff 來將 a 從 0x12345678變成 0x00005678。 另外一提,我們的錢包長相如0x867500576942f95239b3Fa096839d8488f457516,共40個 hex digit(16位元,0-f,每一16位元digit= 4 bits),這題會用到的bytes8 = 8 bytes = 64 bits,uint32則是 32 bits (32/4= 8 個 digit)。所以其實bytes8跟uint64是可以互換的。 理解所有上述後,_gatekey= bytes8(tx.origin) & 0xffffffff0000ffff,可達成gateThree的要求。(這個有點小複雜,建議可以每一個規則自己比對看看,以筆者的例子 bytes8(tx.origin) => 0x6839d8488f457516, 經過&0xffffffff0000ffff =>0x6839d84800007516,將此_gatekey帶入,是不是就符合上述三條件了!)
有了以上的理解後,解題的Remix合約並不難:
pragma solidity ^0.8.0;
interface IGateKeeper_One{
function enter(bytes8 _gateKey) external returns (bool);
}
contract goGateKeeper_One{
IGateKeeper_One one= IGateKeeper_One([your level instance address]);
function beEntrant() public {
bytes8 _gateKey = bytes8(uint64(uint160(tx.origin))) & 0xffffffff0000ffff;
one.enter{gas:82164}(_gateKey);
}
}
部署後,可以用await contract.entrant();試試看是否已經變成自己錢包的address,過關,至此,我們已經正式過了Ethernaut一半的關卡囉。