去中心化银行的基本原理不多说了,看 Compound 的介绍。然后关于质押率(loan to value), 健康因子(health factor)还有爆仓等等概念,可以看白皮书或Google。这里讲一些数值相关的实现细节。

用户资产,债务和权益的计算

首先 资产 = 债务 + 权益(这里不是会计等式),资产就是用户存在 LendingPool 的钱,债务就是用户从 LendingPool 借出的钱,权益就是用户的流动性衡量,和用户可以从 LendingPool 借出的钱成正比,银行通过两个外部计数器来记录资产和债务,然后权益就可以通过计算得出。

Aave 通过铸 aToken 来计算存入 LendingPool 的数量,然后铸 debtToken 来计算已经借出的数量。并且每种资产(token)都有一个自己的 aToken 和 debtToken。比如 DAI,每存入一个 DAI 就铸一个 aDAI 给用户,每借出一个 DAI 就铸一个 debtDAI 给用户。

A 可以把 aToken 可以转给 B,相当于是把权益转给了 B,权益的转移也就意味着资产的转移,因为 Aave 并没有一个变量来专门记录用户存入的资产,而是通过 aToken 来衡量的。A 把 aToken 转给 B 以后,A 可以从池子里赎回的钱就变少了(资产变少了),A 可以借的钱也就少了(权益也变少了),而 B 就可以用 A 给他的 aToken 从池子里取钱,也可以借钱。

如果 A 在池子里借了钱,那把 aToken 转给 B 以后可能会立即爆仓(即 health factor 不再满足条件),所以 aToken 的 transfer 方法有个额外参数 validate ,如果是 true,转账方法执行前会检查是否会爆仓。

用户从 Aave 的 LendingPool 里取钱以后,Aave 会把对应数量的的 aToken 从用户的钱包里销毁。即权益和资产同时减少。

但是 debtToken 是不能转的(这是关键),代码里把 ERC20 的 transfer 和 allowance 方法给禁用了,一旦调用直接报错。也就是说债务没法转给别人,这样 A 借出的钱必须他自己还上。也就是用户钱包里一旦有了 debtToken 如果不还钱,这些 debtToken 就永远记在用户名下了。

总结下,aToken 和 debtToken 都是 LendingPool 的计数器,是独立于 LendingPool 的合约,就算 LendingPool 升级重新部署,也不影响 aToken 和 debtToken 的状态,也就是用户的权益和债务不会受到影响。LendingPool 可以通过铸造和销毁 aToken 和 debtToken 的来记录用户的资产和债务,用户之间可以相互转移 aToken 但是无法相互转移 debtToken。

利息的计算

Aave 还把利息也是记录在 aToken 和 debtToken 上的,实现的方式并不是定时更新,而是覆盖 ERC20 默认的 balanceOf 方法,每次用户想查看余额或者想转账的时候,调用 balanceOf 时都会返回当前区块时刻的本金+利息。所以 Aave 的 aToken 和 debtToken 是一种带利息的 ERC20 代币。

具体可以看 Aave V1 的白皮书,在“3.8 Tokenization”中变量定义的第5条“Current balance”。

普通的 ERC20 代币,余额是固定的,只会在 mint, burn 和 transfer 操作后才会变化(这个余额在 Aave 里面叫做 Principle balance)。但是带利息的 ERC20 代币的余额是动态变化的,而这个变化的余额的意义和普通 ERC20 代币的意义是一样的,也就是可以 transfer 给别人的数量。随着区块增长,用户钱包里的 aToken 的余额就会不断的自动增加,对应的资产和权益也就增加了;然后 debtToken 的余额也会增加……对应的债务也就增加了。

Aave 有两种利率模式,stable 和 variable 利率,所以 debtToken 其实有两种,具体 mint 哪一种到用户钱包,取决于用户借款时候选择的利率模式。然后 stableDebtToken 和 variableDebtToken 的 balanceOf 方法 也是完全不一样的,有各自的实现。

预言机

当 LendingPool 里面有多种资产的时候,用户的 health factor 和可借款额度就不能直接通过 token 的数量来衡量,因为不同资产的 aToken 价值不同,相应的 debtToken 的价值也不同,比如存入 10 个 USDC 可以借出 8 个 DAI 但是只可以借出 0.004 个 WETH。在计算用户可借款额度。

Aave 用了 totalCollateralETH, totalDebtETH 两个指标,计算方式是把所有的 aToken 和 debtToken 的数量转化成 ETH 的价值并求和,然后来计算 availableBorrowsETH 和 healthFactor。

其中 aToken 和 debtToken 数量转成 ETH 的价值需要用到 token/ETH 交易对的市价,这个数据来自预言机,Aave 用的是 ChainLink。

所以攻击预言机可以影响账户的可借款额度和清算条件

银行业务代码中的设计模式