瞬时存储可能存在的安全和编码错误分析

本文主要介绍了Solidity 0.8.24版本中引入的Transient Storage特性,它是一种在EVM中类似于storage的数据存储位置,但数据在每次交易后都会被丢弃。文章通过代码示例展示了Transient Storage在重入锁等场景中的应用,并强调了使用时的安全注意事项,例如需要手动清除已使用的存储槽,以避免潜在的安全风险。

请持续关注关于区块链桥中常见漏洞、我的审计思维模型以及更多内容的资源 ✨🔒

目录:

  1. 什么是 瞬态存储(简短说明)
  2. 瞬态存储的应用(简短说明)
  3. 用例说明
  4. 安全考量

什么是 瞬态存储 (简短说明):

瞬态存储 是 EVM 中专门的数据存储位置,其行为类似于存储,但此数据位置在每次交易后都会被丢弃。瞬态存储比存储更便宜。

Solidity 0.8.24 支持 Cancun 硬分叉中包含的 TSTORE 和 TLOAD 操作码,可用于存储和加载/访问瞬态存储。

有关背景和动机的更多信息,请阅读:

https://learnblockchain.cn/docs/eips/EIPS/eip-1153https://soliditylang.org/blog/2024/01/26/transient-storage/


瞬态存储的应用:

(eip 文档中列出了一些应用/用例:)

  1. 重入锁(我们将在下面看到)
  2. 链上可计算的 CREATE2 地址:构造函数参数从factory合约读取,而不是作为 init 代码哈希的一部分传递
  3. 单笔交易 ERC-20 授权,例如 #temporaryApprove(address spender, uint256 amount)
  4. 手续费转账合约:向token合约支付手续费,以在交易期间解锁转账
  5. “Till”模式:允许用户执行回调中的所有操作,并在最后检查“till”是否平衡
  6. 代理调用元数据:将额外的元数据传递到实现合约,而无需使用 calldata,例如不可变代理构造函数参数的值

此外,请查看 MoodySalem 的 瞬态存储的应用 (EIP-1153)


用例说明:

使用存储的重入锁:

contract ReentrancyGuard {
    bool public entered;
    mapping(address => uint) values;

    modifier nonreentrant {
        require(!entered,"Reentrancy");
        entered = true;
        _;
        entered = false;
    }

    receive() external payable{
        values[msg.sender] = msg.value;
    }

    function withdraw() nonreentrant public {
        uint balance = values[msg.sender];
        (bool success,) = msg.sender.call{value: balance}("");
        require(success);
        values[msg.sender]=0;
    }
}

使用瞬态存储的重入锁:

contract TransientReentrancyGuard {
    uint256 public theStorageForNoReason = 1 ; // 是的,我仍然可以使用存储!
    mapping(address => uint) public values;

    modifier nonreentrant {
        assembly {
            if tload(0) { revert(0, 0) }
            tstore(0, 1)
        }
        _;

        assembly {
            tstore(0, 0)
        }
    }

    receive() external payable{
        values[msg.sender] = msg.value;
    }

    function withdraw() nonreentrant public {
        uint balance = values[msg.sender];
        (bool success,) = msg.sender.call{value: balance}("");
        require(success);
        values[msg.sender]=0;
    }
}

从第二个例子可以看出,可以使用 tstore 将值存储在瞬态存储中,并且可以使用 tload 访问瞬态存储。有关更多信息,请查看 tstoretload

如果我们部署这两个合约并调用每个合约的 withdraw 函数来检查 gas 使用情况,可以看出使用存储的合约比使用瞬态存储的合约使用更多的 gas。


安全考量:

EIP 1153 的安全考量部分重点介绍了一些有趣的考量因素,如果未经过良好测试,可能会导致安全风险或破坏某些功能。

没有将使用的瞬态存储槽设置回默认值:

因为瞬态存储在交易结束时会自动清除,所以智能合约开发者可能会试图避免清除槽作为调用的一部分,以节省 gas。但是,这可能会阻止与合约在同一交易中进行进一步的交互(例如,在重入锁的情况下)或导致其他错误 — EIP 1153 文档

以下是一些更实际地强调此场景的示例:

  1. 未将使用的槽再次设置为默认值(重入保护示例):
contract TransientReentrancyGuardWithOrderExecution {
        struct Order{
            uint pendingPayment;
            address toAddress;
        }
        mapping(address => uint) public values;
        mapping(address => Order) public pendingOrder;

        modifier nonReentrant {
            assembly {
                if tload(0) { revert(0, 0) }
                tstore(0, 1)
            }
            _;

            // 假设:瞬态存储将在此调用结束时被清除,因此不需要 tstore(0,0)。并且 -
            // 如果有人由于第 0 个槽的值为非零而重新进入交易,它将恢复。
        }

        receive() external payable{
            values[msg.sender] = msg.value;
        }

        // 此函数在批量调用中被调用。
        function executeOrder(address _address, bytes32 _addressSignature) nonReentrant public {
            // 签名检查
            // ...

            uint pendingPayment = pendingOrder[_address].pendingPayment;
            (bool success,) = pendingOrder[_address].toAddress.call{value: pendingPayment}("");
            require(success);
            pendingOrder[_address].pendingPayment = 0;
        }
}

TransientReentrancyGuardWithOrderExecution 合约概述:

executeOrder() 函数将由具有签名的人调用,以获取输入的 _address 的待处理付款。

如果在接收 pendingPayment 时 toAddress 尝试重新进入以调用 executeOrder(),则交易将恢复,因为 nonReentrant 将检查 tload(0) 是否为非零,如果是,则它将恢复。

在第一次成功的交易之后,瞬态存储将被擦除,以便对于下一个对 executeOrder() 的交易,nonReentrant 修饰符将正常执行而不会恢复,因为 tload(0) 为零,因此不需要恢复。

在上面的示例中,假设不需要再次将存储设置为默认值,因为它最终会在交易结束时被擦除。

但是,当 executeOrder() 将在批处理中执行时,此假设是不正确的,一个交易中将涉及多个 executeOrder() 调用。但是对于第二个 executeOrder() 调用,nonReentrant 将恢复,因为 tload(0) 没有在第一次调用结束时设置为默认值。

这表明它如何破坏功能并产生问题。

2. 未将使用的槽再次设置为默认值(订单执行器示例):

contract OrderExecutorWithTransientStorage {
    struct Order{
        uint[] numberOfTokenTransferredForIds;
        address from;
        address to;
    }
    event numberOfTokensTransferred( uint);

    function executeOrder(Order calldata order) public {
        uint numberOfTokenTransferred;

        for(uint i=0; i<order.numberOfTokenTransferredForIds.length;i++){
            numberOfTokenTransferred = order.numberOfTokenTransferredForIds[i];
            assembly {
                tstore(0,add(tload(0),numberOfTokenTransferred))
            }

        }

        // 在重要逻辑中使用 numberOfTokenTransferred。
        // ...

        assembly {
            numberOfTokenTransferred := tload(0)
        }

        // 在重要事件中使用 numberOfTokenTransferred。
        emit numberOfTokensTransferred(numberOfTokenTransferred);

        // 假设:瞬态存储将在此调用结束时被清除,因此不需要 tstore(0,0)。
        // assembly {
        //     tstore(0, 0)
        // }
    }
}

OrderExecutorWithTransientStorage 合约概述:

此示例合约使用 executeOrder() 函数执行订单。executeOrder() 将 numberOfTokenTransferredForIds 数组中的元素添加到槽 0 的瞬态存储中。我们可以注意到,该值没有被分配,而是被添加到先前的值。让我们假设存储在瞬态存储中的值被用作进一步函数逻辑的一部分,并且可能会再次存储在瞬态存储中。

然后将该值加载并再次分配给 numberOfTokenTransferred 变量,以便可以在发出 numberOfTokensTransferred 事件时使用它。

在此示例中,也做出了相同的假设,即没有必要再次将存储设置为默认值,因为它最终会在交易结束时被擦除。

但是这个假设和前一个一样是不正确的。假设 executeOrder() 函数将在批处理中执行(例如 2 次),那么因为第一次调用中存储的瞬态存储值尚未清除,所以在 for 循环中它将被添加到先前存储的值中。这使得假设再次不正确。

上述情况可能会根据具体情况产生安全风险。因此,在这种情况下,对于第二次批处理调用,发出的 numberOfTokenTransferred 值将是先前值的加法,再加上在此调用中为 for 循环计算的值。并且取决于此值的使用,影响会有所不同。

使用瞬态存储来存储映射值(类似于上面讨论的 没有将使用的存储槽设置回默认值):

智能合约开发者也可能试图使用瞬态存储来替代内存映射。他们应该意识到,瞬态存储不会像内存一样在调用返回或恢复时被丢弃,并且应该首选内存用于这些用例,以免在同一交易中的重入时产生意外行为 — EIP 1153 文档

引用的语句强调了类似的事情,即在不跟踪和不清除它们的情况下使用瞬态存储来映射值,这可能会在复杂的交易中导致意外行为。

因此,通常良好的做法是清除瞬态存储:

我们建议通常始终在调用智能合约结束时完全清除瞬态存储,以避免这些问题并简化复杂交易中合约行为的分析。 — Solidity lang blogpost

总的来说,当瞬态存储在下一次调用之前没有被清除时,这种情况可能会产生安全风险,因为在这种情况下,瞬态状态的使用在关键逻辑中仍然会被考虑在内,这可能会导致安全风险,具体取决于逻辑。

此外,值得一看的是低gas重入攻击 here and here .


资源:

EIP-1153: 瞬态存储操作码

  • 原文链接: calibersec.com/a-look-in...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
calibersec
calibersec
江湖只有他的大名,没有他的介绍。