干货分享 | 十大DeFi安全最佳实践方式(下)

CertiK
CertiK 机构得得号

Dec 17, 2021 CertiK 专注于区块链安全,被称为区块链安全世界的「普华永道」。

摘要: 继上周发布的十大DeFi安全最佳实践方式(上),本文将为你分享剩余的7个DeFi安全最佳实践方式,先收藏再看!

本教程适用于所有希望向主网推出user-ready应用程序的人员

上周CertiK发布了干货分享 | 十大DeFi安全最佳实践方式(上),为大家分享DeFi安全10个最佳实践的前3个实践方式,今天将为大家带来剩余的7个实践内容,进一步助力各位读者保障自身应用程序的安全。

⭐超长干货,建议先收藏再看哦!

4. 避免常见问题

这一点对 Solidity 来说是一个总括性的问题——如果希望合约安全,就需要在构建它时依次核实所有的DeFi安全准则。而要编写真正可靠的Solidity,就必须了解它的内部工作原理。否则,可能会容易受到以下攻击:

上溢出/下溢出(Overflows/Underflows

在Solidity 中,uint256和int256被 "包裹"了。

这意味着,如果你在uint256中拥有一个最大的数,然后将其添加到其中,它将变成可能存在的最小数字。

这一点务必需要检查与核实,在0.8之前的Solidity版本中,可以使用类似safemath[1]的库来解决这一问题。

在Solidity 0.8.x中,默认情况下会检查算术运算操作。这意味着x+y将在溢出时抛出异常。因此,请确认你正在使用的Solidity版本。

循环gas限制(Loops Gas Limit)

当编写动态大小的循环时,需要注意它的极限规模大小。一个循环的规模可以很容易地超过最大块限制,并使合约在回滚时失效。

避免使用'tx.origin'

`tx.origin`可能会导致类似钓鱼的攻击[2],因此不应该被用于智能合约的身份验证。

代理存储冲突(Proxy Storage Collision) 

对于采用代理实现模式的项目,可以通过更改代理合约中的实现合约地址来更新实现。

通常,代理合约中有一个特定的变量存储实现合约地址。如果这个变量的存储位置是固定的,而在执行合约中恰好有另一个变量具有相同的存储位置索引/偏移量,那么就会发生存储冲突。

pragma solidity 0.8.1;

contract Implementation {
address public myAddress;
uint public myUint;

function setAddress(address _address) public {
myAddress = _address;
}
}

contract Proxy {
address public otherContractAddress;

constructor(address _otherContract) {
otherContractAddress = _otherContract;
}

function setOtherAddress(address _otherContract) public {
otherContractAddress = _otherContract;
}

fallback() external {
address _impl = otherContractAddress;
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)

switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
}

要触发存储冲突,可以在Remix中执行以下步骤:

①部署实现合约;

②部署代理合约,将实现合约的部署地址作为其构造参数;

③在代理合约的部署地址上运行实现合约;

④调用myAddress()函数。它将返回一个非零地址,该地址是存储在代理合约中 otherContractAddress 变量中的部署地址。

在上述四个步骤中发生了什么?

1. 部署实现合约并生成其部署地址;

2. 代理合约使用实现合约的部署地址进行部署,其中代理合同的构造函数被调用,并将otherContractAddress变量赋值为实现合约的部署地址;

3. 在步骤③中,实现合同与代理存储进行交互,即部署的实现合约中的变量可以读取部署的代理合约中相应的哈希碰撞变量的值。

4. myAddress()函数的返回值就是部署的实现合约中myAddress变量的值,它与部署的代理合约中的otherContractAddress变量产生冲突,随即可以在那里获得otherContractAddress变量的值。

为了避免代理存储冲突,我们建议开发者通过为存储变量选择伪随机槽来实现非结构化存储代理。

一种常见的做法是为项目采用一种可靠的代理模式。最广泛采用的代理模式是通用可升级代理标准(UUPS)[3]和透明代理模式[4]。它们都提供了具体的存储 "偏移",以避免在代理合同和实现合同中使用相同的存储槽。

下面是一个使用透明代理模式实现随机存储的例子👇

bytes32 private constant implementationPosition = bytes32(uint256(
keccak256('eip1967.proxy.implementation')) - 1
));

保证代币转移计算的准确性

通常情况下,对于普通的ERC20代币,收到的代币数量应该等于用函数调用的原始数量。

例如——参见下方函数retrieveTokens()👇

function retrieveTokens(uint256 amount) public {
token.transferFrom(msg.sender, address(this), amount);
totalTokenTransferred += amount;
}

然而,如果代币是通缩的,即每次转移都有费用,那么实际收到的代币数量将少于最初要求转移的代币数量。

在如下所示的修改后的函数retrieveTokens(uint256 amount)中,`amount'是根据转账操作前后的余额重新计算的。不管代币转移机制如何,这都将准确地计算出被转移到`address(this)'的代币数量。

function retrieveTokens(uint256 amount) public {
uint256 balanceBefore = deflationaryToken.balanceOf(address(this));
deflationaryToken.transferFrom(msg.sender, address(this), amount);
uint256 balanceAfter = deflationaryToken.balanceOf(address(this));
amount = balanceAfter.sub(balanceBefore);
totalTokenTransferred += amount;
}

正确删除数据

有很多场景需要移除合约中不再需要的某个对象或值。

在像Java这样的标准语言中,有一个垃圾收集机制可以自动和安全地处理删除数据的问题。然而,在Solidity中,开发者必须手动处理 "垃圾"。因此,垃圾处理不当可能会给智能合约带来安全问题。

例如,当用delete(即`delete array[member]')从数组中删除单个成员时,`array[member]'仍将存在,但根据`array[member]'的类型重置为一个默认值。开发者应该要么跳过这个成员,要么重新组织数组并减少其长度。

比如👇

array[member] = array[array.length - 1];
array.pop()

这些只是需要注意的一些漏洞,查看审计师Sigma Prime关于Solidity常见漏洞的文章[5]可以帮助你深入了解Solidity以及避免这些 "陷阱"。

 

5. 函数的可见性和修饰符

在Solidity语言的设计中,有四种类型的函数可见性:
  • private:该函数只在当前合约中可见。

  • internal:该函数在当前合约和派生合约中是可见的。

  • external:该函数只对外部调用可见。

  • public:该函数对内部和外部的调用都是可见的。

可见性是指针对特定功能的上述四种可见性中的一种用于限制某一组用户的访问。 

修饰符则指的是专门为访问限制目的而编写的自定义代码段。

可见性和修饰符结合起来,可以为特定功能设置适当的访问权限。例如,在ERC20实现的函数`_mint()`中:

function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply += amount;
_balances[account] += amount;
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
}

函数_mint()的可见性被设置为internal,这正确地保护了它不被外部调用。为了给mint功能设置一个适当的访问权限,可以使用下方的代码段:

function mint(address account, uint256 amount) public onlyOwner {
_mint(account, amount);
require(MaxTotalSupply >= _totalSupply, "over mint");
}

数mint()只允许合约的所有者铸造,require()语句则可以防止所有者铸造过多的代币。

正确使用可见性和修饰符有利于合约管理。而未正确使用的设置可能会让恶意攻击者调用管理配置函数来操纵项目,过度的修饰符设置也可能会给合约带来中心化的问题,并引起社区的不安。

 

6. 部署到主网前必须获得外部审计

代码审计就像是接受一个以安全为重心的同行评审审计员会逐行查看整个代码库,并使用形式化验证技术来检查智能合约是否存在任何漏洞。

如果不想让自己的项目赤裸裸的暴露于漏洞的威胁之下,切忌在没有审计的情况下部署代码,或者在审计后改变代码并重新部署。

这里有一些帮助确保审计全面的建议:

a. 记录一切,以便审计员更容易跟踪所发生的事情
b. 保持与审计团队的沟通渠道畅通,以防他们有任何疑问可以得到及时解决
c. 在你的代码中添加注释,确保其可以更快被理解

然而,安全是你自己需要切身关注的重中之重,一股脑的将身家全部寄予审计机构对你的安全保障是不该存在的心态。

如果你的协议遭到攻击,最大的受害者是你自己以及你的团队。

尽管安全审计非常有用,提供了额外的一轮审查,并可以帮助你查找未曾发现的漏洞,但它也不能确保100%的安全。

Tincho在推特上开启了一个关于如何最高效率地进行安全审计的话题[6],感兴趣的小伙伴可以去看看。当然,如果你有任何审计需求,或是相关的疑问,随时可在公众号底部留言进行咨询

 

7. 进行测试并使用静态分析工具

你需要对应用程序进行适当的测试。

如果你正苦恼于无处下手,Chainlink starter kit repos提供了一些测试套件样本[7]供你参考。

像Aave和Synthetix这样的协议也有很好的测试套件,建议也可以通过查看他们的代码以了解一些测试的最佳实践(也包括更普遍的编码)。

静态分析工具也会帮助你更早地发现代码的错漏之处。它们可以自动运行监测你的合约并寻找潜在的漏洞。

目前最流行的静态分析工具之一是Slither[8]。CertiK目前还在根据其在审计、验证和监控智能合约方面的丰富经验,建立下一代静态分析、语法分析、漏洞分析和形式验证工具。

 

8.将安全视为重中之重

毫无疑问,在生产部署之前,您应该尽最大努力创建一个安全可靠的智能合约,但区块链和DeFi协议快速发展的现实以及新型攻击方式的不断出现意味着这远远不足以保障安全。

开发者们应当积极获取并跟踪最新的监测和警报数据,并尽可能尝试智能合约本身中引入面向未来的功能以访问快速增长的动态安全洞察数据,这一行为除了安全外更有其他益处。

CertiK Skynet天网动态扫描系统作为一个24*7全天候运行的安全情报引擎,可为智能合约的链上部署提供多维度和实时的透明安全监控。

它包括社会情绪、治理、市场波动、安全评估等安全基元,为区块链用户、社区和代币投资者提供全面的安全见解。

CertiK安全排行榜提供公开透明的、易于理解的安全见解和最新的项目状态所有人都可以通过这个平台查询所有需要的安全数据信息,并向平台提供他们所认可的在安全领域表现优异的DeFi项目,这将激励形成社区问责制。

 

9. 制定一个“翻身”计划

对于一个协议来说,如果在受到攻击后有一个准备许久的“翻身”计划无疑是很好的一步落子。

  • 购买相关保险产品

  • 设置一个紧急 "暂停 "功能

  • 有一个升级计划

随着加密技术的不断发展,保险平台及计划作为一种从损失中被解救出来的保障越来越受到广大项目和用户的欢迎,保证了去中心化的同时更增加了一定程度的安全性。比如CertiK开发的去中心化链上资金保险平台CertiKShield,可保证项目及其投资者的资产安全,避免其受到安全漏洞或黑客攻击导致的资产丢失、被盗或无法访问等情况的影响。

设置紧急 "暂停 "功能是一个有利有弊的策略。

如果发现漏洞,此功能将停止与智能合约的所有交互。如果你设置了这个功能,你需要确保你的用户知道谁能够操作它。

如果只有一个用户,你就不是在运行一个去中心化的协议,那么用户就可以通过代码发现这些。因此注意功能实现方式,因为你实际上可能最终在一个去中心化的平台上得到了一个中心化的协议。

进行升级也有同样的问题。转移到一个没有bug的智能合约可能很好,但升级仍要十分慎重,以免中心化问题的出现。

因此有相当一部分安全机构几乎极力反对可升级的智能合约模式。

更多关于智能合约升级的内容你可以在YouTube上观看帕特里克-柯林斯关于这个话题的视频[9]或是查看《智能合约升级现状》的演[10]

想要了解更多保险方面的建议?欢迎加入Chainlink Discord

https://discord.gg/2YHSAey

 

10. 防止抢先交易Front-running

在区块链中,所有的交易在mempool[11]中都是可见的,这意味着每个人都有机会看到你的交易,并有可能在你的交易进行之前进行交易,以便从你的交易中获利。

例如,假设你使用DEX以当前市场价格将5个ETH兑换成DAI。一旦你把你的交易发送到mempool进行处理,一个先行者可以在你之前进行交易,购买大量的ETH,导致价格上涨。然后他们可以以更高的价格向你出售他们购买的ETH,并以差价获利。

目前,抢先交易机器人[12]在区块链世界中肆意妄为,以牺牲普通用户的利益为代价获利。

这个术语来自于传统金融,交易员会使用同样的操作来获利,涉及到了股票、商品、衍生品等等金融资产和工具。

作为另一个例子,下方列出的函数就具备被抢先的风险。

根据修饰符'initializer',该函数只能被调用一次。

如果攻击者在mempool中监控调用`initialize()`函数的交易,那么攻击者就可以用一组定制的代币、分发服务器(distributor, 和(factory的值来`复制`该交易,并最终控制整个合约。由于函数 "initialize() "只能被调用一次,合约所有者无法对这种攻击进行防御。

function initialize(IERC20 _token, IDistributor _distributor, IFactory _factory) public initializer {
Ownable.initialize();
token = _token;
distributor = _distributor;
factory = _factory;
}

这通常也与矿工可提取值或MEV[13]有关。MEV是指矿工或机器人对交易进行重新排序,以便他们能以某种方式从排序中获利。

就像抢先者支付更多的gas以使他们的交易领先于你的交易一样,矿工可以直接重新排序交易,使他们的交易领先于你的交易。在整个区块链生态系统中,MEV每天从普通用户那里窃取的资产高达数百万美元。

幸运的是,一群世界级的智能合约和密码学研究人员,包括Chainlink实验室的首席科学家Ari Juels,正致力于用一种称为“公平排序服务”的解决方案解决这一问题。

正在开发的解决方案——Chainlink公平排序服务(FSS)

Chainlink 2.0白皮书[14]概述了公平排序服务的主要特点,这是一个由Chainlink去中心化预言机网络(DONs)提供动力的安全链外服务,将用于根据DApp概述的公平时间概念来排序交易。

FSS旨在极大地缓解抢先交易以及MEV的负面影响,并为整个区块链生态系统的用户减少费用消耗。

关于这方面的更多内容可以通过一篇介绍性的博文[15]或是Chainlink 2.0白皮书的第五节中进行了解。

除了FSS之外,缓解抢先交易问题的最好方法之一是尽可能降低交易排序的重要性,从而抑制协议中交易的重新排序及MEV。

 

写在最后

在保护智能合约时,有许多关键的DeFi安全考虑因素,加密世界已经发生了太多的漏洞和攻击,受窃资产更是达到了数千万美元。

了解并掌握DeFi的10个最佳安全实践可以帮助你在构建构建智能合约时避免陷入安全风险。

但是不会有一个100%全面的表单可以涵盖所有漏洞和解决方式。

也许未来还会有更多新型和更复杂的漏洞以及恶意操纵方式,因此这需要整个DeFi社区共同努力,为构建健康的安全生态付诸努力。

通过参考CertiK安全排行榜等权威数据来源的优质项目,学习围绕安全智能合约开发的最佳实践,是提高项目安全性的一个有益方法。

加密世界是一个公正透明且需要我们每一个人协作互助的地方,这一点在开发者们身上体现尤甚。

在保护自己的这一点上,回顾已发生的攻击事件有助于了解恶意攻击者是如何实施攻击的。除此之外,你还可以通过7*24全天候安全洞察系统(如CertiK Skynet天网)实时接收最新的链上安全漏洞相关信息及其更新状况。 

如果你想深入了解安全又怕过程太枯燥,建议一定要看看Ethernaut游戏[16]。你可以通过了解DeFi中的安全问题,它包含了大量DeFi的安全实例,以及Solidity的许多内涵和外延

Damn Vulnerable DeFi[17]也可以娱乐的方式学习相关安全知识。

如果想要了解更多闪电贷攻击的信息,你可以登录Prevent Flash Loan Attacks website[18]进行查看。

对加密世界的每个人来说,从错误中学习并吸取教训是最基本的,这可以确保我们的智能合约被安全地构建并尽可能广泛地采用

如果这篇文章里的解决方案中还有更多你想要了解的信息,请参阅Chainlink文档[19]和CertiK文档[20]

关注CertiK官方公众号,底部对话框发送消息“图谱”,可获取《区块链知识图谱》高清大图哦!

参考链接:

1.https://docs.openzeppelin.com/contracts/2.x/api/math#SafeMath

2. https://blog.sigmaprime.io/solidity-security.html#tx-origin-vuln

3. https://eips.ethereum.org/EIPS/eip-1822

4. https://blog.openzeppelin.com/the-transparent-proxy-pattern/

5. https://blog.sigmaprime.io/solidity-security.html

6. https://twitter.com/tinchoabbate/status/1400170232904400897

7. https://github.com/smartcontractkit

8. https://github.com/crytic/slither

9. https://www.youtube.com/watch?v=bdXJmWajZRY

10.https://blog.openzeppelin.com/the-state-of-smart-contract-upgrades

11.https://academy.binance.com/en/glossary/mempool

12.https://www.coindesk.com/miners-front-running-service-theft

13.https://blog.chain.link/what-is-miner-extractable-value-mev/

14.https://chain.link/whitepaper

15.https://blog.chain.link/chainlink-fair-sequencing-services-enabling-a-provably-fair-defi-ecosystem/

16.https://ethernaut.openzeppelin.com/level/0x4E73b858fD5D7A5fc1c3455061dE52a53F35d966

17.https://www.damnvulnerabledefi.xyz/

18.https://preventflashloanattacks.com/

19.https://docs.chain.link/

20.https://www.certik.io/blog

 

作者:CertiK-链得得专栏;来自链得得内容开放平台“得得号”,本文仅代表作者观点,不代表链得得官方立场凡“得得号”文章,原创性和内容的真实性由投稿人保证,如果稿件因抄袭、作假等行为导致的法律后果,由投稿人本人负责得得号平台发布文章,如有侵权、违规及其他不当言论内容,请广大读者监督,一经证实,平台会立即下线。如遇文章内容问题,请联系微信:chaindd123。

链得得仅提供相关信息展示,不构成任何投资建议
本文系作者 CertiK 授权链得得发表,并经链得得编辑,转载请注明出处、作者和本文链接

更多精彩内容,关注链得得微信号(ID:ChainDD),或者下载链得得App

分享到:

相关推荐

    评论(0

    Oh! no

    您是否确认要删除该条评论吗?

    分享到微信