往往对于一个功能成熟的DAPP,如uniswapV2,多签钱包等,都是通过多个智能合约实现的,那么就多多少少都会涉及到外部的调用,然而这就带来的很大的风险。如果不能确保外部调用的合约是正常且不带恶意逻辑代码,那么对自身合约就是个定时炸弹,不知道哪天就调用了一个不正常的恶意合约,引爆炸弹。

外部调用会引发的一些常见漏洞:重入攻击,外部合约的安全性问题,重放攻击等

具体来看一下代码:

function deposit(
uint256 farmDeposit,
address payable from,
address to
) external returns (uint256 shares) {
require(farmDeposit > 0, "deposits must be nonzero");
require(to != address(0) && to != address(this), "to");
require(from != address(0) && from != address(this), "from");

shares = farmDeposit;
if (xfarm.totalSupply() != 0) {
uint256 farmBalance = farm.balanceOf(address(this));
shares = (shares * xfarm.totalSupply()) / farmBalance;
}

if (isContract(from)) {
require(IAdvisor(from).owner() == msg.sender); // admin
IAdvisor(from).delegatedTransferERC20(address(farm), address(this), farmDeposit);
} else {
require(from == msg.sender); // user
farm.safeTransferFrom(from, address(this), farmDeposit);
}

xfarm.mint(to, shares);
}

这是一个质押合约 RewardsAdvisor中的部分代码,它接受 FARM 代币并铸造等量的 xFARM。xFARM 用于治理defi 生态系统。
通过以上代码。我们发现当质押合约接受xFARM时,如果对面是合约A,不是外部账户,那么质押合约就会调用这个合约A的delegatedTransferERC20,来进行代币的转移,可是,质押合约并没有对转移后代币数量的检查等,所以,合约A中delegatedTransferERC20函数如果什么也没有实现,质押合约也会认为代币接受成功了,这将会引起一个可怕的后果。

同样。也有以下代码:

function stakeFor(address _for, uint256 _amount) public {
require(_amount > 0, "Cannot stake 0");

// pull tokens and apply stake
stakingToken.safeTransferFrom(msg.sender, address(this), _amount);
_applyStake(_for, _amount);
}

也是一个质押合约中的部分代码,进行外部调用 stakingToken.safeTransferFrom(msg.sender, address(this)),都是没有对调用后进行一个检查,攻击者就可以乘虚而入,如:TempleDAO 的 STAX 在2022年就因为同样的原因被黑客入侵,损失了价值约 $2.3M 的 LP 代币。再者就是Visor合约(实现铸造无限奖励),黑客通过利用同样的原理实现了经济套利,铸造了 195k 个 vVISR 代币。然后,这些被burn为 8.8M VISR,通过 Uniswap v2 交换为 ETH,获得了113 ETH(450 美元)。案例1参考 案例2参考

但是,外部调用并不可怕,您可以通过以下措施来防范:
1,使用状态变量控制,在执行外部调用之前先更新合约状态,防止重入攻击。
2,采用检查-效果-交互模式,将合约操作分为三个阶段,确保在外部调用前完成所有状态更新。
3,实现重入保护,使用互斥锁(例如 nonReentrant 修饰符)来防止重入攻击。
4,通过权限控制,确保只有经过授权的用户可以执行特定操作,使用 Ownable 或类似的库来管理权限。
5,使用受信任的外部合约,在与其他合约交互时,确保只与信誉良好的合约交互,避免不必要的风险。