如何防止ERC20智能合约中的前端运行

  • zealynx
  • 发布于 2024-05-22 11:28
  • 阅读 7

本文探讨了以太坊区块链上ERC20代币中存在的前端运行漏洞,重点介绍了如何通过代币授权机制利用这些安全漏洞。详细检查了漏洞,用一个特定的合约例子来说明它,并展示了一个概念验证攻击以突出风险。文章还介绍如何使用increaseAllowancedecreaseAllowance函数来避免ERC20代币中的前端运行漏洞。

学习高级技术和安全的智能合约实践,以保护你的 ERC20 代币免受抢跑交易、漏洞和未经授权的访问。

本指南涵盖的关于 ERC20 抢跑攻击的内容

本文探讨了以太坊区块链上 ERC20 代币中抢跑漏洞的关键问题,重点关注如何通过代币授权机制利用这些安全漏洞。

我们提供了对该漏洞的详细检查,使用特定的合约示例对其进行说明,并展示了概念验证攻击,以突出其风险。

继续阅读,了解如何使用为 ERC20 标准量身定制的增强安全措施来保护你的数字资产。

介绍

抢跑攻击利用了以太坊网络上的交易按照矿工确定的顺序处理这一事实,该顺序基于提供的 gas 价格。 这种漏洞可能导致攻击者可以通过提供更高的 gas 价格来抢跑合法交易,从而在原始交易处理之前操纵合约状态的情况。

这种漏洞的一个常见例子是 ERC20 代币标准的 approve 函数,它允许用户授权另一个地址代表他们花费一定数量的代币。 如果未正确实施,攻击者可以抢跑授权交易并耗尽用户的代币。

ERC20 approve 漏洞如何实现抢跑

1. 抢跑如何在以太坊和 ERC20 代币上运作

定义:

加密货币中的抢跑通常发生在了解即将发生的交易(来自公共 mempool)的某人,通过以某种方式放置交易,使其在已知交易之前得到确认,从而利用此信息来为自己谋取利益。

ERC20 上下文:

对于 ERC20 代币,抢跑通常涉及查看待处理的 approve 交易,并在修改此授权的新 approve 交易被处理前,快速使用当前授权。

2. 易受攻击的 ERC20 approve 合约的示例

这是一个现有的易受攻击的合约的简化示例。 你可以在 Etherscan 中找到完整的实施。

1pragma solidity 0.6.4;
2//ERC20 Interface
3interface ERC20 {
4    [...]
5}
6interface VETH {
7    [...]
8}
9library SafeMath {
10    [...]
11}
12    //======================================VETHER=========================================//
13contract Vether4 is ERC20 {
14    using SafeMath for uint;
15    // ERC-20 Parameters
16    string public name;
17    string public symbol;
18    uint public decimals;
19    uint public override totalSupply;
20    // ERC-20 Mappings
21    mapping(address => uint) private _balances;
22    mapping(address => mapping(address => uint)) private _allowances;
23    // Rest of Parameters
24    [...]
25    //=====================================CREATION=========================================//
26    // Constructor
27    constructor() public {
28       [...]
29    }
30    function _setMappings() internal {
31        [...]
32    }
33

34    //========================================ERC20=========================================//
35    function balanceOf(address account) public view override returns (uint256) {
36        return _balances[account];
37    }
38    function allowance(address owner, address spender) public view virtual override returns (uint256) {
39        return _allowances[owner][spender];
40    }
41    // ERC20 Transfer function
42    function transfer(address to, uint value) public override returns (bool success) {
43        _transfer(msg.sender, to, value);
44        return true;
45    }
46    // ERC20 Approve function
47    function approve(address spender, uint value) public override returns (bool success) {
48        _allowances[msg.sender][spender] = value;
49        emit Approval(msg.sender, spender, value);
50        return true;
51    }
52    // ERC20 TransferFrom function
53    function transferFrom(address from, address to, uint value) public override returns (bool success) {
54        require(value <= _allowances[from][msg.sender], 'Must not send more than allowance');
55        _allowances[from][msg.sender] = _allowances[from][msg.sender].sub(value);
56        _transfer(from, to, value);
57        return true;
58    }
59    // Internal transfer function which includes the Fee
60    function _transfer(address _from, address _to, uint _value) private {
61        require(_balances[_from] >= _value, 'Must not send more than balance');
62        require(_balances[_to] + _value >= _balances[_to], 'Balance overflow');
63        _balances[_from] =_balances[_from].sub(_value);
64        uint _fee = _getFee(_from, _to, _value);                                            // Get fee amount
65        _balances[_to] += (_value.sub(_fee));                                               // Add to receiver
66        _balances[address(this)] += _fee;                                                   // Add fee to self
67        totalFees += _fee;                                                                  // Track fees collected
68        emit Transfer(_from, _to, (_value.sub(_fee)));                                      // Transfer event
69        if (!mapAddress_Excluded[_from] && !mapAddress_Excluded[_to]) {
70            emit Transfer(_from, address(this), _fee);                                      // Fee Transfer event
71        }
72    }
73    // Calculate Fee amount
74    function _getFee(address _from, address _to, uint _value) private view returns (uint) {
75        if (mapAddress_Excluded[_from] || mapAddress_Excluded[_to]) {
76           return 0;                                                                        // No fee if excluded
77        } else {
78            return (_value / 1000);                                                         // Fee amount = 0.1%
79        }
80    }
81

82    //================================REST OF THE CONTRACT======================================//
83    [...]
84}

Vether4 中的特定漏洞点

approvetransferFrom 机制:

场景:

假设 Alice 决定将 Bob 的授权从 5000 VETH 更改为 2500 VETH。 她提交了一个 approve 交易,将 Bob 的授权设置为 2500 VETH。

抢跑机会:

恶意行为者(或 Bob 本人)可以监视待处理的交易,并在 Alice 的新 approve 交易得到确认之前,发出 transferFrom 交易以转移最初批准的 5000 VETH。 如果抢跑者的交易首先被挖掘,他们可以在旧授权减少之前充分利用它。

approve 函数中缺少检查:

approve 函数直接设置消费者的授权,而不考虑任何先前设置的授权。 这可能导致一种称为“竞争条件”的情况,在这种情况下,快速执行的交易可以操纵授权,以转移比代币所有者预期更多的代币。

没有针对双重支出授权的保护:

approve 函数不能防止双重支出问题。 如果用户降低了已经具有一定授权的消费者的授权,则如果 approve 交易在之后被挖掘,则消费者可以快速花费现有授权,并且仍然可以访问新设置的授权。

3. Foundry PoC:模拟抢跑攻击

  1. Alice 批准了 Bob 5000 VETH 的授权。
  2. Alice 尝试将授权降低到 2500 VETH。
  3. Bob 注意到 mempool 中的交易,并通过使用 transferFrom 调用来耗尽全部授权来抢跑它。
  4. Alice 降低的授权已确认,Bob 现在拥有 2500 VETH 的授权,可以进一步花费总计 7500 VETH。
1// SPDX-License-Identifier: MIT
2pragma solidity ^0.8.0;
3

4import {Test, console} from "forge-std/Test.sol";
5

6interface IVether4 {
7    function transfer(address to, uint amount) external returns (bool);
8    function balanceOf(address account) external view returns (uint);
9    function approve(address spender, uint amount) external returns (bool);
10    function transferFrom(address from, address to, uint amount) external returns (bool);
11}
12

13contract AllowanceRaceConditionTest is Test {
14    IVether4 vether;
15

16    address constant vetherAddress = 0x4Ba6dDd7b89ed838FEd25d208D4f644106E34279; // Mainnet address of Vether4
17

18    address alice = 0x273BA31C706b9D9FdAe1FD999183Bfa865895bE9;
19    address bob = 0x159e4E57eD13176A69693c987917651C552d2575;
20

21    function setUp() public {
22        vether = IVether4(vetherAddress);
23    }
24

25    function testAllowanceRaceCondition() public {
26        uint256 bobInitialBalance = vether.balanceOf(bob);
27        uint256 aliceInitialBalance = vether.balanceOf(alice);
28

29        console.log("Bob's initial balance: ", bobInitialBalance);
30        console.log("Alice's initial balance: ", aliceInitialBalance);
31

32        // Alice approves Bob to spend 5000 VETH
33        vm.prank(alice);
34        vether.approve(bob, 5000 ether); // Using 'ether' to convert to correct token units if necessary
35

36        // Bob tries to front-run Alice's next approval by transferring 5000 VETH to himself
37        vm.prank(bob);
38        vether.transferFrom(alice, bob, 5000 ether);
39

40        // Alice lowers the allowance to 2500 VETH
41        vm.prank(alice);
42        vether.approve(bob, 2500 ether);
43

44        // Bob attempts to transfer the newly approved 2500 VETH
45        vm.prank(bob);
46        bool txSuccess = vether.transferFrom(alice, bob, 2500 ether);
47

48        uint256 bobFinalBalance = vether.balanceOf(bob);
49        uint256 expectedBobBalance = bobInitialBalance + 7500 ether; // Sum of first and second transfer
50

51        assertEq(bobFinalBalance, expectedBobBalance, "Bob's final balance should reflect the transfers");
52        assertTrue(txSuccess, "Bob's second transfer should succeed within allowance");
53    }
54}

合约接口和设置:

IVether4 接口:

这定义了 Vether4 合约的预期函数,允许测试合约与 Vether4 交互,就像它是一个本地 Solidity 合约一样。

setUp 函数:

此函数通过将其指向主网上 Vether4 的部署地址来初始化 Vether4 合约接口。 此设置在每次运行测试之前发生。

测试功能 (testAllowanceRaceCondition):

初始余额检查:

测试首先记录 Bob 和 Alice 的初始以太坊余额。 重要的是要注意,如果 bob.balancealice.balance 指的是他们的 ETH 余额,它们可能不会反映 Vether 代币转账的变化。 对于代币余额检查,应改用 vether.balanceOf(address)

Approve 和 TransferFrom:

该测试模拟了一个常见场景,其中 Alice 批准 Bob 花费一定数量的 Vether 代币,然后尝试更改此授权。 但是,在设置新授权之前,Bob 尝试转移最初批准的金额。

  • Alice 首先批准 Bob 花费 5000 VETH。
  • Bob 可能会恶意行事或利用时机,在 Alice 可以降低授权之前将 5000 VETH 转移给自己。
  • 然后,Alice 将授权降低到 2500 VETH。
  • Bob 尝试再次在新授权下进行转移。

余额验证:

操作完成后,测试会检查 Bob 的最终代币余额是否与预期值匹配,并考虑两次转移。 这对于验证代币转账操作是否尊重随时间推移设置和更改的授权至关重要。

安全隐患:

此测试有效地演示了一种竞争条件,Bob 可以在成功减少初始授权之前使用全部初始授权。 它展示了谨慎处理授权的重要性,尤其是在多用户环境中,时间差异可能导致意外或剥削行为。

使用 Forge Test Fork 运行测试

Forge Test Fork:

你正在以太坊主网的一个 fork 上运行这些测试。 这允许你在受控的隔离环境中与当前以太坊主网的状态交互。 --fork-url 参数指向一个 Alchemy 节点,该节点提供对该网络 fork 版本的访问。

运行命令:

1forge test --fork-url https://eth-mainnet.g.alchemy.com/v2/your-api-key

如何预防 ERC20 抢跑漏洞

在各种防止 ERC20 代币中抢跑漏洞的策略中,使用 increaseAllowancedecreaseAllowance 函数尤为有效。 这种方法解决了传统 approve 函数的核心问题,该函数可能会使用户在授权调整期间遭受抢跑攻击。

为什么 increaseAllowancedecreaseAllowance 有效:

最大限度地减少竞争条件窗口:

这些方法通过最大限度地减少授权易受攻击的时间窗口来降低抢跑的风险。 通过逐步调整授权,它可以确保任何抢跑交易都不能利用突然授予的大额授权。

增强对授权更改的控制:

通过允许代币持有者准确指定增加或减少授权的数量,这些函数可以更精确地控制更改,从而降低意外设置不正确授权的可能性。

防止零重置漏洞:

在设置新的更高值之前将授权重置为零的传统方法可能存在风险,如果在零重置之后但在设置新值之前,approve 交易被抢跑。 increaseAllowancedecreaseAllowance 函数通过直接调整现有值来固有地避免了此缺陷。

1function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
2    _approve(msg.sender, spender, allowance(msg.sender, spender).add(addedValue));
3    return true;
4}
5

6function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
7    _approve(msg.sender, spender, allowance(msg.sender, spender).sub(subtractedValue, "ERC20: decreased allowance below zero"));
8    return true;
9}

结论

总之,对 ERC20 代币标准内抢跑漏洞的探索强调了改进安全措施的关键需求。 通过详细说明攻击者如何利用以太坊网络上的交易顺序,特别是通过 approve 函数,我们强调了保护数字资产的重要性。 实施诸如 safeApprove 方法和采用带有 Permit 标准的 ERC20 等解决方案可以显着降低风险,使智能合约更强大地抵御此类漏洞。

对于加密货币领域的开发人员和参与者而言,优先考虑这些增强功能不仅仅是为了保护投资,而是为了在区块链技术中培养信任和稳定性。 随着我们不断创新,集成严格的安全实践将是维持增长并确保区块链实现安全、分散交易承诺的关键。 对于任何希望增强其在以太坊平台上数字资产的完整性和功能的人来说,这些改进都是必不可少的。


关于 Zealynx Security

抢跑漏洞是可以避免,但前提是你知道在哪里查找。 在 Zealynx Security,我们通过专家审核、模糊测试和有针对性的测试来帮助团队加强智能合约,这些测试可以捕获此处涵盖的各种漏洞。 如果你正在使用 ERC20 构建或在启动前需要安心,请联系我们或探索我们如何提供帮助。

与我们联系:

参考文献:

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

0 条评论

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