Solidity入门(9)-合约间调用
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。提示:以下是本篇文章正文内容,下面案例可供参考在Solidity中,接口(Interface)是一种定义合约必须实现的函数签名的方式。接口本身不包含任何实现代码,只声明函数的名称、参数和返回值。定义函数签名:接口规定了合约应该提供哪些功能,就像一份合同不包含实现:接口只告诉你"有
文章目录
1. 合约间调用基础概念
1.1 为什么需要合约间调用
在实际的区块链开发中,很少有孤立存在的合约。更常见的情况是,多个合约之间需要相互协作、调用,共同完成复杂的业务逻辑。
合约间调用的必要性:
模块化设计:
- 将复杂系统拆分为多个专门的合约
- 每个合约负责特定功能
- 提高代码可维护性和可重用性
功能复用:
- 使用标准接口(如ERC20、ERC721)实现互操作性
- 避免重复实现相同功能
- 利用经过审计的成熟合约
业务复杂性:
- DeFi协议需要调用多个外部合约
- 借贷协议需要价格预言机、代币合约、流动性池
- 复杂的业务逻辑需要多个合约协作
升级和维护:
- 通过代理模式实现合约升级
- 分离数据和逻辑,便于维护
- 支持渐进式功能迭代
实际应用场景:
// 场景1:DeFi借贷协议
contract LendingProtocol {
IERC20 public token; // 调用代币合约
IPriceOracle public oracle; // 调用价格预言机
ILiquidityPool public pool; // 调用流动性池
function borrow(uint256 amount) public {
// 1. 从价格预言机获取价格
uint256 price = oracle.getPrice(address(token));
// 2. 从代币合约转移资金
token.transferFrom(msg.sender, address(this), amount);
// 3. 与流动性池交互
pool.deposit(amount);
}
}
1.2 合约间调用的方式
Solidity提供了多种合约间调用的方式,每种方式都有其特点和适用场景:
- 接口调用(Interface):
- 最安全、最规范的方式
- 编译时类型检查
- 代码可读性好
- 适合调用已知接口的合约
- 底层调用方法:
- call:最通用的调用方式,可以发送以太币
- delegatecall:在调用者上下文中执行,用于代理模式
- staticcall:只读调用,不能修改状态
- 合约创建:
- new关键字:传统创建方式
- create2:可预先计算地址的创建方式
1.3 调用上下文的重要性
理解调用上下文是掌握合约间调用的关键。不同的调用方式会在不同的上下文中执行,这直接影响:
- 状态变量的修改位置
- msg.sender的值
- 合约余额的归属
- Gas消耗
2. 接口调用(Interface)
接口调用是合约间交互最安全、最规范的方式。它提供了类型安全、代码可读性好、Gas效率高等优势。
2.1 什么是接口
在Solidity中,接口(Interface)是一种定义合约必须实现的函数签名的方式。接口本身不包含任何实现代码,只声明函数的名称、参数和返回值。
接口的三个重要特征:
- 定义函数签名:接口规定了合约应该提供哪些功能,就像一份合同
- 不包含实现:接口只告诉你"有什么",不告诉你"怎么做"
- 只声明函数:接口不包含状态变量,保持极简,专注于函数定义
接口与合约的区别:
// 接口:只有函数签名,没有实现
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
// 合约:有完整的实现
contract ERC20Token is IERC20 {
mapping(address => uint256) public balanceOf;
// 必须实现接口中声明的所有函数
function transfer(address to, uint256 amount) external returns (bool) {
// 具体实现...
return true;
}
function balanceOf(address account) external view returns (uint256) {
// 具体实现...
return balanceOf[account];
}
}
2.2 接口定义语法
基本语法:
interface InterfaceName {
function functionName(param1, param2) external returns (returnType);
// 更多函数声明...
}
重要规则:
- 函数必须标记为external:接口中的函数不能是public、internal或private
- 没有函数体:接口中的函数只有签名,没有实现代码
- 可以继承:接口可以继承其他接口
- 不能有状态变量:接口中不能声明状态变量
- 不能有构造函数:接口不能有构造函数
完整示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// ERC20代币接口定义
interface IERC20 {
// 转账函数:从调用者地址向指定地址转账
function transfer(address to, uint256 amount) external returns (bool);
// 授权转账函数:从指定地址向另一个地址转账(需要授权)
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
// 查询余额函数:查询指定地址的代币余额
function balanceOf(address account) external view returns (uint256);
// 授权函数:授权指定地址使用调用者的代币
function approve(address spender, uint256 amount) external returns (bool);
// 查询授权额度:查询owner授权给spender的额度
function allowance(address owner, address spender)
external
view
returns (uint256);
// 查询总供应量
function totalSupply() external view returns (uint256);
}
在上面的接口定义中:
- 所有函数都标记为external,这是接口的要求
- view函数(如balanceOf)仍然需要external关键字
- 函数只有签名,没有实现代码
- 返回值类型明确声明,便于调用者处理
2.3 接口使用示例
定义了接口后,我们可以在其他合约中使用它来调用外部合约。
基础使用示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 定义ERC20接口
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
}
// 代币交换合约:使用接口调用外部代币合约
contract TokenSwap {
// 声明两个代币接口变量
IERC20 public tokenA;
IERC20 public tokenB;
// 定义交换事件,记录每次交换的详细信息
event Swap(
address indexed user,
uint256 amountA,
uint256 amountB
);
// 构造函数:初始化两个代币合约地址
constructor(address _tokenA, address _tokenB) {
// 将地址转换为接口类型,这样就可以调用接口中定义的方法
tokenA = IERC20(_tokenA);
tokenB = IERC20(_tokenB);
}
/**
* @notice 执行代币交换
* @param amountA 要交换的tokenA数量
* @dev 使用接口调用确保类型安全
*/
function swap(uint256 amountA) external {
// 步骤1:从用户账户转移tokenA到本合约
// transferFrom需要用户先调用approve授权本合约使用其代币
// 如果转账失败,require会回滚整个交易
require(
tokenA.transferFrom(msg.sender, address(this), amountA),
"TokenA transfer failed"
);
// 步骤2:计算可交换的tokenB数量(简化示例,1:1兑换)
uint256 amountB = amountA;
// 步骤3:从本合约向用户转移tokenB
// 如果转账失败,require会回滚整个交易
require(
tokenB.transfer(msg.sender, amountB),
"TokenB transfer failed"
);
// 步骤4:触发事件,记录交换信息
emit Swap(msg.sender, amountA, amountB);
}
/**
* @notice 查询合约持有的代币余额
* @return balanceA 合约持有的tokenA数量
* @return balanceB 合约持有的tokenB数量
*/
function getBalances() external view returns (uint256 balanceA, uint256 balanceB) {
// 使用接口的view函数查询余额,不消耗Gas
balanceA = tokenA.balanceOf(address(this));
balanceB = tokenB.balanceOf(address(this));
}
}
在上面的代码中:
- TokenSwap合约通过接口调用外部代币合约
- 使用IERC20(_tokenA)将地址转换为接口类型
- 调用transferFrom和transfer时,编译器会检查参数类型
- 如果传入错误的参数类型,编译时就会报错
2.4 接口调用的优势
使用接口调用有四个明显的优势:
- 类型安全(Type Safety):
编译时检查可以减少很多错误。如果你调用的函数不存在,或者参数类型不对,编译器会直接报错,而不是等到运行时才发现问题。
contract TypeSafetyExample {
IERC20 public token;
function transferTokens(address to, uint256 amount) public {
// 正确:编译器会检查参数类型
token.transfer(to, amount);
// 编译错误:参数类型不匹配
// token.transfer(to, "100"); // 字符串不能传给uint256参数
// 编译错误:函数不存在
// token.nonExistentFunction(); // 接口中没有这个函数
}
}
- 代码可读性好(Readability):
当你看到IERC20类型的变量,立刻就知道这是一个符合ERC20标准的代币合约,它支持哪些操作一目了然。
- Gas效率高(Gas Efficiency):
接口调用生成的字节码体积小,执行效率高。相比直接使用低级call调用,接口调用的Gas消耗更少。
- 易于测试(Testability):
在单元测试中,我们可以使用mock合约来模拟接口,而不需要部署真实的合约,这大大简化了测试流程。
// 测试用的Mock代币合约
contract MockERC20 is IERC20 {
mapping(address => uint256) public balanceOf;
// 实现接口中的所有函数,但逻辑可以简化
function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
function balanceOf(address account) external view returns (uint256) {
return balanceOf[account];
}
// 其他接口函数的简化实现...
}
// 在测试中使用Mock合约
contract TokenSwapTest {
function testSwap() public {
// 部署Mock合约而不是真实合约
MockERC20 mockTokenA = new MockERC20();
MockERC20 mockTokenB = new MockERC20();
// 使用Mock合约测试TokenSwap
TokenSwap swap = new TokenSwap(
address(mockTokenA),
address(mockTokenB)
);
// 执行测试...
}
}
2.5 接口继承
接口可以继承其他接口,这样可以组合多个接口的功能。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 基础代币接口
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
// 扩展接口:增加了授权功能
interface IERC20Extended is IERC20 {
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
}
// 使用扩展接口
contract AdvancedTokenSwap {
// 使用扩展接口,可以调用更多方法
IERC20Extended public token;
function swapWithApproval(
address to,
uint256 amount
) external {
// 可以使用扩展接口中的方法
require(
token.transferFrom(msg.sender, address(this), amount),
"TransferFrom failed"
);
require(
token.transfer(to, amount),
"Transfer failed"
);
}
}
2.6 接口调用的完整示例
以下是一个完整的代币交换合约示例,展示了接口调用的实际应用:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 定义ERC20接口
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
function balanceOf(address account) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
}
// 简单的ERC20代币实现(用于演示)
contract SimpleToken is IERC20 {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
string public name;
string public symbol;
uint8 public decimals = 18;
uint256 public totalSupply;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
name = _name;
symbol = _symbol;
totalSupply = _initialSupply * 10**decimals;
_balances[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function transfer(address to, uint256 amount) external returns (bool) {
require(_balances[msg.sender] >= amount, "Insufficient balance");
_balances[msg.sender] -= amount;
_balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool) {
require(_allowances[from][msg.sender] >= amount, "Insufficient allowance");
require(_balances[from] >= amount, "Insufficient balance");
_allowances[from][msg.sender] -= amount;
_balances[from] -= amount;
_balances[to] += amount;
emit Transfer(from, to, amount);
return true;
}
function balanceOf(address account) external view returns (uint256) {
return _balances[account];
}
function approve(address spender, uint256 amount) external returns (bool) {
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
}
// 代币交换合约(使用接口调用)
contract TokenSwap {
// 声明两个代币接口变量
IERC20 public tokenA;
IERC20 public tokenB;
// 交换事件:记录每次交换的详细信息
event Swap(
address indexed user,
address indexed tokenIn,
address indexed tokenOut,
uint256 amountIn,
uint256 amountOut
);
// 构造函数:初始化代币合约地址
constructor(address _tokenA, address _tokenB) {
// 将地址转换为接口类型
// 这样编译器会检查这些地址对应的合约是否实现了接口中的函数
tokenA = IERC20(_tokenA);
tokenB = IERC20(_tokenB);
}
/**
* @notice 执行代币交换
* @param amountA 要交换的tokenA数量
* @dev 用户需要先调用tokenA的approve函数授权本合约使用其代币
*/
function swap(uint256 amountA) external {
// 步骤1:检查合约是否有足够的tokenB用于交换
// 使用接口的view函数查询余额,不消耗Gas
uint256 contractBalanceB = tokenB.balanceOf(address(this));
require(contractBalanceB >= amountA, "Insufficient tokenB in contract");
// 步骤2:从用户账户转移tokenA到本合约
// transferFrom需要用户先调用tokenA.approve授权本合约
// 如果转账失败,require会回滚整个交易
require(
tokenA.transferFrom(msg.sender, address(this), amountA),
"TokenA transfer failed"
);
// 步骤3:从本合约向用户转移tokenB
// 简化示例:1:1兑换比例
uint256 amountB = amountA;
require(
tokenB.transfer(msg.sender, amountB),
"TokenB transfer failed"
);
// 步骤4:触发事件,记录交换信息
// 前端应用可以监听这个事件来更新UI
emit Swap(msg.sender, address(tokenA), address(tokenB), amountA, amountB);
}
/**
* @notice 查询合约持有的代币余额
* @return balanceA 合约持有的tokenA数量
* @return balanceB 合约持有的tokenB数量
*/
function getContractBalances()
external
view
returns (uint256 balanceA, uint256 balanceB)
{
// 使用接口的view函数查询余额
// view函数不修改状态,外部调用不消耗Gas
balanceA = tokenA.balanceOf(address(this));
balanceB = tokenB.balanceOf(address(this));
}
/**
* @notice 查询用户持有的代币余额
* @param user 要查询的用户地址
* @return balanceA 用户的tokenA余额
* @return balanceB 用户的tokenB余额
*/
function getUserBalances(address user)
external
view
returns (uint256 balanceA, uint256 balanceB)
{
balanceA = tokenA.balanceOf(user);
balanceB = tokenB.balanceOf(user);
}
}
3. 底层调用方法
除了接口调用,Solidity还提供了三种底层调用方法:call、delegatecall和staticcall。这三个方法功能强大但也更危险,需要谨慎使用。
3.1 call方法
call是最通用的底层调用方法,它可以调用任意合约的任意函数,甚至可以发送以太币。
call方法的特点:
- 可以发送以太币:这是call相比其他方法的独特优势
- 在被调用合约的上下文中执行:被调用的合约会使用它自己的storage、自己的余额
- 最通用的调用方式:当你不确定用哪种方法时,call通常是安全的选择
基本语法:
(bool success, bytes memory data) = address.call{value: amount}(
abi.encodeWithSignature("functionName(type1,type2)", arg1, arg2)
);
参数说明:
- address:要调用的合约地址
- {value: amount}:可选,要发送的以太币数量(wei)
- abi.encodeWithSignature(…):编码函数调用数据
- 返回值:success表示调用是否成功,data是返回的数据
完整示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 被调用的目标合约
contract TargetContract {
uint256 public value;
address public sender;
// 接收以太币的函数
function setValue(uint256 _value) external payable {
value = _value;
sender = msg.sender;
}
// 普通函数
function getValue() external view returns (uint256) {
return value;
}
}
// 调用者合约:使用call方法
contract CallerContract {
// 使用call调用目标合约的函数
function callSetValue(address target, uint256 newValue) external payable {
// 使用call调用setValue函数,并发送以太币
// abi.encodeWithSignature编码函数签名和参数
(bool success, bytes memory data) = target.call{value: msg.value}(
abi.encodeWithSignature("setValue(uint256)", newValue)
);
// 必须检查返回值,call失败不会自动revert
require(success, "Call failed");
}
// 使用call调用view函数
function callGetValue(address target) external view returns (uint256) {
// 调用view函数,不发送以太币
(bool success, bytes memory returnData) = target.call(
abi.encodeWithSignature("getValue()")
);
require(success, "Call failed");
// 解码返回值
// abi.decode用于解码ABI编码的数据
uint256 value = abi.decode(returnData, (uint256));
return value;
}
// 使用call发送以太币(不调用函数)
function sendEther(address payable recipient) external payable {
// 直接向地址发送以太币,不调用任何函数
(bool success, ) = recipient.call{value: msg.value}("");
require(success, "Ether transfer failed");
}
}
在上面的代码中:
- callSetValue使用call调用目标合约的函数并发送以太币
- callGetValue使用call调用view函数并解码返回值
- sendEther使用call直接发送以太币
call方法的执行上下文:
contract ContextExample {
uint256 public callerValue;
address public callerSender;
function testCall(address target) external {
// 调用目标合约的setValue函数
(bool success, ) = target.call(
abi.encodeWithSignature("setValue(uint256)", 42)
);
require(success, "Call failed");
// 注意:callerValue和callerSender不会被修改
// 因为setValue是在target合约的上下文中执行的
// 它修改的是target合约的storage,不是本合约的
}
}
3.2 delegatecall方法
delegatecall是一个特殊的方法,它的关键特点是在调用者的上下文中执行。
delegatecall的核心特点:
-
在调用者的上下文中执行:
- 被调用的函数会修改调用者合约的storage
- 被调用的函数使用的是调用者合约的余额
- 但代码逻辑来自被调用的合约
-
msg.sender保持不变:
- 在delegatecall中,msg.sender仍然是原始调用者
- 这对于权限控制非常重要
-
不能发送以太币:
- delegatecall不支持value参数
- 不能通过delegatecall发送以太币
基本语法:
(bool success, bytes memory data) = address.delegatecall(
abi.encodeWithSignature("functionName(type1,type2)", arg1, arg2)
);
完整示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 逻辑合约:包含业务逻辑
contract LogicContract {
// 注意:这些变量的存储位置必须与代理合约匹配
uint256 public value;
address public owner;
// 设置值的函数
function setValue(uint256 _value) external {
// 这个函数会修改调用者合约的storage
value = _value;
owner = msg.sender; // msg.sender是原始调用者,不是代理合约
}
// 获取值的函数
function getValue() external view returns (uint256) {
return value;
}
}
// 代理合约:存储数据,通过delegatecall调用逻辑合约
contract ProxyContract {
// 存储布局必须与LogicContract完全一致
address public implementation; // 逻辑合约地址
uint256 public value; // 与LogicContract的value对应
address public owner; // 与LogicContract的owner对应
event Upgraded(address indexed newImplementation);
constructor(address _implementation) {
implementation = _implementation;
owner = msg.sender;
}
// fallback函数:将所有调用转发到逻辑合约
fallback() external payable {
address impl = implementation;
require(impl != address(0), "Implementation not set");
// 使用delegatecall调用逻辑合约
// 逻辑合约的代码会在本合约的上下文中执行
(bool success, bytes memory returnData) = impl.delegatecall(msg.data);
if (!success) {
// 如果调用失败,回滚
assembly {
returndatacopy(0, 0, returndatasize())
revert(0, returndatasize())
}
}
// 返回数据
assembly {
return(add(returnData, 0x20), mload(returnData))
}
}
// 升级函数:更换逻辑合约
function upgrade(address newImplementation) external {
require(msg.sender == owner, "Not owner");
implementation = newImplementation;
emit Upgraded(newImplementation);
}
}
delegatecall的执行流程:
用户调用 ProxyContract.setValue(100)
↓
ProxyContract的fallback函数被触发
↓
delegatecall到 LogicContract.setValue(100)
↓
LogicContract的代码在ProxyContract的上下文中执行
↓
修改的是ProxyContract的value和owner(不是LogicContract的)
↓
msg.sender仍然是原始用户(不是ProxyContract)
关键理解:
contract DelegatecallDemo {
uint256 public value;
address public sender;
function testDelegatecall(address target) external {
// 调用目标合约的setValue函数
(bool success, ) = target.delegatecall(
abi.encodeWithSignature("setValue(uint256)", 88)
);
require(success, "Delegatecall failed");
// 注意:value和sender会被修改!
// 因为delegatecall在调用者的上下文中执行
// 目标合约的代码修改的是本合约的storage
}
}
3.3 staticcall方法
staticcall是最安全但功能最受限的调用方法。它的核心特点是保证不修改状态。
staticcall的核心特点:
-
保证不修改状态:
- 如果被调用的函数尝试修改状态,调用会直接失败
- 只能调用view和pure函数
-
只读查询:
- 非常适合调用view和pure函数
- 提供额外的安全保障
-
不能发送以太币:
- staticcall不支持value参数
基本语法:
(bool success, bytes memory data) = address.staticcall(
abi.encodeWithSignature("functionName()")
);
完整示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 目标合约
contract TargetContract {
uint256 public value = 100;
// view函数:可以读取状态
function getValue() external view returns (uint256) {
return value;
}
// 修改状态的函数
function setValue(uint256 _value) external {
value = _value;
}
}
// 调用者合约:使用staticcall
contract StaticcallDemo {
// 使用staticcall调用view函数(安全)
function safeGetValue(address target) external view returns (uint256) {
(bool success, bytes memory returnData) = target.staticcall(
abi.encodeWithSignature("getValue()")
);
require(success, "Staticcall failed");
// 解码返回值
uint256 value = abi.decode(returnData, (uint256));
return value;
}
// 尝试使用staticcall调用修改状态的函数(会失败)
function unsafeSetValue(address target, uint256 newValue) external {
// 这个调用会失败,因为setValue会修改状态
(bool success, ) = target.staticcall(
abi.encodeWithSignature("setValue(uint256)", newValue)
);
// success会是false,因为staticcall不允许修改状态
require(success, "Staticcall failed: cannot modify state");
}
}
staticcall的安全保障:
contract SecurityExample {
// 使用staticcall确保不会意外修改状态
function safeQuery(address target) external view returns (uint256) {
(bool success, bytes memory data) = target.staticcall(
abi.encodeWithSignature("getValue()")
);
require(success, "Query failed");
// 即使target合约有恶意代码,也无法修改本合约的状态
// staticcall保证了这一点
return abi.decode(data, (uint256));
}
}
3.4 三种方法的对比
让我们通过一个完整的对比示例来理解三种方法的区别:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 目标合约:用于演示三种调用方法的区别
contract Target {
uint256 public value;
address public sender;
event ValueChanged(uint256 newValue, address caller);
// 修改状态的函数
function setValue(uint256 _value) external {
value = _value;
sender = msg.sender;
emit ValueChanged(_value, msg.sender);
}
// 只读函数
function getValue() external view returns (uint256) {
return value;
}
}
// 调用者合约:对比三种调用方法
contract Caller {
// 本合约的状态变量
uint256 public value;
address public sender;
event CallResult(string method, bool success, uint256 value, address sender);
/**
* @notice 使用call方法调用
* @dev call在目标合约的上下文中执行,修改目标合约的状态
*/
function testCall(address target, uint256 newValue) external {
// 记录调用前的状态
uint256 callerValueBefore = value;
uint256 targetValueBefore = Target(target).value();
// 使用call调用目标合约的setValue函数
(bool success, ) = target.call(
abi.encodeWithSignature("setValue(uint256)", newValue)
);
require(success, "Call failed");
// 检查状态变化
uint256 callerValueAfter = value;
uint256 targetValueAfter = Target(target).value();
// 结论:call修改了目标合约的状态,没有修改调用者的状态
emit CallResult(
"call",
success,
targetValueAfter, // 目标合约的值被修改
Target(target).sender() // msg.sender是调用者合约
);
}
/**
* @notice 使用delegatecall方法调用
* @dev delegatecall在调用者的上下文中执行,修改调用者的状态
*/
function testDelegatecall(address target, uint256 newValue) external {
// 记录调用前的状态
uint256 callerValueBefore = value;
uint256 targetValueBefore = Target(target).value();
// 使用delegatecall调用目标合约的setValue函数
(bool success, ) = target.delegatecall(
abi.encodeWithSignature("setValue(uint256)", newValue)
);
require(success, "Delegatecall failed");
// 检查状态变化
uint256 callerValueAfter = value;
uint256 targetValueAfter = Target(target).value();
// 结论:delegatecall修改了调用者的状态,没有修改目标合约的状态
emit CallResult(
"delegatecall",
success,
callerValueAfter, // 调用者的值被修改
sender // msg.sender是原始调用者(不是调用者合约)
);
}
/**
* @notice 使用staticcall方法调用
* @dev staticcall只能调用view/pure函数,不能修改状态
*/
function testStaticcall(address target) external view returns (uint256) {
// 使用staticcall调用view函数
(bool success, bytes memory returnData) = target.staticcall(
abi.encodeWithSignature("getValue()")
);
require(success, "Staticcall failed");
// 解码返回值
uint256 value = abi.decode(returnData, (uint256));
// 结论:staticcall只读取数据,不修改任何状态
return value;
}
}

3.5 选择正确的调用方法
根据不同的场景,选择合适的调用方法:
使用call的场景:
- 需要发送以太币
- 调用外部合约的普通函数
- 不确定使用哪种方法时的默认选择
使用delegatecall的场景:
- 代理模式(可升级合约)
- 库合约调用
- 需要保持msg.sender不变
使用staticcall的场景:
- 只读查询
- 调用view/pure函数
- 需要额外的安全保障
4. 安全的外部调用
外部调用是合约开发中最危险的操作之一。如果处理不当,可能导致重入攻击等严重安全问题。让我们详细学习如何安全地进行外部调用。
4.1 重入攻击防范
重入攻击是智能合约中最常见和最危险的安全漏洞之一。2016年的The DAO攻击就是利用了这个漏洞,导致损失了价值5000万美元的以太币。
重入攻击的原理:
重入攻击发生在合约在执行外部调用之前没有更新状态的情况下。恶意合约可以在接收以太币时再次调用原函数,利用未更新的状态重复提取资金。
不安全的示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 存在重入漏洞的银行合约
contract VulnerableBank {
mapping(address => uint256) public balances;
event Deposit(address indexed user, uint256 amount);
event Withdrawal(address indexed user, uint256 amount);
// 存款函数
function deposit() external payable {
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
// 危险!存在重入漏洞的提现函数
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// 问题:先转账,后更新状态
// 如果msg.sender是恶意合约,它可以在receive函数中再次调用withdraw
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// 状态更新在外部调用之后,存在重入风险
balances[msg.sender] = 0;
emit Withdrawal(msg.sender, amount);
}
// 查询合约总余额
function getContractBalance() external view returns (uint256) {
return address(this).balance;
}
}
// 攻击合约:利用重入漏洞
contract Attacker {
VulnerableBank public vulnerableBank;
uint256 public attackCount;
constructor(address _vulnerableBank) {
vulnerableBank = VulnerableBank(_vulnerableBank);
}
// 接收以太币时触发重入攻击
receive() external payable {
// 限制攻击次数,避免Gas耗尽
if (attackCount < 3 && address(vulnerableBank).balance > 0) {
attackCount++;
// 再次调用withdraw,此时balances[msg.sender]还没有被清零
vulnerableBank.withdraw();
}
}
// 发起攻击
function attack() external payable {
require(msg.value >= 1 ether, "Need at least 1 ether");
attackCount = 0;
// 步骤1:先存款
vulnerableBank.deposit{value: msg.value}();
// 步骤2:发起第一次提现
vulnerableBank.withdraw();
// 在receive函数中会触发多次重入调用
}
// 查询攻击者余额
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
1. 攻击者调用attack(),存入1 ether
2. 攻击者调用withdraw()
3. 合约向攻击者转账1 ether
4. 攻击者的receive()函数被触发
5. receive()中再次调用withdraw()
6. 此时balances[攻击者]还是1 ether(因为还没被清零)
7. 合约再次向攻击者转账1 ether
8. 重复步骤4-7,直到攻击次数达到限制
9. 最终攻击者提取了4 ether(1 ether本金 + 3 ether窃取)
安全的写法:
遵循"检查-效果-交互"(Checks-Effects-Interactions,CEI)模式:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 安全的银行合约
contract SecureBank {
mapping(address => uint256) public balances;
// 重入锁:防止函数被重入调用
bool private locked;
event Deposit(address indexed user, uint256 amount);
event Withdrawal(address indexed user, uint256 amount);
// 重入锁修饰符
modifier noReentrant() {
require(!locked, "No reentrancy");
locked = true; // 设置锁
_; // 执行函数
locked = false; // 释放锁
}
// 存款函数
function deposit() external payable {
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
// 安全的提现函数:遵循CEI模式
function withdraw() external noReentrant {
// 1. Checks(检查):验证所有条件
uint256 amount = balances[msg.sender];
require(amount > 0, "No balance");
// 2. Effects(效果):先更新状态
// 这是关键:在外部调用之前更新状态
balances[msg.sender] = 0;
// 3. Interactions(交互):然后进行外部调用
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
emit Withdrawal(msg.sender, amount);
}
// 查询合约总余额
function getContractBalance() external view returns (uint256) {
return address(this).balance;
}
}
CEI模式的关键点:
- Checks(检查):首先验证所有前置条件
- Effects(效果):然后更新合约状态
- Interactions(交互):最后进行外部调用
这样即使发生重入,状态已经更新,攻击无法成功。
4.2 防护措施
除了CEI模式,还有其他重要的防护措施:
- 使用重入锁:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ReentrancyGuard {
// 使用布尔变量作为锁
bool private locked;
// 重入锁修饰符
modifier noReentrant() {
require(!locked, "No reentrancy");
locked = true;
_;
locked = false;
}
// 在关键函数上使用修饰符
function withdraw(uint256 amount) external noReentrant {
// 安全地执行提现逻辑
}
}
- 限制Gas:
在调用外部合约时,可以限制传递的Gas数量,防止被调用合约执行过于复杂的操作。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract GasLimitExample {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
// 限制Gas:最多使用50000 Gas
// 如果被调用的合约需要更多Gas,调用会失败
(bool success, ) = msg.sender.call{gas: 50000, value: amount}("");
require(success, "Transfer failed");
}
}
- 检查返回值:
永远要检查外部调用的返回值,不检查返回值可能导致交易失败却不自知。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ReturnValueCheck {
// 危险:不检查返回值
function badTransfer(address to, uint256 amount) external {
(bool success, ) = to.call{value: amount}("");
// 如果success是false,代码继续执行,可能导致状态不一致
}
// 正确:检查返回值
function goodTransfer(address to, uint256 amount) external {
(bool success, ) = to.call{value: amount}("");
require(success, "Transfer failed");
// 如果失败,整个交易会回滚
}
}
- 使用OpenZeppelin的ReentrancyGuard:
OpenZeppelin提供了经过审计的重入锁实现,可以直接使用:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract SecureContract is ReentrancyGuard {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external nonReentrant {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}
4.3 Gas限制的最佳实践
关于Gas限制,需要注意以下几点:
- 设置合理的Gas限制:
contract GasLimitBestPractice {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
// 根据实际测试确定合适的Gas限制
// 太低了会导致正常操作失败
// 太高了会增加被攻击的风险
(bool success, ) = msg.sender.call{gas: 50000, value: amount}("");
require(success, "Transfer failed");
}
}
- 避免Gas限制过低:
Gas限制不能太低,否则正常的操作也无法完成。通常建议根据实际测试来确定合适的Gas限制值。
- 考虑使用transfer或send:
对于简单的以太币转账,可以使用transfer或send,它们有固定的Gas限制(2300 Gas),更安全:
contract TransferExample {
mapping(address => uint256) public balances;
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
// transfer有固定的2300 Gas限制,更安全
payable(msg.sender).transfer(amount);
}
}
5. 合约创建方式
在合约间调用的场景中,有时我们需要动态创建新的合约。Solidity提供了两种创建合约的方式:new关键字和create2。
5.1 new关键字
new是传统的创建方式,使用起来非常简单直接。
new关键字的特点:
-
地址由创建者和nonce决定:
- 新合约地址 = f(创建者地址, nonce)
- nonce是创建者的交易计数
- 地址是随机的,无法提前知道
-
立即部署:
- 创建后,合约会立即部署到链上
- 返回新合约的地址
-
简单易用:
- 语法简单,一行代码即可创建
基本语法:
ContractType newContract = new ContractType(arg1, arg2, ...);
完整示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 简单的计数器合约
contract Counter {
uint256 public count;
address public owner;
constructor(address _owner) {
owner = _owner;
count = 0;
}
function increment() external {
require(msg.sender == owner, "Not owner");
count++;
}
}
// 工厂合约:使用new创建新合约
contract CounterFactory {
// 记录所有创建的计数器地址
address[] public counters;
event CounterCreated(address indexed counterAddress, address owner);
/**
* @notice 使用new创建新的计数器合约
* @return 新创建的计数器合约地址
*/
function createCounter() external returns (address) {
// 使用new关键字创建新合约
// 构造函数参数是msg.sender(调用者地址)
Counter newCounter = new Counter(msg.sender);
// 获取新合约的地址
address counterAddress = address(newCounter);
// 记录新合约地址
counters.push(counterAddress);
// 触发事件
emit CounterCreated(counterAddress, msg.sender);
return counterAddress;
}
/**
* @notice 查询所有创建的计数器数量
*/
function getCounterCount() external view returns (uint256) {
return counters.length;
}
/**
* @notice 查询指定索引的计数器地址
*/
function getCounter(uint256 index) external view returns (address) {
require(index < counters.length, "Index out of range");
return counters[index];
}
}
适用场景:
- 一般的合约创建需求
- 不需要预先知道合约地址的场景
- 每次用户请求就创建一个新的合约实例
5.2 create2
create2是一个更高级的创建方式,它最大的特点是地址可预先计算。
create2的核心特点:
-
地址可预先计算:
- 在合约部署之前,就可以计算出它将来会被部署到哪个地址
- 地址计算公式:address = keccak256(0xff, sender, salt, bytecode)
-
通过salt控制地址:
- salt是一个你提供的随机数(bytes32类型)
- 通过改变salt,可以控制生成不同的地址
- 相同的salt、sender和bytecode会产生相同的地址
-
确定性部署:
- 可以在多个链上部署相同地址的合约
- 便于跨链交互
基本语法:
ContractType newContract = new ContractType{salt: saltValue}(arg1, arg2, ...);
完整示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// 简单的计数器合约
contract Counter {
uint256 public count;
address public owner;
constructor(address _owner) {
owner = _owner;
count = 0;
}
function increment() external {
require(msg.sender == owner, "Not owner");
count++;
}
}
// 工厂合约:使用create2创建确定性地址的合约
contract CounterFactory {
event CounterCreated(address indexed counterAddress, bytes32 salt);
/**
* @notice 使用new创建(地址不可预测)
*/
function createWithNew() external returns (address) {
Counter counter = new Counter(msg.sender);
return address(counter);
}
/**
* @notice 使用create2创建(地址可预测)
* @param salt 用于计算地址的盐值
* @return 新创建的计数器合约地址
*/
function createWithCreate2(bytes32 salt) external returns (address) {
// 使用create2创建,指定salt值
Counter counter = new Counter{salt: salt}(msg.sender);
address counterAddress = address(counter);
emit CounterCreated(counterAddress, salt);
return counterAddress;
}
/**
* @notice 预计算create2地址
* @param salt 盐值
* @param deployer 部署者地址(通常是本合约地址)
* @return 预计算的合约地址
*/
function computeAddress(bytes32 salt, address deployer)
external
view
returns (address)
{
// 获取合约的创建字节码
// type(Counter).creationCode 获取Counter合约的字节码
// abi.encode(msg.sender) 编码构造函数参数
bytes memory bytecode = abi.encodePacked(
type(Counter).creationCode,
abi.encode(msg.sender)
);
// 计算create2地址
// 公式:keccak256(0xff + deployer + salt + keccak256(bytecode))
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
deployer, // 工厂合约地址
salt, // 盐值
keccak256(bytecode) // 字节码的哈希
)
);
// 将哈希转换为地址(取后20字节)
return address(uint160(uint256(hash)));
}
}
适用场景:
-
状态通道:
- 链下计算好合约地址
- 用户可以先向地址转账
- 需要时再实际部署合约
-
确定性部署:
- 多链部署相同地址的合约
- 便于跨链交互
- 简化地址管理
-
Uniswap V2的应用:
- 每个交易对的地址可以通过公式计算
- 用户可以在Pair未创建时就知道地址
- Router可以直接计算目标地址,无需查询
create2 vs new对比:
更多推荐



所有评论(0)