十大DeFi安全实践

Top 10 DeFi Best Practices

无论是开发DeFi协议还是其他的智能合约应用,在上线到区块链主网前都需要考虑到许多安全因素。很多团队在审核代码时只关注Solidity相关的陷阱,但要确保dApp的安全性足够支撑上线主网,通常还有很多工作要做。了解大多数流行的DeFi安全漏洞可能会为你和你的用户节省数十亿美元并且免除后续的各种烦恼,如预言机攻击、暴力攻击和许多其他威胁等。

考虑到这一点,我们将在下文研究有关DeFi安全的十大最佳实践,这将有助于防止你的应用程序成为攻击的受害者、避免与用户的不愉快对话,并能保护和加强你作为一个超级安全的开发者的声誉。

1. 了解重入攻击

一种常见的DeFi安全攻击类型是重入攻击,这也是臭名昭著DAO攻击的形式。这种情况就是当一个合约在更新自己的状态之前调用了一个外部合约。

引用Solidity文档的内容:

"一个合约(A)与另一个合约(B)的任何交互,以及任何ETH的转账都会将控制权移交给该合约(B)。这使得B有可能在这个交互完成前回调到A。"
我们来看看一个例子:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

// THIS CONTRACT CONTAINS A BUG - DO NOT USE
contract Fund {
    /// @dev Mapping of ether shares of the contract.
    mapping(address => uint) shares;
    /// Withdraw your share.
    function withdraw() public {
        (bool success,) = msg.sender.call{value: shares[msg.sender]}("");
        if (success)
            shares[msg.sender] = 0;
    }
}

在这个函数中,我们用msg.sender.call调用另一个账户。我们要记住的是,这可能是另一个智能合约!

在(bool success,) = msg.sender.call{value: shares[msg.sender]}(""); 返回之前,被调用的外部合约可以被编码为再次调用withdraw(提款)函数。这将允许用户在状态更新前提取合约中的所有资金。

合约可以有几个特殊函数,即receive(接收)和fallback(回退)函数。如果你发送ETH到另一个合约,它将自动被路由到receive函数。如果该receive(接收)函数再指向原来的合约,那么在你有机会将余额更新为0之前,你就可以不断提款。

让我们看看这种合约可能是什么样子的:

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

// THIS CONTRACT IS EVIL - DO NOT USE
contract Steal {
    receive() external payable {
        IFundContract(addressOfFundContract).withdraw();
    }
}

在这个函数中,当你把ETH发送到steal合约后,它将调用receive函数,该函数指向Fund合约。此时,我们还没有运行shares[msg.sender] = 0,所以合约仍然认为用户有可以提取的余额。

解决方案:在转移ETH/通证或调用不受信任的外部合约之前,更新合约的内部状态

有几种方法可以做到这一点,从使用互斥锁到甚至简单地排序你的函数调用,你只在状态被更新后才能接触到外部合约或函数。一种简单的修复方法是在调用任何外部未知合约之前更新状态:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

contract Fund {
    /// @dev Mapping of ether shares of the contract.
    mapping(address => uint) shares;

    /// Withdraw your share.
    function withdraw() public {
        uint share = shares[msg.sender];
        shares[msg.sender] = 0;
        (bool success,) = msg.sender.call{value: shares[msg.sender]}("");
    }
}

转移、调用和发送

长期以来,Solidity安全专家建议不要使用上述方法。他们建议不使用 call 函数,而是使用 transfer ,像下面这样:
payable(msg.sender).transfer(shares[msg.sender]);
我们之所以提到这一点,是因为你可能会看到外面有一些相互矛盾的资料,它们的建议与我们的建议相反。此外,你也会听到 send 函数。每一个函数都可以用来发送ETH,但都有轻微的差异。
  • transfer: 最多需要2300个gas,失败时会抛出一个错误
  • send: 最多需要2300个gas,失败时返回false
  • call: 将所有gas转移到下一个合约,失败时返回false
transfer 和 send 在很长一段时间内被认为是 "更好 "的做法,因为2300个gas真的只够发出一个事件或其他无害的操作;接收合约除了发出事件不能回调或做任何恶意操作,因为如果他们尝试这样做的话,他们会耗尽gas。

然而,这只是目前的设置,由于不断变化的基础设施生态,gas成本在未来可能会发生变化。我们已经看到有EIP改变了不同操作码的gas成本。这意味着未来可能有一段时间,你可以以低于2300个gas的价格调用一个函数,或者事件的成本将超过2300个gas,这意味着任何现在要发出事件的接收函数会在未来会失败。

这意味着最好的做法是在调用项目外的任何合约之前更新状态。另一个可能的缓解措施是对关键函数施加一个互斥锁,例如ReentrancyGuard中的非重入修改器。采用这样的互斥锁将阻止交易合约被重入。这实质上是增加了一个“锁”,所以在合约执行过程中,任何调用合约的人都不能“重新进入”该合约。

重入攻击的另一个版本是跨函数重入。下面是一个跨函数重入攻击的例子,为了便于阅读,使用了transfer函数:

mapping (address => uint) private userBalances;

function transfer(address _recipient, uint _amount) {
    require(userBalances[msg.sender] >= _amount);
    userBalances[_recipient] += _amount;
    userBalances[msg.sender] -= _amount;
}

function withdrawBalance() public {
    uint amountToWithdraw = userBalances[msg.sender];
    msg.sender.transfer(amountToWithdraw);
    userBalances[msg.sender] = 0;
}

有可能在另一个函数完成之前调用一个函数。这应该是一个明确的提醒,在你发送ETH之前一定要先更新状态。一些协议甚至在他们的函数上添加了互斥锁,这样如果另一个函数还没有返回,这些函数就不能被调用。

除了常见的重入漏洞外,还有一些重入攻击可以由特定的EIP机制触发,如ERC777。ERC-777(EIP-777)是建立在ERC-20(EIP-20)之上的以太坊代币标准。它向后兼容ERC-20并增加了一个功能,使“运营商”能够代表通证所有者发送通证。关键是该协议还允许为通证所有者添加“send/receive钩子”,以便在发送/接收交易时自动采取进一步行动。

从Uniswap imBTC黑客事件中可以看出,该漏洞实际上是由Uniswap交易所在余额变化之前发送ETH造成的。在那次攻击中,Uniswap功能的实现没有遵循已被广泛采用的“Check-Effect-Interact”模式,该模式是为了保护智能合约免受重入攻击而发明的,按照该模式,通证转移应该在任何ETH转移之前进行。

2.使用DEX或AMM储备作为价格预言机将导致漏洞攻击

这既是用于攻击协议的最常见方法之一,也是最容易防止的DeFi安全攻击面之一。如果你使用 getReserves() 作为量化价格的方法,这应该是一个警示信号。当用户操纵订单簿或基于自动做市商的去中心化交易所(DEX)的现货价格时,这种集中式价格预言机攻击就会发生,通常是使用闪电贷。然后使用DEX报告价格作为他们的价格预言机的协议,会导致智能合约的执行出现偏差,其形式包括触发虚假清算、发放过多的贷款或触发不公平交易。由于这个漏洞的存在,即使是流行的DEX,如Uniswap,也不建议单独使用他们的储备池作为价格预
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值