Solidity 合约安全,常见漏洞(第四篇)
权力过大的管理员
仅仅因为一个合约有一个所有者或管理员,这并不意味着他们需要无限权力。考虑一个 NFT。按理说,只有所有者才能从 NFT 销售中提取收益,但如果所有者的私钥被泄露,能够暂停合约(阻止转账)就会造成严重的破坏。一般来说,管理员的权限应该尽可能的小,以减少不必要的风险。
使用 Ownable2Step 而不是 Ownable
这在技术上不是一个漏洞,但OpenZeppelin ownable如果所有权被转移到一个不存在的地址,会导致合约所有权的丧失。Ownable2step 要求接收者确认所有权。这可以防止意外地将所有权发送到一个错误的地址。
四舍五入的错误
Solidity 没有浮点,所以舍入错误是不可避免的。设计者必须意识到正确的做法是向上舍入还是向下舍入,以及舍入应该对谁有利。
除法应该总是最后进行。下面的代码在小数位数不同的稳定币之间进行了错误的转换。下面的兑换机制允许用户在兑换 dai(精度为 18)时免费获得少量的 USDC(精度为 6)。变量 daiToTake 将四舍五入为零,换取非零的 usdcAmount 时,用户拿不到任何东西。
contract Exchange {
uint256 private constant CONVERSION = 1e12;
function swapDAIForUSDC(uint256 usdcAmount) external pure returns (uint256 a) {
uint256 daiToTake = usdcAmount / CONVERSION;
conductSwap(daiToTake, usdcAmount);
}
}
抢跑(Frontrunning)
在 Etheruem(和类似的链)的背景下,Frontrunning 意味着观察一个待定的交易,并通过支付更高的 交易成本在它之前执行另一个交易。也就是说,攻击者已经 "跑到了 "交易的前面。如果该交易是一个有利可图的交易,那么除了支付更高的 交易成本,完全复制该交易是有意义的。
这种现象有时被称为 MEV,意思是矿工可提取的价值,但有时在其他情况下是最大可提取的价值。区块生产者有无限的权力来重新排序交易和插入自己的交易,从历史上看,在以太坊进入股权证明之前,区块生产者就是矿工,因此而得名。
抢跑:不受限制的提款
从智能合约中提取以太币可以被认为是一种 “有利可图的交易”。你执行了一个零成本的交易(除了 Gas),最终拥有的加密货币比你开始时更多。
contract UnprotectedWithdraw {
constructor() payable {
require(msg.value == 1 ether, "must create with 1 eth");
}
function unsafeWithdraw() external {
(bool ok, ) = msg.sender.call{value: address(this).value}("");
require(ok, "transfer failed").
}
}
如果你部署了这个合约并试图退出,一个先行者机器人会注意到你在 mempool 中对 "unsafeWithdraw "的调用,并复制它来先获得以太币。
抢跑:ERC4626 通膨攻击,是抢跑和四舍五入错误的组合
我们已经在ERC4626 教程中深入介绍了 ERC-4626 的通膨攻击。但它的要点是,ERC4626 合约根据交易者贡献的 "资产 "的百分比来分配 "份额"代币。大致上,它的运作方式如下:
function getShares(...) external {
// code
shares_received = assets_contributed / total_assets;
// more code
}
当然,没有人会贡献资产而得不到任何股份,但他们无法预测这种情况会发生,如果有人能在交易中先发制人,获得股份。
例如,当池子里有 20 个资产时,他们贡献了 200 个资产,他们期望得到 100 份额。但是,如果有人在交易中提前存入 200 个资产,那么公式将是 200/220,四舍五入为零,导致受害者失去资产,获得零份额。
抢跑:ERC20 授权
我用一个真实的例子来说明这一点,而不是抽象地描述它:
- 假设 Alice 授权了 Eve 的 100 个代币。Eve 是邪恶的代表,而不是用 Bob ,所以我们将保持惯例。
- Alice 改变了主意,发送了一个交易,将 Ev e 的授权改为 50。
- 在将授权额度改为 50 的交易纳入区块之前,它位于 Mempool 中,Eve 可以看到它。
- Eve 发送了一个交易,要求获得她的 100 个代币,这在将授权改为 50 之前。
- 对 50 的授权的交易通过了。
- Eve 又获取了 50 个代币。
现在 Eve 有 150 个代币,而不是 100 或 50。解决这个问题的办法是,在处理不受信任的授权时,在增加或减少授权之前,将授权设置为零。
抢跑:三明治攻击
一项资产的价格会随着买卖压力的变化而变化。如果一个大订单在 Mempool 中,交易者有动力去复制这个订单,但要有更高的 gas 成本。这样一来,他们就会购买资产,让用户的大额订单使价格上涨,然后他们马上卖出。卖出订单有时被称为 “尾随”。卖出订单可以通过放置一个较低 gas 成本的卖出订单来完成,这样的序列看起来像这样的
- 抢跑买入
- 大额买入(用户)
- 卖出
对这种攻击的主要防御是提供一个 "滑点"参数。如果 "抢跑买入 "本身将价格推高到某个阈值以上,"大额买入"订单将回退,使抢跑者的交易失败。
这就是所谓的三明治攻击(sandwhich),因为大额买入被抢跑买入和尾随卖出夹在中间。这种攻击也适用于大额卖单,只是方向相反。
了解更多关于抢跑的信息
抢跑是一个巨大的话题。Flashbots已经对这个话题进行了广泛的研究,并发表了一些工具和研究文章,以帮助最大限度地减少它的负面外部因素。通过适当的区块链架构是否可以 "设计掉 "抢跑是一个争论不休的话题,还没有得到最终的解决。以下两篇文章是关于这个问题的永恒的经典之作:
以太坊是一个黑暗的森林
逃离黑暗森林
签名相关
数字签名在智能合约的背景下有两种用途:
- 使得地址能够授权区块链上的一些交易,而不进行实际交易
- 根据预定的地址,向智能合约证明发起者有某种权力去做某事
下面是一个安全使用数字签名的例子,让用户拥有铸造 NFT 的特权:
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract NFT is ERC721("name", "symbol") {
function mint(bytes calldata signature) external {
addres