uint112
那个奇怪的 uint112 类型是什么?为什么不用 uint256?答案是:gas优化。
GitHub - XuHugo/solidityproject: DApp go go go !!!
EVM 的每个操作都会消耗一定量的gas。简单的运算,如算术运算,消耗的gas很少,但有些运算消耗的gas很多。最耗gas的操作是 SSTORE,将数值保存到合约存储区。与之对应的 SLOAD 也很昂贵。因此,如果智能合约开发者能尽量优化合约的耗gas量,对用户是有好处的。使用 uuint112 作为储备变量正是为了实现这一目的。
看看我们是如何布置变量的:
address public token0;
address public token1;
uint112 private reserve0;
uint112 private reserve1;
uint32 private blockTimestampLast;
uint256 public price0CumulativeLast;
uint256 public price1CumulativeLast;
必须完全按照这个顺序排列。原因是每个状态变量对应一个特定的存储槽,而 EVM 使用 32 字节的存储槽(每个存储槽正好是 32 字节)。当你读取一个状态变量的值时,它会从该变量所链接的存储槽中读取。每个 SLOAD 调用一次读取 32 字节,每个 SSTORE 调用一次写入 32 字节。由于这些操作都很昂贵,所以我们非常希望减少存储读取和写入的次数。而这正是对状态变量进行适当布局可能会有所帮助的地方。
如果有几个连续的状态变量占用的时间少于 32 字节,该怎么办?我们需要分别读取它们吗?事实证明,不需要。EMV 对小于 32 字节的邻接变量进行了打包。
再来看看我们的状态变量:
1、前两个是地址变量。地址需要 20 个字节,而两个地址需要 40 个字节,这意味着它们必须使用不同的存储槽。它们不能存储在一个槽中,因为根本放不下。
2、两个 uint112 变量和一个 uint32 变量,这看起来很有趣: 112+112+32=256! 这意味着它们可以放在一个存储槽中!这就是选择 uint112 作为储备变量的原因:储备变量总是一起读取的,最好从存储空间一次性加载,而不是分开加载。这样可以节省一次 SLOAD 操作,而由于储备变量使用频率很高,这就大大节省了油耗。
3、两个 uint256 变量。这两个变量不能打包,因为每个变量都要占用一个完整的槽。
同样重要的是,两个 uint112 变量必须位于占用一个满槽的变量之后,这样才能确保第一个变量不会被打包到前一个槽中。
整数溢出
我们为什么将累计价格计算放到unchecked中?
智能合约的另一个常见漏洞是整数溢出或下溢。uint256 整数的最大值是
最小值最小为0;整数溢出是指增加整数变量的值,使其大于最大值,这将导致溢出:该值从0,重新开始计数。例如:
同样,从0中减去一个数字会得到一个非常大的数字,例如:
在 0.8.0 版之前,Solidity 无法检查溢出和溢出不足,因此开发人员开发了一个库: SafeMath 库。如今,这个库已不再需要,因为 Solidity 现在会在检测到溢出或下溢时抛出异常。
Solidity 0.8.0 还引入了unchecked block,顾名思义,在它内不禁止溢出/下溢检测。在计算 timeElapsed 和累积价格时,我们使用了unchecked block。这似乎不利于合约的安全性,但时间戳和累计价格溢出是意料之中的:溢出时不会发生任何坏事。我们希望它们溢出时不会出错,这样它们才能正常运行。
这种情况很少发生,因此溢出/溢出检测功能几乎不应该被禁用。
安全发送
你可能已经注意到了我们使用的发送代币的函数很奇怪:
function _safeTransfer(
address token,
address to,
uint256 value
) private {
(bool success, bytes memory data) = token.call(
abi.encodeWithSignature("transfer(address,uint256)", to, value)
);
if (!success || (data.length != 0 && !abi.decode(data, (bool))))
revert TransferFailed();
}
为什么不直接在 ERC20 接口上调用传输方法?
在pair合约中,当进行代币转账时,我们总是希望确保转账成功。根据 ERC20,转账方法必须返回一个布尔值:成功则返回 true,失败则返回 fails。大多数代币都能正确实现这一点,但也有一些代币不能,它们什么也不返回。当然,我们无法检查代币合约的执行情况,也无法确定代币是否真的进行了转移,但我们至少可以检查转移结果。如果转移失败,我们也不想继续。
这里的调用是address的一个方法,这是一个底层函数,可以让我们对合约调用进行更精细的控制。在这种特殊情况下,无论转移方法是否返回结果,它都能让我们获得转移结果