Solidity

abi.encodePacked() 產生與 abi.encodeWithSignature() 不同的結果

  • July 23, 2022

我在玩 Ethernaut Gatekeeper 2:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;

import "forge-std/Test.sol";

contract GatekeeperTwo is Test {
   address public entrant;

   modifier gateOne() {
       require(msg.sender != tx.origin, "err1");
       _;
   }

   modifier gateTwo() {
       uint x;
       assembly {
           x := extcodesize(caller())
       }
       require(x == 0, "err2");
       _;
   }

   modifier gateThree(bytes8 _gateKey) {
       emit log_named_uint("part1", uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))));

       require(
           uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^
               uint64(_gateKey) ==
               uint64(0) - 1, "err3"
       );
       _;
   }

   function enter(bytes8 _gateKey)
       public
       gateOne
       gateTwo
       gateThree(_gateKey)
       returns (bool)
   {
       entrant = tx.origin;
       return true;
   }
}

contract Attack is Test {
   constructor() public {
       GatekeeperTwo gk = new GatekeeperTwo();
       address g = address(gk);
       
       bytes4 sig = bytes4(keccak256("enter(bytes8)"));
       log_named_bytes("first method",  abi.encode(sig, key()));
       log_named_bytes("second method", abi.encodePacked(sig, key()));
       log_named_bytes("third method",  abi.encodeWithSignature("enter(bytes8)", key()));

       (bool success, ) = g.call(abi.encodePacked(sig, key()));
       require(success);
   }

   function key() private returns (bytes8) {
       uint64 val = uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^
           (type(uint64).max);
       return bytes8(val);
   }
}

contract ContractTest is Test {
   function setUp() public {}

   function testExample() public {
       new Attack();
   }
}

在我的測試程式碼中,這三行,

log_named_bytes("first method",  abi.encode(sig, key()));
log_named_bytes("second method", abi.encodePacked(sig, key()));
log_named_bytes("third method",  abi.encodeWithSignature("enter(bytes8)", key()));

產生不同的輸出:

Logs:
 first method: 0x3370204e00000000000000000000000000000000000000000000000000000000f719f96375b9c994000000000000000000000000000000000000000000000000
 second method: 0x3370204ef719f96375b9c994
 third method: 0x3370204ef719f96375b9c994000000000000000000000000000000000000000000000000

只有第三個產生正確的呼叫數據並破解關卡。為什麼其他兩個不工作?

合約函式呼叫需要一個 bytes4 函式選擇器以及 abi 編碼參數(每個填充到 32 個字節)。

abi.encode(sig, key());

sig(bytes4) 和key()(bytes8) 都填充到 32 個字節。因為sig被視為 bytes32,這會產生不正確的編碼。

abi.encodePacked(sig, key());

sig(bytes4) 和key()(bytes8) 都打包,沒有任何額外的填充。因為key()不是正確的 (bytes32) 格式,所以這是不正確的編碼。

abi.encodeWithSelector(sig, key());

返回正確的編碼,一個 bytes4sig以及一個 32 字節的填充值key()

以下是等價的:

abi.encodeWithSelector(sig, key());
abi.encodeWithSignature("enter(bytes8)", key());
abi.encodePacked(sig, abi.encode(key()));

引用自:https://ethereum.stackexchange.com/questions/132270