DEFI安全问题之基础篇
区块链技术的诞生,为传统金融、数据隐私、供应链、跨境汇款等应用领域带来革命性的突破。其中「去中心化金融()」便是当前最为火热的应用之一。
DEFI作为一个金融概念,其基石就是一个个的代币,代币分为很多种类,一般都是以代币标准进行分类的,比如知名的ERC20 代币标准,以及非同质化代币(NFT)标准ERC721等等。因此作为DEFI的基石, 代币层面的安全问题就不可忽视了。
今天成都链安技术团队为大家科普DEFI安全问题代币层面存在的安全问题:第一大类是代币本身的问题,第二大类是代币与DEFI交互中可能会遇到的安全问题。
#A. 代币层面的问题
1.整型溢出问题
为什么整型溢出这么重要呢?最主要的原因就是因为他们一旦出现就会造成较大的资金损失。在0.8.0之前,EVM并不存在溢出检查机制,需要特别关注数值运算时的整型溢出问题;而在0.8.0之后,solidity推出了自带的溢出检查机制,从根本上避免了整型溢出问题,但是同样也要注意,当使用unchecked关键字时,其涉及的数字运算也是不检查溢出的。
整型溢出一般分为上溢和下溢,上溢是指当运算结果大于uint数据类型规定的取值上限时,会导致溢出到取值下限开始重新计算(一般即为0)。
例如uint8的取值范围是0-255,当给一个数据类型为uint8的变量a赋值为260时,就会导致溢出成0+(260-256)=4,从而a的取值就会变成4。同理当变量赋值比0小时,会导致下溢问题,比如一个uint8类型的变量b,赋值的时候为b=0 - 5,那么这时候b的取值不会是-5(uint8类型是无符号整型,所以没有负数),因此b的取值就会变成256-5=251。
下图为某代币的multiTransfer函数,该函数在对处理输入参数tokens数组累加时,未使用SafeMath进行整型溢出检查,使得攻击者有机会构造整型溢出攻击,导致totalTokensToTransfer的值上溢变为一个较小值而通过余额检查,转出巨额资产。
Ammbr合约的multiTtansfer函数
修改建议
在0.8.0版本以前建议引入SafeMath安全运算库来对数据进行运算,在0.8.0及以上版本使用unchecked时,建议使用require等函数对结果溢出进行检查。
2. 函数权限设置错误
函数权限设置错误通常都是由于合约开发者的疏忽所致,很多内部函数在运行时会直接更改合约储存数据,而不进行相关的检查(例如更改合约管理者权限、调用合约关键参数等),如果这部分函数的可见性被设置为public或者external,将产生重大的安全漏洞。今年十月份AVATerra Finance就出现过这个问题,它将铸币函数mint的可见性修饰词设置为了public,这导致任意攻击者都能够进行铸币操作。
AvaterraToken的合约代码
修改建议
对铸币、权限更改等敏感函数做严格的权限检查;并根据业务逻辑确定这类函数的可见性。
3. 权限过大
管理者拥有过大的合约权限,会出现用户资产量不可控和资产价值不稳定的情况。如管理员拥有随意转走和销毁用户余额的权限,则可将用户的资产随时归零;如管理员拥有无限铸币的函数权限,则可大量发行此类代币,使得代币价格迅速贬值。
下图为管理员能够随意销毁用户代币的函数,这里在修饰器上使用的是onlyRole的修饰器,并且重写了burnFrom函数导致可以任意销毁指定账户的代币。
高权的burnFrom函数
修改建议
对管理员的权限范围做严格的审查,关注转移用户代币、违规销毁用户代币的操作点。建议删除这些留有隐患的代码,保障用户的财产安全。
4. 自我增发漏洞
这种漏洞是一种很特别的逻辑漏洞,当用户自己给自己转账时,由于转账函数中设置了多个局部变量,导致了变量之间的互相覆盖,从而引起的自我增发漏洞。
这个漏洞很典型的例子是Troncrashcoin代币,其转账函数逻辑如下:
1. 新建变量oldFromVal和oldToVal存储旧balances
2. 新建变量newFromVal和newToVal来存储新的balances,即oldFromVal+_value和oldToVal+_value
3. 将newFromVal和newToVal赋值给balances[_from]和balances[_to]
这时候就出现了一个问题,一旦_to地址和_from地址相同,balances[_from]就会被balances[_to]的值覆盖,导致之前减少的_value不起作用,从而产生了自我增发_value却没有减少_value的漏洞。
Troncrashcoin的_transfer函数
修改建议
项目方在开发完毕后需要对项目进行完整的测试,对各个功能点是否正常执行、对所有可调用的函数及其输入的参数进行完整的输入测试,验证业务逻辑是否符合要求,避免给用户带来损失。比如测试极值,自我转账等一系列特殊情况是否满足逻辑。
5. 未正确校验传入参数
在函数的执行中如果未验证传入参数的合理性,就可能导致函数不按照预想的结果执行,比如permit函数如果未做零地址校验,且对应的代币的销毁代币方式是将代币发送至零地址,那么攻击者可以转移零地址中被销毁的代币。还例如在一些智能合约中会存在freeze函数,用于冻结账户,但是在进行代币转账时,只验证了来源账户,未对转入地址进行验证导致转入的代币无法提出,还需注意的有transferFrom要额外验证from地址。黑名单验证也有类似问题。
未检查0地址的permit函数
未验证转入地址的transfer函数
还比如ethernaut靶场中,NaughtCoin这道题,由于这个ERC20的只对了transfer做了lockTokens限定,没有对transferfrom进行限定,导致攻击者可以直接通过标准的erc20接口调用approve和transferfrom进行代币提取。
Ethernaut靶场的NaughtCoin
修改建议
所有由用户调用的函数都要对传入的参数进行合理性检查。避免参数使用不合理导致的异常。在使用具有限制的函数时,要验证传入不合要求的参数是否会绕过限制执行、或者有其他类似的函数可以进行绕过。
6. 开发者后门
部分管理员在开发阶段会请人代为开发,这种情况下开发者如果在Token里面留下了后门,后续带着后门上线的Token会对项目和用户都造成损失。比如下面是代币HJL在铸造函数留的后门,导致每次铸币都有1%的增发代币流入0xfa这个地址,导致该代币的实际流通量大于显示值。
HJL代币的mint函数
修改建议
建议代币上线前,多进行几次审核,并且验证部署代币的哈希和最终审核版本的哈希是否一致。
#B. DEFI交互中的代币问题
1.通缩型代币的差额套利
今年DEFI出现了一批以safemoon为代表的通缩型代币,用户在使用此类币交易时,会销毁部分代币,导致实际到账数量和支出数量并不一致。因此,如果类似于抵押池一类的DEFI项目根据转账数量来记录资产,一旦与此类代币进行交互,很容易出现项目实际拥有资产与记录值不一致的情况,这很容易被攻击者所利用,比如SafeDollar攻击事件。
SafeDollar攻击事件就是攻击者利用PLX代币转账时实际到账数量小于发送数量以及SdoRewardPool合约抵押和计算奖励上存在的逻辑缺陷,借助闪电贷控制SdoRewardPool合约中抵押池的抵押代币数量,进而操纵奖励计算,获得利益。
SdoRewardPool合约的deposit
这里我们用公式大概描述一下,奖励计算系数A = 新产生的奖励代币SAO的数量/抵押池中抵押代币PLX数量,攻击者首先利用闪电贷获取大额资金,然后利用SdoRewardPool合约中更新奖励参数存在的逻辑漏洞和抵押代币PLX转账数量与实际到账数量可能不一致的特殊机制,导致奖励计算系数A急剧增大。然后利用另一个提前进行了抵押的攻击合约领取大量抵押奖励,从而恶意获得了大量的SAO代币。
修改建议
建议合约用转账前后的资金变化作为实际合约收到的转账数量,而不是用户传入的参数,尤其是流动性交易更需要注意。以免出现代币自带手续费或者自动销毁等情况导致实际金额与记录金额不一致的情况,让攻击者利用来造成大量损失。
2.代币接口规范问题
在DEFI与代币交互时,遵循的是统一的代币接口规范,如果代币实现时没有遵循标准的接口规范,则可能会在交互过程中导致代码的逻辑执行异常。比如著名的泰达币(USDT)在部分链上代码实现就不符合ERC20的标准,在波场(TRON)和以太坊(ETH)的USDT代码中,都有着返回值不规范的情况。以太坊的USDT主合约中,transfer函数虽然有返回值,但是函数声明时却没有声明,导致后续没有返回值。
以太坊USDT主合约的transfer代码
同样在波场的USDT中,主合约中的transfer函数继承的父合约的transfer。但是父合约的transfer虽然有声明返回值,但是函数中却没有返回值,因此会导致主合约的transfer会永久返回false。
波场usdt主合约的transfer代码
波场usdt子合约的transfer代码
因此这两个链上的USDT合约都不符合ERC20的标准,如果DEFI未注意该问题,可能就会导致资金被锁死在合约之中。
修改建议
在进行代币转账时,需要检查调用的代币合约的转账函数是否满足ERC20标准。对没有返回值的转账函数使用SafeERC20来执行转账,但是波场USDT的transfer函数未遵循其自有的TRC20规范,会导致函数返回默认的false。使得SafeTransfer执行失败,需要专门写函数调用。
3.ERC721,ERC777,ERC1155
可能引发的重入风险
重入漏洞算是一个比较知名的基础漏洞了,代币中当然也有这样的重入风险,比较典型的例子就比如ERC1155里面safeTransferFrom函数中会调用_doSafeTransferAcceptanceCheck函数,然而_doSafeTransferAcceptanceCheck里面会检测如果目标地址是合约的话,会调用他的onERC1155Received方法,这里如果DEFI合约编写不恰当,调用safeTransferFrom位置在重要操作(例如修改余额)之前,则会引发重入漏洞。
ERC1155的safeTransferFrom函数
ERC1155的_doSafeTransferAcceptanceCheck函数
比如下面这个合约,withdraw函数的amount减少放在了safeTransferFrom之后,一旦有恶意合约构建一个onERC1155Received来进行重入,那么多次重入以后就能够提取合约中的所有代币。
危险的DEFI代码
攻击合约
修改建议
重入漏洞可以使用检查-生效-交互模式来进行避免,对转账函数可以使用openzeppelin官方的ReentrancyGuard进行修饰。注意是否存在重要操作(例如修改余额)在外部调用之前,可能会导致重入风险。
4.无限授权
用户在与DEFI进行代币交互时,部分DEFI项目可能会直接向用户要求无限授权,然而这其实是个很不安全的行为,一旦DEFI项目的前端或者项目内部出现了一些漏洞和问题,用户的代币安全将会无法获得保障,因此通常来说DEFI项目最好让用户有选择性的给予授权值,以免造成不必要的代币资产损失。
在流动性挖矿项目UNICats中就存在无限授权。用户可以存入 Uniswap(UNI)代币,然后通过流动性挖矿获得项目方发行的MEOW 代币。但是如果要参与挖矿,前端要求用户必须提供无限授权。用户在项目上质押了UNI代币,也可以将奖励与质押取回,但是项目方利用合约中的后门任何时候都可以将代币转走。所以用户在参与DEFI时必须注意保护账户的授权。用户在授权UniCats时收到了钱包提示,但是由于授权一般是DEFI的常见操作,用户往往会因为忽略细节而向合约无限授权。
MetaMask的无限授权提醒
用户收到挖矿奖励取回质押后,已经从项目中退出,这时候往往更容易松懈。因为授权还未取消,项目合约有后门来让项目方进行调用,管理员就可以通过授权来转走用户的UNI。之后项目方提款跑路,使广大用户蒙受巨大的损失。
UniCats合约项目方使用的后门
另一个例子是 Degen Money 项目。Degen Money这个项目没有在智能合约中暗留后门,而是创建了一个前端来进行两次授权交易。Degen Money的参与者面临着窃取用户资金的风险。第一次授权是针对质押合约,第二次授权恶意地址,会导致资金被攻击者通过第二次授权地址使用transferFrom提取。用户提走合约里的代币无济于事,必须取消授权才能避免盗币事件的发生。
为了防止类似的事件再次发生,有网站可以提供查询服务来方便用户。使得用户及时了解授权信息,避免盗币风险。
修改建议
授权是代币的常见操作,建议用户重视自身财产安全,不要盲目相信项目方。定期使用查询网站来判断是否有授权过度的风险,发现之后及时取消。下面为地址授权查询平台
币安链官方区块链授权查询:
https://bscscan.com/tokenapprovalchecker
以太坊链官方区块链授权查询:
https://etherscan.io/tokenapprovalchecker
链上工具:
https://mycointool.com/ApprovalChecker
微信掃描關注公眾號,及時掌握新動向
2.本文版權歸屬原作所有,僅代表作者本人觀點,不代表比特範的觀點或立場
2.本文版權歸屬原作所有,僅代表作者本人觀點,不代表比特範的觀點或立場