掌握 Solidity 调用、低级低调用、Create2、ETH转账 四大核心技能,15 分钟读完即可在 DApp 开发与审计中游刃有余。
目录
- 合约如何调用合约
- 四种低级调用深度对比(call、staticcall、delegatecall、multicall)
- Create 与 Create2——地址可控的合约创建
- 发送 / 接收 ETH 的三种安全姿势
- FAQ:开发必踩坑全解析
合约如何调用合约
在 Solidity 的世界里,合约调用合约就像前端调 API,最常见的有两条路:
- 源码级接口调用(Interface)——“我知道你要什么”
- 低级函数调用(call / staticcall / delegatecall)——“我不关心你是谁,能用就行”
1. 源码接口调用
被调用合约 Callee
contract Callee {
uint public x;
function setX(uint _x) public { x = _x; }
function getX() public view returns (uint) { return x; }
}调用者合约 Caller
interface ICallee {
function setX(uint _x) external;
function getX() external view returns (uint);
}
contract Caller {
function callSetX(address calleeAddress, uint _x) public {
ICallee(calleeAddress).setX(_x);
}
function callGetX(address calleeAddress) public view returns (uint) {
return ICallee(calleeAddress).getX();
}
}在实际监听与调试中,Solidity 接口调用天然携带严格的类型检查,既防手抖又能让 EVM 优化 gas,优先推荐。
Foundry 快速测试
👉 跟我用一条命令跑通接口级调用测试
contract CallerTest is Test {
Callee callee;
Caller caller;
function setUp() public {
callee = new Callee();
caller = new Caller();
}
function testCallSetAndGetX() public {
caller.callSetX(address(callee), 123);
assertEq(caller.callGetX(address(callee)), 123);
}
}四种低级调用深度对比
| call | staticcall | delegatecall | multicall |
|---|---|---|---|
| 可写、可读、可转账 | 只读 | 在调用方状态上执行逻辑 | 批量 call 打包 |
2. Call:万能型“瑞士军刀”
典型场景:向未知合约发送 ETH,或手动拼编码触发函数。
(bool success, ) = calleeAddress.call{value: msg.value}(
abi.encodeWithSignature("setX(uint256)", _x)
);
require(success, "call failed");注意事项:
- 不抛出异常时会返回低层错误码;手动判断
success=true才能确保交易一致。 - gas 不设上限,攻击者或可回调本合约,务必做 重入保护。
2. staticcall:只读安全屋
仅适用于 view / pure 函数,一旦内部改状态就会直接 revert。
(bool ok, bytes memory data) = calleeAddress.staticcall(
abi.encodeWithSignature("getX()")
);
uint val = abi.decode(data, (uint));3. delegatecall:代理合约的灵魂
行为类比:“我把你的代码粘到我身体里跑”。
// Proxy → Logic
(bool ok,) = logic.delegatecall(
abi.encodeWithSignature("setNum(uint256)", _num)
);两件事必须同构:
- slot 顺序与类型要一致;2. 流程逻辑需完全兼容,否则数据错位就是 灾难级事故。
快速验证:
👉 三分钟写对 delegatecall 测试用例
4. multicall:批量压缩,gas 省一笔
通过一个事务批量调用多个函数,既保持交易原子性,又把链上 gas 损耗降到最低。
struct Call {
address target;
bytes data;
}
bytes[] memory results = multicall.multicall(calls);实际应用:DEX 前端一次广播即可完成 授权 + 交换 + 回调,大幅提高用户成功率。
Create 与 Create2——地址可控的合约创建
| 维度 | Create | Create2 |
|---|---|---|
| 决定因素地址 | 部署者 nonce | 部署者+ salt+ initcode |
| 链间一致性 | ❌ | ✅ |
| 地址是否可预测 | ❌ | ✅ |
Create:最普通的 new 关键字
Foo foo = new Foo(_age);地址计算:address = keccak256(rlp([deployer, nonce])),不同链 nonce 必变,因此 CREATE 推链后地址会变。
Create2:确定性地址
Foo foo = new Foo{salt: SALT}(_age);地址计算:address = keccak256(0xff ++ deployer ++ salt ++ keccak256(initcode))[12:],只要 salt + initcode 一致,即可在多链部署到同一地址。大名鼎鼎的 Uniswap Pair 工厂用的就是它。
地址预计算示例
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(factory),
salt,
keccak256(bytecode)
)
);
address predicted = address(uint160(uint256(hash)));发送 / 接收 ETH 的三种安全姿势
| 方法 | gas 限制 | 失败行为 |
|---|---|---|
| call | 不封顶 | 返回布尔 |
| transfer | 2300 | 自动 revert |
| send | 2300 | 返回布尔 |
推荐顺序:call > transfer > send(send 几乎已退役)。
接收 ETH 的三种触发器
- receive():
msg.data为空时触发 - fallback():数据非空或函数不存在时触发
- payable 普通函数:如
deposit() payable
receive() external payable {
emit Received(msg.sender, msg.value, "receive");
}完整部署脚本(Foundry)示例可省去前端交互繁琐;用 Forge Script 一键脚本 + 本地 Anvil 测试网即可快速验证。
FAQ:开发必踩坑全解析
Q1:delegatecall 与 call 的 存储冲突 如何避免?
A:保持代理合约与逻辑合约的存储结构 1:1 映射,最简单的方案是 统一使用可升级标准库(如 OpenZeppelin Proxy Upgrade Pattern)。
Q2:为什么我的 multicall gas 并更高?
A:如果在循环里还出现大量 storage 写入,gas 反而暴涨。正确用法是将只读调用打包成一批,批量读数据后由前端再做 链下聚合。
Q3:Create2 能否在多链重置同一代码?
A:可以。只要保证 salt 和 initcode 不变,新老合约字节码更新时地址也会变,需重新计算预期地址并在前端提示用户。
Q4:ETH 转账一定要用 call 吗?
A:call 灵活无 gas 限制,可附带 data;但对不可信合约需 防重入锁。简单提现代币可用 transfer,写新逻辑首选 call。
Q5:fallback() 与 receive() 谁的优先级更高?
A:receive() 优先,msg.data 为空就进 receive,否则才 fallback。
Q6:如何在 Foundry 某次测试失败后保留链上状态?
A:使用 forge test --fork-url -vvv 并把 fail() 替换为 console2.log() 断点,借助 Anvil 的 --dump-state 持久化。
小结
理解合约调用、低级调用和地址创建,是 Web3 工程师的 高速公路通行证。熟练掌握后,你不仅能轻松写出 可升级代理,也能用 multicall 给用户省下大把 gas。下一步,不妨把上述代码粘贴到本地,启动 Anvil,亲自跑一次完整流程。