深入解析AAVE智能合约:计算和利率
Wong Shouhao

概述

本文主要讨论AAVE V3中的数学计算模块,该模块位于src/protocol/libraries/math文件夹内,基础合约为WadRayMath

本文主要包含以下内容:

  1. 浮点数的在solidity内的表示及四则运算
  2. 单利和复利的计算
  3. 百分比的乘除

相比于上一篇文章,本文较为简单且篇幅较短。

浮点数表示与计算

在AAVE中,我们使用定点浮点数进行浮点数表示,具体单位如下:

1
2
uint256 internal constant WAD = 1e18;
uint256 internal constant RAY = 1e27;

显然,使用wad表示的浮点数精度为小数点后18位,而使用ray进行标识则精度为小数点后27位。

WayRayMath中,我们也定义了一些必要的常量以用于后续的计算,如下:

1
2
3
4
5
uint256 internal constant HALF_WAD = 0.5e18;

uint256 internal constant HALF_RAY = 0.5e27;

uint256 internal constant WAD_RAY_RATIO = 1e9;

我们定义了基础的数据类型后,我们需要完善其基础的乘除操作。此处仅完善乘除操作是因为自行定义的方法可以保证乘除操作可以实现四舍五入。

我们首先分析wad的乘法操作:

1
2
3
4
5
6
7
8
9
10
11
12
function wadMul(uint256 a, uint256 b) internal pure returns (uint256 c) {
// to avoid overflow, a <= (type(uint256).max - HALF_WAD) / b
assembly {
if iszero(
or(iszero(b), iszero(gt(a, div(sub(not(0), HALF_WAD), b))))
) {
revert(0, 0)
}

c := div(add(mul(a, b), HALF_WAD), WAD)
}
}

此函数首先进行了条件判断,要求同时满足以下条件:

  1. b的数值不为0
  2. a 小于 (type(uint256).max - HALF_WAD) / b

这些条件存在的原因为:

  1. 避免相除时分母为 0
  2. 避免a * b溢出

为什么使用(type(uint256).max - HALF_WAD) / b而不是type(uint256).max / b 作为判断是否溢出的条件?正如前文所述,此乘法会进行四舍五入,所以如果使用type(uint256).max / b进行判断,可能导致进一后溢出。

此处,对于第二个条件的判定稍有复杂,我们在此处特别分析iszero(gt(a, div(sub(not(0), HALF_WAD), b)))部分代码。not(0)的数值大小即为type(uint256).max(0使用not全部取反后即为type(uint256).max)。sub(a, b)操作符的含义即为a - b,而gt(a, b)则会判断a > b,如不符合条件,则返回0,符合条件则返回1

上述操作符可以在EVM Codes内找到其具体内容

如果不满足上述条件则会revert抛出异常。部分读者可以对revert(offset, size)操作符不熟悉,简单来说,此操作符会中止函数运行并回滚,退回未使用gas,并将从offset开始的长度为size大小的内容作为异常信息抛出。

revert的更加详细的介绍请参考evm codes

介绍了对参数的具体要求后,我们接下来介绍真正的计算步骤c := div(add(mul(a, b), HALF_WAD), WAD),使用数学语言描述为(a * b + HALF_WAD) // WAD,其中//即为整除符号,但此整除仅会向下取整。

我们通过将a * b的实际值与HALF_WAD(相当于 0.5)相加再与WAD(相当于1)进行整除获得四舍五入的结果。

此处除去了一个WAD的原因有以下两点:

  1. 保证单位仍未WAD
  2. 通过整除实现四舍五入

此处,我们可以讨论一下此函数的函数类型,此函数的函数类型为:

  1. internal 表示此函数仅能用于合约内部调用
  2. pure 表示此函数不读取合约内的状态变量也不修改合约变量

在介绍完乘法相关的运算的方法后,我们介绍除法的具体实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
function wadDiv(uint256 a, uint256 b) internal pure returns (uint256 c) {
// to avoid overflow, a <= (type(uint256).max - halfB) / WAD
assembly {
if or(
iszero(b),
iszero(iszero(gt(a, div(sub(not(0), div(b, 2)), WAD))))
) {
revert(0, 0)
}

c := div(add(mul(a, WAD), div(b, 2)), b)
}
}

在讨论具体的代码限制条件前,我们首先分析其具体的逻辑代码部分即c := div(add(mul(a, WAD), div(b, 2)), b),改写为数学表达式为((a * WAD) + b // 2) // b

具体推导过程如下:

最终获得的 与我们代码中的公式是一致的。注意此处的的1不可以直接省去,原因是当 后即意味着整个数学式子的单位(即WAD)的丢失,此处我们需要补齐此单位。

从式 到 式 进行转化的原因是在solidity中不存在正常的除法,仅存在整除,我们只能进行分子分母同乘b的操作使除法消失。

通过最后的公式,我们可以得到如下限制条件:

上式等同于:
$$
a \leqq \frac