GAS:二十三种 DeFi 安全事故汇总:智能合约风险与防范

撰文:AustinZhang,JonLi,AsymmetriesTechnologies

智能合约的安全性问题一直是业界的一个重点话题,由于程序员的某些疏忽造成了思维和逻辑上的漏洞,从而导致黑客有了可乘之机。我们搜集了目前在DeFi领域已经发生了安全事故的智能合约,并根据我们编写的示例代码来实证分析其中的原因,希望能给到同事和同行们一些启示。

重入攻击

主要攻击方式之一:合约调用恶意外部合约结束之前,恶意外部合约函数反向调用原合约函数利用相关漏洞。

示例代码

案例1:2021年12月22日UniswapV3流动性管理协议Visor被盗120ETH

事故原因:deposit函数没有防重入锁也没有验证from地址是否是合法的Visor合约地址。攻击者传入攻击合约地址,重复调用deposit函数绕过取款金额检查多次取款。

案例2:2021年6月5日BurgerSwap被盗700万美金

事故原因:类似Uniswap的原创dex,分为Platform和Pool两个合约。Platform类似Uniswap的Router,Pair类似Uniswap的Pool,开发者错误的将K值校验放在Platform计算,攻击者在Platform中进行重入攻击,多次以旧的K值换取代币,造成流动性提供者损失。

解决方案:调用外部合约前确保所有中间状态变量已更新并使用再入锁。

未检查函数返回值

调用外部合约函数时,有些函数调用失败不会抛出错误回滚交易而是返回false,如果忘记检查函数返回值会导致误以为调用成功。

示例代码

案例:2021年4月4日ForceDao到被攻击损失183ETH

过去的二十个小时里价值7.06亿美元的以太坊进行转移:10月2日消息,据Whale Alert DLT sleuth数据显示,在过去的二十个小时里,大型加密货币巨鲸和顶级数字交易所进行了大约十次转移,共携带价值7.06亿美元的以太坊。在匿名的大型以太坊持有者(又称鲸鱼)和主要的加密货币交易所,如Gemini、Binance、Kucoin的参与下,大约有214861个以太坊被转移。这里最大的转移有51300个ETH和42859个ETH(价值169,000,000美元和146,000,000美元)之间。(u.today)[2021/10/3 17:21:38]

事故原因:Force代币的transferFrom余额不足时返回false而不是直接回滚交易,合约中未做判断导致转账失败时也被认为成功,可以换取到对应代币。

解决方案:使用call函数调用外部合约时必须检查调用是否成功。注:call调用外部合约未匹配到函数时,会调用外部合约fallback或者receive函数,如果外部合约有定义receive函数且call函数未携带calldata则会调用外部合约receive函数,其他情况调用fallback函数。

未正确设置函数可见性

Solidity中函数默认为public,可以被外部调用,一旦未将关键函数设置为Private,就会导致安全风险。

示例代码

案例1:2022年1月22日DexCrosswise被攻击损失80万美金

事故原因:Crosswise虽然实现了权限验证函数onlyOwner,但忘记设置setTrustedForwarder为private,导致被攻击者利用,将自己设置为池子的Owner将代币全部转走。

案例2:2020年6月18日跨链桥BancorNetwork被攻击损失14万美金

据官方Gate.io 理财宝第二十期DeFi流动性挖矿赚USDG今日已开抢:据官方公告,Gate.io “天天理财” 第二十期DeFi流动性挖矿赚USDG已于今日中午12点开抢,锁仓7天;Gate.io“天天理财” 第十五期 DeFi流动性挖矿赚USDG仅剩2天结束;USDT理财体验金活期福利专场正持续进行中。两大专区含各大主流币种与热门币种理财项目。[2020/10/31 11:18:21]

事故原因:合约用于转账的函数默认为public,攻击者可以直接调用转走合约中的代币。

解决方案:提款函数事关合约资产的转移,需谨慎设置权限控制,确保初始化函数只能运行一次。

未验证Map中Key不存在的情况

Solidity中的Mapping在获取对应Key的Value时,如果Key不存在,会返回对应类型的默认值,而不是报错。例如Mapping(int→int),如果对应int的Key不存在,会返回默认值0。

示例代码

案例:2021年7月11日跨链桥ChainSwap被攻击损失400万美金

事故原因:ChainSwap依赖其网络中的validator进行转账。为了限制validator一次转走超过其质押的代币,设置了配额。结果合约中存在漏洞可以绕过配额限制,当地址变量signatory不存在时,authQuotes和lasttimeUpdateQuoteOf会返回0,导致配额计算错误返回预期外的大量配额。

解决方案:使用map时必须检查key是否存在。

在状态变更前进行转账

转账时有可能被重入,利用未变更的状态进行攻击。

案例:2021年8月17日XSURGE被攻击损失500万美金

Bitget一键跟单第二十期“明星交易员”公布:据官方消息,Bitget今日公布了一键跟单的第二十期明星交易员:币圈老吴。该交易员在平台累计交易笔数105笔,累计跟随人数230人,累计收益率1547.44%。近三周数据:总收益3614.13USDT,收益率1547.44%,胜率90.48%。据介绍,币圈老吴,2008-2016年,在上海期货交易所做培训讲师,外汇期货最高峰时近千用户进行实盘跟单交易,操盘跟单最高资金达2亿,最高波段22连胜。2016年-2019年入场BTC、ETH现货,年均年化率130%-160%翻仓4倍。2019年6月离开期货交易所,正式转战币圈合约及现货交易。[2020/9/30]

事故原因:在转账后才修改totalSupply,转账时被重入另外一个未加重入锁的函数损失500万美金。

解决方案:使用了再入锁也要在所有状态变更之后在转账。

初始化函数未做调用和权限限制

很多合约需要初始化子合约,例如Uniswap需要通过Factory合约初始化Pool合约,这时候如果忘记对子合约的初始化函数做权限和重复初始化限制,可能被攻击者进行恶意初始化。

案例:2021年8月11日PunkProtocol被攻击损失400万美金

事故原因:池子的initialize函数未做权限和重复调用限制,攻击者调用该函数将自己设置为Forge管理员权限,并调用withdrawToForge将池子所有资金都发送到攻击者地址。

解决方案:初始化函数必须设置成只能初始化一次。

未正确检查对应合约函数实现

通常智能合约被调用的函数不存在时会报错,但如果合约实现了fallback函数,则会自动调用fallback函数。有时fallback函数并不会报错,导致调用方误以为调用成功。

动态 | 中铁二十三局成功上线人民普惠平台 首笔供应链业务正式落地:12月6日,中铁二十三局集团有限公司(以下简称“中铁二十三局”)在人民普惠平台上完成首笔供应链融资业务落地。人民普惠平台是人民金服推出的一款集成了人工智能、生物识别、区块链存证等多项前沿技术的供应链金融科技服务平台。平台以区块链作为数据存储与智能合约的底层技术,旨在将链上所有关键节点实现智能合约应用,可实时监督溯源,可为完整交易做最详尽的安全管控。所有核心审批操作,贸易凭证等原始材料均有区块链存证备份。(人民网)[2019/12/9]

案例:2022年1月18日跨链桥Multichain被攻击损失450ETH

事故原因:通常ERC20的合约会实现permit函数,用于签名检查与授权操作。但WETH、PERI、OMT、WBNB、MATIC、AVAX六种代币的合约没有实现permit却实现了fallback,Multichain在检查这些代币的权限时误以为用户已经授权转账给攻击者,导致代币被盗。

解决方案:不同代币的实现方式不同,引入新代币之前应仔细检查其具体实现。

未正确处理带转账费的代币

有些代币在转账时会销毁一部分转账费用,导致实际收到的代币余额偏少,如果开发者没考虑到这一点,以转账值计算,会导致出现偏差。

案例:2021年8月19日Pinecone被盗20万美金

事故原因:Pinecone使用其代币PCT作为资金池的质押代币,PCT转账会有手续费的损耗。合约并没有考虑相关损耗导致用户份额和质押的PCT总额出现偏差,被攻击者利用领取多余的奖励。

解决方案:谨记不是所有的代币转账费都为nativetoken。

签名验证漏洞

签名被重复使用,或者利用椭圆曲线签名算法的对称性,根据已有签名构造合法签名。

行情 | XTZ领涨市值前二十币种:据coinmarketcap数据,目前XTZ领涨市值前十币种,涨幅为9.97%,现全球均价为14.34元。Tezos(XTZ)是一个新的多中心化公有区块链项目,他的特点是支持智能合约,拥有自己创建的智能合约语言,提出了通过数学证明的代码自制交易和网络共识机制,以解决目前棘手的网络升级分叉问题。此外,昨日领涨的XLM今日表现依然不俗,当前涨幅7.90%。[2018/7/16]

案例:2021年7月12日AnySwap被盗800万美金

事故原因:对交易签名除了私钥外需要一个随机数R,但是Anyswap部署新合约失误,导致在BSC上的V3路由器MPC帐户下有两个交易具有相同的R值签名,攻击者反推到这个MPC账户的私钥转走了被盗资金。

解决方案:使用EIP-712标准验证签名,参考OpenZeppelin的实现:https://docs.openzeppelin.com/contracts/3.x/api/drafts。

未考虑合约余额可能产生的变化

矿工挖出块时或者智能合约调用selfdestruct函数销毁自己时可以向任意地址强行打币改变其原生代币的余额。当使用余额函数返回值作为判断条件时,余额有可能被强行改变导致风险,极端情况下甚至导致合约拒绝服务。

示例代码

即使捐赠合约不能接受代币转账,合约余额也可能在部署后被改变,严格检查已空投总量与合约余额之和等于总供应量可能导致捐赠合约拒绝服务。

解决方案:在合约中避免对合约余额做严格相等的检查。

使用delegatecall调用外部合约

delegatecall可以将对应合约的函数代码内嵌到当前上下文中执行,就像调用内置函数一般。如果不小心调用了恶意合约极易导致攻击。

示例代码

当攻击者调用forward函数并传入Attack合约地址以及函数setOwner()作为参数时,Proxy合约owner将被修改为攻击者地址。

解决方案:不推荐使用delegatecall调用外部合约。

授权tx.origin

tx.origin是交易的发起者地址,合约如果使用tx.origin做权限检查,当合约的授权用户与恶意合约交互时,恶意合约调用合约即可通过合约权限检查。

示例代码

当MyWallet合约owner使用transferTo函数向Attack合约转账时,Attack合约会重入MyWallet合约,并调用transferTo函数,此时tx.origin仍然为MyWalletowner,require条件满足,MyWallet余额将被全部转移至Attack合约。

解决方案:不使用tx.origin做权限检查。

交易排序竞争

全节点运行者可以在交易被确认之前获取交易信息,进而根据获取的交易信息,构造高手续费交易,让矿工优先打包自己的交易以执行对自己有利的策略。例如,谜语合约奖励最快找出谜底的用户,恶意用户可以在获悉诚实用户提交的谜底后,构造高手续费交易优先诚实用户提交谜底,从而获取奖励;又如当用户更新授权额度时,被授权用户可以在更新授权额度交易被确认之前转移旧的授权额度,如此,被授权人实际获得的授权额度为两次授权额度之和。

解决方案:针对谜语合约,获得谜底的用户先提交「随机数+自身地址+谜底」的哈希值,谜语合约存储该哈希值后,用户再提交随机信息与答案,合约检查哈希值匹配后再发放奖励;更新授权额度时先置零授权额度。

使用block.timestamp或者block.number作为合约时间参考

block.timestamp与block.number都不能获得精确都时间,用作智能合约的时间参考会引入潜在的风险。

解决方案:使用oracle获取时间信息。

Denial-of-Service(DoS)拒绝服务

调用外部合约可能永久失败导致本合约不能接受新的指令,例如当合约主动对另外一个合约转账,而被转账合约没有接受转账的函数时,转账失败,此时合约可能进入拒绝服务状态。

示例代码

当合约向其中一个账号转账失败会导致所有转账全部失败。

解决方案:合约调用外部合约时可能出现的失败,合约需包含处理调用失败情况的代码,防止合约进入拒绝服务状态。

使用链属性作为随机源

链属性如block.timestamp,blockhash,bock.difficulty以及其他属性可被矿工操控,存在风险。

解决方案:考虑使用RANDAO,oracle或比特币区块hash作为随机源。

继承顺序错误

多个被继承合约都定义了同一个函数时,继承合约调用该函数的优先级由继承顺序决定,错误的继承顺序将导致函数调用错误。

解决方案:继承顺序说明请参考官方实例:https://solidity-by-example.org/inheritance/。

Gas不足攻击

多签情况下或者需要其他人帮自己代付Gas时,用户准备好签名交易并交给代执行人,代执行人再将用户交易提交给执行合约,代执行人可以提前审查用户代交易,恶意的代执行人或当交易内容不利于代执行人时,可以通过限制Gas的供给,使交易的执行失败,从而阻止交易的执行。

示例代码

当Relayer调用者通过限制Gas使用导致某个交易失败,那么失败的交易将永远不能再被提交。

解决方案:选择信任的代执行人,或者在执行合约中检查代理人提供的Gas费是否足够。

函数类型变量跳转

solidity支持函数类型变量,当函数类型变量使用汇编指令赋值时,函数类型变量有可能被指向恶意构造当函数。

解决方案:如无必要,尽量避免在智能合约中使用汇编指令。

GasLimit服务拒绝攻击

区块设置有Gas使用上限,如果合约当执行超过了区块Gas使用上限,则合约永远不能被执行成功。

示例代码

当操作的循环次数过大时,执行合约所需Gas将超过区块上限,导致合约执行失败。

解决方案:在智能合约中谨慎操作大数组,或循环。

abi.encodePacked()哈希碰撞

abi.encodePacked()采用非填充序列化,当序列化参数包含多个变长数组时,攻击者可以在保持所有元素顺序不变的前提下,改变两个变长数组的元素,如此序列化的结果相同。

示例代码

通过构造addUser的输入,攻击者可以将regularUsers的成员加入admins成员,但是构造的输入和原输入的签名相同。

解决方案:使用定长数组,或者不让调用者传入abi.encodePacked()的参数,或者使用abi.encode()。

transfer()和send()函数Gas不足

transfer()和send()函数使用2300gas以防止重入攻击,公链升级后可能导致gas不足。

解决方案:推荐使用call()函数,但需做好重入攻击防护。

链上未加密隐私数据

链上数据完全透明,合约的private关键字不能阻止合约的隐私数据泄漏。

示例代码

虽然players为private,但攻击者仍然可以通过解析链上数据读取players。

解决方案:隐私数据需要加密放在链上。

以上是我们分析和总结的二十三种安全事故类型汇总,希望能够给到您些许参考和启示。

郑重声明: 本文版权归原作者所有, 转载文章仅为传播更多信息之目的, 如作者信息标记有误, 请第一时间联系我们修改或删除, 多谢。

金星链

[0:15ms0-0:790ms