;)
7.DOS攻击
在 Web2 中,拒绝服务攻击(DoS)是指通过向服务器发送大量垃圾信息或干扰信息的方式,导致服务器无法向正常用户提供服务的现象。而在 Web3,它指的是利用漏洞使得智能合约无法正常提供服务。
先看过程:
1 | // 游戏结束,退款开始,所有玩家将依次收到退款 |
这里的漏洞在于,refund() 函数中利用循环退款的时候,是使用的 call 函数,将激活目标地址的回调函数,如果目标地址为一个恶意合约,在回调函数中加入了恶意逻辑,退款将不能正常进行。
攻击:
1 | contract Attack { |
attack() 函数中将调用 DoSGame 合约的 deposit() 存款并参与游戏;fallback() 回调函数将回退所有向该合约发送ETH的交易,对DoSGame 合约中的 DoS 漏洞进行了攻击,所有退款将不能正常进行,资金被锁在合约中,就像 Akutar 合约中的一万多枚 ETH 一样。
原理:这里导致代码中断的本质代码是: require(success, “Refund Fail!”);因为低级调用call收到的是false所以require检测失败自动回退。(在调用外部合约时,其实调用合约无所谓成功或失败,只需要接受调用结果的data就行,所以成功和失败完全不干扰当前合约执行,只有自己的合约执行自己的代码出现错误才会中断或回退,外部调用别的代码不会中断自己)
解决办法:
- 删除 require(success, “Refund Fail!”)
- 退款时,让用户从合约自行领取(pull),而非批量发送给用户(push)。
总结:防 DoS 的核心写法
🌟 规则 1:所有付款都用 Pull Payment(用户自己领)
永远不要在循环中:
- 发 ETH
- 发 ERC20
- 发 NFT
这条就能防住 50% 的 DoS。
🌟 规则 2:不要依赖外部合约调用成功与否
永远不要写:
1 | require(externalContract.do()); |
改成:
- 判断 success
- 稳健处理
- 失败不让你核心逻辑停止
🌟 规则 3:不要遍历无限增长的数组
会打爆 gas,使函数永远执行不了。
🌟 规则 4:使用 chunk 分片执行来替代一次性大循环(避免gas不够用)
🌟 规则 5:所有对外 call 使用非阻塞模式(ignore failure)
示例:
1 | (bool success,) = target.call{value:amount}(""); |
不要 require(success)。
🌟 规则 6:使用重入锁(nonReentrant)
避免因重入导致意外的死锁、状态乱序,进而形成 DoS。
8.貔貅合约
貔貅是中国的一个神兽,因为在天庭犯了戒,被玉帝揍的肛门封闭了,只能吃不能拉,可以帮人们聚财。但在Web3中,貔貅变为了不详之兽,韭菜的天敌。貔貅盘的特点:投资人只能买不能卖,仅有项目方地址能卖出。
通常一个貔貅盘有如下的生命周期:
- 恶意项目方部署貔貅代币合约。
- 宣传貔貅代币让散户上车,由于只能买不能卖,代币价格会一路走高。
- 项目方
rug pull卷走资金。
1 | /** |
当前合约是貔貅代币合约用于代币的转账、铸造、销毁等逻辑,要卖出代币必须卖到交易对合约也就是pair地址的合约
解决:
- 在区块链浏览器上(比如etherscan)查看合约是否开源,如果开源,则分析它的代码,看是否有貔貅漏洞。
- 如果没有编程能力,可以使用貔貅识别工具,比如 Token Sniffer 和 Ave Check,分低的话大概率是貔貅。
- 看项目是否有审计报告。
- 仔细检查项目的官网和社交媒体。
- 只投资你了解的项目,做好研究(DYOR)。
- 使用tenderly、phalcon分叉模拟卖出貔貅,如果失败则确定是貔貅代币。
9.抢先交易
抢跑最初诞生于传统金融市场,是一场单纯为了利益的竞赛。在金融市场中,信息差催生了金融中介机构,他们可以通过最先了解某些行业信息并最先做出反应从而实现获利。这些攻击主要发生在股票市场交易和早期的域名注册。链上抢跑指的是搜索者或矿工通过调高gas或其他方法将自己的交易安插在其他交易之前,来攫取价值。在区块链中,矿工可以通过打包、排除或重新排序他们产生的区块中的交易来获得一定的利润,而MEV是衡量这种利润的指标。
在用户的交易被矿工打包进以太坊区块链之前,大部分交易会汇集到Mempool(交易内存池)中,矿工在这里寻找费用高的交易优先打包出块,实现利益最大化。通常来说,gas price越高的交易,越容易被打包。同时,一些MEV机器人也会搜索mempool中有利可图的交易。比如,一笔在去中心化交易所中滑点设置过高的swap交易可能会被三明治攻击:通过调整gas,套利者会在这笔交易之前插一个买单,再在之后发送一个卖单,并从中盈利。这等效于哄抬市价。
总结:通过收集消息,判断市价即将发生的涨跌情况,然后在市价变化之前抢先影响市价变化的交易完成准备交易,然后在市价变化后卖出或买入从而赚取差价。
解决:
- 使用预提交方案(commit-reveal scheme)。
- 使用暗池,用户发出的交易将不进入公开的
mempool,而是直接到矿工手里。例如 flashbots 和 TaiChi。 - 在调用参数中加上保护性参数,如滑点保护,从而减少抢跑者的潜在收益。(滑点是当你往流动性池添加/取出代币时,实际获得的数量与理论应该得到的数量之间的差距。滑点越大差值越大,就越亏。市场价也会变化更大)
10.钓鱼攻击
tx.origin:在solidity中,使用tx.origin可以获得启动交易的原始地址,tx.origin 是当前交易(transaction)最初发起者的地址(一定是EOA外部账户)
银行合约
我们先看银行合约,它非常简单,包含一个owner状态变量用于记录合约的拥有者,包含一个构造函数和一个public函数:
- 构造函数: 在创建合约时给
owner变量赋值. transfer(): 该函数会获得两个参数_to和_amount,先检查tx.origin == owner,无误后再给_to转账_amount数量的ETH。注意:这个函数有被钓鱼攻击的风险!
1 | contract Bank { |
攻击合约
然后是攻击合约,它的攻击逻辑非常简单,就是构造出一个attack()函数进行钓鱼,将银行合约拥有者的余额转账给黑客。它有2个状态变量hacker和bank,分别用来记录黑客地址和要攻击的银行合约地址。
它包含2个函数:
- 构造函数:初始化
bank合约地址. attack():攻击函数,该函数需要银行合约的owner地址调用,owner调用攻击合约,攻击合约再调用银行合约的transfer()函数,确认tx.origin == owner后,将银行合约内的余额全部转移到黑客地址中。
1 | contract Attack { |
解决:
1.使用msg.sender代替tx.origin
11.未检查的低级调用
1 | function withdraw() external { |
如果 bool success = payable(msg.sender).send(balance)执行失败,则提款失败但是余额会归0,需要修改为require(success)
12.NFT重入攻击
因为普通的重入攻击需要被攻击合约调用攻击合约的代码,而nft转账时会检查目标合约有没有能力接受nft而不是黑洞合约,所以会调用目标合约的检查函数判断,
此时就有可能触发重入攻击
漏洞合约
1 | contract NFTReentrancy is ERC721 { |
攻击合约
1 | // ERC721的回调函数,会重复调用mint函数,铸造10个 |
解决方法:
和一般重入攻击一样:
1.检查-影响-交互模式(checks-effect-interaction)
2.重入锁。
13.函数multicall
1 | function multicall(bytes[] calldata data) external payable onlyWhitelisted { |
首先,自己call自己是无意义的不会修改链上变量但是delegatecall可以;此函数用memory的depositCalled追踪有漏洞,如果delegatecall中的参数是multicall也就是调用自己就会创建新的函数空间,depositCalled会重置为false无法阻止双花。
关于msg.value的变化情况分析:
- 每个
msg.value只属于一个CALL msg.value在CALL指令执行时立即转移- **后续的
delegatecall不消耗新的msg.value**:代理调用只是借用代码没有跳出外部,也就是没有call所以msg.value不会改变
比如:用户A → 合约b.f1() → 合约b.f2() → 合约c.call(f3) → 合约c.f4() → 合约d.call(f5)
| 特性 | call | delegatecall |
|---|---|---|
| 是否转移ETH | ✅ 可以 | ❌ 不可以 |
| msg.value | 可指定新值 | 保持调用者值 |
| 执行上下文 | 目标合约的上下文 | 调用者的上下文 |
| 存储访问 | 目标合约的存储 | 调用者的存储 |
1 | f1: msg.value = X |







