当我们已知合约接口和已部署合约地址时,可以通过合约实例调用合约方法
interface IContract {
function x() public view returns (uint);
}
contract C {
function example() public returns (uint resData) {
IContract instance = IContract(address);
resData = instance.x();
}
}
如果我们只知道已部署合约地址,想要调用该合约中的函数时,Solidity 提供了三个低级别方法 call
delegatecall
staticcall
,可以更直接地控制编码。所有这些函数都是低级别的函数,由于它们绕过了类型检查、函数存在性检查和参数打包,应该谨慎使用。具体来说,任何未知的合约都可能是恶意的,如果您调用它,您就把控制权交给了该合约,这些方法只应该作为最后的手段来使用。
// 函数选择器签名
bytes memory payload = abi.encodeWithSignature("register(string)", "MyName");
// call 调用函数
(bool success, bytes memory rawData) = address(nameReg).call(payload);
// 判断调用是否成功,这一步不可或缺
if (!success) {
revert();
}
// 解码返回数据
string returnData = abi.decode(rawData, (string));
对于 call 方法,可以指定 gas 和 value,其他两个方法只能指定 gas。最好避免在代码中硬编码 gas 值,这可能有很多隐患。另外,对 gas 的访问在未来可能会改变。
address(nameReg).call{gas: 1000000, value: 1 ether}(abi.encodeWithSignature("register(string)", "MyName"));
如果被调用的账户不存在,低级别函数的第一个返回值为 true,这是 EVM 设计的一部分(EVM 认为对一个不存在的合约的调用总是成功的)。Solidity 在执行外部调用时使用 extcodesize
操作码进行额外的检查。这确保了即将被调用的合约要么实际存在(它包含代码),要么就会产生一个异常。而低级别调用并不会进行这种检查,这使得它们在gas方面更便宜,但也更不安全。因此有必要在调用之前检查合约地址是否存在。
require(address(nameReg).isContract());
delegatecall 执行目标合约函数时,上下文环境是当前环境。这意味着只有代码逻辑来源于目标合约,而 状态变量 msg.value msg.sender 等数据都来源于当前合约。且当前合约的状态变量结构应与目标合约保持一致。该方法通常应用于升级合约,具体内容可参考 升级合约。
staticcall 只能执行目标合约中的只读函数,无法改变合约状态,使用该方法可以安全的调用外部只读函数。