撰文: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
肖飒:国内从事USDT兑换业务可能会构成非法经营罪:10月30日,北京大成律师事务所合伙人肖飒撰文表示,在国内从事USDT的兑换业务可能会构成《中华人民共和国刑法》第二百二十五条规定的非法经营罪。该条款的第三、四项规定:“违反国家规定,有下列非法经营行为之一,扰乱市场秩序,情节严重的,处五年以下有期徒刑或者拘役,并处或者单处违法所得一倍以上五倍以下罚金;情节特别严重的,处五年以上有期徒刑,并处违法所得一倍以上五倍以下罚金或者没收财产:(三)未经国家有关主管部门批准非法经营证券、期货、保险业务的,或者非法从事资金支付结算业务的;(四)其他严重扰乱市场秩序的非法经营行为。”USDT具有法币性质,而兑换业务常常与资金结算业务挂钩,其代替法币流通的可能性也容易被上述兜底条款所覆盖,?肖飒团队认为,USDT兑换业务的表现容易与非法经营罪所列举的非法经营行为相契合。[2020/10/30]
事故原因: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将代币全部转走。
声音 | 肖飒:稳定币真的想‘稳定’必然要接受严格的监管来去除其本身内在的风险:2019年10月17-18日,中国人民银行行长易纲、副行长陈雨露出席了在美国华盛顿举行的二十国集团(G20)财政和央行部长级和副手级会议。会议一致同意发布G20关于稳定币的声明。今日,关于稳定币的政策和风险中国银行法学研究会理事肖飒在接受采访时表示,稳定币的政策和监管风险总的来说就是来源于两个方面:一个是稳定币作为去中心化的数字货币其本身具有的、等潜在的风险,另一个就是越来越严的监管。这两方面是稳定币问题的一体两面,若是稳定币真的想‘稳定’,那么必然要接受严格的监管来去除其本身内在的风险。”[2019/10/19]
案例2:2020年6月18日跨链桥BancorNetwork被攻击损失14万美金
事故原因:合约用于转账的函数默认为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,导致配额计算错误返回预期外的大量配额。
声音 | 律师肖飒:把币还原成其本来的令牌功能等 也许还有一丝机会可以好好做下去:肖飒在新浪发表专栏文章《币圈为啥还是那样红?》,文章表示,币圈如果只是那个“炒作”和“割韭菜”的币圈就一定会死的很难看。把币还原成其本来的令牌功能,让贡献工作量的极客拿到未来技术使用的“预先授权”或者在一个商业王国将token当做宣传、促销的手段和方法,也许,还有一丝机会可以好好做下去。[2019/7/24]
解决方案:使用map时必须检查key是否存在。
在状态变更前进行转账
转账时有可能被重入,利用未变更的状态进行攻击。
案例:2021年8月17日XSURGE被攻击损失500万美金
事故原因:在转账后才修改totalSupply,转账时被重入另外一个未加重入锁的函数损失500万美金。
解决方案:使用了再入锁也要在所有状态变更之后在转账。
初始化函数未做调用和权限限制
很多合约需要初始化子合约,例如Uniswap需要通过Factory合约初始化Pool合约,这时候如果忘记对子合约的初始化函数做权限和重复初始化限制,可能被攻击者进行恶意初始化。
案例:2021年8月11日PunkProtocol被攻击损失400万美金
事故原因:池子的initialize函数未做权限和重复调用限制,攻击者调用该函数将自己设置为Forge管理员权限,并调用withdrawToForge将池子所有资金都发送到攻击者地址。
解决方案:初始化函数必须设置成只能初始化一次。
未正确检查对应合约函数实现
通常智能合约被调用的函数不存在时会报错,但如果合约实现了fallback函数,则会自动调用fallback函数。有时fallback函数并不会报错,导致调用方误以为调用成功。
声音 | 肖飒:使用数字货币代替工资 存在逃税漏税的嫌疑:据核财经消息,近日,中国银行法学研究会理事肖飒表示,无论是“九四公告”还是“风险提示”,既不属于行政法规,也不是法律,其仅是各主管部门联合发布的规范性文件,具有规范效力,但并不是真正的法律文本。使用数字货币代替工资,存在逃税漏税的嫌疑。目前,我们并没有出台专门的法律以规制发币行为,但发币的行为逻辑背后确实违反了我国现有法律法规。涉嫌的罪名包括但不限于非法发售代币票券、非法发行证券、非法集资、金融、组织领导活动等。[2018/9/26]
案例: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。
签名验证漏洞
签名被重复使用,或者利用椭圆曲线签名算法的对称性,根据已有签名构造合法签名。
声音 | 肖飒:为币站台将依正犯或单独的罪名进行刑事处罚:9月12日消息,中国银行法学研究会理事、律师肖飒表示,为参与币项目进行宣传的媒体和提供技术支持的软件外包公司,主观上明知或应当知道是犯罪活动,仍为其提供帮助,或者特定技术开发只用于犯罪行为的,将构成帮助犯。应定的具体罪名与刑罚当依据币发行、运作的本质逻辑、侵害的法益重大性等,依正犯的罪名及刑罚从轻或减轻处罚,或者以单独的罪名进行刑事处罚。[2018/9/12]
案例: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。
解决方案:隐私数据需要加密放在链上。
以上是我们分析和总结的二十三种安全事故类型汇总,希望能够给到您些许参考和启示。
作者:蒋海波,PANews根据CryptoFees.info的数据,截至6月30日,Synthetix过去一周平均每天的收入为30.17万美元,除去智能合约平台外.
作者:肖飒lawyer NFT=or≠数字藏品 本次倡议中使用了NFT的字样进行描述,一经发出,有些从业者留言:我们不是NFT,我们是数字藏品、数字艺术品。这种掩耳盗铃的做法无益于事情的解决.
作者:RayXiao,IOSGVentures拥抱MachineFi:去中心化机器金融的未来是新瓶装老酒还是一代新人换旧人?最近笔者的一个圈外朋友说自己原来从不运动的老婆这一个月天天准时到楼下跑.
衍生品是一种合约,其价值来自于其他对标资产,例如股票、商品、货币、指数、债券或利率。期货、期权和掉期是几种常见的衍生品。每种衍生品的用途不同,投资者的交易目的也不一样.
作者:Marina,W3.Hitchhiker 修订:Evelyn 6月,DAO的管理平台Dework完成了500万美元的种子轮融资.
作者:GoPlusSecurity可租赁NFT提案EIP-4907已过审,未来会有越来越多的使用了ERC4907的NFT上线。我们看一下官方demo的实现.