AAVE调研报告
项目描述
Aave是一种去中心化的基于以太坊的非托管开源协议,用户可以使用它赚取存款利息或者借入资产。同时,它还支持在无中介的情况下发放和获得贷款,首创了DEFI生态无抵押贷款模式(也就是闪电贷)。
AAVE的借贷池由管理者创建,对于每一个借贷池都对应一个特定的Atoken。用户存入的资产可以作为流动性挖矿的本金,同时也可以作为抵押借贷的抵押物。
对于投资者,想以AAVE项目获利的人来说,可以通过流动性挖矿的方式赚取稳定的收入;同时也可以通过清算抵押资产的方式来获取清算的奖励(折扣购买清算资产);也可以通过闪电贷的方式借取资产进行套利。
截止3.13日,AAVE项目在defi Pulse的排名为第四名,超过了uniswap,项目内资产超过176亿美金。
功能描述
目前市面上V2版本规模最大,V3版本还没有正式上线,这里着重分析概括V2版本的基本逻辑和功能,以提供一个项目的基本概念。
基础概念
术语
APY
Annual Percentage Yield,即年化利率。这里APY和APR(Annual Percentage Rate)是两个概念,APY有点类似利滚利,而APR是定死的收益。比如信用卡A的APR为每月1%,那么他最终在还款时需要还12%(1%*12)的利息。信用卡B的APY为每月1%,那么他最终一年还款需要12.68%((1+0.01)^12-1=12.68)。
LTV(Loan To Value)
贷款价值比,用于衡量抵押物和贷款的比值。比如用户抵押了100元的资产,LTV为80%,那么用户可以贷出来80元资产。在AAVE中,LTV的数值由用户抵押物来进行确定。
Liquidation Threshold
翻译过来就是清算阈值,用于标定抵押资产价值被清算的阈值。这个概念被用于金融领域,当借款用户抵押的资产价值总额低于这个阈值的时候就会被清算。比如用户抵押了100个ETH贷出了800个USDT,阈值被设定为80%,如果ETH或者USDT价格发生波动,当用户抵押的100个ETH的价值总额低于或等于640(800*80%)个USDT的时候,用户抵押的资产就会被清算拍卖。
Liquidation Bonus
清算奖励,用来奖励那些流动性提供者购买被清算资产的行为。那么被清算资产的判定是根据一个健康因子的参数,当这个参数高于1,这个资产就被定性为被清算资产。
Health factor
健康因子,用来判断一个抵押资产是否达到清算的标准,其实就是(抵押资产价值*清算阈值/借贷金额)。
Stable Rate
稳定利率,在短期贷款中可能保持一个固定的利率,但是在长期贷款中可能利率会根据市场价的波动而导致利率变化。
ray&wad
ray: 27位精度的十进制数
wad: 18位精度的十进制数
在solidity中,这两种表示都本质都是uint256。
项目内概念
reserve
表示的是每一个交易池的储备资产,每一个交易池都有不同的虚拟货币的储备,所有储备资产换算成ETH的总额被称之为这个交易池的总流动资金。储备资产接收用户的存储,用户同时也可以通过超额抵押资产的方式来借出这些资金,这些抵押就被标记为抵押资产。储备中的每一个资产都可以设置为抵押资产或者流动资产。
principal balance
借贷本金,指的是最初借款的金额。
compound balance
复合贷款金额,指的是借贷本金加利息,利息可能是稳定利率或者可变利率产生的。
liquidity index
当前(流动性本金+流动性挖矿奖励)/流动性本金。
liquidity rate
当前流动性奖励利率,也就是流动性挖矿奖励利率。
variable index
(可变利率贷款本金+可变利率贷款利息)/可变利率贷款本金。
variable rate
可变利率贷款利率。
stable index & stable rate
概念同上。
整体数据存储逻辑
资产存储和收益逻辑
AAVE交易池将不同资产划分成了各个独立的储备,用户通过调用交易池的存款函数来向储备中存储相应的标的资产。
每个储备都有一个特定的atoken为其服务,用户存入的资产会被等量的兑换成对应的atoken作为其财产凭证。
用户存入的资产可以作为抵押贷款的抵押物,同时也可以作为流动性提供给储备。
对于每一个特定的资产,用户都可以将其状态设置为抵押状态或者非抵押状态。区别在于抵押状态的财产可能受到未偿还债务影响不能随时随地取出(甚至可能被清算),两种状态都会产生流动性收益。
用户随时可以通过从交易池将自己存入的资产和产生的收益取出,不过前提需要满足以下条件其中之一:
- 用户没有借款
- 用户取出的资产没有作为抵押物
- 如果有借款且作为抵押物,那么需要保证取出资产后用户的贷款健康因子不低于预设的阈值。
收益来源
用户获得的收益来自于下面列出的来源:
- 贷款利息
- 闪电贷手续费
这里需要说明的是,用户提款的资金源自于各个储备的流动性,当流动性不足时(没人还款、抵押资产无人清算)可能会出现用户不能完全取出存款和收益的情况。
收益去向
- 用户流动性收益
- 协议方收取,在每一次交易池资产变动时的储备更新操作中,都会将收益的一部分(定义为每个储备金的reserveFactor)存储到treasury地址中。在这里需要说明,转到treasury的资产是以生成atoken的形式转入(mint利息的一部分),并没有直接在标的资产上做操作。
存入资金去向
用户在交易池中存储的标的资产最终都是将资产转移到atoken地址,然后atoken合约生成等量atoken交给用户作为用户的资产凭证。用户与交易池的一切操作最终的资产交互都是和atoken合约地址进行。
抵押借贷逻辑
用户在AAVE规定范围内的所有抵押物资产都可以作为任何一个抵押贷款的抵押物,价值换算到ETH来进行衡量。
用户在每一个储备中产生的贷款(包括本金和利息)都以债务代币(debt token)的形式表示。债务代币本质是一个阉割版的ERC20token,只能由交易池进行铸币和销毁操作。
AAVE采用常规的超额抵押借贷逻辑,每一个交易池的资产都对应一个预设的ltv。
同时,用户也有一个加权平均的ltv,其值源自于用户所有贷款资产ltv的加权平均乘以用户所有抵押资产价值总和(avgLtv = totalValueToBorrow / totalCollateralValue)。
用户借款时,会通过用户的平均Ltv来计算新增的贷款数额对应的抵押物资产数额,需要满足当前抵押物资产数额大于借贷发生后的抵押物数额。(amountOfCollateralNeededETH <= vars.userCollateralBalanceETH)
用户抵押借贷不需要还款期限以及最低还款数额,用户甚至可以选择永远不还款等待清算。当用户的贷款健康因子低于一个阈值时,清算者就可以利用低价来购买被清算者的抵押资产。
在进行抵押借贷时,有两种利率模式可以选择:可变利率和稳定利率,二者在交易池中都有属于自己的债务代币实现,用于记录用户的贷款。用户也可以将手中的债务利率模式进行切换(前提是资产支持两种利率模式),不过在切换时会将其间产生的利息一起纳入借贷本金。
利率计算公式
稳定利率和可变利率的计算公式是一样的,如下:
二者的区别在于参数不同。
其中Rv0代表的是基础利率,U代表资金利用率,Uoptimal表示最佳利用率,为预先设定的值,Rslope1和Rslope2表示在达到最佳资产利用率前后的基准利率。
可以看到,在没有达到最佳利用率之前,利率主要根据Rslope1变化,在达到最佳利用率之后,主要根据Rslope2变化。整体的利率是和利用率正相关的。
也就是说,当交易池中流动性高时,低利率鼓励贷款;当流动性低时,高利率来维持流动性。
稳定利率和可变利率的区别
前小节提到,二者计算公式相同且在代码中更新时间顺序也相同。那么二者的最主要区别就在于上述公式中的Rv0、Rslope1、Rslope2以及Uoptimal参数的区别。
对于AAVE中各个资产的参数明细,在这里可以查询:https://docs.aave.com/risk/liquidity-risk/borrow-interest-rate
通过图表,可以明显看出其区别:
- 可变利率的Rv0(也就是基础利率)都是0,而稳定利率的Rv0则不为0为一个常数(在代码中稳定利率的基础利率是由预言机获取的市场平均利率)。
- 稳定利率的Rslope1普遍高于可变利率的Rslope1。
- 可变利率的Rslope2普遍显著高于稳定利率的Rslope2。
- 只有行情比较稳定的资产(如ETH、USDT等)才支持稳定利率。
所以,当交易池流动性较高时,可变利率会低于稳定利率;当流动性低时,可变利率会显著高于稳定利率。
对于支持稳定利率的资产,大多数都是投资者准备长期持有的,所以流动性一般不会过低。而对于其他次级代币,市场价格波动较大,交易池的流动性也不能保持稳定在最佳利用率以下,超过最佳利用率的话,那么利率会增长到一个可怕的数值。
所以对于准备长期借贷持有的币种,更推荐选择稳定利率:对于短期套利币种则可变利率是个更好的选择。
还款逻辑
对于抵押借贷产生的贷款,用户可以不限时间不限数额偿还债务。对于某一特定资产的债务,用户偿还时需要使用同一种资产偿还贷款。
清算逻辑
当用户的债务状况不佳,即健康因子低于一个阈值时,用户的抵押资产就可以被清算者清算,也就是将抵押资产拍卖还债。
这些被清算的资产会以一个折扣价出售给清算者,清算者可以选择以atoken的形式或者标的资产本身的方式来接受清算的抵押物。
与还款逻辑类似,清算者需要与债务资产相同的代币来清算被清算者的抵押资产。
AAVE市场
目前,AAVE协议运行在eth、polygon以及avalanche上。
AAVE实现了对AMM的流动性代币的交易市场(即支持AMM LP代币的抵押和借贷),由于LP代币的特殊性,AAVE对于每一个交易对都定义了其风险参数(也就是之前利率计算一节中公式参数),具体参考:https://docs.aave.com/risk/asset-risk/amm
eth:https://etherscan.io/address/0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9
polygon:https://polygonscan.com/address/0x8dff5e27ea6b7ac08ebfdf9eb090f32ee9a30fcf
avalanche:https://avascan.info/blockchain/c/address/0x4F01AeD16D97E3aB5ab2B501154DC9bb0F1A5A2C/transactions#tabletop
ETH部署(v2)
地址 | 名称 | 作用 |
---|---|---|
0x7d2768dE32b0b80b7a3454c06BdAc94A69DDc7A9 | AAVE Lending Pool V2 | 用户与AAVE交互的接口 |
0x464C71f6c2F760DdA6093dCB91C24c39e5d6e18c | AAVE Tresury(Fee Collector) | 存储AAVE协议手续费 |
0xBcca60bB61934080951369a648Fb03DF4F96263C | aUSDC | 用户存入交易池资产最终流向的atoken地址,发送给用户等量的atoken,用于作为用户资产和收益凭证。 |
0x028171bCA77440897B824Ca71D1c56caC55b68A3 | aDAI | |
0x3Ed3B47Dd13EC9a98b44e6204A523E766B225811 | aUSDT | |
0x8dAE6Cb04688C62d939ed9B68d32Bc62e49970b1 | aCRV | |
0x6C5024Cd4F8A59110119C56f8933403A539555EB | aSUSD | |
0x101cc05f4A51C0319f570d5E146a8C625198e636 | aTUSD | |
0xc9BC48c72154ef3e5425641a3c747242112a46AF | aRAI | |
0x030bA81f1c18d280636F32af80b9AAd02Cf0854e | aWETH | |
0xa685a61171bb30d4072B338c80Cb7b2c865c873E | aMANA | |
0x5165d24277cD063F5ac44Efd447B27025e888f37 | aYFI | |
0xA361718326c15715591c299427c62086F69923D9 | aBUSD | |
0x35f6B052C598d933D69A4EEC4D04c73A191fE6c2 | aSNX | |
0x9ff58f4fFB29fA2266Ab25e75e2A8b3503311656 | aWBTC | |
0xD37EE7e4f452C6638c96536e68090De8cBcdb583 | aGUSD | |
0xd24946147829DEaA935bE2aD85A3291dbf109c80 | aAMMUSDC | amm市场的USDC atoken(名称中带有AMM的atoken地址与之类似) |
0x39C6b3e42d6A679d7D776778Fe880BC9487C2EDA | aKNC | |
0x17a79792Fe6fE5C95dFE95Fe3fCEE3CAf4fE4Cb7 | aAMMUSDT | |
0xf9Fb4AD91812b704Ba883B11d2B576E890a6730A | aAMMWETH | |
0x272F97b7a56a387aE942350bBC7Df5700f8a4576 | aBAL | |
0x79bE75FFC64DD58e66787E4Eae470c8a1FD08ba4 | aAMMDAI | |
0xc713e5E149D5D0715DcD1c156a020976e7E56B88 | aMKR | |
0xF256CC7847E919FAc9B808cC216cAc87CCF2f47a | aXSUSHI | |
0xB9D7CB55f463405CDfBe4E90a6D2Df01C2B92BF1 | aUNI | |
0xaC6Df26a590F08dcC95D5a4705ae8abbc88509Ef | aENJ | |
0xa06bC25B5805d5F8d82847D191Cb4Af5A3e873E0 | aLINK | |
0x05Ec93c0365baAeAbF7AefFb0972ea7ECdD39CF1 | aBAT | |
0xCC12AbE4ff81c9378D670De1b57F8e0Dd228D77a | aREN | |
0x13B2f6928D7204328b0E8E4BCd0379aA06EA21FA | aAMMWBTC | |
0xDf7FF54aAcAcbFf42dfe29DD6144A69b629f8C9e | aZRX | |
0x2e8F4bdbE3d47d7d7DE490437AeA9915D930F1A3 | aPAX | |
0x514cd6756CCBe28772d4Cb81bC3156BA9d1744aa | aRENFIL | |
0x1E6bb68Acec8fefBD87D192bE09bb274170a0548 | aAMPL | |
0x6F634c6135D2EBD550000ac92F494F9CB8183dAe | aDPI | |
0x9a14e23A58edf4EFDcB360f68cd1b95ce2081a2F | aENS | |
0x683923dB55Fead99A79Fa01A27EeC3cB19679cC3 | aFEI | |
0xd4937682df3C8aEF4FE912A96A74121C0829E664 | aFRAX |
polygon部署(v2)
地址 | 名称 | 作用 |
---|---|---|
0x8dFf5E27EA6b7AC08EbFdf9eB090F32ee9a30fcf | AAVE Lending Pool V2 | 用户与AAVE交互的接口 |
0x7734280A4337F37Fbf4651073Db7c28C80B339e9 | AAVE Tresury(Fee Collector) | 存储AAVE协议手续费 |
0x1a13F4Ca1d028320A707D99520AbFefca3998b7F | amUSDC(AAVE MATIC Market USDC) | polygon(matic)网络的atoken,命名于L1不同,作用相同。 |
0x27F8D03b3a2196956ED754baDc28D73be8830A6e | amDAI | |
0x60D55F02A771d515e077c9C2403a1ef324885CeC | amUSDT | |
0x8dF3aad3a84da6b69A4DA8aeC3eA40d9091B2Ac4 | amWMATIC | |
0x5c2ed810328349100A66B82b78a1791B101C9D61 | amWBTC | |
0x28424507fefb6f7f8E9D3860F56504E4e5f5f390 | amWETH | |
0xc4195D4060DaEac44058Ed668AA5EfEc50D77ff6 | amBAL | |
0x3Df8f92b7E798820ddcCA2EBEA7BAbda2c90c4aD | amCRV | |
0x080b5BF8f360F624628E0fb961F4e67c9e3c7CF1 | amGHST | |
0x080b5BF8f360F624628E0fb961F4e67c9e3c7CF1 | amLINK |
avalanche部署(v2)
地址 | 名称 | 作用 |
---|---|---|
0x4F01AeD16D97E3aB5ab2B501154DC9bb0F1A5A2C | AAVE Lending Pool V2 | 用户与AAVE交互的接口 |
0x467b92aF281d14cB6809913AD016a607b5ba8A36 | AAVE Treasury(Fee Collector) | 存储AAVE协议手续费 |
0x47AFa96Cdc9fAb46904A55a6ad4bf6660B53c38a | avDAI(AAVE Avalanche DAI) | Avalanche网络的atoken。 |
0xDFE521292EcE2A4f44242efBcD66Bc594CA9714B | avWAVAX | |
0x53f7c5869a859F0AeC3D334ee8B4Cf01E3492f21 | avWETH | |
0x46A51127C3ce23fb7AB1DE06226147F446e4a857 | avUSDC | |
0x532E6537FEA298397212F09A61e03311686f548e | avUSDT | |
0x686bEF2417b6Dc32C50a3cBfbCC3bb60E1e9a15D | avWBTC |
AAVE代币
AAVE本身也发行了自己项目代币AAVE,同时在AAVE本身交易市场中,AAVE代币会有更高的效率(低利率高ltv等)。
除此之外,AAVE代币的重要功能就是参与AAVE项目社区的维护,作为DAO的投票使用。
总共发行了 16,000,000 个 AAVE。13,000,000 AAVE 分配给用户,其余 3,000,000 AAVE 分配给生态系统储备。
角色 | 平均持币数量 | 比例 |
---|---|---|
项目方 | 416 | 26% |
散户 | 520 | 33% |
交易所 | 264 | 17% |
合约+挖矿奖励 | 400 | 25% |
代币质押
为了保证AAVE协议的安全性,AAVE协议鼓励AAVE代币持有者将持有的AAVE代币质押到AAVE协议中。这些被质押的AAVE代币的状态被称之为安全模式,安全模式中的AAVE质押代币用于应对一些协议运行中出现的亏空风险,用户存入安全模式的质押代币需要被锁定一段时间后才能被用户取出。
注意,AAVE AMM市场的交易池目前不受到安全模式的保护。
亏空风险定义为:
- 合约漏洞利用。
- 流动性不足,抵押资产无人清算。
- 预言机失灵。
当出现亏空时,部分质押AAVE代币会在市场上抛售来弥补协议中出现的财务缺口;抛售流程中有防护措施来限制抛售的AAVE代币数量,防止大量的AAVE代币流入市场进一步造成AAVE代币价格下跌。
对于将持有AAVE代币并将其质押在AAVE协议中的用户,他们承担了风险,所以AAVE协议会给予这些质押AAVE代币的持有人奖励。质押者获得的质押奖励来自于项目方的AAVE代币储备以及部分协议手续费用。
项目治理
AAVE改进建议(AIP)包括了AAVE项目中参数的变动(利息参数、奖励系数等)、协议的改进以及管理措施的改进。AAVE代币持有者可以在管理社区发布新的AIP,当新的AIP被管理者采纳,会发起一个投票,AAVE代币的持有者通过手中的AAVE代币来决定提案是否通过。注意,质押的AAVE代币同样可以作为投票使用。
基本调用流程
前置概念
由于EVM不能自己运行,所以每次涉及到交易池资金变动的操作都会调用交易池的更新函数,更新贷款总量、利率以及流动资金总量,同时将产生的利息以mint等量的atoken的形式存储到treasury中。
对于atoken和债务代币,每个用户的本金是一个缩减后的量(scaled amount)。
比如APY为20%(也就是用户存入100元一年后可以取出120元)用户如果在交易池初始化时投入100元,那么这个缩减后的存款为100;如果用户在交易池创建一年后存入120元,那么这个缩减后的存款同样为100元。(债务代币同理)
也就是说,用户的存款会被缩减到对标交易池初始化时的比率,方便计算。
基本功能介绍中,采用的样例版本为V2,详细逻辑见后续的代码分析一节。
基本用户所有的操作都是和LendingPool合约交互进行的。
deposit
场景:用户将手中的资产存入对应的交易池中,或用于流动性挖矿或用于抵押借款。
函数接口:
function deposit(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)
参数说明:
- asset:将要存入AAVE协议的资产。
- amount:存入数量
- onBehalfOf:接收atoken的地址
- referralCode:用于标记操作源,0表示没有中间人。
函数逻辑:
- 检查对应的交易池状态是否允许存款
- 更新交易池状态
- 将用户手中的资产转移到交易池对应的atoken中,生成对等的atoken给用户作为资产凭证。
- 如果用户是第一次存储此类资产,则默认将此资产作为抵押物。
资金流:用户资产从用户手中流向到了atoken地址,atoken合约生成等量的atoken给用户。
withdraw
场景:用户将存储在交易池中一定数量的资产以及产生的收益取回。
函数原型:
function withdraw(address asset,uint256 amount,address to) external override whenNotPaused returns (uint256)
参数说明:
- asset:用户将要取回的资产。
- amount:用户想要取回的资产数量(本金+利息)。
- to:接收取回资产的地址。
- 返回值:最终成功取回的资产数量。
函数逻辑:
-
检查是否允许用户取回该数目的资产:
-
1) 交易池状态是否激活。
-
- 如果用户名下没有贷款或者该资产用户没有将其作为抵押资产,则可以取回。
-
- 如果用户名下有贷款,且该资产作为抵押物,会判断如果取回该资产后用户的健康因子是否超过阈值,如果超过则不允许取出。z注意:判断抵押资产价值时考虑了抵押资产的本金和产生的收益。
-
如果可以取出,那么更新交易池状态。
-
如果用户将该交易池中的所有资产取出,则将用户的在该资产的状态设置为非抵押状态。
- 销毁atokens,将资产转出给用户。
资金流:若成功执行,则atoken将所持有的amount数量的asset资产转向用户设置的to地址。
borrow
场景:用户通过自己在其他资产的抵押借去特定数目的目标资产。
函数原型:
function borrow(address asset,uint256 amount,uint256 interestRateMode,uint16 referralCode, address onBehalfOf)
参数说明:
- asset:用户想要借取的资产。
- amount:贷款数额
- interestMode:利率模式
- referralCode:用于标记操作源,0表示没有中间人。
函数逻辑:
-
计算借出特定数量的目标资产的ETH对标价值(通过预言机)。
-
检查是否允许借款:
-
- 检查交易池状态是否允许借款。
-
- 检查用户的健康因子、当前剩余抵押资产是否能够满足用户的借款需求。
-
- 如果用户选择稳定利率模式进行贷款,则交易池需要开启稳定利率模式且用户不可以拥有目标资产的抵押资产。
-
- 用户贷款数额需小于交易池目前最高的贷款限额
-
更新交易池状态
-
根据用户选择的贷款利率模式,生成对应的债务代币,并更新对应的利率信息。注意,在这里借出去的
- 将贷款转移到借贷者账户。
资金流:若执行成功,则atoken会将所持有的标的资产转给onBehalfOf地址,并且在对应利率模式下给用户生成贷款代币。
repay
场景:用户偿还特定数量的目标资产特定利率模式的贷款。
函数原型:
function repay( address asset,uint256 amount,uint256 rateMode,address onBehalfOf) external override whenNotPaused returns (uint256)
参数说明:
- asset:用户偿还的贷款资产
- amount:偿还数额
- rateMode:利率模式
- onBehalfOf:贷款偿还人(借贷者)
- 返回值:最终偿还数额。
函数逻辑:
- 计算用户在当前交易池的欠款额以及利率模式。
- 检查当前储备状态是否激活,用户在当前利率模式下是否有欠款。
- 销毁指定数量的利率模式债务代币。
- 更新交易池状态。
- 如果用户还清了欠款,则将用户在此交易池的借贷状态设置为false。
- 将用户的atoken转到交易池。
资金流:用户将资产转移到atoken中,销毁onBehalfOf的贷款代币。
liquidation
场景:清算者购买被清算者一定数量的抵押资产(collateral asset)来偿还被清算者特定资产债务(debt asset)
函数原型:
function liquidationCall(address collateralAsset,address debtAsset,address user,uint256 debtToCover,bool receiveAToken)
参数说明:
- collateralAsset:清算者(用户)想要购买的抵押资产。
- debtAsset:被清算者的债务资产,同时也是清算者要支付的资产。
- user:被清算者
- debtToCover:替被清算者偿还的贷款额
- receiveAToken:清算者是否以atoken的形式接收清算得到的抵押资产。
函数逻辑:
- 检查用户的债务信息以及健康因子是否达到清算标准,并且用户是否持有该抵押资产。
- 计算用户在debt asset中的总贷款额,计算出实际能够被清算的贷款数额。
- 更新债务资产状态。
- 销毁对应的债务代币。(优先销毁可变利率债务)
- 更新债务资产利率。
- 如果清算者接收atoken则将抵押资产对应的atoken转入清算者账户;否则将抵押资产转入清算者地址并更新交易池状态(因为减少了流动性)。
- 最后,将清算者用于购买的资产(debt token)转入到交易池中。
资金流:清算者将用于购买的债务资产转移到atoken,atoken将抵押物或者抵押物对应的atoken转移到清算者,销毁被清算者的一部分债务代币。
flash loan
场景:闪电贷接口,用户可以一次贷出多个资产,并且在还款时可以选择用债务的方式偿还闪电贷。
函数原型:
function flashLoan(
address receiverAddress,
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata modes,
address onBehalfOf,
bytes calldata params,
uint16 referralCode
)
参数说明:
- receiverAddress:接收闪电贷贷款的地址,需要为合约执行receive逻辑。
- assets:用户选择借贷的一系列资产地址。
- amounts:对应的资产数额
- modes:对应的还款逻辑,0为正常闪电贷还款,1为稳定利率模式,2为可变利率模式
- onBehalfOf:如果选择贷款还款,承担债务方。
- params:传递给闪电贷接受函数的参数
- referralCode:用于标记操作源,0表示没有中间人。
函数逻辑:
- 检查传入参数数组的大小是否对其
- 计算各个资产的手续费,转款给receiver
- 判断闪电贷还款是否成功
- 如果模式为0,没有还完款直接revert
- 如果模式为1或者2,则执行前面提到的抵押贷款逻辑,债务由onBehalfOf承担。
资金流:由各个资产对应的atoken传入给receiver,再由receiver的还款逻辑,如果正常执行完闪电贷,则直接从receiver转到对应的atoken;如果选择抵押借贷还款,则部分资金可能由receiver转到对应的atoken,其余的欠款以生成对应的债务代币的方式由onBehalfOf承担。
代码分析
v0
ETHLend立项于2017年,是一个构设在以太坊上的Dapp。ETHLend旨在通过取消大型金融机构和传统银行的控制权和权力,使复杂的贷款程序民主化,使贷款人和借款人能够在不需要中间人的情况下决定贷款的重要细节。这意味着全世界的买方和贷方都可以根据自己的条款创建贷款合约,实现区块链上的点对点借贷。
借款人在ETHLend平台上创建贷款请求。对于提出贷款申请,借款人应设置数据,如贷款的持续时间,抵押资产和利息。
如果贷方同意这些条款,则可以创建贷款协议。到期限后,如果借款方没有偿还所有的债务以及利息,那么贷方就会将抵押资产收走。
ETHLend是AAVE的雏形,这个项目现在已经被废弃,这里不做过多讨论。
v1
实现功能
v1版本的出现使得之前的ETHLend项目从一个去中心化的P2P借贷平台转换成了一个基于交易池的平台,用户可以通过交易池里存储虚拟货币的方式提供流动资产,同时其他用户也可以进行抵押贷款(永久时限)或者闪电贷。
整体架构
项目组成:
├── configuration
│ ├── AddressStorage.sol
│ ├── LendingPoolAddressesProvider.sol
│ ├── LendingPoolParametersProvider.sol
│ └── UintStorage.sol
├── fees
│ ├── FeeProvider.sol
│ └── TokenDistributor.sol
├── flashloan
│ ├── base
│ │ └── FlashLoanReceiverBase.sol
│ └── interfaces
│ └── IFlashLoanReceiver.sol
├── interfaces
│ ├── IChainlinkAggregator.sol
│ ├── IFeeProvider.sol
│ ├── IKyberNetworkProxyInterface.sol
│ ├── ILendingPoolAddressesProvider.sol
│ ├── ILendingRateOracle.sol
│ ├── IPriceOracle.sol
│ ├── IPriceOracleGetter.sol
│ └── IReserveInterestRateStrategy.sol
├── lendingpool
│ ├── DefaultReserveInterestRateStrategy.sol
│ ├── LendingPool.sol
│ ├── LendingPoolConfigurator.sol
│ ├── LendingPoolCore.sol
│ ├── LendingPoolDataProvider.sol
│ └── LendingPoolLiquidationManager.sol
├── libraries
│ ├── CoreLibrary.sol
│ ├── EthAddressLib.sol
│ ├── WadRayMath.sol
│ └── openzeppelin-upgradeability
│ ├── AdminUpgradeabilityProxy.sol
│ ├── BaseAdminUpgradeabilityProxy.sol
│ ├── BaseUpgradeabilityProxy.sol
│ ├── Initializable.sol
│ ├── InitializableAdminUpgradeabilityProxy.sol
│ ├── InitializableUpgradeabilityProxy.sol
│ ├── Proxy.sol
│ ├── UpgradeabilityProxy.sol
│ └── VersionedInitializable.sol
├── misc
│ ├── ChainlinkProxyPriceProvider.sol
│ ├── IERC20DetailedBytes.sol
│ └── WalletBalanceProvider.sol
├── mocks
│ ├── flashloan
│ │ └── MockFlashLoanReceiver.sol
│ ├── oracle
│ │ ├── CLAggregators
│ │ │ ├── MockAggregatorBAT.sol
│ │ │ ├── MockAggregatorBase.sol
│ │ │ ├── MockAggregatorDAI.sol
│ │ │ ├── MockAggregatorKNC.sol
│ │ │ ├── MockAggregatorLEND.sol
│ │ │ ├── MockAggregatorLINK.sol
│ │ │ ├── MockAggregatorMANA.sol
│ │ │ ├── MockAggregatorMKR.sol
│ │ │ ├── MockAggregatorREP.sol
│ │ │ ├── MockAggregatorSUSD.sol
│ │ │ ├── MockAggregatorTUSD.sol
│ │ │ ├── MockAggregatorUSDC.sol
│ │ │ ├── MockAggregatorUSDT.sol
│ │ │ ├── MockAggregatorWBTC.sol
│ │ │ └── MockAggregatorZRX.sol
│ │ ├── GenericOracleI.sol
│ │ ├── LendingRateOracle.sol
│ │ └── PriceOracle.sol
│ ├── tokens
│ │ ├── MintableERC20.sol
│ │ ├── MockBAT.sol
│ │ ├── MockDAI.sol
│ │ ├── MockKNC.sol
│ │ ├── MockLEND.sol
│ │ ├── MockLINK.sol
│ │ ├── MockMANA.sol
│ │ ├── MockMKR.sol
│ │ ├── MockREP.sol
│ │ ├── MockSUSD.sol
│ │ ├── MockTUSD.sol
│ │ ├── MockUSDC.sol
│ │ ├── MockUSDT.sol
│ │ ├── MockWBTC.sol
│ │ └── MockZRX.sol
│ └── upgradeability
│ └── MockLendingPoolCore.sol
└── tokenization
└── AToken.sol
核心合约
lendingpool
lendingpool目录下的合约是整个项目中最核心关键的部分。
DefaultReserveInterestRateStrategy.sol
用于计算和更新特定储备(reserve)的利率,每一个特定的储备都有一个特定的DefaultReserveInterestRateStrategy合约来为其服务。在这一合约中,定义了如下变量:
- 基础可变利率R0
- 低于最佳利用率的斜率Rslope1
- 高于最佳利用的斜率Rslope2
利率计算公式如下:
对于浮动利率和稳定利率,其计算公式是一样的,只不过是参数不同。
从代码中可以看出,公示中的利用率u其实就是储备(reserve)中的借贷总额 /(总流动资产+借贷总额),当这一比率大于或小于等于预先设定的阈值(optimal utilization)时(在这里这一比值被设定为80%),会得出不一样的利率,具体参考上面的公式。
而公式的结果R和U是正相关,也就是说资金利用率越高,利率越高;利用率越低则利率越低。
首先会从预言机获取市场平均的利率currentStableBorrowRate,以此作为稳定利率(stable rate)的Rv0。而可变利率的Rv0则是在合约内部初始化时设置。
根据当前利用率U与最佳利用率的大小来通过不同的公式来计算可变利率与稳定利率的数值(见上面公式)。
而后计算总借贷利率((可变利率贷款总额*可变利率+稳定利率贷款总额*稳定利率)/总贷款额),得到最后的流动性挖矿利率(总贷款利率*总利用率)。
最后将结果返回。
代码实现:
function calculateInterestRates(
address _reserve,
uint256 _availableLiquidity,
uint256 _totalBorrowsStable,
uint256 _totalBorrowsVariable,
uint256 _averageStableBorrowRate
)
external
view
returns (
uint256 currentLiquidityRate,
uint256 currentStableBorrowRate,
uint256 currentVariableBorrowRate
)
{
uint256 totalBorrows = _totalBorrowsStable.add(_totalBorrowsVariable);
uint256 utilizationRate = (totalBorrows == 0 && _availableLiquidity == 0)
? 0
: totalBorrows.rayDiv(_availableLiquidity.add(totalBorrows));
currentStableBorrowRate = ILendingRateOracle(addressesProvider.getLendingRateOracle())
.getMarketBorrowRate(_reserve);
if (utilizationRate > OPTIMAL_UTILIZATION_RATE) {
uint256 excessUtilizationRateRatio = utilizationRate
.sub(OPTIMAL_UTILIZATION_RATE)
.rayDiv(EXCESS_UTILIZATION_RATE);
currentStableBorrowRate = currentStableBorrowRate.add(stableRateSlope1).add(
stableRateSlope2.rayMul(excessUtilizationRateRatio)
);
currentVariableBorrowRate = baseVariableBorrowRate.add(variableRateSlope1).add(
variableRateSlope2.rayMul(excessUtilizationRateRatio)
);
} else {
currentStableBorrowRate = currentStableBorrowRate.add(
stableRateSlope1.rayMul(
utilizationRate.rayDiv(
OPTIMAL_UTILIZATION_RATE
)
)
);
currentVariableBorrowRate = baseVariableBorrowRate.add(
utilizationRate.rayDiv(OPTIMAL_UTILIZATION_RATE).rayMul(variableRateSlope1)
);
}
currentLiquidityRate = getOverallBorrowRateInternal(
_totalBorrowsStable,
_totalBorrowsVariable,
currentVariableBorrowRate,
_averageStableBorrowRate
)
.rayMul(utilizationRate);
}
function getOverallBorrowRateInternal(
uint256 _totalBorrowsStable,
uint256 _totalBorrowsVariable,
uint256 _currentVariableBorrowRate,
uint256 _currentAverageStableBorrowRate
) internal pure returns (uint256) {
uint256 totalBorrows = _totalBorrowsStable.add(_totalBorrowsVariable);
if (totalBorrows == 0) return 0;
uint256 weightedVariableRate = _totalBorrowsVariable.wadToRay().rayMul(
_currentVariableBorrowRate
);
uint256 weightedStableRate = _totalBorrowsStable.wadToRay().rayMul(
_currentAverageStableBorrowRate
);
uint256 overallBorrowRate = weightedVariableRate.add(weightedStableRate).rayDiv(
totalBorrows.wadToRay()
);
return overallBorrowRate;
}
LendingPool.sol
LendingPool合约是整个V1版本用户与协议交互的接口,通过与core合约交互实现了一整套用户借贷、流动性挖矿的逻辑。
用户通过deposit函数向协议存款时,可以选择ERC20token资产或者ETH,不可以两者混用,如果是ETH的话那么reserve地址就是0xEEEE…EEEE。主要逻辑就是调用core合约的更新储备状态函数,而后mint出一笔新的atoken作为用户的资产凭证,随后通过core合约向储备地址转款。其中referralCode只在最后的emit事件时使用,用于判断用户是否会接受奖励。
/**
* @dev deposits The underlying asset into the reserve. A corresponding amount of the overlying asset (aTokens)
* is minted.
* @param _reserve the address of the reserve
* @param _amount the amount to be deposited
* @param _referralCode integrators are assigned a referral code and can potentially receive rewards.
**/
function deposit(address _reserve, uint256 _amount, uint16 _referralCode)
external
payable
nonReentrant
onlyActiveReserve(_reserve)
onlyUnfreezedReserve(_reserve)
onlyAmountGreaterThanZero(_amount)
{
AToken aToken = AToken(core.getReserveATokenAddress(_reserve));
bool isFirstDeposit = aToken.balanceOf(msg.sender) == 0;
core.updateStateOnDeposit(_reserve, msg.sender, _amount, isFirstDeposit);
//minting AToken to user 1:1 with the specific exchange rate
aToken.mintOnDeposit(msg.sender, _amount);
//transfer to the core contract
core.transferToReserve.value(msg.value)(_reserve, msg.sender, _amount);
//solium-disable-next-line
emit Deposit(_reserve, msg.sender, _amount, _referralCode, block.timestamp);
}
用户可以通过redeemUnderlying来赎回标的资产,首先会判断储备中是否有足够的流动资金来满足用户的赎回操作,而后通过core合约的updateStateOnRedeem和transferToUser来完成更新储备信息和转款操作。
/**
* @dev Redeems the underlying amount of assets requested by _user.
* This function is executed by the overlying aToken contract in response to a redeem action.
* @param _reserve the address of the reserve
* @param _user the address of the user performing the action
* @param _amount the underlying amount to be redeemed
**/
function redeemUnderlying(
address _reserve,
address payable _user,
uint256 _amount,
uint256 _aTokenBalanceAfterRedeem
)
external
nonReentrant
onlyOverlyingAToken(_reserve)
onlyActiveReserve(_reserve)
onlyAmountGreaterThanZero(_amount)
{
uint256 currentAvailableLiquidity = core.getReserveAvailableLiquidity(_reserve);
require(
currentAvailableLiquidity >= _amount,
"There is not enough liquidity available to redeem"
);
core.updateStateOnRedeem(_reserve, _user, _amount, _aTokenBalanceAfterRedeem == 0);
core.transferToUser(_reserve, _user, _amount);
//solium-disable-next-line
emit RedeemUnderlying(_reserve, _user, _amount, block.timestamp);
}
对于借款的逻辑,为了缩减局部变量,使用了一个结构体来当作局部的用户全局信息。首先会检查用户借贷的目标储备是否允许借贷,确定借款利率模式以及有没有足够的储备金。随后计算当前发起借款用户的抵押资金、借贷总额、总手续费、当前LTV、清算阈值以及健康因子。当上述条件都满足后会计算当前借款的必须抵押资金,只有抵押金额大于借款额后才会调用core合约执行转账逻辑,将借出款从储备中转给用户。
/**
* @dev data structures for local computations in the borrow() method.
*/
struct BorrowLocalVars {
uint256 principalBorrowBalance;
uint256 currentLtv;
uint256 currentLiquidationThreshold;
uint256 borrowFee;
uint256 requestedBorrowAmountETH;
uint256 amountOfCollateralNeededETH;
uint256 userCollateralBalanceETH;
uint256 userBorrowBalanceETH;
uint256 userTotalFeesETH;
uint256 borrowBalanceIncrease;
uint256 currentReserveStableRate;
uint256 availableLiquidity;
uint256 reserveDecimals;
uint256 finalUserBorrowRate;
CoreLibrary.InterestRateMode rateMode;
bool healthFactorBelowThreshold;
}
/**
* @dev Allows users to borrow a specific amount of the reserve currency, provided that the borrower
* already deposited enough collateral.
* @param _reserve the address of the reserve
* @param _amount the amount to be borrowed
* @param _interestRateMode the interest rate mode at which the user wants to borrow. Can be 0 (STABLE) or 1 (VARIABLE)
**/
function borrow(
address _reserve,
uint256 _amount,
uint256 _interestRateMode,
uint16 _referralCode
)
external
nonReentrant
onlyActiveReserve(_reserve)
onlyUnfreezedReserve(_reserve)
onlyAmountGreaterThanZero(_amount)
{
// Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables
BorrowLocalVars memory vars;
//check that the reserve is enabled for borrowing
require(core.isReserveBorrowingEnabled(_reserve), "Reserve is not enabled for borrowing");
//validate interest rate mode
require(
uint256(CoreLibrary.InterestRateMode.VARIABLE) == _interestRateMode ||
uint256(CoreLibrary.InterestRateMode.STABLE) == _interestRateMode,
"Invalid interest rate mode selected"
);
//cast the rateMode to coreLibrary.interestRateMode
vars.rateMode = CoreLibrary.InterestRateMode(_interestRateMode);
//check that the amount is available in the reserve
vars.availableLiquidity = core.getReserveAvailableLiquidity(_reserve);
require(
vars.availableLiquidity >= _amount,
"There is not enough liquidity available in the reserve"
);
(
,
vars.userCollateralBalanceETH,
vars.userBorrowBalanceETH,
vars.userTotalFeesETH,
vars.currentLtv,
vars.currentLiquidationThreshold,
,
vars.healthFactorBelowThreshold
) = dataProvider.calculateUserGlobalData(msg.sender);
require(vars.userCollateralBalanceETH > 0, "The collateral balance is 0");
require(
!vars.healthFactorBelowThreshold,
"The borrower can already be liquidated so he cannot borrow more"
);
//calculating fees
vars.borrowFee = feeProvider.calculateLoanOriginationFee(msg.sender, _amount);
require(vars.borrowFee > 0, "The amount to borrow is too small");
vars.amountOfCollateralNeededETH = dataProvider.calculateCollateralNeededInETH(
_reserve,
_amount,
vars.borrowFee,
vars.userBorrowBalanceETH,
vars.userTotalFeesETH,
vars.currentLtv
);
require(
vars.amountOfCollateralNeededETH <= vars.userCollateralBalanceETH,
"There is not enough collateral to cover a new borrow"
);
/**
* Following conditions need to be met if the user is borrowing at a stable rate:
* 1. Reserve must be enabled for stable rate borrowing
* 2. Users cannot borrow from the reserve if their collateral is (mostly) the same currency
* they are borrowing, to prevent abuses.
* 3. Users will be able to borrow only a relatively small, configurable amount of the total
* liquidity
**/
if (vars.rateMode == CoreLibrary.InterestRateMode.STABLE) {
//check if the borrow mode is stable and if stable rate borrowing is enabled on this reserve
require(
core.isUserAllowedToBorrowAtStable(_reserve, msg.sender, _amount),
"User cannot borrow the selected amount with a stable rate"
);
//calculate the max available loan size in stable rate mode as a percentage of the
//available liquidity
uint256 maxLoanPercent = parametersProvider.getMaxStableRateBorrowSizePercent();
uint256 maxLoanSizeStable = vars.availableLiquidity.mul(maxLoanPercent).div(100);
require(
_amount <= maxLoanSizeStable,
"User is trying to borrow too much liquidity at a stable rate"
);
}
//all conditions passed - borrow is accepted
(vars.finalUserBorrowRate, vars.borrowBalanceIncrease) = core.updateStateOnBorrow(
_reserve,
msg.sender,
_amount,
vars.borrowFee,
vars.rateMode
);
//if we reached this point, we can transfer
core.transferToUser(_reserve, msg.sender, _amount);
emit Borrow(
_reserve,
msg.sender,
_amount,
_interestRateMode,
vars.finalUserBorrowRate,
vars.borrowFee,
vars.borrowBalanceIncrease,
_referralCode,
//solium-disable-next-line
block.timestamp
);
}
用户可以通过repay函数来偿还贷款,不过没有金额的限制,还款金额为-1时代表全部还款。
首先计算用户当前欠款额、利息、办理手续费等信息,而后计算出用户的所需还款具体数目(本金+利息+手续费)。如果用户的还款额度小于手续费数目,那么协议优先考虑将用户的还款转入手续费收集地址;如果用户还款的额度大于没有还清的手续费,那么一样的优先偿还手续费。剩下的金额才会转入用户借款储备地址。
/**
* @notice repays a borrow on the specific reserve, for the specified amount (or for the whole amount, if uint256(-1) is specified).
* @dev the target user is defined by _onBehalfOf. If there is no repayment on behalf of another account,
* _onBehalfOf must be equal to msg.sender.
* @param _reserve the address of the reserve on which the user borrowed
* @param _amount the amount to repay, or uint256(-1) if the user wants to repay everything
* @param _onBehalfOf the address for which msg.sender is repaying.
**/
struct RepayLocalVars {
uint256 principalBorrowBalance;
uint256 compoundedBorrowBalance;
uint256 borrowBalanceIncrease;
bool isETH;
uint256 paybackAmount;
uint256 paybackAmountMinusFees;
uint256 currentStableRate;
uint256 originationFee;
}
function repay(address _reserve, uint256 _amount, address payable _onBehalfOf)
external
payable
nonReentrant
onlyActiveReserve(_reserve)
onlyAmountGreaterThanZero(_amount)
{
// Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables
RepayLocalVars memory vars;
(
vars.principalBorrowBalance,
vars.compoundedBorrowBalance,
vars.borrowBalanceIncrease
) = core.getUserBorrowBalances(_reserve, _onBehalfOf);
vars.originationFee = core.getUserOriginationFee(_reserve, _onBehalfOf);
vars.isETH = EthAddressLib.ethAddress() == _reserve;
require(vars.compoundedBorrowBalance > 0, "The user does not have any borrow pending");
require(
_amount != UINT_MAX_VALUE || msg.sender == _onBehalfOf,
"To repay on behalf of an user an explicit amount to repay is needed."
);
//default to max amount
vars.paybackAmount = vars.compoundedBorrowBalance.add(vars.originationFee);
if (_amount != UINT_MAX_VALUE && _amount < vars.paybackAmount) {
vars.paybackAmount = _amount;
}
require(
!vars.isETH || msg.value >= vars.paybackAmount,
"Invalid msg.value sent for the repayment"
);
//if the amount is smaller than the origination fee, just transfer the amount to the fee destination address
if (vars.paybackAmount <= vars.originationFee) {
core.updateStateOnRepay(
_reserve,
_onBehalfOf,
0,
vars.paybackAmount,
vars.borrowBalanceIncrease,
false
);
core.transferToFeeCollectionAddress.value(vars.isETH ? vars.paybackAmount : 0)(
_reserve,
_onBehalfOf,
vars.paybackAmount,
addressesProvider.getTokenDistributor()
);
emit Repay(
_reserve,
_onBehalfOf,
msg.sender,
0,
vars.paybackAmount,
vars.borrowBalanceIncrease,
//solium-disable-next-line
block.timestamp
);
return;
}
vars.paybackAmountMinusFees = vars.paybackAmount.sub(vars.originationFee);
core.updateStateOnRepay(
_reserve,
_onBehalfOf,
vars.paybackAmountMinusFees,
vars.originationFee,
vars.borrowBalanceIncrease,
vars.compoundedBorrowBalance == vars.paybackAmountMinusFees
);
//if the user didn't repay the origination fee, transfer the fee to the fee collection address
if(vars.originationFee > 0) {
core.transferToFeeCollectionAddress.value(vars.isETH ? vars.originationFee : 0)(
_reserve,
msg.sender,
vars.originationFee,
addressesProvider.getTokenDistributor()
);
}
//sending the total msg.value if the transfer is ETH.
//the transferToReserve() function will take care of sending the
//excess ETH back to the caller
core.transferToReserve.value(vars.isETH ? msg.value.sub(vars.originationFee) : 0)(
_reserve,
msg.sender,
vars.paybackAmountMinusFees
);
emit Repay(
_reserve,
_onBehalfOf,
msg.sender,
vars.paybackAmountMinusFees,
vars.originationFee,
vars.borrowBalanceIncrease,
//solium-disable-next-line
block.timestamp
);
}
swapBorrowRate用于更换贷款的利率模式,主要的逻辑就是做一些参数检查然后调用core合约的updateStateOnSwapRate来完成,详见core合约分析小节。
/**
* @dev borrowers can user this function to swap between stable and variable borrow rate modes.
* @param _reserve the address of the reserve on which the user borrowed
**/
function swapBorrowRateMode(address _reserve)
external
nonReentrant
onlyActiveReserve(_reserve)
onlyUnfreezedReserve(_reserve)
{
(uint256 principalBorrowBalance, uint256 compoundedBorrowBalance, uint256 borrowBalanceIncrease) = core
.getUserBorrowBalances(_reserve, msg.sender);
require(
compoundedBorrowBalance > 0,
"User does not have a borrow in progress on this reserve"
);
CoreLibrary.InterestRateMode currentRateMode = core.getUserCurrentBorrowRateMode(
_reserve,
msg.sender
);
if (currentRateMode == CoreLibrary.InterestRateMode.VARIABLE) {
/**
* user wants to swap to stable, before swapping we need to ensure that
* 1. stable borrow rate is enabled on the reserve
* 2. user is not trying to abuse the reserve by depositing
* more collateral than he is borrowing, artificially lowering
* the interest rate, borrowing at variable, and switching to stable
**/
require(
core.isUserAllowedToBorrowAtStable(_reserve, msg.sender, compoundedBorrowBalance),
"User cannot borrow the selected amount at stable"
);
}
(CoreLibrary.InterestRateMode newRateMode, uint256 newBorrowRate) = core
.updateStateOnSwapRate(
_reserve,
msg.sender,
principalBorrowBalance,
compoundedBorrowBalance,
borrowBalanceIncrease,
currentRateMode
);
emit Swap(
_reserve,
msg.sender,
uint256(newRateMode),
newBorrowRate,
borrowBalanceIncrease,
//solium-disable-next-line
block.timestamp
);
}
当用户的借款是稳定利率模式时,可以通过rebalance函数来调整利率数值。有两种情形需要进行rebalance,第一种情况是储备资产的流动比率高于稳定利率,那么此时这一笔贷款就需要重新调整利率。因为在这种情况下用户可以通过将借出的流动性资产放回储备中来进行赚取差额(换句话说就是流动性挖矿的产出比利息高)。第二种情况是用户的稳定利率高于市场平均利率,而且用户的使用率(贷款和抵押的比值)较低,避免用户付出过多的利息。
首先会确定用户的贷款是否符合rebalance的条件。然后会根据上述两种情况判断是否符合,复合的话就进行稳定利率的调整。
/**
* @dev rebalances the stable interest rate of a user if current liquidity rate > user stable rate.
* this is regulated by Aave to ensure that the protocol is not abused, and the user is paying a fair
* rate. Anyone can call this function though.
* @param _reserve the address of the reserve
* @param _user the address of the user to be rebalanced
**/
function rebalanceStableBorrowRate(address _reserve, address _user)
external
nonReentrant
onlyActiveReserve(_reserve)
{
(, uint256 compoundedBalance, uint256 borrowBalanceIncrease) = core.getUserBorrowBalances(
_reserve,
_user
);
//step 1: user must be borrowing on _reserve at a stable rate
require(compoundedBalance > 0, "User does not have any borrow for this reserve");
require(
core.getUserCurrentBorrowRateMode(_reserve, _user) ==
CoreLibrary.InterestRateMode.STABLE,
"The user borrow is variable and cannot be rebalanced"
);
uint256 userCurrentStableRate = core.getUserCurrentStableBorrowRate(_reserve, _user);
uint256 liquidityRate = core.getReserveCurrentLiquidityRate(_reserve);
uint256 reserveCurrentStableRate = core.getReserveCurrentStableBorrowRate(_reserve);
uint256 rebalanceDownRateThreshold = reserveCurrentStableRate.rayMul(
WadRayMath.ray().add(parametersProvider.getRebalanceDownRateDelta())
);
//step 2: we have two possible situations to rebalance:
//1. user stable borrow rate is below the current liquidity rate. The loan needs to be rebalanced,
//as this situation can be abused (user putting back the borrowed liquidity in the same reserve to earn on it)
//2. user stable rate is above the market avg borrow rate of a certain delta, and utilization rate is low.
//In this case, the user is paying an interest that is too high, and needs to be rescaled down.
if (
userCurrentStableRate < liquidityRate ||
userCurrentStableRate > rebalanceDownRateThreshold
) {
uint256 newStableRate = core.updateStateOnRebalance(
_reserve,
_user,
borrowBalanceIncrease
);
emit RebalanceStableBorrowRate(
_reserve,
_user,
newStableRate,
borrowBalanceIncrease,
//solium-disable-next-line
block.timestamp
);
return;
}
revert("Interest rate rebalance conditions were not met");
}
setUserUseReserveAsCollateral用于将用户的储备转换成抵押资产。核心逻辑就是调用core合约的setUserUseReserveAsCollateral函数,详见core合约小节。
/**
* @dev allows depositors to enable or disable a specific deposit as collateral.
* @param _reserve the address of the reserve
* @param _useAsCollateral true if the user wants to user the deposit as collateral, false otherwise.
**/
function setUserUseReserveAsCollateral(address _reserve, bool _useAsCollateral)
external
nonReentrant
onlyActiveReserve(_reserve)
onlyUnfreezedReserve(_reserve)
{
uint256 underlyingBalance = core.getUserUnderlyingAssetBalance(_reserve, msg.sender);
require(underlyingBalance > 0, "User does not have any liquidity deposited");
require(
dataProvider.balanceDecreaseAllowed(_reserve, msg.sender, underlyingBalance),
"User deposit is already being used as collateral"
);
core.setUserUseReserveAsCollateral(_reserve, msg.sender, _useAsCollateral);
if (_useAsCollateral) {
emit ReserveUsedAsCollateralEnabled(_reserve, msg.sender);
} else {
emit ReserveUsedAsCollateralDisabled(_reserve, msg.sender);
}
}
对于资不抵债需要清算的抵押资产,用户可以通过调用liquidationCall来进行资产清算,也就是购买清算资产,同时也会得到一些奖励。具体的逻辑是在liquidationManager中,详见liquidationManager小节。
/**
* @dev users can invoke this function to liquidate an undercollateralized position.
* @param _reserve the address of the collateral to liquidated
* @param _reserve the address of the principal reserve
* @param _user the address of the borrower
* @param _purchaseAmount the amount of principal that the liquidator wants to repay
* @param _receiveAToken true if the liquidators wants to receive the aTokens, false if
* he wants to receive the underlying asset directly
**/
function liquidationCall(
address _collateral,
address _reserve,
address _user,
uint256 _purchaseAmount,
bool _receiveAToken
) external payable nonReentrant onlyActiveReserve(_reserve) onlyActiveReserve(_collateral) {
address liquidationManager = addressesProvider.getLendingPoolLiquidationManager();
//solium-disable-next-line
(bool success, bytes memory result) = liquidationManager.delegatecall(
abi.encodeWithSignature(
"liquidationCall(address,address,address,uint256,bool)",
_collateral,
_reserve,
_user,
_purchaseAmount,
_receiveAToken
)
);
require(success, "Liquidation call failed");
(uint256 returnCode, string memory returnMessage) = abi.decode(result, (uint256, string));
if (returnCode != 0) {
//error found
revert(string(abi.encodePacked("Liquidation failed: ", returnMessage)));
}
}
闪电贷的话就没啥可说的了,每个项目写法都差不多,不同的是在最后会更新一些储备的状态。v1版本的实现只是一个最基本的功能,由于有了重入的限制,所以闪电贷的逻辑不能涉及到aave内部的逻辑:
/**
* @dev allows smartcontracts to access the liquidity of the pool within one transaction,
* as long as the amount taken plus a fee is returned. NOTE There are security concerns for developers of flashloan receiver contracts
* that must be kept into consideration. For further details please visit https://developers.aave.com
* @param _receiver The address of the contract receiving the funds. The receiver should implement the IFlashLoanReceiver interface.
* @param _reserve the address of the principal reserve
* @param _amount the amount requested for this flashloan
**/
function flashLoan(address _receiver, address _reserve, uint256 _amount, bytes memory _params)
public
nonReentrant
onlyActiveReserve(_reserve)
onlyAmountGreaterThanZero(_amount)
{
//check that the reserve has enough available liquidity
//we avoid using the getAvailableLiquidity() function in LendingPoolCore to save gas
uint256 availableLiquidityBefore = _reserve == EthAddressLib.ethAddress()
? address(core).balance
: IERC20(_reserve).balanceOf(address(core));
require(
availableLiquidityBefore >= _amount,
"There is not enough liquidity available to borrow"
);
(uint256 totalFeeBips, uint256 protocolFeeBips) = parametersProvider
.getFlashLoanFeesInBips();
//calculate amount fee
uint256 amountFee = _amount.mul(totalFeeBips).div(10000);
//protocol fee is the part of the amountFee reserved for the protocol - the rest goes to depositors
uint256 protocolFee = amountFee.mul(protocolFeeBips).div(10000);
require(
amountFee > 0 && protocolFee > 0,
"The requested amount is too small for a flashLoan."
);
//get the FlashLoanReceiver instance
IFlashLoanReceiver receiver = IFlashLoanReceiver(_receiver);
address payable userPayable = address(uint160(_receiver));
//transfer funds to the receiver
core.transferToUser(_reserve, userPayable, _amount);
//execute action of the receiver
receiver.executeOperation(_reserve, _amount, amountFee, _params);
//check that the actual balance of the core contract includes the returned amount
uint256 availableLiquidityAfter = _reserve == EthAddressLib.ethAddress()
? address(core).balance
: IERC20(_reserve).balanceOf(address(core));
require(
availableLiquidityAfter == availableLiquidityBefore.add(amountFee),
"The actual balance of the protocol is inconsistent"
);
core.updateStateOnFlashLoan(
_reserve,
availableLiquidityBefore,
amountFee.sub(protocolFee),
protocolFee
);
//solium-disable-next-line
emit FlashLoan(_receiver, _reserve, _amount, amountFee, protocolFee, block.timestamp);
}
LendingPoolConfigurator.sol
用于执行LendingPoolCore合约的配置逻辑,可以启用和停用储备,以及设置不同的协议参数。
initReserve用于初始化储备其主要的逻辑是创建一个新的atoken,然后调用LendingPoolCore合约中initReserve,
/**
* @dev initializes a reserve
* @param _reserve the address of the reserve to be initialized
* @param _underlyingAssetDecimals the decimals of the reserve underlying asset
* @param _interestRateStrategyAddress the address of the interest rate strategy contract for this reserve
**/
function initReserve(
address _reserve,
uint8 _underlyingAssetDecimals,
address _interestRateStrategyAddress
) external onlyLendingPoolManager {
ERC20Detailed asset = ERC20Detailed(_reserve);
string memory aTokenName = string(abi.encodePacked("Aave Interest bearing ", asset.name()));
string memory aTokenSymbol = string(abi.encodePacked("a", asset.symbol()));
initReserveWithData(
_reserve,
aTokenName,
aTokenSymbol,
_underlyingAssetDecimals,
_interestRateStrategyAddress
);
}
/**
* @dev initializes a reserve using aTokenData provided externally (useful if the underlying ERC20 contract doesn't expose name or decimals)
* @param _reserve the address of the reserve to be initialized
* @param _aTokenName the name of the aToken contract
* @param _aTokenSymbol the symbol of the aToken contract
* @param _underlyingAssetDecimals the decimals of the reserve underlying asset
* @param _interestRateStrategyAddress the address of the interest rate strategy contract for this reserve
**/
function initReserveWithData(
address _reserve,
string memory _aTokenName,
string memory _aTokenSymbol,
uint8 _underlyingAssetDecimals,
address _interestRateStrategyAddress
) public onlyLendingPoolManager {
LendingPoolCore core = LendingPoolCore(poolAddressesProvider.getLendingPoolCore());
AToken aTokenInstance = new AToken(
poolAddressesProvider,
_reserve,
_underlyingAssetDecimals,
_aTokenName,
_aTokenSymbol
);
core.initReserve(
_reserve,
address(aTokenInstance),
_underlyingAssetDecimals,
_interestRateStrategyAddress
);
emit ReserveInitialized(
_reserve,
address(aTokenInstance),
_interestRateStrategyAddress
);
}
其他合约参数的逻辑大同小异,都是通过调用core合约中的配置函数来完成设置,这里就不逐个分析了。
LendingPoolCore.sol
字如其名,为整个项目核心,他维护了所有储备和存储资产的状态信息,并且处理一些核心逻辑。其实在代码中体现的维护的逻辑就是记录和更新储备金(reserve)和用户储备数据(userReservedata)两个结构体。具体定义见corelib。
由于代码量过大这里只对于发生写入的函数做详细分析。
core合约利用字典存储了整个项目的储备地址以及用户的储备地址,以及一个外部查询的储备地址动态数组,如下所示:
mapping(address => CoreLibrary.ReserveData) internal reserves;
mapping(address => mapping(address => CoreLibrary.UserReserveData)) internal usersReserveData;
address[] public reservesList;
uint256 public constant CORE_REVISION = 0x6;
updateReserveInterestRatesAndTimestampInternal函数用于计算当前稳定利率和可变利率,并更新时间戳。
function updateReserveInterestRatesAndTimestampInternal(
address _reserve,
uint256 _liquidityAdded,
uint256 _liquidityTaken
) internal {
CoreLibrary.ReserveData storage reserve = reserves[_reserve];
(uint256 newLiquidityRate, uint256 newStableRate, uint256 newVariableRate) = IReserveInterestRateStrategy(
reserve
.interestRateStrategyAddress
)
.calculateInterestRates(
_reserve,
getReserveAvailableLiquidity(_reserve).add(_liquidityAdded).sub(_liquidityTaken),
reserve.totalBorrowsStable,
reserve.totalBorrowsVariable,
reserve.currentAverageStableBorrowRate
);
reserve.currentLiquidityRate = newLiquidityRate;
reserve.currentStableBorrowRate = newStableRate;
reserve.currentVariableBorrowRate = newVariableRate;
//solium-disable-next-line
reserve.lastUpdateTimestamp = uint40(block.timestamp);
emit ReserveUpdated(
_reserve,
newLiquidityRate,
newStableRate,
newVariableRate,
reserve.lastLiquidityCumulativeIndex,
reserve.lastVariableBorrowCumulativeIndex
);
}
transferFlashLoanProtocolFeeInternal用于将闪电贷的协议手续费转给收集地址(addressesProvider.getTokenDistributor())。
function transferFlashLoanProtocolFeeInternal(address _token, uint256 _amount) internal {
address payable receiver = address(uint160(addressesProvider.getTokenDistributor()));
if (_token != EthAddressLib.ethAddress()) {
ERC20(_token).safeTransfer(receiver, _amount);
} else {
//solium-disable-next-line
(bool result, ) = receiver.call.value(_amount)("");
require(result, "Transfer to token distributor failed");
}
}
在core合约创建时,address provider会调用initialize函数,记录lending pool和provider地址信息:
/**
* @dev initializes the Core contract, invoked upon registration on the AddressesProvider
* @param _addressesProvider the addressesProvider contract
**/
function initialize(LendingPoolAddressesProvider _addressesProvider) public initializer {
addressesProvider = _addressesProvider;
refreshConfigInternal();
}
/**
* @dev updates the internal configuration of the core
**/
function refreshConfigInternal() internal {
lendingPoolAddress = addressesProvider.getLendingPool();
}
当用户通过lendingpool合约调用deposit时,lendingpool合约会调用core合约的updateStateOnDeposit函数,更新状态信息,具体分为三步:更新资产累加系数(见coreLiabrary),更新利率以及时间戳,默认存入的资产为流动资产(liquidity),如果是第一次存储,那么这个资产则会被默认当作抵押资产(collateral)。
/**
* @dev updates the state of the core as a result of a deposit action
* @param _reserve the address of the reserve in which the deposit is happening
* @param _user the address of the the user depositing
* @param _amount the amount being deposited
* @param _isFirstDeposit true if the user is depositing for the first time
**/
function updateStateOnDeposit(
address _reserve,
address _user,
uint256 _amount,
bool _isFirstDeposit
) external onlyLendingPool {
reserves[_reserve].updateCumulativeIndexes();
updateReserveInterestRatesAndTimestampInternal(_reserve, _amount, 0);
if (_isFirstDeposit) {
//if this is the first deposit of the user, we configure the deposit as enabled to be used as collateral
setUserUseReserveAsCollateral(_reserve, _user, true);
}
}
当用户通过LendingPool合约赎回自己的抵押资产时,LendingPool合约也会通过调用core合约中的updateStateOnRedeem来更新储备状态,具体做法为:更新资产累加系数(见coreLiabrary),更新利率以及时间戳,由于是赎回资产,所以被认作为去除流动资产,如果用户赎回了所有抵押资产,则将用户抵押资产的储备状态设置为非抵押状态。
/**
* @dev updates the state of the core as a result of a redeem action
* @param _reserve the address of the reserve in which the redeem is happening
* @param _user the address of the the user redeeming
* @param _amountRedeemed the amount being redeemed
* @param _userRedeemedEverything true if the user is redeeming everything
**/
function updateStateOnRedeem(
address _reserve,
address _user,
uint256 _amountRedeemed,
bool _userRedeemedEverything
) external onlyLendingPool {
//compound liquidity and variable borrow interests
reserves[_reserve].updateCumulativeIndexes();
updateReserveInterestRatesAndTimestampInternal(_reserve, 0, _amountRedeemed);
//if user redeemed everything the useReserveAsCollateral flag is reset
if (_userRedeemedEverything) {
setUserUseReserveAsCollateral(_reserve, _user, false);
}
}
同样,当用户通过LendingPool进行闪电贷时,LendingPool合约也会通过调用core合约的updateStateOnFlashLoan函数来更新储备状态,具体做法为:向闪电贷手续费手机合约转入协议费用,更新资产累加系数,记录闪电贷之前的总流动性并通过调用cumulateToLiquidityIndex的方式来更新增加了闪电贷收入后的流动性累加系数,最后更新利率信息。
/**
* @dev updates the state of the core as a result of a flashloan action
* @param _reserve the address of the reserve in which the flashloan is happening
* @param _income the income of the protocol as a result of the action
**/
function updateStateOnFlashLoan(
address _reserve,
uint256 _availableLiquidityBefore,
uint256 _income,
uint256 _protocolFee
) external onlyLendingPool {
transferFlashLoanProtocolFeeInternal(_reserve, _protocolFee);
//compounding the cumulated interest
reserves[_reserve].updateCumulativeIndexes();
uint256 totalLiquidityBefore = _availableLiquidityBefore.add(
getReserveTotalBorrows(_reserve)
);
//compounding the received fee into the reserve
reserves[_reserve].cumulateToLiquidityIndex(totalLiquidityBefore, _income);
//refresh interest rates
updateReserveInterestRatesAndTimestampInternal(_reserve, _income, 0);
}
updateStateOnBorrow用于当用户通过lendingpool合约借贷时,由lendingpool合约调用更新储备信息,具体做法为从借贷用户userReserveData结构体中获得用户的之前的借贷信息,包括借款额和用户的抵押余额,而后根据借贷信息更新用户储备和借贷储备信息,最后更新利率和时间戳,返回新的借贷利率。
/**
* @dev updates the state of the core as a consequence of a borrow action.
* @param _reserve the address of the reserve on which the user is borrowing
* @param _user the address of the borrower
* @param _amountBorrowed the new amount borrowed
* @param _borrowFee the fee on the amount borrowed
* @param _rateMode the borrow rate mode (stable, variable)
* @return the new borrow rate for the user
**/
function updateStateOnBorrow(
address _reserve,
address _user,
uint256 _amountBorrowed,
uint256 _borrowFee,
CoreLibrary.InterestRateMode _rateMode
) external onlyLendingPool returns (uint256, uint256) {
// getting the previous borrow data of the user
(uint256 principalBorrowBalance, , uint256 balanceIncrease) = getUserBorrowBalances(
_reserve,
_user
);
updateReserveStateOnBorrowInternal(
_reserve,
_user,
principalBorrowBalance,
balanceIncrease,
_amountBorrowed,
_rateMode
);
updateUserStateOnBorrowInternal(
_reserve,
_user,
_amountBorrowed,
balanceIncrease,
_borrowFee,
_rateMode
);
updateReserveInterestRatesAndTimestampInternal(_reserve, 0, _amountBorrowed);
return (getUserCurrentBorrowRate(_reserve, _user), balanceIncrease);
}
updateupdateStateOnRepay用于当用户还款时更新储备状态,具体就是调用了updateReserveStateOnRepayInternal和updateUserStateOnRepayInternal函数。
/**
* @dev updates the state of the core as a consequence of a repay action.
* @param _reserve the address of the reserve on which the user is repaying
* @param _user the address of the borrower
* @param _paybackAmountMinusFees the amount being paid back minus fees
* @param _originationFeeRepaid the fee on the amount that is being repaid
* @param _balanceIncrease the accrued interest on the borrowed amount
* @param _repaidWholeLoan true if the user is repaying the whole loan
**/
function updateStateOnRepay(
address _reserve,
address _user,
uint256 _paybackAmountMinusFees,
uint256 _originationFeeRepaid,
uint256 _balanceIncrease,
bool _repaidWholeLoan
) external onlyLendingPool {
updateReserveStateOnRepayInternal(
_reserve,
_user,
_paybackAmountMinusFees,
_balanceIncrease
);
updateUserStateOnRepayInternal(
_reserve,
_user,
_paybackAmountMinusFees,
_originationFeeRepaid,
_balanceIncrease,
_repaidWholeLoan
);
updateReserveInterestRatesAndTimestampInternal(_reserve, _paybackAmountMinusFees, 0);
}
updateReserveStateOnRepayInternal会根据用户的借贷利率模式来分别更新稳定利率信息或者可变利率信息,详见corelib合约。
/**
* @dev updates the state of the reserve as a consequence of a repay action.
* @param _reserve the address of the reserve on which the user is repaying
* @param _user the address of the borrower
* @param _paybackAmountMinusFees the amount being paid back minus fees
* @param _balanceIncrease the accrued interest on the borrowed amount
**/
function updateReserveStateOnRepayInternal(
address _reserve,
address _user,
uint256 _paybackAmountMinusFees,
uint256 _balanceIncrease
) internal {
CoreLibrary.ReserveData storage reserve = reserves[_reserve];
CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve];
CoreLibrary.InterestRateMode borrowRateMode = getUserCurrentBorrowRateMode(_reserve, _user);
//update the indexes
reserves[_reserve].updateCumulativeIndexes();
//compound the cumulated interest to the borrow balance and then subtracting the payback amount
if (borrowRateMode == CoreLibrary.InterestRateMode.STABLE) {
reserve.increaseTotalBorrowsStableAndUpdateAverageRate(
_balanceIncrease,
user.stableBorrowRate
);
reserve.decreaseTotalBorrowsStableAndUpdateAverageRate(
_paybackAmountMinusFees,
user.stableBorrowRate
);
} else {
reserve.increaseTotalBorrowsVariable(_balanceIncrease);
reserve.decreaseTotalBorrowsVariable(_paybackAmountMinusFees);
}
}
updateUserStateOnRepayInternal会更新用户在此储备中的状态,如果用户还清了贷款,那么稳定利率和可变借贷累加系数清零。
/**
* @dev updates the state of the user as a consequence of a repay action.
* @param _reserve the address of the reserve on which the user is repaying
* @param _user the address of the borrower
* @param _paybackAmountMinusFees the amount being paid back minus fees
* @param _originationFeeRepaid the fee on the amount that is being repaid
* @param _balanceIncrease the accrued interest on the borrowed amount
* @param _repaidWholeLoan true if the user is repaying the whole loan
**/
function updateUserStateOnRepayInternal(
address _reserve,
address _user,
uint256 _paybackAmountMinusFees,
uint256 _originationFeeRepaid,
uint256 _balanceIncrease,
bool _repaidWholeLoan
) internal {
CoreLibrary.ReserveData storage reserve = reserves[_reserve];
CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve];
//update the user principal borrow balance, adding the cumulated interest and then subtracting the payback amount
user.principalBorrowBalance = user.principalBorrowBalance.add(_balanceIncrease).sub(
_paybackAmountMinusFees
);
user.lastVariableBorrowCumulativeIndex = reserve.lastVariableBorrowCumulativeIndex;
//if the balance decrease is equal to the previous principal (user is repaying the whole loan)
//and the rate mode is stable, we reset the interest rate mode of the user
if (_repaidWholeLoan) {
user.stableBorrowRate = 0;
user.lastVariableBorrowCumulativeIndex = 0;
}
user.originationFee = user.originationFee.sub(_originationFeeRepaid);
//solium-disable-next-line
user.lastUpdateTimestamp = uint40(block.timestamp);
}
当用户发生转换利率模式时,lendingpool合约会调用updateStateOnSwapRate函数,主体逻辑和repay差不多,都是通过调用两个内部函数的方式来更新储备金状态和用户储备状态,不过这里需要注意的是之前贷款的利息会被加到借款总额中。
/**
* @dev updates the state of the core as a consequence of a swap rate action.
* @param _reserve the address of the reserve on which the user is repaying
* @param _user the address of the borrower
* @param _principalBorrowBalance the amount borrowed by the user
* @param _compoundedBorrowBalance the amount borrowed plus accrued interest
* @param _balanceIncrease the accrued interest on the borrowed amount
* @param _currentRateMode the current interest rate mode for the user
**/
function updateStateOnSwapRate(
address _reserve,
address _user,
uint256 _principalBorrowBalance,
uint256 _compoundedBorrowBalance,
uint256 _balanceIncrease,
CoreLibrary.InterestRateMode _currentRateMode
) external onlyLendingPool returns (CoreLibrary.InterestRateMode, uint256) {
updateReserveStateOnSwapRateInternal(
_reserve,
_user,
_principalBorrowBalance,
_compoundedBorrowBalance,
_currentRateMode
);
CoreLibrary.InterestRateMode newRateMode = updateUserStateOnSwapRateInternal(
_reserve,
_user,
_balanceIncrease,
_currentRateMode
);
updateReserveInterestRatesAndTimestampInternal(_reserve, 0, 0);
return (newRateMode, getUserCurrentBorrowRate(_reserve, _user));
}
updateReserveStateOnSwapRateInternal的核心逻辑就是将远利率状态的参数清零并设置另一个利率模式的参数,具体的,在这里只是简单的将存款从一个利率模式转移到另一个利率模式的储备金中。
/**
* @dev updates the state of the user as a consequence of a swap rate action.
* @param _reserve the address of the reserve on which the user is performing the rate swap
* @param _user the address of the borrower
* @param _principalBorrowBalance the the principal amount borrowed by the user
* @param _compoundedBorrowBalance the principal amount plus the accrued interest
* @param _currentRateMode the rate mode at which the user borrowed
**/
function updateReserveStateOnSwapRateInternal(
address _reserve,
address _user,
uint256 _principalBorrowBalance,
uint256 _compoundedBorrowBalance,
CoreLibrary.InterestRateMode _currentRateMode
) internal {
CoreLibrary.ReserveData storage reserve = reserves[_reserve];
CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve];
//compounding reserve indexes
reserve.updateCumulativeIndexes();
if (_currentRateMode == CoreLibrary.InterestRateMode.STABLE) {
uint256 userCurrentStableRate = user.stableBorrowRate;
//swap to variable
reserve.decreaseTotalBorrowsStableAndUpdateAverageRate(
_principalBorrowBalance,
userCurrentStableRate
); //decreasing stable from old principal balance
reserve.increaseTotalBorrowsVariable(_compoundedBorrowBalance); //increase variable borrows
} else if (_currentRateMode == CoreLibrary.InterestRateMode.VARIABLE) {
//swap to stable
uint256 currentStableRate = reserve.currentStableBorrowRate;
reserve.decreaseTotalBorrowsVariable(_principalBorrowBalance);
reserve.increaseTotalBorrowsStableAndUpdateAverageRate(
_compoundedBorrowBalance,
currentStableRate
);
} else {
revert("Invalid rate mode received");
}
}
利率的更新转换和记录体现在updateUserStateOnSwapRateInternal中,也就是说,用户的结构体中记录了利率的信息。具体的做法就是将用户结构体中之前利率模式的信息清零,稳定利率为stableBorrowRate,可变利率为lastVariableBorrowCumulativeIndex,两种利率都初始化为储备结构体中存储的信息。在最后将之前产生的利息加到借款总额中。
/**
* @dev updates the state of the user as a consequence of a swap rate action.
* @param _reserve the address of the reserve on which the user is performing the swap
* @param _user the address of the borrower
* @param _balanceIncrease the accrued interest on the borrowed amount
* @param _currentRateMode the current rate mode of the user
**/
function updateUserStateOnSwapRateInternal(
address _reserve,
address _user,
uint256 _balanceIncrease,
CoreLibrary.InterestRateMode _currentRateMode
) internal returns (CoreLibrary.InterestRateMode) {
CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve];
CoreLibrary.ReserveData storage reserve = reserves[_reserve];
CoreLibrary.InterestRateMode newMode = CoreLibrary.InterestRateMode.NONE;
if (_currentRateMode == CoreLibrary.InterestRateMode.VARIABLE) {
//switch to stable
newMode = CoreLibrary.InterestRateMode.STABLE;
user.stableBorrowRate = reserve.currentStableBorrowRate;
user.lastVariableBorrowCumulativeIndex = 0;
} else if (_currentRateMode == CoreLibrary.InterestRateMode.STABLE) {
newMode = CoreLibrary.InterestRateMode.VARIABLE;
user.stableBorrowRate = 0;
user.lastVariableBorrowCumulativeIndex = reserve.lastVariableBorrowCumulativeIndex;
} else {
revert("Invalid interest rate mode received");
}
//compounding cumulated interest
user.principalBorrowBalance = user.principalBorrowBalance.add(_balanceIncrease);
//solium-disable-next-line
user.lastUpdateTimestamp = uint40(block.timestamp);
return newMode;
}
updateStateOnLiquidation用于发生资产清算时更新储备的状态信息,在这里principalReserve指的是用于偿还储备(reserve)地址,collateralReserve指的是被清算的抵押资产,user指的是被清算资产的所有者,amountToLiquidate是指清算人偿还的资金,collateralToLiquidate指的是被清算的抵押资产的数目,feeLiquidated指的是抵押贷款的办理费,liquidatedCollateralForFee指的是清算资产费用,等价于抵押贷款办理费加清算人购买清算抵押资产的奖金,balanceIncrease指的是应付未付利息,liquidatorReceivesAToken指的是清算方是否接受atoken。
整个函数的逻辑就是调用三个更新状态的内部函数,如下:
/**
* @dev updates the state of the core as a consequence of a liquidation action.
* @param _principalReserve the address of the principal reserve that is being repaid
* @param _collateralReserve the address of the collateral reserve that is being liquidated
* @param _user the address of the borrower
* @param _amountToLiquidate the amount being repaid by the liquidator
* @param _collateralToLiquidate the amount of collateral being liquidated
* @param _feeLiquidated the amount of origination fee being liquidated
* @param _liquidatedCollateralForFee the amount of collateral equivalent to the origination fee + bonus
* @param _balanceIncrease the accrued interest on the borrowed amount
* @param _liquidatorReceivesAToken true if the liquidator will receive aTokens, false otherwise
**/
function updateStateOnLiquidation(
address _principalReserve,
address _collateralReserve,
address _user,
uint256 _amountToLiquidate,
uint256 _collateralToLiquidate,
uint256 _feeLiquidated,
uint256 _liquidatedCollateralForFee,
uint256 _balanceIncrease,
bool _liquidatorReceivesAToken
) external onlyLendingPool {
updatePrincipalReserveStateOnLiquidationInternal(
_principalReserve,
_user,
_amountToLiquidate,
_balanceIncrease
);
updateCollateralReserveStateOnLiquidationInternal(
_collateralReserve
);
updateUserStateOnLiquidationInternal(
_principalReserve,
_user,
_amountToLiquidate,
_feeLiquidated,
_balanceIncrease
);
updateReserveInterestRatesAndTimestampInternal(_principalReserve, _amountToLiquidate, 0);
if (!_liquidatorReceivesAToken) {
updateReserveInterestRatesAndTimestampInternal(
_collateralReserve,
0,
_collateralToLiquidate.add(_liquidatedCollateralForFee)
);
}
}
我们逐个分析。
首先updatePrincipalReserveStateOnLiquidationInternal用于更新被清算资产储备的状态,可以看到首先会判断清算资产的利率模式,然后根据利率模式将应付未付利息增加到储备金中,而后将被清算资产的总额从储备金中剔除。主要就是做了储备的资金调整:
/**
* @dev updates the state of the principal reserve as a consequence of a liquidation action.
* @param _principalReserve the address of the principal reserve that is being repaid
* @param _user the address of the borrower
* @param _amountToLiquidate the amount being repaid by the liquidator
* @param _balanceIncrease the accrued interest on the borrowed amount
**/
function updatePrincipalReserveStateOnLiquidationInternal(
address _principalReserve,
address _user,
uint256 _amountToLiquidate,
uint256 _balanceIncrease
) internal {
CoreLibrary.ReserveData storage reserve = reserves[_principalReserve];
CoreLibrary.UserReserveData storage user = usersReserveData[_user][_principalReserve];
//update principal reserve data
reserve.updateCumulativeIndexes();
CoreLibrary.InterestRateMode borrowRateMode = getUserCurrentBorrowRateMode(
_principalReserve,
_user
);
if (borrowRateMode == CoreLibrary.InterestRateMode.STABLE) {
//increase the total borrows by the compounded interest
reserve.increaseTotalBorrowsStableAndUpdateAverageRate(
_balanceIncrease,
user.stableBorrowRate
);
//decrease by the actual amount to liquidate
reserve.decreaseTotalBorrowsStableAndUpdateAverageRate(
_amountToLiquidate,
user.stableBorrowRate
);
} else {
//increase the total borrows by the compounded interest
reserve.increaseTotalBorrowsVariable(_balanceIncrease);
//decrease by the actual amount to liquidate
reserve.decreaseTotalBorrowsVariable(_amountToLiquidate);
}
}
当更新完了储备的资金变动后,会调用updateCollateralReserveStateOnLiquidationInternal,逻辑很简单,就是更新一下储备的累加指数:
/**
* @dev updates the state of the collateral reserve as a consequence of a liquidation action.
* @param _collateralReserve the address of the collateral reserve that is being liquidated
**/
function updateCollateralReserveStateOnLiquidationInternal(
address _collateralReserve
) internal {
//update collateral reserve
reserves[_collateralReserve].updateCumulativeIndexes();
}
更新完了储备金状态后,就会调用updateUserStateOnLiquidationInternal更新用户的储备状态,具体逻辑就是将应付未付资金算到用户头上,然后将用户的被清算总额从贷款中减去。而后判断用户的利率模式,如果是可变利率模式那么就更新用户可变贷款累加系数为最新(上一步更新),如果清算手续费大于0,那么久从用户的清算手续费中扣除:
/**
* @dev updates the state of the user being liquidated as a consequence of a liquidation action.
* @param _reserve the address of the principal reserve that is being repaid
* @param _user the address of the borrower
* @param _amountToLiquidate the amount being repaid by the liquidator
* @param _feeLiquidated the amount of origination fee being liquidated
* @param _balanceIncrease the accrued interest on the borrowed amount
**/
function updateUserStateOnLiquidationInternal(
address _reserve,
address _user,
uint256 _amountToLiquidate,
uint256 _feeLiquidated,
uint256 _balanceIncrease
) internal {
CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve];
CoreLibrary.ReserveData storage reserve = reserves[_reserve];
//first increase by the compounded interest, then decrease by the liquidated amount
user.principalBorrowBalance = user.principalBorrowBalance.add(_balanceIncrease).sub(
_amountToLiquidate
);
if (
getUserCurrentBorrowRateMode(_reserve, _user) == CoreLibrary.InterestRateMode.VARIABLE
) {
user.lastVariableBorrowCumulativeIndex = reserve.lastVariableBorrowCumulativeIndex;
}
if(_feeLiquidated > 0){
user.originationFee = user.originationFee.sub(_feeLiquidated);
}
//solium-disable-next-line
user.lastUpdateTimestamp = uint40(block.timestamp);
}
而后调用updateReserveInterestRatesAndTimestampInternal更新偿还储备的利率。
最后根据清算人是否接受atoken来判断是否改变抵押资产储备的状态,如果用户不接受atoken,那么就直接从抵押贷款中扣除相关费用。
updateStateOnRebalance用于当稳定利率重新平衡时更新储备信息,主要的逻辑就是先更新储备的状态,然后更新用户储备状态,最后更新储备利率信息,由三个内部函数调用组成,最后返回新的稳定利率,如下:
/**
* @dev updates the state of the core as a consequence of a stable rate rebalance
* @param _reserve the address of the principal reserve where the user borrowed
* @param _user the address of the borrower
* @param _balanceIncrease the accrued interest on the borrowed amount
* @return the new stable rate for the user
**/
function updateStateOnRebalance(address _reserve, address _user, uint256 _balanceIncrease)
external
onlyLendingPool
returns (uint256)
{
updateReserveStateOnRebalanceInternal(_reserve, _user, _balanceIncrease);
//update user data and rebalance the rate
updateUserStateOnRebalanceInternal(_reserve, _user, _balanceIncrease);
updateReserveInterestRatesAndTimestampInternal(_reserve, 0, 0);
return usersReserveData[_user][_reserve].stableBorrowRate;
}
updateReserveStateOnRebalanceInternal逻辑很简单,就是将用户的应付未付利息加到总储备中,然后更新稳定利率。
/**
* @dev updates the state of the reserve as a consequence of a stable rate rebalance
* @param _reserve the address of the principal reserve where the user borrowed
* @param _user the address of the borrower
* @param _balanceIncrease the accrued interest on the borrowed amount
**/
function updateReserveStateOnRebalanceInternal(
address _reserve,
address _user,
uint256 _balanceIncrease
) internal {
CoreLibrary.ReserveData storage reserve = reserves[_reserve];
CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve];
reserve.updateCumulativeIndexes();
reserve.increaseTotalBorrowsStableAndUpdateAverageRate(
_balanceIncrease,
user.stableBorrowRate
);
}
updateUserStateOnRebalanceInternal将用户的应付未付利息算进用户的借贷额中,然后更新用户稳定利率为上一步计算得到的新的利率
/**
* @dev updates the state of the user as a consequence of a stable rate rebalance
* @param _reserve the address of the principal reserve where the user borrowed
* @param _user the address of the borrower
* @param _balanceIncrease the accrued interest on the borrowed amount
**/
function updateUserStateOnRebalanceInternal(
address _reserve,
address _user,
uint256 _balanceIncrease
) internal {
CoreLibrary.UserReserveData storage user = usersReserveData[_user][_reserve];
CoreLibrary.ReserveData storage reserve = reserves[_reserve];
user.principalBorrowBalance = user.principalBorrowBalance.add(_balanceIncrease);
user.stableBorrowRate = reserve.currentStableBorrowRate;
//solium-disable-next-line
user.lastUpdateTimestamp = uint40(block.timestamp);
}
至此,core合约中所有关键的状态信息维护函数分析完毕。
LendingPoolLiquidationManager.sol
主要用于执行资产清算的逻辑。
主体就两个函数,但是比较复杂。
calculateAvailableCollateralToLiquidate函数用于计算对于特定的购买资产目标抵押资产中有多少可以被清算。首先从预言机获取当前购买使用资产和被清算资产的价格,同时从core合约中获取清算奖励系数。之后计算当前用于购买的资产的总额可以购买多少清算资产并加上奖励系数,如果这个值大于用户抵押资产总额,那么就按照全部清算的价格更新清算者实际花费的资产。如果低于,则不用更新花费数额。最后,将刚才得出的实际会被购买的清算资产总额和实际花费的资产总额返回。
struct AvailableCollateralToLiquidateLocalVars {
uint256 userCompoundedBorrowBalance;
uint256 liquidationBonus;
uint256 collateralPrice;
uint256 principalCurrencyPrice;
uint256 maxAmountCollateralToLiquidate;
}
/**
* @dev calculates how much of a specific collateral can be liquidated, given
* a certain amount of principal currency. This function needs to be called after
* all the checks to validate the liquidation have been performed, otherwise it might fail.
* @param _collateral the collateral to be liquidated
* @param _principal the principal currency to be liquidated
* @param _purchaseAmount the amount of principal being liquidated
* @param _userCollateralBalance the collatera balance for the specific _collateral asset of the user being liquidated
* @return the maximum amount that is possible to liquidated given all the liquidation constraints (user balance, close factor) and
* the purchase amount
**/
function calculateAvailableCollateralToLiquidate(
address _collateral,
address _principal,
uint256 _purchaseAmount,
uint256 _userCollateralBalance
) internal view returns (uint256 collateralAmount, uint256 principalAmountNeeded) {
collateralAmount = 0;
principalAmountNeeded = 0;
IPriceOracleGetter oracle = IPriceOracleGetter(addressesProvider.getPriceOracle());
// Usage of a memory struct of vars to avoid "Stack too deep" errors due to local variables
AvailableCollateralToLiquidateLocalVars memory vars;
vars.collateralPrice = oracle.getAssetPrice(_collateral);
vars.principalCurrencyPrice = oracle.getAssetPrice(_principal);
vars.liquidationBonus = core.getReserveLiquidationBonus(_collateral);
//this is the maximum possible amount of the selected collateral that can be liquidated, given the
//max amount of principal currency that is available for liquidation.
vars.maxAmountCollateralToLiquidate = vars
.principalCurrencyPrice
.mul(_purchaseAmount)
.div(vars.collateralPrice)
.mul(vars.liquidationBonus)
.div(100);
if (vars.maxAmountCollateralToLiquidate > _userCollateralBalance) {
collateralAmount = _userCollateralBalance;
principalAmountNeeded = vars
.collateralPrice
.mul(collateralAmount)
.div(vars.principalCurrencyPrice)
.mul(100)
.div(vars.liquidationBonus);
} else {
collateralAmount = vars.maxAmountCollateralToLiquidate;
principalAmountNeeded = _purchaseAmount;
}
return (collateralAmount, principalAmountNeeded);
}
}
对于清算函数,首先声明了一个局部的结构体用于记录用户储备信息:
struct LiquidationCallLocalVars {
uint256 userCollateralBalance;
uint256 userCompoundedBorrowBalance;
uint256 borrowBalanceIncrease;
uint256 maxPrincipalAmountToLiquidate;
uint256 actualAmountToLiquidate;
uint256 liquidationRatio;
uint256 collateralPrice;
uint256 principalCurrencyPrice;
uint256 maxAmountCollateralToLiquidate;
uint256 originationFee;
uint256 feeLiquidated;
uint256 liquidatedCollateralForFee;
CoreLibrary.InterestRateMode borrowRateMode;
uint256 userStableRate;
bool isCollateralEnabled;
bool healthFactorBelowThreshold;
}
首先进行一系列的检查,包括抵押资产的健康系数、用户的抵押资产金额、用户储备是否被设置为抵押资产、用户是否有借款。当这一系列的检查通过后才会执行之后的清算逻辑。
LiquidationCallLocalVars memory vars;
(, , , , , , , vars.healthFactorBelowThreshold) = dataProvider.calculateUserGlobalData(
_user
);
if (!vars.healthFactorBelowThreshold) {
return (
uint256(LiquidationErrors.HEALTH_FACTOR_ABOVE_THRESHOLD),
"Health factor is not below the threshold"
);
}
vars.userCollateralBalance = core.getUserUnderlyingAssetBalance(_collateral, _user);
//if _user hasn't deposited this specific collateral, nothing can be liquidated
if (vars.userCollateralBalance == 0) {
return (
uint256(LiquidationErrors.NO_COLLATERAL_AVAILABLE),
"Invalid collateral to liquidate"
);
}
vars.isCollateralEnabled =
core.isReserveUsageAsCollateralEnabled(_collateral) &&
core.isUserUseReserveAsCollateralEnabled(_collateral, _user);
//if _collateral isn't enabled as collateral by _user, it cannot be liquidated
if (!vars.isCollateralEnabled) {
return (
uint256(LiquidationErrors.COLLATERAL_CANNOT_BE_LIQUIDATED),
"The collateral chosen cannot be liquidated"
);
}
//if the user hasn't borrowed the specific currency defined by _reserve, it cannot be liquidated
(, vars.userCompoundedBorrowBalance, vars.borrowBalanceIncrease) = core
.getUserBorrowBalances(_reserve, _user);
if (vars.userCompoundedBorrowBalance == 0) {
return (
uint256(LiquidationErrors.CURRRENCY_NOT_BORROWED),
"User did not borrow the specified currency"
);
}
而后根据传入的购买资产数目以及抵押资产数目算出真正的双方交易额。
//all clear - calculate the max principal amount that can be liquidated
vars.maxPrincipalAmountToLiquidate = vars
.userCompoundedBorrowBalance
.mul(LIQUIDATION_CLOSE_FACTOR_PERCENT)
.div(100);
vars.actualAmountToLiquidate = _purchaseAmount > vars.maxPrincipalAmountToLiquidate
? vars.maxPrincipalAmountToLiquidate
: _purchaseAmount;
(uint256 maxCollateralToLiquidate, uint256 principalAmountNeeded) = calculateAvailableCollateralToLiquidate(
_collateral,
_reserve,
vars.actualAmountToLiquidate,
vars.userCollateralBalance
);
vars.originationFee = core.getUserOriginationFee(_reserve, _user);
//if there is a fee to liquidate, calculate the maximum amount of fee that can be liquidated
if (vars.originationFee > 0) {
(
vars.liquidatedCollateralForFee,
vars.feeLiquidated
) = calculateAvailableCollateralToLiquidate(
_collateral,
_reserve,
vars.originationFee,
vars.userCollateralBalance.sub(maxCollateralToLiquidate)
);
}
//if principalAmountNeeded < vars.ActualAmountToLiquidate, there isn't enough
//of _collateral to cover the actual amount that is being liquidated, hence we liquidate
//a smaller amount
if (principalAmountNeeded < vars.actualAmountToLiquidate) {
vars.actualAmountToLiquidate = principalAmountNeeded;
}
如果清算方不接受Atoken作为购买凭证,那么就直接将抵押资产对应的储备中转给清算方。
//if liquidator reclaims the underlying asset, we make sure there is enough available collateral in the reserve
if (!_receiveAToken) {
uint256 currentAvailableCollateral = core.getReserveAvailableLiquidity(_collateral);
if (currentAvailableCollateral < maxCollateralToLiquidate) {
return (
uint256(LiquidationErrors.NOT_ENOUGH_LIQUIDITY),
"There isn't enough liquidity available to liquidate"
);
}
}
core.updateStateOnLiquidation(
_reserve,
_collateral,
_user,
vars.actualAmountToLiquidate,
maxCollateralToLiquidate,
vars.feeLiquidated,
vars.liquidatedCollateralForFee,
vars.borrowBalanceIncrease,
_receiveAToken
);
反之,如果清算方接受Atoken,那么就将Atoken转给清算方,最后将购买使用的资产赚到对应的储备中。
AToken collateralAtoken = AToken(core.getReserveATokenAddress(_collateral));
//if liquidator reclaims the aToken, he receives the equivalent atoken amount
if (_receiveAToken) {
collateralAtoken.transferOnLiquidation(_user, msg.sender, maxCollateralToLiquidate);
} else {
//otherwise receives the underlying asset
//burn the equivalent amount of atoken
collateralAtoken.burnOnLiquidation(_user, maxCollateralToLiquidate);
core.transferToUser(_collateral, msg.sender, maxCollateralToLiquidate);
}
//transfers the principal currency to the pool
core.transferToReserve.value(msg.value)(_reserve, msg.sender, vars.actualAmountToLiquidate);
最后,处理交易手续费:
if (vars.feeLiquidated > 0) {
//if there is enough collateral to liquidate the fee, first transfer burn an equivalent amount of
//aTokens of the user
collateralAtoken.burnOnLiquidation(_user, vars.liquidatedCollateralForFee);
//then liquidate the fee by transferring it to the fee collection address
core.liquidateFee(
_collateral,
vars.liquidatedCollateralForFee,
addressesProvider.getTokenDistributor()
);
emit OriginationFeeLiquidated(
_collateral,
_reserve,
_user,
vars.feeLiquidated,
vars.liquidatedCollateralForFee,
//solium-disable-next-line
block.timestamp
);
}
LendingPoolDataProvider.sol
从核心合约中取出数据,并且将其整合,以用于计算复合余额以及账户存款的ETH表示。
由于都是view函数,在这里对于LendingPoolDataProvider的函数接口只当作黑盒。
corelib
CoreLibrary
定义了储备和用户数据的数据结构,如下:
struct UserReserveData {
//principal amount borrowed by the user.
uint256 principalBorrowBalance;
//cumulated variable borrow index for the user. Expressed in ray
uint256 lastVariableBorrowCumulativeIndex;
//origination fee cumulated by the user
uint256 originationFee;
// stable borrow rate at which the user has borrowed. Expressed in ray
uint256 stableBorrowRate;
uint40 lastUpdateTimestamp;
//defines if a specific deposit should or not be used as a collateral in borrows
bool useAsCollateral;
}
struct ReserveData {
/**
* @dev refer to the whitepaper, section 1.1 basic concepts for a formal description of these properties.
**/
//the liquidity index. Expressed in ray
uint256 lastLiquidityCumulativeIndex;
//the current supply rate. Expressed in ray
uint256 currentLiquidityRate;
//the total borrows of the reserve at a stable rate. Expressed in the currency decimals
uint256 totalBorrowsStable;
//the total borrows of the reserve at a variable rate. Expressed in the currency decimals
uint256 totalBorrowsVariable;
//the current variable borrow rate. Expressed in ray
uint256 currentVariableBorrowRate;
//the current stable borrow rate. Expressed in ray
uint256 currentStableBorrowRate;
//the current average stable borrow rate (weighted average of all the different stable rate loans). Expressed in ray
uint256 currentAverageStableBorrowRate;
//variable borrow index. Expressed in ray
uint256 lastVariableBorrowCumulativeIndex;
//the ltv of the reserve. Expressed in percentage (0-100)
uint256 baseLTVasCollateral;
//the liquidation threshold of the reserve. Expressed in percentage (0-100)
uint256 liquidationThreshold;
//the liquidation bonus of the reserve. Expressed in percentage
uint256 liquidationBonus;
//the decimals of the reserve asset
uint256 decimals;
/**
* @dev address of the aToken representing the asset
**/
address aTokenAddress;
/**
* @dev address of the interest rate strategy contract
**/
address interestRateStrategyAddress;
uint40 lastUpdateTimestamp;
// borrowingEnabled = true means users can borrow from this reserve
bool borrowingEnabled;
// usageAsCollateralEnabled = true means users can use this reserve as collateral
bool usageAsCollateralEnabled;
// isStableBorrowRateEnabled = true means users can borrow at a stable rate
bool isStableBorrowRateEnabled;
// isActive = true means the reserve has been activated and properly configured
bool isActive;
// isFreezed = true means the reserve only allows repays and redeems, but not deposits, new borrowings or rate swap
bool isFreezed;
}
calculateCompoundedInterest用于计算复合利率,是一个利率的时间次方的计算公式(也就是之前提到的APY):
/**
* @dev function to calculate the interest using a compounded interest rate formula
* @param _rate the interest rate, in ray
* @param _lastUpdateTimestamp the timestamp of the last update of the interest
* @return the interest rate compounded during the timeDelta, in ray
**/
function calculateCompoundedInterest(uint256 _rate, uint40 _lastUpdateTimestamp)
internal
view
returns (uint256)
{
//solium-disable-next-line
uint256 timeDifference = block.timestamp.sub(uint256(_lastUpdateTimestamp));
uint256 ratePerSecond = _rate.div(SECONDS_PER_YEAR);
return ratePerSecond.add(WadRayMath.ray()).rayPow(timeDifference);
}
CalculateLinearInterest用于计算线性利率,只是利率和时间的线性累加,注意这里算的不是利息而是利率,函数用于计算流动性挖矿的收益:
function calculateLinearInterest(uint256 _rate, uint40 _lastUpdateTimestamp)
internal
view
returns (uint256)
{
//solium-disable-next-line
uint256 timeDifference = block.timestamp.sub(uint256(_lastUpdateTimestamp));
uint256 timeDelta = timeDifference.wadToRay().rayDiv(SECONDS_PER_YEAR.wadToRay());
return _rate.rayMul(timeDelta).add(WadRayMath.ray());
}
updateCumulativeIndexes用于计算流动资产的累加系数和借贷累加系数。注意,此函数只有在储备资产发生变化时才会调用。具体做法是先得到储备的总借贷额,如果借贷额大于0那么才进行更新储备结构体中的最新流动资金累加指数和最新可变借贷累加指数。其中,最新流动资金借贷指数每次更新会自乘线性利率,也就是说流动资产累计系数为现行利率的累乘,类似的,可变借贷累加指数是复合利率的累乘。
/**
* @dev Updates the liquidity cumulative index Ci and variable borrow cumulative index Bvc. Refer to the whitepaper for
* a formal specification.
* @param _self the reserve object
**/
function updateCumulativeIndexes(ReserveData storage _self) internal {
uint256 totalBorrows = getTotalBorrows(_self);
if (totalBorrows > 0) {
//only cumulating if there is any income being produced
uint256 cumulatedLiquidityInterest = calculateLinearInterest(
_self.currentLiquidityRate,
_self.lastUpdateTimestamp
);
_self.lastLiquidityCumulativeIndex = cumulatedLiquidityInterest.rayMul(
_self.lastLiquidityCumulativeIndex
);
uint256 cumulatedVariableBorrowInterest = calculateCompoundedInterest(
_self.currentVariableBorrowRate,
_self.lastUpdateTimestamp
);
_self.lastVariableBorrowCumulativeIndex = cumulatedVariableBorrowInterest.rayMul(
_self.lastVariableBorrowCumulativeIndex
);
}
}
cumulateToLiquidityIndex用于将流动资产产生的收入通过更新流动资产累加系数的方式来记录。具体做法是将收入在总流动性资产中的占比加一累乘到流动资产累加系数中。
/**
* @dev accumulates a predefined amount of asset to the reserve as a fixed, one time income. Used for example to accumulate
* the flashloan fee to the reserve, and spread it through the depositors.
* @param _self the reserve object
* @param _totalLiquidity the total liquidity available in the reserve
* @param _amount the amount to accomulate
**/
function cumulateToLiquidityIndex(
ReserveData storage _self,
uint256 _totalLiquidity,
uint256 _amount
) internal {
uint256 amountToLiquidityRatio = _amount.wadToRay().rayDiv(_totalLiquidity.wadToRay());
uint256 cumulatedLiquidity = amountToLiquidityRatio.add(WadRayMath.ray());
_self.lastLiquidityCumulativeIndex = cumulatedLiquidity.rayMul(
_self.lastLiquidityCumulativeIndex
);
}
increaseBorrowsStableAndUpdateAverageRate用于在用户还款或者其它种形式储备金稳定借贷储备增多时更新储备稳定借款总量和利率。
/**
* @dev increases the total borrows at a stable rate on a specific reserve and updates the
* average stable rate consequently
* @param _reserve the reserve object
* @param _amount the amount to add to the total borrows stable
* @param _rate the rate at which the amount has been borrowed
**/
function increaseTotalBorrowsStableAndUpdateAverageRate(
ReserveData storage _reserve,
uint256 _amount,
uint256 _rate
) internal {
uint256 previousTotalBorrowStable = _reserve.totalBorrowsStable;
//updating reserve borrows stable
_reserve.totalBorrowsStable = _reserve.totalBorrowsStable.add(_amount);
//update the average stable rate
//weighted average of all the borrows
uint256 weightedLastBorrow = _amount.wadToRay().rayMul(_rate);
uint256 weightedPreviousTotalBorrows = previousTotalBorrowStable.wadToRay().rayMul(
_reserve.currentAverageStableBorrowRate
);
_reserve.currentAverageStableBorrowRate = weightedLastBorrow
.add(weightedPreviousTotalBorrows)
.rayDiv(_reserve.totalBorrowsStable.wadToRay());
}
与前面increaseTotalBorrowsStableAndUpdateAverageRate对应的decreaseTotalBorrowsStableAndUpdateAverageRate函数,作用与之类似,不过会做一些边界的检查。
/**
* @dev decreases the total borrows at a stable rate on a specific reserve and updates the
* average stable rate consequently
* @param _reserve the reserve object
* @param _amount the amount to substract to the total borrows stable
* @param _rate the rate at which the amount has been repaid
**/
function decreaseTotalBorrowsStableAndUpdateAverageRate(
ReserveData storage _reserve,
uint256 _amount,
uint256 _rate
) internal {
require(_reserve.totalBorrowsStable >= _amount, "Invalid amount to decrease");
uint256 previousTotalBorrowStable = _reserve.totalBorrowsStable;
//updating reserve borrows stable
_reserve.totalBorrowsStable = _reserve.totalBorrowsStable.sub(_amount);
if (_reserve.totalBorrowsStable == 0) {
_reserve.currentAverageStableBorrowRate = 0; //no income if there are no stable rate borrows
return;
}
//update the average stable rate
//weighted average of all the borrows
uint256 weightedLastBorrow = _amount.wadToRay().rayMul(_rate);
uint256 weightedPreviousTotalBorrows = previousTotalBorrowStable.wadToRay().rayMul(
_reserve.currentAverageStableBorrowRate
);
require(
weightedPreviousTotalBorrows >= weightedLastBorrow,
"The amounts to subtract don't match"
);
_reserve.currentAverageStableBorrowRate = weightedPreviousTotalBorrows
.sub(weightedLastBorrow)
.rayDiv(_reserve.totalBorrowsStable.wadToRay());
}
对于可变利率的贷款储备金,也有与前面两个函数功能类似的函数,不过只是简单的将数额加或减到总额中。如下:
/**
* @dev increases the total borrows at a variable rate
* @param _reserve the reserve object
* @param _amount the amount to add to the total borrows variable
**/
function increaseTotalBorrowsVariable(ReserveData storage _reserve, uint256 _amount) internal {
_reserve.totalBorrowsVariable = _reserve.totalBorrowsVariable.add(_amount);
}
/**
* @dev decreases the total borrows at a variable rate
* @param _reserve the reserve object
* @param _amount the amount to substract to the total borrows variable
**/
function decreaseTotalBorrowsVariable(ReserveData storage _reserve, uint256 _amount) internal {
require(
_reserve.totalBorrowsVariable >= _amount,
"The amount that is being subtracted from the variable total borrows is incorrect"
);
_reserve.totalBorrowsVariable = _reserve.totalBorrowsVariable.sub(_amount);
}
atoken
Atoken是AAVE协议中通信的代币,本质上是一个ERC20Token,但是里面加了很多其他的函数接口,这里只分析一些关键的函数。
这里的Atoken继承于ERC20token ` contract AToken is ERC20, ERC20Detailed `。其中Atoken的ERC20的balance,也就是super.balanceOf查询的是用户的本金余额,而Atoken自身实现的balanceOf则是查询包括本金余额、本金余额产生的利息以及重定向余额产生的利息的和。
在这里,如果用户重定向目标地址为0的话说明用户没有进行利息的重定向,于是乎计算利息的话就根据本金余额和重定向余额加到一起算产生的利息。如果用户使用了,那么就只有重定向的资产能够产生利息,本金不会产生利息,代码如下:
/**
* @dev returns the principal balance of the user. The principal balance is the last
* updated stored balance, which does not consider the perpetually accruing interest.
* @param _user the address of the user
* @return the principal balance of the user
**/
function principalBalanceOf(address _user) external view returns(uint256) {
return super.balanceOf(_user);
}
/**
* @dev calculates the balance of the user, which is the
* principal balance + interest generated by the principal balance + interest generated by the redirected balance
* @param _user the user for which the balance is being calculated
* @return the total balance of the user
**/
function balanceOf(address _user) public view returns(uint256) {
//current principal balance of the user
uint256 currentPrincipalBalance = super.balanceOf(_user);
//balance redirected by other users to _user for interest rate accrual
uint256 redirectedBalance = redirectedBalances[_user];
if(currentPrincipalBalance == 0 && redirectedBalance == 0){
return 0;
}
//if the _user is not redirecting the interest to anybody, accrues
//the interest for himself
if(interestRedirectionAddresses[_user] == address(0)){
//accruing for himself means that both the principal balance and
//the redirected balance partecipate in the interest
return calculateCumulatedBalanceInternal(
_user,
currentPrincipalBalance.add(redirectedBalance)
)
.sub(redirectedBalance);
}
else {
//if the user redirected the interest, then only the redirected
//balance generates interest. In that case, the interest generated
//by the redirected balance is added to the current principal balance.
return currentPrincipalBalance.add(
calculateCumulatedBalanceInternal(
_user,
redirectedBalance
)
.sub(redirectedBalance)
);
}
}
关键数据结构:
address public underlyingAssetAddress;
mapping (address => uint256) private userIndexes;
mapping (address => address) private interestRedirectionAddresses;
mapping (address => uint256) private redirectedBalances;
mapping (address => address) private interestRedirectionAllowances;
LendingPoolAddressesProvider private addressesProvider;
LendingPoolCore private core;
LendingPool private pool;
LendingPoolDataProvider private dataProvider;
用户可以调用重定向函数redirectInterestStream将产生的利息转发给重定向付款地址,当发生重定向时会将用户的资产加到接受者的重定向余额中。首先会判断重定向的地址是否与之前的重定向地址相同,不允许重复设置两个一样的重定向地址,而后会通过cumulateBalanceInternal函数来计算发送者本金余额产生的利息和当前余额。如果用户在之前有重定向的地址,那么通过updateRedirectedBalanceOfRedirectionAddressInternal将之前的重定向地址余额清零。如果重定向地址是发送者本身,那么就将接受者地址改成0代表自己,如果不是的话,那么就更新重定向地址并且更新状态信息。
/**
* @dev redirects the interest generated to a target address.
* when the interest is redirected, the user balance is added to
* the recepient redirected balance.
* @param _to the address to which the interest will be redirected
**/
function redirectInterestStream(address _to) external {
redirectInterestStreamInternal(msg.sender, _to);
}
...
/**
* @dev executes the redirection of the interest from one address to another.
* immediately after redirection, the destination address will start to accrue interest.
* @param _from the address from which transfer the aTokens
* @param _to the destination address
**/
function redirectInterestStreamInternal(
address _from,
address _to
) internal {
address currentRedirectionAddress = interestRedirectionAddresses[_from];
require(_to != currentRedirectionAddress, "Interest is already redirected to the user");
//accumulates the accrued interest to the principal
(uint256 previousPrincipalBalance,
uint256 fromBalance,
uint256 balanceIncrease,
uint256 fromIndex) = cumulateBalanceInternal(_from);
require(fromBalance > 0, "Interest stream can only be redirected if there is a valid balance");
//if the user is already redirecting the interest to someone, before changing
//the redirection address we substract the redirected balance of the previous
//recipient
if(currentRedirectionAddress != address(0)){
updateRedirectedBalanceOfRedirectionAddressInternal(_from,0, previousPrincipalBalance);
}
//if the user is redirecting the interest back to himself,
//we simply set to 0 the interest redirection address
if(_to == _from) {
interestRedirectionAddresses[_from] = address(0);
emit InterestStreamRedirected(
_from,
address(0),
fromBalance,
balanceIncrease,
fromIndex
);
return;
}
//first set the redirection address to the new recipient
interestRedirectionAddresses[_from] = _to;
//adds the user balance to the redirected balance of the destination
updateRedirectedBalanceOfRedirectionAddressInternal(_from,fromBalance,0);
emit InterestStreamRedirected(
_from,
_to,
fromBalance,
balanceIncrease,
fromIndex
);
}
其中,cumulateBalanceInternal会计算用户本金余额产生的利息收入,并且通过调用corelib函数的方式计算用户的指数。
/**
* @dev accumulates the accrued interest of the user to the principal balance
* @param _user the address of the user for which the interest is being accumulated
* @return the previous principal balance, the new principal balance, the balance increase
* and the new user index
**/
function cumulateBalanceInternal(address _user)
internal
returns(uint256, uint256, uint256, uint256) {
uint256 previousPrincipalBalance = super.balanceOf(_user);
//calculate the accrued interest since the last accumulation
uint256 balanceIncrease = balanceOf(_user).sub(previousPrincipalBalance);
//mints an amount of tokens equivalent to the amount accumulated
_mint(_user, balanceIncrease);
//updates the user index
uint256 index = userIndexes[_user] = core.getReserveNormalizedIncome(underlyingAssetAddress);
return (
previousPrincipalBalance,
previousPrincipalBalance.add(balanceIncrease),
balanceIncrease,
index
);
}
updateRedirectedBalanceOfRedirectionAddressInternal主要做的内容就是更新redirectedBalances数组的数值,更新余额。
/**
* @dev updates the redirected balance of the user. If the user is not redirecting his
* interest, nothing is executed.
* @param _user the address of the user for which the interest is being accumulated
* @param _balanceToAdd the amount to add to the redirected balance
* @param _balanceToRemove the amount to remove from the redirected balance
**/
function updateRedirectedBalanceOfRedirectionAddressInternal(
address _user,
uint256 _balanceToAdd,
uint256 _balanceToRemove
) internal {
address redirectionAddress = interestRedirectionAddresses[_user];
//if there isn't any redirection, nothing to be done
if(redirectionAddress == address(0)){
return;
}
//compound balances of the redirected address
(,,uint256 balanceIncrease, uint256 index) = cumulateBalanceInternal(redirectionAddress);
//updating the redirected balance
redirectedBalances[redirectionAddress] = redirectedBalances[redirectionAddress]
.add(_balanceToAdd)
.sub(_balanceToRemove);
//if the interest of redirectionAddress is also being redirected, we need to update
//the redirected balance of the redirection target by adding the balance increase
address targetOfRedirectionAddress = interestRedirectionAddresses[redirectionAddress];
if(targetOfRedirectionAddress != address(0)){
redirectedBalances[targetOfRedirectionAddress] = redirectedBalances[targetOfRedirectionAddress].add(balanceIncrease);
}
emit RedirectedBalanceUpdated(
redirectionAddress,
balanceIncrease,
index,
_balanceToAdd,
_balanceToRemove
);
}
类似于ERC20的approve函数,针对于重定向也有allowance结构来记录允许的代理方重定向委托方资产:
/**
* @dev gives allowance to an address to execute the interest redirection
* on behalf of the caller.
* @param _to the address to which the interest will be redirected. Pass address(0) to reset
* the allowance.
**/
function allowInterestRedirectionTo(address _to) external {
require(_to != msg.sender, "User cannot give allowance to himself");
interestRedirectionAllowances[msg.sender] = _to;
emit InterestRedirectionAllowanceChanged(
msg.sender,
_to
);
}
用户可以调用redeem函数来通过自己所持有的Atoken来赎回标的资产,主要逻辑就是将用户的redirect和本金余额等清零,然后烧掉Atoken,最后通过lendingpool的redeemUnderlying来进行最终的转账操作。
/**
* @dev redeems aToken for the underlying asset
* @param _amount the amount being redeemed
**/
function redeem(uint256 _amount) external {
require(_amount > 0, "Amount to redeem needs to be > 0");
//cumulates the balance of the user
(,
uint256 currentBalance,
uint256 balanceIncrease,
uint256 index) = cumulateBalanceInternal(msg.sender);
uint256 amountToRedeem = _amount;
//if amount is equal to uint(-1), the user wants to redeem everything
if(_amount == UINT_MAX_VALUE){
amountToRedeem = currentBalance;
}
require(amountToRedeem <= currentBalance, "User cannot redeem more than the available balance");
//check that the user is allowed to redeem the amount
require(isTransferAllowed(msg.sender, amountToRedeem), "Transfer cannot be allowed.");
//if the user is redirecting his interest towards someone else,
//we update the redirected balance of the redirection address by adding the accrued interest,
//and removing the amount to redeem
updateRedirectedBalanceOfRedirectionAddressInternal(msg.sender, balanceIncrease, amountToRedeem);
// burns tokens equivalent to the amount requested
_burn(msg.sender, amountToRedeem);
bool userIndexReset = false;
//reset the user data if the remaining balance is 0
if(currentBalance.sub(amountToRedeem) == 0){
userIndexReset = resetDataOnZeroBalanceInternal(msg.sender);
}
// executes redeem of the underlying asset
pool.redeemUnderlying(
underlyingAssetAddress,
msg.sender,
amountToRedeem,
currentBalance.sub(amountToRedeem)
);
emit Redeem(msg.sender, amountToRedeem, balanceIncrease, userIndexReset ? 0 : index);
}
其中,如果用户赎回了所有资产,那么调用resetDataOnZeroBalanceInternal来将用户相关的资产信息重置为0.
/**
* @dev function to reset the interest stream redirection and the user index, if the
* user has no balance left.
* @param _user the address of the user
* @return true if the user index has also been reset, false otherwise. useful to emit the proper user index value
**/
function resetDataOnZeroBalanceInternal(address _user) internal returns(bool) {
//if the user has 0 principal balance, the interest stream redirection gets reset
interestRedirectionAddresses[_user] = address(0);
//emits a InterestStreamRedirected event to notify that the redirection has been reset
emit InterestStreamRedirected(_user, address(0),0,0,0);
//if the redirected balance is also 0, we clear up the user index
if(redirectedBalances[_user] == 0){
userIndexes[_user] = 0;
return true;
}
else{
return false;
}
}
当用户通过lendingpool进行存款操作时,会调用mintOnDeposit函数,由于Atoken适用于标的资产的凭证,所以在Atoken中只维护了标的资产的地址。主要逻辑就是更新用户的余额并且初始化重定向信息,最后产生Atoken给用户。
/**
* @dev mints token in the event of users depositing the underlying asset into the lending pool
* only lending pools can call this function
* @param _account the address receiving the minted tokens
* @param _amount the amount of tokens to mint
*/
function mintOnDeposit(address _account, uint256 _amount) external onlyLendingPool {
//cumulates the balance of the user
(,
,
uint256 balanceIncrease,
uint256 index) = cumulateBalanceInternal(_account);
//if the user is redirecting his interest towards someone else,
//we update the redirected balance of the redirection address by adding the accrued interest
//and the amount deposited
updateRedirectedBalanceOfRedirectionAddressInternal(_account, balanceIncrease.add(_amount), 0);
//mint an equivalent amount of tokens to cover the new deposit
_mint(_account, _amount);
emit MintOnDeposit(_account, _amount, balanceIncrease, index);
}
当发生资产清算时,lendingpool会调用Atoken的burnOnLiquidation函数,为了防止用户重新取回标的资产,清算资产的转账操作在lendingpool中进行。主要的逻辑还是更新余额,如果全部清算则更新指数信息。
/**
* @dev burns token in the event of a borrow being liquidated, in case the liquidators reclaims the underlying asset
* Transfer of the liquidated asset is executed by the lending pool contract.
* only lending pools can call this function
* @param _account the address from which burn the aTokens
* @param _value the amount to burn
**/
function burnOnLiquidation(address _account, uint256 _value) external onlyLendingPool {
//cumulates the balance of the user being liquidated
(,uint256 accountBalance,uint256 balanceIncrease,uint256 index) = cumulateBalanceInternal(_account);
//adds the accrued interest and substracts the burned amount to
//the redirected balance
updateRedirectedBalanceOfRedirectionAddressInternal(_account, balanceIncrease, _value);
//burns the requested amount of tokens
_burn(_account, _value);
bool userIndexReset = false;
//reset the user data if the remaining balance is 0
if(accountBalance.sub(_value) == 0){
userIndexReset = resetDataOnZeroBalanceInternal(_account);
}
emit BurnOnLiquidation(_account, _value, balanceIncrease, userIndexReset ? 0 : index);
}
/**
* @dev transfers tokens in the event of a borrow being liquidated, in case the liquidators reclaims the aToken
* only lending pools can call this function
* @param _from the address from which transfer the aTokens
* @param _to the destination address
* @param _value the amount to transfer
**/
function transferOnLiquidation(address _from, address _to, uint256 _value) external onlyLendingPool {
//being a normal transfer, the Transfer() and BalanceTransfer() are emitted
//so no need to emit a specific event here
executeTransferInternal(_from, _to, _value);
}
最后的transfer函数,记录收益维护重定向数组,最后的转账操作是通过super._transfer完成,也就是转出本金余额。
/**
* @dev executes the transfer of aTokens, invoked by both _transfer() and
* transferOnLiquidation()
* @param _from the address from which transfer the aTokens
* @param _to the destination address
* @param _value the amount to transfer
**/
function executeTransferInternal(
address _from,
address _to,
uint256 _value
) internal {
require(_value > 0, "Transferred amount needs to be greater than zero");
//cumulate the balance of the sender
(,
uint256 fromBalance,
uint256 fromBalanceIncrease,
uint256 fromIndex
) = cumulateBalanceInternal(_from);
//cumulate the balance of the receiver
(,
,
uint256 toBalanceIncrease,
uint256 toIndex
) = cumulateBalanceInternal(_to);
//if the sender is redirecting his interest towards someone else,
//adds to the redirected balance the accrued interest and removes the amount
//being transferred
updateRedirectedBalanceOfRedirectionAddressInternal(_from, fromBalanceIncrease, _value);
//if the receiver is redirecting his interest towards someone else,
//adds to the redirected balance the accrued interest and the amount
//being transferred
updateRedirectedBalanceOfRedirectionAddressInternal(_to, toBalanceIncrease.add(_value), 0);
//performs the transfer
super._transfer(_from, _to, _value);
bool fromIndexReset = false;
//reset the user data if the remaining balance is 0
if(fromBalance.sub(_value) == 0){
fromIndexReset = resetDataOnZeroBalanceInternal(_from);
}
emit BalanceTransfer(
_from,
_to,
_value,
fromBalanceIncrease,
toBalanceIncrease,
fromIndexReset ? 0 : fromIndex,
toIndex
);
}
v2
主要改进
v2主要对于v1版本做了优化,提升了v1版本的最优-次优解决方案,体现在了如下几点:
- atoken的不可升级性
- gas优化问题
- 结构对于自动化测试和审计的不友好性
- 代码简化
提供了以下新的特性:
- 债务代币化表示:借款方债务在v1版本中由内部记账计算完成,在v2中由一个专门的代币来表示借款方的债务信息,有以下几种好处:
- 代码逻辑简化,现在债务的计算被隐形的表示为代币的铸币和销毁操作中
- 用户现在可以同时进行可变利率和稳定利率模式的贷款。在v1版本中,用户只能根据之前贷款的利率模式来进行新的贷款利率模式的选择。在v2中,用户可以同时持有多个稳定利率模式和可变利率模式的复合贷款。多个稳定利率模式的贷款利率为用户稳定利率贷款的稳定利率加权平均。
- 原生信贷委托,通过委托这一概念,用户通过开设信贷额度的方式来委托给其他地址。
- 闪电贷v2:在v1版本中,闪电贷是一个比较混乱的功能,虽然可以做到许多比如套利、抵押资产转换或清算功能,但是最大的局限在于不能在AAVE项目框架中使用。v2版本的闪电贷允许用户在闪电贷中使用一些协议本身具有的功能,使得以下几种概念成为可能:
- 抵押资产交易,在不需要关闭债务条件下将抵押物资产允许其他用户访问。
- 用抵押物资产进行还贷。
- 边缘交易。
- 债务转换。将欠款债务转移到另一个资产。
- 边缘存款
整体架构
与v1版本不同
- 资产在v1中由lendingPoolCore合约维护,在v2版本中被存储在了各个特定的atoken中。这给予了各个资产间更好的分隔,并且对于流动性挖矿更为友好。
- LendingPoolCore和LendingPoolDataProvider被移除取而代之的是库函数,减少了15%-20%的gas以及优化了代码的可读性。
- 所有的行为现在都在LendingPool合约完成。
- 债务代币用于记录用户债务信息。
- 移除了利息重定向功能
项目构成
.
├── adapters
│ ├── BaseParaSwapAdapter.sol
│ ├── BaseParaSwapSellAdapter.sol
│ ├── BaseUniswapAdapter.sol
│ ├── FlashLiquidationAdapter.sol
│ ├── ParaSwapLiquiditySwapAdapter.sol
│ ├── UniswapLiquiditySwapAdapter.sol
│ ├── UniswapRepayAdapter.sol
│ └── interfaces
│ └── IBaseUniswapAdapter.sol
├── dependencies
│ └── openzeppelin
│ ├── contracts
│ │ ├── Address.sol
│ │ ├── Context.sol
│ │ ├── ERC20.sol
│ │ ├── IERC20.sol
│ │ ├── IERC20Detailed.sol
│ │ ├── Ownable.sol
│ │ ├── ReentrancyGuard.sol
│ │ ├── SafeERC20.sol
│ │ └── SafeMath.sol
│ └── upgradeability
│ ├── AdminUpgradeabilityProxy.sol
│ ├── BaseAdminUpgradeabilityProxy.sol
│ ├── BaseUpgradeabilityProxy.sol
│ ├── Initializable.sol
│ ├── InitializableAdminUpgradeabilityProxy.sol
│ ├── InitializableUpgradeabilityProxy.sol
│ ├── Proxy.sol
│ └── UpgradeabilityProxy.sol
├── deployments
│ ├── ATokensAndRatesHelper.sol
│ ├── StableAndVariableTokensHelper.sol
│ └── StringLib.sol
├── flashloan
│ ├── base
│ │ └── FlashLoanReceiverBase.sol
│ └── interfaces
│ └── IFlashLoanReceiver.sol
├── interfaces
│ ├── IAToken.sol
│ ├── IAaveIncentivesController.sol
│ ├── IChainlinkAggregator.sol
│ ├── ICreditDelegationToken.sol
│ ├── IDelegationToken.sol
│ ├── IERC20WithPermit.sol
│ ├── IExchangeAdapter.sol
│ ├── IInitializableAToken.sol
│ ├── IInitializableDebtToken.sol
│ ├── ILendingPool.sol
│ ├── ILendingPoolAddressesProvider.sol
│ ├── ILendingPoolAddressesProviderRegistry.sol
│ ├── ILendingPoolCollateralManager.sol
│ ├── ILendingPoolConfigurator.sol
│ ├── ILendingRateOracle.sol
│ ├── IParaSwapAugustus.sol
│ ├── IParaSwapAugustusRegistry.sol
│ ├── IPriceOracle.sol
│ ├── IPriceOracleGetter.sol
│ ├── IReserveInterestRateStrategy.sol
│ ├── IScaledBalanceToken.sol
│ ├── IStableDebtToken.sol
│ ├── IUniswapExchange.sol
│ ├── IUniswapV2Router02.sol
│ └── IVariableDebtToken.sol
├── misc
│ ├── AaveOracle.sol
│ ├── AaveProtocolDataProvider.sol
│ ├── UiIncentiveDataProviderV2.sol
│ ├── UiIncentiveDataProviderV2V3.sol
│ ├── UiPoolDataProvider.sol
│ ├── UiPoolDataProviderV2.sol
│ ├── UiPoolDataProviderV2V3.sol
│ ├── WETHGateway.sol
│ ├── WalletBalanceProvider.sol
│ └── interfaces
│ ├── IAaveOracle.sol
│ ├── IERC20DetailedBytes.sol
│ ├── IUiIncentiveDataProviderV2.sol
│ ├── IUiIncentiveDataProviderV3.sol
│ ├── IUiPoolDataProvider.sol
│ ├── IUiPoolDataProviderV2.sol
│ ├── IUiPoolDataProviderV3.sol
│ ├── IUniswapV2Router01.sol
│ ├── IUniswapV2Router02.sol
│ ├── IWETH.sol
│ └── IWETHGateway.sol
├── mocks
│ ├── attacks
│ │ └── SefldestructTransfer.sol
│ ├── dependencies
│ │ └── weth
│ │ └── WETH9.sol
│ ├── flashloan
│ │ └── MockFlashLoanReceiver.sol
│ ├── oracle
│ │ ├── CLAggregators
│ │ │ └── MockAggregator.sol
│ │ ├── ChainlinkUSDETHOracleI.sol
│ │ ├── GenericOracleI.sol
│ │ ├── IExtendedPriceAggregator.sol
│ │ ├── LendingRateOracle.sol
│ │ └── PriceOracle.sol
│ ├── swap
│ │ ├── MockParaSwapAugustus.sol
│ │ ├── MockParaSwapAugustusRegistry.sol
│ │ ├── MockParaSwapTokenTransferProxy.sol
│ │ └── MockUniswapV2Router02.sol
│ ├── tokens
│ │ ├── MintableDelegationERC20.sol
│ │ ├── MintableERC20.sol
│ │ └── WETH9Mocked.sol
│ └── upgradeability
│ ├── MockAToken.sol
│ ├── MockStableDebtToken.sol
│ └── MockVariableDebtToken.sol
└── protocol
├── configuration
│ ├── LendingPoolAddressesProvider.sol
│ └── LendingPoolAddressesProviderRegistry.sol
├── lendingpool
│ ├── DefaultReserveInterestRateStrategy.sol
│ ├── LendingPool.sol
│ ├── LendingPoolCollateralManager.sol
│ ├── LendingPoolConfigurator.sol
│ └── LendingPoolStorage.sol
├── libraries
│ ├── aave-upgradeability
│ │ ├── BaseImmutableAdminUpgradeabilityProxy.sol
│ │ ├── InitializableImmutableAdminUpgradeabilityProxy.sol
│ │ └── VersionedInitializable.sol
│ ├── configuration
│ │ ├── ReserveConfiguration.sol
│ │ └── UserConfiguration.sol
│ ├── helpers
│ │ ├── Errors.sol
│ │ └── Helpers.sol
│ ├── logic
│ │ ├── GenericLogic.sol
│ │ ├── ReserveLogic.sol
│ │ └── ValidationLogic.sol
│ ├── math
│ │ ├── MathUtils.sol
│ │ ├── PercentageMath.sol
│ │ └── WadRayMath.sol
│ └── types
│ └── DataTypes.sol
└── tokenization
├── AToken.sol
├── DelegationAwareAToken.sol
├── IncentivizedERC20.sol
├── StableDebtToken.sol
├── VariableDebtToken.sol
└── base
└── DebtTokenBase.sol
核心合约
数据结构
在datatype.sol中定义了储备和用户的存储数据结构,其中不涉及高精度的百分数或者模式信息用一个uint256的flag来表示,具体每一位代表的含义见代码:
struct ReserveData {
//stores the reserve configuration
ReserveConfigurationMap configuration;
//the liquidity index. Expressed in ray
uint128 liquidityIndex;
//variable borrow index. Expressed in ray
uint128 variableBorrowIndex;
//the current supply rate. Expressed in ray
uint128 currentLiquidityRate;
//the current variable borrow rate. Expressed in ray
uint128 currentVariableBorrowRate;
//the current stable borrow rate. Expressed in ray
uint128 currentStableBorrowRate;
uint40 lastUpdateTimestamp;
//tokens addresses
address aTokenAddress;
address stableDebtTokenAddress;
address variableDebtTokenAddress;
//address of the interest rate strategy
address interestRateStrategyAddress;
//the id of the reserve. Represents the position in the list of the active reserves
uint8 id;
}
struct ReserveConfigurationMap {
//bit 0-15: LTV
//bit 16-31: Liq. threshold
//bit 32-47: Liq. bonus
//bit 48-55: Decimals
//bit 56: Reserve is active
//bit 57: reserve is frozen
//bit 58: borrowing is enabled
//bit 59: stable rate borrowing enabled
//bit 60-63: reserved
//bit 64-79: reserve factor
uint256 data;
}
struct UserConfigurationMap {
uint256 data;
}
enum InterestRateMode {NONE, STABLE, VARIABLE}
配套的在ReserveConfiguration.sol中对应了具体的设置和查看配置的函数。如setLtv和getLtv函数,用相应的mask来设置flag(reserve的configuration字段)。
uint256 constant LTV_MASK = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000; // prettier-ignore
/**
* @dev Sets the Loan to Value of the reserve
* @param self The reserve configuration
* @param ltv the new ltv
**/
function setLtv(DataTypes.ReserveConfigurationMap memory self, uint256 ltv) internal pure {
require(ltv <= MAX_VALID_LTV, Errors.RC_INVALID_LTV);
self.data = (self.data & LTV_MASK) | ltv;
}
/**
* @dev Gets the Loan to Value of the reserve
* @param self The reserve configuration
* @return The loan to value
**/
function getLtv(DataTypes.ReserveConfigurationMap storage self) internal view returns (uint256) {
return self.data & ~LTV_MASK;
}
ReserveConfiguration.sol其余的函数与之类似,这里就不展开分析。
对于UserConfigurationMap虽然在type里没有说明,不过根据UserConfiguration.sol的代码逻辑可以知道,用户的bitmap可以表示128种资产的状态,每个资产用两位表示。低位用来表示用户是否在这个资产中有贷款,0表示没有,1表示有。高位用于表示用户是否有这个资产的抵押物。由于每个函数的逻辑都是查看或者设置bitmap,这里同样也只举一个例子来阐述相关逻辑,如setUsingAsCollateral函数,就是将序号为index的资产设置为抵押,也就是将高位设置为1. isBorrowingAny就是用于查看用户有没有借款。
uint256 internal constant BORROWING_MASK =
0x5555555555555555555555555555555555555555555555555555555555555555; // 010101010101.....01
/**
* @dev Sets if the user is using as collateral the reserve identified by reserveIndex
* @param self The configuration object
* @param reserveIndex The index of the reserve in the bitmap
* @param usingAsCollateral True if the user is usin the reserve as collateral, false otherwise
**/
function setUsingAsCollateral(
DataTypes.UserConfigurationMap storage self,
uint256 reserveIndex,
bool usingAsCollateral
) internal {
require(reserveIndex < 128, Errors.UL_INVALID_INDEX);
self.data =
(self.data & ~(1 << (reserveIndex * 2 + 1))) |
(uint256(usingAsCollateral ? 1 : 0) << (reserveIndex * 2 + 1));
}
/**
* @dev Used to validate if a user has been borrowing from any reserve
* @param self The configuration object
* @return True if the user has been borrowing any reserve, false otherwise
**/
function isBorrowingAny(DataTypes.UserConfigurationMap memory self) internal pure returns (bool) {
return self.data & BORROWING_MASK != 0;
}
主要逻辑
v2版本将v1版本中core合约和dataprovider的逻辑分散到一些库中,主题功能没变,这里我们分析一些主要的逻辑
利率计算
对于线性利率,公式没有变,依旧是直接的线性累加:
function calculateLinearInterest(uint256 rate, uint40 lastUpdateTimestamp)
internal
view
returns (uint256)
{
//solium-disable-next-line
uint256 timeDifference = block.timestamp.sub(uint256(lastUpdateTimestamp));
return (rate.mul(timeDifference) / SECONDS_PER_YEAR).add(WadRayMath.ray());
}
对于复合利率,也就是利滚利模式,采用了多项式逼近的方式来计算((1+x)^n = 1+nx+[n/2(n-1)]x^2+[n/6(n-1)(n-2)x^3…)
/**
* @dev Function to calculate the interest using a compounded interest rate formula
* To avoid expensive exponentiation, the calculation is performed using a binomial approximation:
*
* (1+x)^n = 1+n*x+[n/2*(n-1)]*x^2+[n/6*(n-1)*(n-2)*x^3...
*
* The approximation slightly underpays liquidity providers and undercharges borrowers, with the advantage of great gas cost reductions
* The whitepaper contains reference to the approximation and a table showing the margin of error per different time periods
*
* @param rate The interest rate, in ray
* @param lastUpdateTimestamp The timestamp of the last update of the interest
* @return The interest rate compounded during the timeDelta, in ray
**/
function calculateCompoundedInterest(
uint256 rate,
uint40 lastUpdateTimestamp,
uint256 currentTimestamp
) internal pure returns (uint256) {
//solium-disable-next-line
uint256 exp = currentTimestamp.sub(uint256(lastUpdateTimestamp));
if (exp == 0) {
return WadRayMath.ray();
}
uint256 expMinusOne = exp - 1;
uint256 expMinusTwo = exp > 2 ? exp - 2 : 0;
uint256 ratePerSecond = rate / SECONDS_PER_YEAR;
uint256 basePowerTwo = ratePerSecond.rayMul(ratePerSecond);
uint256 basePowerThree = basePowerTwo.rayMul(ratePerSecond);
uint256 secondTerm = exp.mul(expMinusOne).mul(basePowerTwo) / 2;
uint256 thirdTerm = exp.mul(expMinusOne).mul(expMinusTwo).mul(basePowerThree) / 6;
return WadRayMath.ray().add(ratePerSecond.mul(exp)).add(secondTerm).add(thirdTerm);
}
储备逻辑
getNormalizedIncome用于计算储备归一化收入,也就是流动资金产生的利息收入,逻辑就是计算一下线性利率:
/**
* @dev Returns the ongoing normalized income for the reserve
* A value of 1e27 means there is no income. As time passes, the income is accrued
* A value of 2*1e27 means for each unit of asset one unit of income has been accrued
* @param reserve The reserve object
* @return the normalized income. expressed in ray
**/
function getNormalizedIncome(DataTypes.ReserveData storage reserve)
internal
view
returns (uint256)
{
uint40 timestamp = reserve.lastUpdateTimestamp;
//solium-disable-next-line
if (timestamp == uint40(block.timestamp)) {
//if the index was updated in the same block, no need to perform any calculation
return reserve.liquidityIndex;
}
uint256 cumulated =
MathUtils.calculateLinearInterest(reserve.currentLiquidityRate, timestamp).rayMul(
reserve.liquidityIndex
);
return cumulated;
}
与之对应的,getNormalizedDebt用于计算可变利率的贷款本息。
/**
* @dev Returns the ongoing normalized variable debt for the reserve
* A value of 1e27 means there is no debt. As time passes, the income is accrued
* A value of 2*1e27 means that for each unit of debt, one unit worth of interest has been accumulated
* @param reserve The reserve object
* @return The normalized variable debt. expressed in ray
**/
function getNormalizedDebt(DataTypes.ReserveData storage reserve)
internal
view
returns (uint256)
{
uint40 timestamp = reserve.lastUpdateTimestamp;
//solium-disable-next-line
if (timestamp == uint40(block.timestamp)) {
//if the index was updated in the same block, no need to perform any calculation
return reserve.variableBorrowIndex;
}
uint256 cumulated =
MathUtils.calculateCompoundedInterest(reserve.currentVariableBorrowRate, timestamp).rayMul(
reserve.variableBorrowIndex
);
return cumulated;
}
与v1版本不同的是,在更新储备状态时会将一部分产生的利息收入存入金库(treasury)中,具体的比例由储备的reserveFactor(configuration字段的一个值)来确定。
/**
* @dev Updates the liquidity cumulative index and the variable borrow index.
* @param reserve the reserve object
**/
function updateState(DataTypes.ReserveData storage reserve) internal {
uint256 scaledVariableDebt =
IVariableDebtToken(reserve.variableDebtTokenAddress).scaledTotalSupply();
uint256 previousVariableBorrowIndex = reserve.variableBorrowIndex;
uint256 previousLiquidityIndex = reserve.liquidityIndex;
uint40 lastUpdatedTimestamp = reserve.lastUpdateTimestamp;
(uint256 newLiquidityIndex, uint256 newVariableBorrowIndex) =
_updateIndexes(
reserve,
scaledVariableDebt,
previousLiquidityIndex,
previousVariableBorrowIndex,
lastUpdatedTimestamp
);
_mintToTreasury(
reserve,
scaledVariableDebt,
previousVariableBorrowIndex,
newLiquidityIndex,
newVariableBorrowIndex,
lastUpdatedTimestamp
);
}
通用逻辑
在GenericLogic中,主要是实现了针对用户资产健康程度、用户账户信息、可借款额度的调查实现。
balanceDecreaseAllowed建立在保证用户资产健康因子不低于阈值的条件下是否允许用户将抵押资产转移走。(具体概念见v1版本分析一节)。
calculateUserAccountData用于计算用户账户信息,包括:总流动性资金、抵押总额、借款总额、平均LTV、平均清算比率以及健康因子。
验证逻辑
ValidationLogic主要功能是检查一些用户的行为是否合法。
validateDeposit用于检查deposit行为的合法性,考量标准是存款额度是否为0、储备是否为激活以及是否是冻结状态。
/**
* @dev Validates a deposit action
* @param reserve The reserve object on which the user is depositing
* @param amount The amount to be deposited
*/
function validateDeposit(DataTypes.ReserveData storage reserve, uint256 amount) external view {
(bool isActive, bool isFrozen, , ) = reserve.configuration.getFlags();
require(amount != 0, Errors.VL_INVALID_AMOUNT);
require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
require(!isFrozen, Errors.VL_RESERVE_FROZEN);
}
validateWithdraw用于判断用户对于某一特定储备的取款行为是否合法,考量标准是取款金额是否介于0和用户余额之间,且满足取款后健康因子等参数小于阈值,并且目标储备是否激活。
/**
* @dev Validates a withdraw action
* @param reserveAddress The address of the reserve
* @param amount The amount to be withdrawn
* @param userBalance The balance of the user
* @param reservesData The reserves state
* @param userConfig The user configuration
* @param reserves The addresses of the reserves
* @param reservesCount The number of reserves
* @param oracle The price oracle
*/
function validateWithdraw(
address reserveAddress,
uint256 amount,
uint256 userBalance,
mapping(address => DataTypes.ReserveData) storage reservesData,
DataTypes.UserConfigurationMap storage userConfig,
mapping(uint256 => address) storage reserves,
uint256 reservesCount,
address oracle
) external view {
require(amount != 0, Errors.VL_INVALID_AMOUNT);
require(amount <= userBalance, Errors.VL_NOT_ENOUGH_AVAILABLE_USER_BALANCE);
(bool isActive, , , ) = reservesData[reserveAddress].configuration.getFlags();
require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
require(
GenericLogic.balanceDecreaseAllowed(
reserveAddress,
msg.sender,
amount,
reservesData,
userConfig,
reserves,
reservesCount,
oracle
),
Errors.VL_TRANSFER_NOT_ALLOWED
);
}
validateBorrow用于判断用户的借款是否合法,首先会对储备的基础信息如激活状态、冻结状态等做一个检查,并且获得借款的模式以及用户的基础信息:
ValidateBorrowLocalVars memory vars;
(vars.isActive, vars.isFrozen, vars.borrowingEnabled, vars.stableRateBorrowingEnabled) = reserve
.configuration
.getFlags();
require(vars.isActive, Errors.VL_NO_ACTIVE_RESERVE);
require(!vars.isFrozen, Errors.VL_RESERVE_FROZEN);
require(amount != 0, Errors.VL_INVALID_AMOUNT);
require(vars.borrowingEnabled, Errors.VL_BORROWING_NOT_ENABLED);
//validate interest rate mode
require(
uint256(DataTypes.InterestRateMode.VARIABLE) == interestRateMode ||
uint256(DataTypes.InterestRateMode.STABLE) == interestRateMode,
Errors.VL_INVALID_INTEREST_RATE_MODE_SELECTED
);
(
vars.userCollateralBalanceETH,
vars.userBorrowBalanceETH,
vars.currentLtv,
vars.currentLiquidationThreshold,
vars.healthFactor
) = GenericLogic.calculateUserAccountData(
userAddress,
reservesData,
userConfig,
reserves,
reservesCount,
oracle
);
而后会对于用户的抵押额度、借款额度、健康因子以及借贷需要的抵押金额做一个比较检查:
require(vars.userCollateralBalanceETH > 0, Errors.VL_COLLATERAL_BALANCE_IS_0);
require(
vars.healthFactor > GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD,
Errors.VL_HEALTH_FACTOR_LOWER_THAN_LIQUIDATION_THRESHOLD
);
//add the current already borrowed amount to the amount requested to calculate the total collateral needed.
vars.amountOfCollateralNeededETH = vars.userBorrowBalanceETH.add(amountInETH).percentDiv(
vars.currentLtv
); //LTV is calculated in percentage
require(
vars.amountOfCollateralNeededETH <= vars.userCollateralBalanceETH,
Errors.VL_COLLATERAL_CANNOT_COVER_NEW_BORROW
);
如果用户用的是稳定利率模式的贷款,则要做额外的检查,分别为:
- 储备必须支持稳定利率借贷模式
- 如果用户本身持有目标借贷资产作为抵押,则不能重复贷出这一资产。
- 用户只能借出目标储备的一部分总流动性资产。
if (interestRateMode == uint256(DataTypes.InterestRateMode.STABLE)) {
//check if the borrow mode is stable and if stable rate borrowing is enabled on this reserve
require(vars.stableRateBorrowingEnabled, Errors.VL_STABLE_BORROWING_NOT_ENABLED);
require(
!userConfig.isUsingAsCollateral(reserve.id) ||
reserve.configuration.getLtv() == 0 ||
amount > IERC20(reserve.aTokenAddress).balanceOf(userAddress),
Errors.VL_COLLATERAL_SAME_AS_BORROWING_CURRENCY
);
vars.availableLiquidity = IERC20(asset).balanceOf(reserve.aTokenAddress);
//calculate the max available loan size in stable rate mode as a percentage of the
//available liquidity
uint256 maxLoanSizeStable = vars.availableLiquidity.percentMul(maxStableLoanPercent);
require(amount <= maxLoanSizeStable, Errors.VL_AMOUNT_BIGGER_THAN_MAX_LOAN_SIZE_STABLE);
}
validateRepay用于判断用户的还款行为是否合法,主要考量就是储备的状态和还款利率合法性。并且如果还款用户不是msgsender的话,那么需要指明还款额度。
bool isActive = reserve.configuration.getActive();
require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
require(amountSent > 0, Errors.VL_INVALID_AMOUNT);
require(
(stableDebt > 0 &&
DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.STABLE) ||
(variableDebt > 0 &&
DataTypes.InterestRateMode(rateMode) == DataTypes.InterestRateMode.VARIABLE),
Errors.VL_NO_DEBT_OF_SELECTED_TYPE
);
require(
amountSent != uint256(-1) || msg.sender == onBehalfOf,
Errors.VL_NO_EXPLICIT_AMOUNT_TO_REPAY_ON_BEHALF
);
validateSwapRateMode用于检查用户的转换利率模式的合法性,除了基本的检查外,如果用户从可变利率模式转换到稳定利率模式,会考察用户是否存在恶意操纵利率的行为:
(bool isActive, bool isFrozen, , bool stableRateEnabled) = reserve.configuration.getFlags();
require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
require(!isFrozen, Errors.VL_RESERVE_FROZEN);
if (currentRateMode == DataTypes.InterestRateMode.STABLE) {
require(stableDebt > 0, Errors.VL_NO_STABLE_RATE_LOAN_IN_RESERVE);
} else if (currentRateMode == DataTypes.InterestRateMode.VARIABLE) {
require(variableDebt > 0, Errors.VL_NO_VARIABLE_RATE_LOAN_IN_RESERVE);
/**
* user wants to swap to stable, before swapping we need to ensure that
* 1. stable borrow rate is enabled on the reserve
* 2. user is not trying to abuse the reserve by depositing
* more collateral than he is borrowing, artificially lowering
* the interest rate, borrowing at variable, and switching to stable
**/
require(stableRateEnabled, Errors.VL_STABLE_BORROWING_NOT_ENABLED);
require(
!userConfig.isUsingAsCollateral(reserve.id) ||
reserve.configuration.getLtv() == 0 ||
stableDebt.add(variableDebt) > IERC20(reserve.aTokenAddress).balanceOf(msg.sender),
Errors.VL_COLLATERAL_SAME_AS_BORROWING_CURRENCY
);
} else {
revert(Errors.VL_INVALID_INTEREST_RATE_MODE_SELECTED);
}
validateRebalanceStableBorrowRate用于检查稳定利率重平衡条件,与v1版本检查条件基本相同,如果债务比例在流动资金的占比高于95或者流动性挖矿收益大于借贷利率时才会考虑重平衡:
(bool isActive, , , ) = reserve.configuration.getFlags();
require(isActive, Errors.VL_NO_ACTIVE_RESERVE);
//if the usage ratio is below 95%, no rebalances are needed
uint256 totalDebt =
stableDebtToken.totalSupply().add(variableDebtToken.totalSupply()).wadToRay();
uint256 availableLiquidity = IERC20(reserveAddress).balanceOf(aTokenAddress).wadToRay();
uint256 usageRatio = totalDebt == 0 ? 0 : totalDebt.rayDiv(availableLiquidity.add(totalDebt));
//if the liquidity rate is below REBALANCE_UP_THRESHOLD of the max variable APR at 95% usage,
//then we allow rebalancing of the stable rate positions.
uint256 currentLiquidityRate = reserve.currentLiquidityRate;
uint256 maxVariableBorrowRate =
IReserveInterestRateStrategy(reserve.interestRateStrategyAddress).getMaxVariableBorrowRate();
require(
usageRatio >= REBALANCE_UP_USAGE_RATIO_THRESHOLD &&
currentLiquidityRate <=
maxVariableBorrowRate.percentMul(REBALANCE_UP_LIQUIDITY_RATE_THRESHOLD),
Errors.LP_INTEREST_RATE_REBALANCE_CONDITIONS_NOT_MET
);
validateSetUseReserveAsCollateral用于验证用户将要抵押的资产的合法性,如果将要抵押的资产在此之前就被当作抵押资产,那么检查抵押资产所在储备的健康因子:
/**
* @dev Validates the action of setting an asset as collateral
* @param reserve The state of the reserve that the user is enabling or disabling as collateral
* @param reserveAddress The address of the reserve
* @param reservesData The data of all the reserves
* @param userConfig The state of the user for the specific reserve
* @param reserves The addresses of all the active reserves
* @param oracle The price oracle
*/
function validateSetUseReserveAsCollateral(
DataTypes.ReserveData storage reserve,
address reserveAddress,
bool useAsCollateral,
mapping(address => DataTypes.ReserveData) storage reservesData,
DataTypes.UserConfigurationMap storage userConfig,
mapping(uint256 => address) storage reserves,
uint256 reservesCount,
address oracle
) external view {
uint256 underlyingBalance = IERC20(reserve.aTokenAddress).balanceOf(msg.sender);
require(underlyingBalance > 0, Errors.VL_UNDERLYING_BALANCE_NOT_GREATER_THAN_0);
require(
useAsCollateral ||
GenericLogic.balanceDecreaseAllowed(
reserveAddress,
msg.sender,
underlyingBalance,
reservesData,
userConfig,
reserves,
reservesCount,
oracle
),
Errors.VL_DEPOSIT_ALREADY_IN_USE
);
}
validateLiquidationCall用于检查清算操作的合法性,具体检查内容和v1版本相同,这里不再赘述:
if (
!collateralReserve.configuration.getActive() || !principalReserve.configuration.getActive()
) {
return (
uint256(Errors.CollateralManagerErrors.NO_ACTIVE_RESERVE),
Errors.VL_NO_ACTIVE_RESERVE
);
}
if (userHealthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD) {
return (
uint256(Errors.CollateralManagerErrors.HEALTH_FACTOR_ABOVE_THRESHOLD),
Errors.LPCM_HEALTH_FACTOR_NOT_BELOW_THRESHOLD
);
}
bool isCollateralEnabled =
collateralReserve.configuration.getLiquidationThreshold() > 0 &&
userConfig.isUsingAsCollateral(collateralReserve.id);
//if collateral isn't enabled as collateral by user, it cannot be liquidated
if (!isCollateralEnabled) {
return (
uint256(Errors.CollateralManagerErrors.COLLATERAL_CANNOT_BE_LIQUIDATED),
Errors.LPCM_COLLATERAL_CANNOT_BE_LIQUIDATED
);
}
if (userStableDebt == 0 && userVariableDebt == 0) {
return (
uint256(Errors.CollateralManagerErrors.CURRRENCY_NOT_BORROWED),
Errors.LPCM_SPECIFIED_CURRENCY_NOT_BORROWED_BY_USER
);
}
return (uint256(Errors.CollateralManagerErrors.NO_ERROR), Errors.LPCM_NO_ERRORS);
validateTransfer用于检查转账操作合法性,主要就是检查健康因子。
/**
* @dev Validates an aToken transfer
* @param from The user from which the aTokens are being transferred
* @param reservesData The state of all the reserves
* @param userConfig The state of the user for the specific reserve
* @param reserves The addresses of all the active reserves
* @param oracle The price oracle
*/
function validateTransfer(
address from,
mapping(address => DataTypes.ReserveData) storage reservesData,
DataTypes.UserConfigurationMap storage userConfig,
mapping(uint256 => address) storage reserves,
uint256 reservesCount,
address oracle
) internal view {
(, , , , uint256 healthFactor) =
GenericLogic.calculateUserAccountData(
from,
reservesData,
userConfig,
reserves,
reservesCount,
oracle
);
require(
healthFactor >= GenericLogic.HEALTH_FACTOR_LIQUIDATION_THRESHOLD,
Errors.VL_TRANSFER_NOT_ALLOWED
);
}
}
token
atoken
与V1版本一样,atoken的作用还是代表用户存储的标的资产的凭证。
用户可以通过burn和mint函数来用atoken取回标的资产或者用标的资产换成atoken。
在这里需要说明的是,铸币和销毁函数传入的数额并不是最终真正产生或销毁的数额,而是scaled amount,具体来说就是传入的数值除以一个指数index。
index为储备(交易池)创建以来,单位流动性累积的本息乘数因子。也就是说,这里的index指的是(流动性本金+累积的利息收入)/流动性本金。
那么为什么要在这里将用户存入的数量进行一个缩减呢?原因就是要保证本金和利息的比值不变(因为用户的本金会随着时间产生额外的收入),方便后续计算。具体来说就是:假设用户在 t_current 时刻存入 amount 数量,那么如果 t_0 时刻(池子创建时)存入了 amountScaled ,通过复利累计本息,直到 t_current 时刻,其数量正好等于 amount。也就是保证即 amount = amountScaled * index
,这里的 index 记录的就是 t_0 时刻到当前 t_current 时刻,流动性累计的本息乘数因子。amountScaled 就是 aToken 实际记录的数量,由于全部缩放至 t_0 时刻,这样就抹平了不同抵押操作的时间和利率的不同,可以全部统一计算。
/**
* @dev Burns aTokens from `user` and sends the equivalent amount of underlying to `receiverOfUnderlying`
* - Only callable by the LendingPool, as extra state updates there need to be managed
* @param user The owner of the aTokens, getting them burned
* @param receiverOfUnderlying The address that will receive the underlying
* @param amount The amount being burned
* @param index The new liquidity index of the reserve
**/
function burn(
address user,
address receiverOfUnderlying,
uint256 amount,
uint256 index
) external override onlyLendingPool {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT);
_burn(user, amountScaled);
IERC20(_underlyingAsset).safeTransfer(receiverOfUnderlying, amount);
emit Transfer(user, address(0), amount);
emit Burn(user, receiverOfUnderlying, amount, index);
}
/**
* @dev Mints `amount` aTokens to `user`
* - Only callable by the LendingPool, as extra state updates there need to be managed
* @param user The address receiving the minted tokens
* @param amount The amount of tokens getting minted
* @param index The new liquidity index of the reserve
* @return `true` if the the previous balance of the user was 0
*/
function mint(
address user,
uint256 amount,
uint256 index
) external override onlyLendingPool returns (bool) {
uint256 previousBalance = super.balanceOf(user);
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT);
_mint(user, amountScaled);
emit Transfer(address(0), user, amount);
emit Mint(user, amount, index);
return previousBalance == 0;
}
在这里,balanceOf与total supply代表的是本金加产生的利息收益,同样是缩放成scaled形式来计算。
/**
* @dev Calculates the balance of the user: principal balance + interest generated by the principal
* @param user The user whose balance is calculated
* @return The balance of the user
**/
function balanceOf(address user)
public
view
override(IncentivizedERC20, IERC20)
returns (uint256)
{
return super.balanceOf(user).rayMul(_pool.getReserveNormalizedIncome(_underlyingAsset));
}
/**
* @dev calculates the total supply of the specific aToken
* since the balance of every single user increases over time, the total supply
* does that too.
* @return the current total supply
**/
function totalSupply() public view override(IncentivizedERC20, IERC20) returns (uint256) {
uint256 currentSupplyScaled = super.totalSupply();
if (currentSupplyScaled == 0) {
return 0;
}
return currentSupplyScaled.rayMul(_pool.getReserveNormalizedIncome(_underlyingAsset));
}
对于token的approve,这里采用了EIP2612,使用签名的方式来验证合法性:
/**
* @dev implements the permit function as for
* https://github.com/ethereum/EIPs/blob/8a34d644aacf0f9f8f00815307fd7dd5da07655f/EIPS/eip-2612.md
* @param owner The owner of the funds
* @param spender The spender
* @param value The amount
* @param deadline The deadline timestamp, type(uint256).max for max deadline
* @param v Signature param
* @param s Signature param
* @param r Signature param
*/
function permit(
address owner,
address spender,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
require(owner != address(0), 'INVALID_OWNER');
//solium-disable-next-line
require(block.timestamp <= deadline, 'INVALID_EXPIRATION');
uint256 currentValidNonce = _nonces[owner];
bytes32 digest =
keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, currentValidNonce, deadline))
)
);
require(owner == ecrecover(digest, v, r, s), 'INVALID_SIGNATURE');
_nonces[owner] = currentValidNonce.add(1);
_approve(owner, spender, value);
}
debtTokenBase
用于表示债务的代币基础表示,可变利率或者稳定利率债务都派生自这一代币类型。本质上是erc20代币,不过这一token代表的是用户持有债务数量,所以transfer、allowance、approve、transferfrom等功能都不允许,只保留了mint和burn操作。同时增加了允许用户信贷委托的功能,允许其他的地址以用户的名义借款:
function approveDelegation(address delegatee, uint256 amount) external override {
_borrowAllowances[_msgSender()][delegatee] = amount;
emit BorrowAllowanceDelegated(_msgSender(), delegatee, _getUnderlyingAssetAddress(), amount);
}
function _decreaseBorrowAllowance(
address delegator,
address delegatee,
uint256 amount
) internal {
uint256 newAllowance =
_borrowAllowances[delegator][delegatee].sub(amount, Errors.BORROW_ALLOWANCE_NOT_ENOUGH);
_borrowAllowances[delegator][delegatee] = newAllowance;
emit BorrowAllowanceDelegated(delegator, delegatee, _getUnderlyingAssetAddress(), newAllowance);
}
stableDebtToken
稳定利率的贷款代币继承于debtTokenBase,在这里需要明确一下,用户的ERC20代币数量代表的是借贷本金,而通过balanceOf查询到的余额指的是本金+利息。
/**
* @dev Calculates the current user debt balance
* @return The accumulated debt of the user
**/
function balanceOf(address account) public view virtual override returns (uint256) {
uint256 accountBalance = super.balanceOf(account);
uint256 stableRate = _usersStableRate[account];
if (accountBalance == 0) {
return 0;
}
uint256 cumulatedInterest =
MathUtils.calculateCompoundedInterest(stableRate, _timestamps[account]);
return accountBalance.rayMul(cumulatedInterest);
}
所以在这里计算利息就是两者的差额:
/**
* @dev Calculates the increase in balance since the last user interaction
* @param user The address of the user for which the interest is being accumulated
* @return The previous principal balance, the new principal balance and the balance increase
**/
function _calculateBalanceIncrease(address user)
internal
view
returns (
uint256,
uint256,
uint256
)
{
uint256 previousPrincipalBalance = super.balanceOf(user);
if (previousPrincipalBalance == 0) {
return (0, 0, 0);
}
// Calculation of the accrued interest since the last accumulation
uint256 balanceIncrease = balanceOf(user).sub(previousPrincipalBalance);
return (
previousPrincipalBalance,
previousPrincipalBalance.add(balanceIncrease),
balanceIncrease
);
}
贷款代币的铸币和销毁原子操作比较直白,就是将用户的贷款记录做一个简单的加减:
/**
* @dev Mints stable debt tokens to an user
* @param account The account receiving the debt tokens
* @param amount The amount being minted
* @param oldTotalSupply the total supply before the minting event
**/
function _mint(
address account,
uint256 amount,
uint256 oldTotalSupply
) internal {
uint256 oldAccountBalance = _balances[account];
_balances[account] = oldAccountBalance.add(amount);
if (address(_incentivesController) != address(0)) {
_incentivesController.handleAction(account, oldTotalSupply, oldAccountBalance);
}
}
/**
* @dev Burns stable debt tokens of an user
* @param account The user getting his debt burned
* @param amount The amount being burned
* @param oldTotalSupply The total supply before the burning event
**/
function _burn(
address account,
uint256 amount,
uint256 oldTotalSupply
) internal {
uint256 oldAccountBalance = _balances[account];
_balances[account] = oldAccountBalance.sub(amount, Errors.SDT_BURN_EXCEEDS_BALANCE);
if (address(_incentivesController) != address(0)) {
_incentivesController.handleAction(account, oldTotalSupply, oldAccountBalance);
}
}
当用户产生贷款时,lendingpool会调用mint函数,其中,user是接受贷款标的资产的地址,onbehalfof是承贷债务代币的地址。如果二者不相同,则会减少相应的贷款allowance。在铸造贷款代币之前,会计算onbehalfof累计的利息,并且更新贷款代币的总额,更新用户新的稳定贷款利率以及平均加权稳定利率。
/**
* @dev Mints debt token to the `onBehalfOf` address.
* - Only callable by the LendingPool
* - The resulting rate is the weighted average between the rate of the new debt
* and the rate of the previous debt
* @param user The address receiving the borrowed underlying, being the delegatee in case
* of credit delegate, or same as `onBehalfOf` otherwise
* @param onBehalfOf The address receiving the debt tokens
* @param amount The amount of debt tokens to mint
* @param rate The rate of the debt being minted
**/
function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 rate
) external override onlyLendingPool returns (bool) {
MintLocalVars memory vars;
if (user != onBehalfOf) {
_decreaseBorrowAllowance(onBehalfOf, user, amount);
}
(, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(onBehalfOf);
vars.previousSupply = totalSupply();
vars.currentAvgStableRate = _avgStableRate;
vars.nextSupply = _totalSupply = vars.previousSupply.add(amount);
vars.amountInRay = amount.wadToRay();
vars.newStableRate = _usersStableRate[onBehalfOf]
.rayMul(currentBalance.wadToRay())
.add(vars.amountInRay.rayMul(rate))
.rayDiv(currentBalance.add(amount).wadToRay());
require(vars.newStableRate <= type(uint128).max, Errors.SDT_STABLE_DEBT_OVERFLOW);
_usersStableRate[onBehalfOf] = vars.newStableRate;
//solium-disable-next-line
_totalSupplyTimestamp = _timestamps[onBehalfOf] = uint40(block.timestamp);
// Calculates the updated average stable rate
vars.currentAvgStableRate = _avgStableRate = vars
.currentAvgStableRate
.rayMul(vars.previousSupply.wadToRay())
.add(rate.rayMul(vars.amountInRay))
.rayDiv(vars.nextSupply.wadToRay());
_mint(onBehalfOf, amount.add(balanceIncrease), vars.previousSupply);
emit Transfer(address(0), onBehalfOf, amount);
emit Mint(
user,
onBehalfOf,
amount,
currentBalance,
balanceIncrease,
vars.newStableRate,
vars.currentAvgStableRate,
vars.nextSupply
);
return currentBalance == 0;
}
与之对应的,当用户发生还款操作时,LendingPool会调用burn函数销毁用户的贷款代币。由于总贷款额和每个用户的贷款额是分别增长的,所以可能会出现用户还款超过了总贷款额的情况(比如就一个用户借款,总贷款额只记录了用户的本金,没有计算利率,就会出现这样的情况),出现这种情况的话就将总贷款额和平均加权稳定利率设置为0,否则的话就正常设置。如果用户的还款额少于产生的利息,那么优先还利息,并铸造产生的利息数目的贷款代币,否则就消除用户的债务代币。
/**
* @dev Burns debt of `user`
* @param user The address of the user getting his debt burned
* @param amount The amount of debt tokens getting burned
**/
function burn(address user, uint256 amount) external override onlyLendingPool {
(, uint256 currentBalance, uint256 balanceIncrease) = _calculateBalanceIncrease(user);
uint256 previousSupply = totalSupply();
uint256 newAvgStableRate = 0;
uint256 nextSupply = 0;
uint256 userStableRate = _usersStableRate[user];
// Since the total supply and each single user debt accrue separately,
// there might be accumulation errors so that the last borrower repaying
// mght actually try to repay more than the available debt supply.
// In this case we simply set the total supply and the avg stable rate to 0
if (previousSupply <= amount) {
_avgStableRate = 0;
_totalSupply = 0;
} else {
nextSupply = _totalSupply = previousSupply.sub(amount);
uint256 firstTerm = _avgStableRate.rayMul(previousSupply.wadToRay());
uint256 secondTerm = userStableRate.rayMul(amount.wadToRay());
// For the same reason described above, when the last user is repaying it might
// happen that user rate * user balance > avg rate * total supply. In that case,
// we simply set the avg rate to 0
if (secondTerm >= firstTerm) {
newAvgStableRate = _avgStableRate = _totalSupply = 0;
} else {
newAvgStableRate = _avgStableRate = firstTerm.sub(secondTerm).rayDiv(nextSupply.wadToRay());
}
}
if (amount == currentBalance) {
_usersStableRate[user] = 0;
_timestamps[user] = 0;
} else {
//solium-disable-next-line
_timestamps[user] = uint40(block.timestamp);
}
//solium-disable-next-line
_totalSupplyTimestamp = uint40(block.timestamp);
if (balanceIncrease > amount) {
uint256 amountToMint = balanceIncrease.sub(amount);
_mint(user, amountToMint, previousSupply);
emit Mint(
user,
user,
amountToMint,
currentBalance,
balanceIncrease,
userStableRate,
newAvgStableRate,
nextSupply
);
} else {
uint256 amountToBurn = amount.sub(balanceIncrease);
_burn(user, amountToBurn, previousSupply);
emit Burn(user, amountToBurn, currentBalance, balanceIncrease, newAvgStableRate, nextSupply);
}
emit Transfer(user, address(0), amount);
}
variableDebtToken
与稳定利率债务代币类似的逻辑,主要区别在于mint和burn函数,这里没有债务区分利息和本金,将二者合二为一,还款欠款的数额会除以一个指数:
/**
* @dev Mints debt token to the `onBehalfOf` address
* - Only callable by the LendingPool
* @param user The address receiving the borrowed underlying, being the delegatee in case
* of credit delegate, or same as `onBehalfOf` otherwise
* @param onBehalfOf The address receiving the debt tokens
* @param amount The amount of debt being minted
* @param index The variable debt index of the reserve
* @return `true` if the the previous balance of the user is 0
**/
function mint(
address user,
address onBehalfOf,
uint256 amount,
uint256 index
) external override onlyLendingPool returns (bool) {
if (user != onBehalfOf) {
_decreaseBorrowAllowance(onBehalfOf, user, amount);
}
uint256 previousBalance = super.balanceOf(onBehalfOf);
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_MINT_AMOUNT);
_mint(onBehalfOf, amountScaled);
emit Transfer(address(0), onBehalfOf, amount);
emit Mint(user, onBehalfOf, amount, index);
return previousBalance == 0;
}
/**
* @dev Burns user variable debt
* - Only callable by the LendingPool
* @param user The user whose debt is getting burned
* @param amount The amount getting burned
* @param index The variable debt index of the reserve
**/
function burn(
address user,
uint256 amount,
uint256 index
) external override onlyLendingPool {
uint256 amountScaled = amount.rayDiv(index);
require(amountScaled != 0, Errors.CT_INVALID_BURN_AMOUNT);
_burn(user, amountScaled);
emit Transfer(user, address(0), amount);
emit Burn(user, amount, index);
}
defaultInterestStrategy
与v1版本略有不同,体现在最终的流动性收益计算时会考虑将流动性挖矿的一些奖励传入给金库(tresury)中。
vars.currentLiquidityRate = _getOverallBorrowRate(
totalStableDebt,
totalVariableDebt,
vars
.currentVariableBorrowRate,
averageStableBorrowRate
)
.rayMul(vars.utilizationRate)
.percentMul(PercentageMath.PERCENTAGE_FACTOR.sub(reserveFactor));
lendingpool
主题逻辑和V1版本差别不大,只不过债务的关系通过代比表示而不是记账的方式。在这里,细节的逻辑都在之前的主要逻辑一节涉及到,实现与v1版本一样的函数就不再赘述(如deposit、withdraw等等),差异比较大的是闪电贷相关的实现。
在v1版本中,闪电贷的实现只是一个雏形,提供了一个基本的接口;在v2版本中,aave协议的闪电贷融合了一些自己的实现,具体体现在:
- 用户可以在一次闪电贷中借出多个种类的资产。
- 如果闪电贷没有还清,可以用aave中抵押借贷的逻辑来用长期还款的方式结清余款。
参数分析:
- receiverAddress:接受闪电贷贷款的合约地址,需要内置IFlashLoanReceiver接口函数来处理闪电贷逻辑。
- assets:地址数组,表示要借取的资产地址。
- amounts:uint数组,表示assets数组中的借取资产数额。
- modes:uint数组,标记assets资产数组中的资产如果没有在闪电贷周期结束时还清,采用何种方式处理余款。0表示直接revert,1表示稳定利率贷款,2表示可变利率贷款。
- onBehalfOf:承担债务方
- params:传递给接收闪电贷合约的参数
- referralCode:标记整合代理费中间人,用于获取潜在的奖励,0代表没有中间人。
前面的逻辑就是算出来各个资产的闪电贷手续费,然后将贷款转给接受者,等待其执行完成:
for (vars.i = 0; vars.i < assets.length; vars.i++) {
aTokenAddresses[vars.i] = _reserves[assets[vars.i]].aTokenAddress;
premiums[vars.i] = amounts[vars.i].mul(_flashLoanPremiumTotal).div(10000);
IAToken(aTokenAddresses[vars.i]).transferUnderlyingTo(receiverAddress, amounts[vars.i]);
}
require(
vars.receiver.executeOperation(assets, amounts, premiums, msg.sender, params),
Errors.LP_INVALID_FLASH_LOAN_EXECUTOR_RETURN
);
当闪电贷结束后,会检查用户是否还完贷款,如果还完的话则正常更新资产状态,收取手续费。
如果没有还完的话,则执行长期贷款的逻辑:
for (vars.i = 0; vars.i < assets.length; vars.i++) {
vars.currentAsset = assets[vars.i];
vars.currentAmount = amounts[vars.i];
vars.currentPremium = premiums[vars.i];
vars.currentATokenAddress = aTokenAddresses[vars.i];
vars.currentAmountPlusPremium = vars.currentAmount.add(vars.currentPremium);
if (DataTypes.InterestRateMode(modes[vars.i]) == DataTypes.InterestRateMode.NONE) {
_reserves[vars.currentAsset].updateState();
_reserves[vars.currentAsset].cumulateToLiquidityIndex(
IERC20(vars.currentATokenAddress).totalSupply(),
vars.currentPremium
);
_reserves[vars.currentAsset].updateInterestRates(
vars.currentAsset,
vars.currentATokenAddress,
vars.currentAmountPlusPremium,
0
);
IERC20(vars.currentAsset).safeTransferFrom(
receiverAddress,
vars.currentATokenAddress,
vars.currentAmountPlusPremium
);
} else {
// If the user chose to not return the funds, the system checks if there is enough collateral and
// eventually opens a debt position
_executeBorrow(
ExecuteBorrowParams(
vars.currentAsset,
msg.sender,
onBehalfOf,
vars.currentAmount,
modes[vars.i],
vars.currentATokenAddress,
referralCode,
false
)
);
}
emit FlashLoan(
receiverAddress,
msg.sender,
vars.currentAsset,
vars.currentAmount,
vars.currentPremium,
referralCode
);
}
v3
主要改进
v3版本在保留了AAVE v1 和v2版本的核心功能(流动性挖矿、抵押借贷等)的同时,提供了更多的新特性并且做了一些优化改动。具体体现在如下方面:
- 资本效率提升:允许用户更将手中的资产的流动性挖矿收益和抵押借贷能力发挥最大效能。
- potal:直译过来就是传送门,允许不同区块链网络上的AAVE v3协议的流动性代币进行流通,也就是跨链桥接口,在一条链销毁流动性代币的同时在另一条链上生成流动性代币。此时原链上的资产可以作为另一条链上的抵押资产来使用。
- 效率模式(eMode):当用户抵押资产和借贷资产类别相近或在价格上有关联(比如都是稳定币),用户就可以拥有更高的借款额度。可以将抵押借贷率提升到 95%+,这将极大的提高资本利用率。当然这个抵押率的前提是同类币种,例如:抵押 USDT - 借 DAI,最多可支持 255 种币种。
- 隔离模式:当管理者(可以是DAO)将抵押资产设置为隔离模式后这一抵押资产就不能用作其他资产的抵押物,并且这一抵押资产只能借贷稳定币。这一模式主要针对的是一些不是主流币的次级项目,由于次级项目具有较大的波动性,所以aave不允许将多个次级资产同时作为隔离模式的抵押物。AAVE V2 中支持抵押的资产都是主流资产,但是用户受众可能会有很多次级资产,V2 中这部分资金目前是没有被利用起来的,如果将次级资产和主流资产同等对待作为抵押资产,AAVE 将会承担更大的风险,与时 V3 更新了这个隔离模式,用户只能将一种次级资产作为隔离模式的抵押资产,无法和主流资产混合,两者之间必须隔离。如果你使用了一个次级资产作为抵押,则其他资产不能作为抵押品,只能当做存款。如果使用了其他资产作为抵押物,则任意一个次级资产都无法作为抵押品。
- 风险管理
- 借贷&供给阈值:每一种资产现在都可以明确一个阈值,规定有多少资产可以用于借贷&供给给aave协议。
- 颗粒化借贷能力控制:在v3中每一种资产都可以将其借贷能力做一个控制甚至是0%,同时对于贷款者没有影响。
- 风险管理:aave的管理者现在可以是DAO组织。
- 预言机哨兵:现在预言机的价格会用一个有效期。
- 可变清算逼近指数:v3将资产清算做了一个改良,当资产达到清算阈值时可以被全部清算。
项目构成
├── dependencies
│ ├── chainlink
│ │ └── AggregatorInterface.sol
│ ├── gnosis
│ │ └── contracts
│ │ └── GPv2SafeERC20.sol
│ ├── openzeppelin
│ │ ├── contracts
│ │ │ ├── AccessControl.sol
│ │ │ ├── Address.sol
│ │ │ ├── Context.sol
│ │ │ ├── ERC165.sol
│ │ │ ├── ERC20.sol
│ │ │ ├── IAccessControl.sol
│ │ │ ├── IERC165.sol
│ │ │ ├── IERC20.sol
│ │ │ ├── IERC20Detailed.sol
│ │ │ ├── Ownable.sol
│ │ │ ├── SafeCast.sol
│ │ │ ├── SafeMath.sol
│ │ │ └── Strings.sol
│ │ └── upgradeability
│ │ ├── AdminUpgradeabilityProxy.sol
│ │ ├── BaseAdminUpgradeabilityProxy.sol
│ │ ├── BaseUpgradeabilityProxy.sol
│ │ ├── Initializable.sol
│ │ ├── InitializableAdminUpgradeabilityProxy.sol
│ │ ├── InitializableUpgradeabilityProxy.sol
│ │ ├── Proxy.sol
│ │ └── UpgradeabilityProxy.sol
│ └── weth
│ └── WETH9.sol
├── deployments
│ └── ReservesSetupHelper.sol
├── flashloan
│ ├── base
│ │ ├── FlashLoanReceiverBase.sol
│ │ ├── FlashLoanSimpleReceiverBase.sol
│ │ └── LICENSE.md
│ └── interfaces
│ ├── IFlashLoanReceiver.sol
│ ├── IFlashLoanSimpleReceiver.sol
│ └── LICENSE.md
├── interfaces
│ ├── IACLManager.sol
│ ├── IAToken.sol
│ ├── IAaveIncentivesController.sol
│ ├── IAaveOracle.sol
│ ├── ICreditDelegationToken.sol
│ ├── IDelegationToken.sol
│ ├── IERC20WithPermit.sol
│ ├── IInitializableAToken.sol
│ ├── IInitializableDebtToken.sol
│ ├── IL2Pool.sol
│ ├── IPool.sol
│ ├── IPoolAddressesProvider.sol
│ ├── IPoolAddressesProviderRegistry.sol
│ ├── IPoolConfigurator.sol
│ ├── IPoolDataProvider.sol
│ ├── IPriceOracle.sol
│ ├── IPriceOracleGetter.sol
│ ├── IPriceOracleSentinel.sol
│ ├── IReserveInterestRateStrategy.sol
│ ├── IScaledBalanceToken.sol
│ ├── ISequencerOracle.sol
│ ├── IStableDebtToken.sol
│ ├── IVariableDebtToken.sol
│ └── LICENSE.md
├── misc
│ ├── AaveOracle.sol
│ ├── AaveProtocolDataProvider.sol
│ ├── L2Encoder.sol
│ └── interfaces
│ ├── IWETH.sol
│ └── LICENSE.md
├── mocks
│ ├── flashloan
│ │ ├── MockFlashLoanReceiver.sol
│ │ └── MockSimpleFlashLoanReceiver.sol
│ ├── helpers
│ │ ├── MockIncentivesController.sol
│ │ ├── MockL2Pool.sol
│ │ ├── MockPool.sol
│ │ ├── MockReserveConfiguration.sol
│ │ └── SelfDestructTransfer.sol
│ ├── oracle
│ │ ├── CLAggregators
│ │ │ └── MockAggregator.sol
│ │ ├── PriceOracle.sol
│ │ └── SequencerOracle.sol
│ ├── tests
│ │ ├── FlashloanAttacker.sol
│ │ ├── MockReserveInterestRateStrategy.sol
│ │ └── WadRayMathWrapper.sol
│ ├── tokens
│ │ ├── MintableDelegationERC20.sol
│ │ ├── MintableERC20.sol
│ │ └── WETH9Mocked.sol
│ └── upgradeability
│ ├── MockAToken.sol
│ ├── MockInitializableImplementation.sol
│ ├── MockStableDebtToken.sol
│ └── MockVariableDebtToken.sol
└── protocol
├── configuration
│ ├── ACLManager.sol
│ ├── PoolAddressesProvider.sol
│ ├── PoolAddressesProviderRegistry.sol
│ └── PriceOracleSentinel.sol
├── libraries
│ ├── aave-upgradeability
│ │ ├── BaseImmutableAdminUpgradeabilityProxy.sol
│ │ ├── InitializableImmutableAdminUpgradeabilityProxy.sol
│ │ └── VersionedInitializable.sol
│ ├── configuration
│ │ ├── ReserveConfiguration.sol
│ │ └── UserConfiguration.sol
│ ├── helpers
│ │ ├── Errors.sol
│ │ └── Helpers.sol
│ ├── logic
│ │ ├── BorrowLogic.sol
│ │ ├── BridgeLogic.sol
│ │ ├── CalldataLogic.sol
│ │ ├── ConfiguratorLogic.sol
│ │ ├── EModeLogic.sol
│ │ ├── FlashLoanLogic.sol
│ │ ├── GenericLogic.sol
│ │ ├── IsolationModeLogic.sol
│ │ ├── LiquidationLogic.sol
│ │ ├── PoolLogic.sol
│ │ ├── ReserveLogic.sol
│ │ ├── SupplyLogic.sol
│ │ └── ValidationLogic.sol
│ ├── math
│ │ ├── MathUtils.sol
│ │ ├── PercentageMath.sol
│ │ └── WadRayMath.sol
│ └── types
│ ├── ConfiguratorInputTypes.sol
│ └── DataTypes.sol
├── pool
│ ├── DefaultReserveInterestRateStrategy.sol
│ ├── L2Pool.sol
│ ├── Pool.sol
│ ├── PoolConfigurator.sol
│ └── PoolStorage.sol
└── tokenization
├── AToken.sol
├── DelegationAwareAToken.sol
├── StableDebtToken.sol
├── VariableDebtToken.sol
└── base
├── DebtTokenBase.sol
├── EIP712Base.sol
├── IncentivizedERC20.sol
├── MintableIncentivizedERC20.sol
└── ScaledBalanceTokenBase.sol
35 directories, 123 files
核心合约
数据结构
v3版本在v2版本本身的数据结构上又增加了一些新的内容。
首先,储备的数据结构和flag标识位有了新的变化,增加了accruedToTreasury、unbacked和isolationModeTotalDebt字段,并且flag标识位也增加了从80-251bit的标识。其中80-115位为borrowcap,即贷款预算,与之对应的supply cap为供给预算,二者规定了储备的借款和供给额度,0代表没有上限。emodecatagory用于标记储备资产所属的类型,用于emode模式(进行相同类型的资产借贷时可以采用emode来达到更高的抵押借贷比率)。
struct ReserveData {
//stores the reserve configuration
ReserveConfigurationMap configuration;
//the liquidity index. Expressed in ray
uint128 liquidityIndex;
//the current supply rate. Expressed in ray
uint128 currentLiquidityRate;
//variable borrow index. Expressed in ray
uint128 variableBorrowIndex;
//the current variable borrow rate. Expressed in ray
uint128 currentVariableBorrowRate;
//the current stable borrow rate. Expressed in ray
uint128 currentStableBorrowRate;
//timestamp of last update
uint40 lastUpdateTimestamp;
//the id of the reserve. Represents the position in the list of the active reserves
uint16 id;
//aToken address
address aTokenAddress;
//stableDebtToken address
address stableDebtTokenAddress;
//variableDebtToken address
address variableDebtTokenAddress;
//address of the interest rate strategy
address interestRateStrategyAddress;
//the current treasury balance, scaled
uint128 accruedToTreasury;
//the outstanding unbacked aTokens minted through the bridging feature
uint128 unbacked;
//the outstanding debt borrowed against this asset in isolation mode
uint128 isolationModeTotalDebt;
}
struct ReserveConfigurationMap {
//bit 0-15: LTV
//bit 16-31: Liq. threshold
//bit 32-47: Liq. bonus
//bit 48-55: Decimals
//bit 56: reserve is active
//bit 57: reserve is frozen
//bit 58: borrowing is enabled
//bit 59: stable rate borrowing enabled
//bit 60: asset is paused
//bit 61: borrowing in isolation mode is enabled
//bit 62-63: reserved
//bit 64-79: reserve factor
//bit 80-115 borrow cap in whole tokens, borrowCap == 0 => no cap
//bit 116-151 supply cap in whole tokens, supplyCap == 0 => no cap
//bit 152-167 liquidation protocol fee
//bit 168-175 eMode category
//bit 176-211 unbacked mint cap in whole tokens, unbackedMintCap == 0 => minting disabled
//bit 212-251 debt ceiling for isolation mode with (ReserveConfiguration::DEBT_CEILING_DECIMALS) decimals
//bit 252-255 unused
uint256 data;
}
同时为了减少合约函数执行时多次的读取链上数据以及函数调用造成效率的下降,这里利用了缓存的方式来保存储备的基础信息。
struct ReserveCache {
uint256 currScaledVariableDebt;
uint256 nextScaledVariableDebt;
uint256 currPrincipalStableDebt;
uint256 currAvgStableBorrowRate;
uint256 currTotalStableDebt;
uint256 nextAvgStableBorrowRate;
uint256 nextTotalStableDebt;
uint256 currLiquidityIndex;
uint256 nextLiquidityIndex;
uint256 currVariableBorrowIndex;
uint256 nextVariableBorrowIndex;
uint256 currLiquidityRate;
uint256 currVariableBorrowRate;
uint256 reserveFactor;
ReserveConfigurationMap reserveConfiguration;
address aTokenAddress;
address stableDebtTokenAddress;
address variableDebtTokenAddress;
uint40 reserveLastUpdateTimestamp;
uint40 stableDebtLastUpdateTimestamp;
}
对于新增的emode,采用了一个数据结构来记录每一类别资产的emode信息:
struct EModeCategory {
// each eMode category has a custom ltv and liquidation threshold
uint16 ltv;
uint16 liquidationThreshold;
uint16 liquidationBonus;
// each eMode category may or may not have a custom oracle to override the individual assets price oracles
address priceSource;
string label;
}
tokens
atoken
基本实现与v2版本相同,不同的是增加了rescue函数,用于在项目出现意外情况时由管理者转移出资金:
/// @inheritdoc IAToken
function rescueTokens(
address token,
address to,
uint256 amount
) external override onlyPoolAdmin {
require(token != _underlyingAsset, Errors.UNDERLYING_CANNOT_BE_RESCUED);
IERC20(token).safeTransfer(to, amount);
}
}
debtTokenBase
基本实现与v2相同,但是增加了一个delegationWithSig函数,通过签名的方式来进行贷款的代理,主要的思路就是通过ercrecover函数来验证代理发起人的合法性。
function approveDelegation(address delegatee, uint256 amount) external override {
_approveDelegation(_msgSender(), delegatee, amount);
}
/// @inheritdoc ICreditDelegationToken
function delegationWithSig(
address delegator,
address delegatee,
uint256 value,
uint256 deadline,
uint8 v,
bytes32 r,
bytes32 s
) external {
require(delegator != address(0), Errors.ZERO_ADDRESS_NOT_VALID);
//solium-disable-next-line
require(block.timestamp <= deadline, Errors.INVALID_EXPIRATION);
uint256 currentValidNonce = _nonces[delegator];
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR(),
keccak256(
abi.encode(DELEGATION_WITH_SIG_TYPEHASH, delegatee, value, currentValidNonce, deadline)
)
)
);
require(delegator == ecrecover(digest, v, r, s), Errors.INVALID_SIGNATURE);
_nonces[delegator] = currentValidNonce + 1;
_approveDelegation(delegator, delegatee, value);
}
stableDebtToken
与v2版本实现相同,详见v2版本分析
variableDebtToken
与v2版本实现基本相同,不同的是将scaledtoken单独拿出来做了个token base,原理一样,这里不再赘述。
delegationAwaredToken
赋予了Atoken投票的功能,主要实现就是一个delegate函数,将标的资产的投票权交给委托人。
/**
* @notice Delegates voting power of the underlying asset to a `delegatee` address
* @param delegatee The address that will receive the delegation
**/
function delegateUnderlyingTo(address delegatee) external onlyPoolAdmin {
IDelegationToken(_underlyingAsset).delegate(delegatee);
emit DelegateUnderlyingTo(delegatee);
}
主要逻辑
利率计算
与之前版本不同,之前版本的稳定利率的基准利率是由预言机获取的市场均值,但是在v3版本中,这一基准利率为slope1+offset:
/**
* @notice Returns the base stable borrow rate
* @return The base stable borrow rate
**/
function getBaseStableBorrowRate() public view returns (uint256) {
return _variableRateSlope1 + _baseStableRateOffset;
}
其余的逻辑不变。
储备逻辑
与v2版本基本相同,这里不做赘述。
验证逻辑
validateSupply与v2版本基本相同,不过由于有了supply cap的限制,在v3版本中会额外检查储备的资产是否超过这个限额。
/**
* @notice Validates a supply action.
* @param reserveCache The cached data of the reserve
* @param amount The amount to be supplied
*/
function validateSupply(DataTypes.ReserveCache memory reserveCache, uint256 amount)
internal
view
{
require(amount != 0, Errors.INVALID_AMOUNT);
(bool isActive, bool isFrozen, , , bool isPaused) = reserveCache
.reserveConfiguration
.getFlags();
require(isActive, Errors.RESERVE_INACTIVE);
require(!isPaused, Errors.RESERVE_PAUSED);
require(!isFrozen, Errors.RESERVE_FROZEN);
uint256 supplyCap = reserveCache.reserveConfiguration.getSupplyCap();
require(
supplyCap == 0 ||
(IAToken(reserveCache.aTokenAddress).scaledTotalSupply().rayMul(
reserveCache.nextLiquidityIndex
) + amount) <=
supplyCap * (10**reserveCache.reserveConfiguration.getDecimals()),
Errors.SUPPLY_CAP_EXCEEDED
);
}
validateBorrow在之前v2的基础上增加了对emode的检查和isolation mode的检查。
如果贷款行为是isolation mode,那么会检查对应的贷款资产是否允许在isolation模式下进行贷款,并且贷款的数目是否小于isolationmode的最高额度。
if (params.isolationModeActive) {
// check that the asset being borrowed is borrowable in isolation mode AND
// the total exposure is no bigger than the collateral debt ceiling
require(
params.reserveCache.reserveConfiguration.getBorrowableInIsolation(),
Errors.ASSET_NOT_BORROWABLE_IN_ISOLATION
);
require(
reservesData[params.isolationModeCollateralAddress].isolationModeTotalDebt +
(params.amount / 10**(vars.reserveDecimals - ReserveConfiguration.DEBT_CEILING_DECIMALS))
.toUint128() <=
params.isolationModeDebtCeiling,
Errors.DEBT_CEILING_EXCEEDED
);
}
如果是在emode来进行借款,那么会检查抵押的资产和贷款的目标资产是否属于同一个emode catagory
if (params.userEModeCategory != 0) {
require(
params.reserveCache.reserveConfiguration.getEModeCategory() == params.userEModeCategory,
Errors.INCONSISTENT_EMODE_CATEGORY
);
vars.eModePriceSource = eModeCategories[params.userEModeCategory].priceSource;
}
validateSetUserEMode用于检查用户在某一储备中将要使用emode的合法性,主要判断逻辑就是看抵押资产和借贷资产是否处于一个emode catagory。
/**
* @notice Validates the action of setting efficiency mode.
* @param reservesData The state of all the reserves
* @param reservesList The addresses of all the active reserves
* @param eModeCategories a mapping storing configurations for all efficiency mode categories
* @param userConfig the user configuration
* @param reservesCount The total number of valid reserves
* @param categoryId The id of the category
**/
function validateSetUserEMode(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
DataTypes.UserConfigurationMap memory userConfig,
uint256 reservesCount,
uint8 categoryId
) internal view {
// category is invalid if the liq threshold is not set
require(
categoryId == 0 || eModeCategories[categoryId].liquidationThreshold != 0,
Errors.INCONSISTENT_EMODE_CATEGORY
);
//eMode can always be enabled if the user hasn't supplied anything
if (userConfig.isEmpty()) {
return;
}
// if user is trying to set another category than default we require that
// either the user is not borrowing, or it's borrowing assets of categoryId
if (categoryId != 0) {
unchecked {
for (uint256 i = 0; i < reservesCount; i++) {
if (userConfig.isBorrowing(i)) {
DataTypes.ReserveConfigurationMap memory configuration = reservesData[reservesList[i]]
.configuration;
require(
configuration.getEModeCategory() == categoryId,
Errors.INCONSISTENT_EMODE_CATEGORY
);
}
}
}
}
}
emode逻辑
executeSetUserEMode用于将用户从某一个emodecatagory转换到另一个emodecatagory,用户的emode catagory在同一时间只能处在一个,以一个字典的方式来记录。主要检查是对于用户的健康因子和用户持有对应catagory的资产。
/**
* @notice Updates the user efficiency mode category
* @dev Will revert if user is borrowing non-compatible asset or change will drop HF < HEALTH_FACTOR_LIQUIDATION_THRESHOLD
* @dev Emits the `UserEModeSet` event
* @param reservesData The state of all the reserves
* @param reservesList The addresses of all the active reserves
* @param eModeCategories The configuration of all the efficiency mode categories
* @param usersEModeCategory The state of all users efficiency mode category
* @param userConfig The user configuration mapping that tracks the supplied/borrowed assets
* @param params The additional parameters needed to execute the setUserEMode function
*/
function executeSetUserEMode(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
mapping(uint8 => DataTypes.EModeCategory) storage eModeCategories,
mapping(address => uint8) storage usersEModeCategory,
DataTypes.UserConfigurationMap storage userConfig,
DataTypes.ExecuteSetUserEModeParams memory params
) external {
ValidationLogic.validateSetUserEMode(
reservesData,
reservesList,
eModeCategories,
userConfig,
params.reservesCount,
params.categoryId
);
uint8 prevCategoryId = usersEModeCategory[msg.sender];
usersEModeCategory[msg.sender] = params.categoryId;
if (prevCategoryId != 0) {
ValidationLogic.validateHealthFactor(
reservesData,
reservesList,
eModeCategories,
userConfig,
msg.sender,
params.categoryId,
params.reservesCount,
params.oracle
);
}
emit UserEModeSet(msg.sender, params.categoryId);
}
闪电贷逻辑
与v2逻辑一样,这里不再赘述。
借贷逻辑
借款大体逻辑与v2一样,不过在这里增加了对于isolationmode的处理逻辑,如果处在isolation mode,那么会计算isolationmode的总贷款,以事件的形式发送这一结果。
if (isolationModeActive) {
uint256 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress]
.isolationModeTotalDebt += (params.amount /
10 **
(reserveCache.reserveConfiguration.getDecimals() -
ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128();
emit IsolationModeTotalDebtUpdated(
isolationModeCollateralAddress,
nextIsolationModeTotalDebt
);
}
在用户还款时,同样也会调用updateIsolatedDebtIfIsolated来更新储备的isolation mode参数。
IsolationModeLogic.updateIsolatedDebtIfIsolated(
reservesData,
reservesList,
userConfig,
reserveCache,
paybackAmount
);
isolation mode 逻辑
只有一个updateIsolatedDebtIfIsolated函数,用于处理用户在isolation的还款,这里isolation mode的贷款限额没有考虑利息,所以用户还款的话可能超过这一限额,对于这一情况只需要将总贷款额设置为0.其余情况用正常逻辑来计算isolationmode 的总贷款额。
/**
* @notice updated the isolated debt whenever a position collateralized by an isolated asset is repaid or liquidated
* @param reservesData The state of all the reserves
* @param reservesList The addresses of all the active reserves
* @param userConfig The user configuration mapping
* @param reserveCache The cached data of the reserve
* @param repayAmount The amount being repaid
*/
function updateIsolatedDebtIfIsolated(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
DataTypes.UserConfigurationMap storage userConfig,
DataTypes.ReserveCache memory reserveCache,
uint256 repayAmount
) internal {
(bool isolationModeActive, address isolationModeCollateralAddress, ) = userConfig
.getIsolationModeState(reservesData, reservesList);
if (isolationModeActive) {
uint128 isolationModeTotalDebt = reservesData[isolationModeCollateralAddress]
.isolationModeTotalDebt;
uint128 isolatedDebtRepaid = (repayAmount /
10 **
(reserveCache.reserveConfiguration.getDecimals() -
ReserveConfiguration.DEBT_CEILING_DECIMALS)).toUint128();
// since the debt ceiling does not take into account the interest accrued, it might happen that amount
// repaid > debt in isolation mode
if (isolationModeTotalDebt <= isolatedDebtRepaid) {
reservesData[isolationModeCollateralAddress].isolationModeTotalDebt = 0;
emit IsolationModeTotalDebtUpdated(isolationModeCollateralAddress, 0);
} else {
uint256 nextIsolationModeTotalDebt = reservesData[isolationModeCollateralAddress]
.isolationModeTotalDebt = isolationModeTotalDebt - isolatedDebtRepaid;
emit IsolationModeTotalDebtUpdated(
isolationModeCollateralAddress,
nextIsolationModeTotalDebt
);
}
}
}
清算逻辑
DEFAULT_LIQUIDATION_CLOSE_FACTOR:被清算方贷款有多少可以用清算来偿还,这里定义为50%
MAX_LIQUIDATION_CLOSE_FACTOR:被清算方贷款最高可以用多少来偿还,定义为100%
CLOSE_FACTOR_HF_THRESHOLD:健康因子阈值,当健康因子小于此值时就可以进行清算。
/**
* @dev Default percentage of borrower's debt to be repaid in a liquidation.
* @dev Percentage applied when the users health factor is above `CLOSE_FACTOR_HF_THRESHOLD`
* Expressed in bps, a value of 0.5e4 results in 50.00%
*/
uint256 internal constant DEFAULT_LIQUIDATION_CLOSE_FACTOR = 0.5e4;
/**
* @dev Maximum percentage of borrower's debt to be repaid in a liquidation
* @dev Percentage applied when the users health factor is below `CLOSE_FACTOR_HF_THRESHOLD`
* Expressed in bps, a value of 1e4 results in 100.00%
*/
uint256 public constant MAX_LIQUIDATION_CLOSE_FACTOR = 1e4;
/**
* @dev This constant represents below which health factor value it is possible to liquidate
* an amount of debt corresponding to `MAX_LIQUIDATION_CLOSE_FACTOR`.
* A value of 0.95e18 results in 0.95
*/
uint256 public constant CLOSE_FACTOR_HF_THRESHOLD = 0.95e18;
如果用户的健康因子小于CLOSE_FACTOR_HF_THRESHOLD,那么用户的所有贷款都以资产清算的方式来还,反之则是一半的比率。
vars.closeFactor = vars.healthFactor > CLOSE_FACTOR_HF_THRESHOLD
? DEFAULT_LIQUIDATION_CLOSE_FACTOR
: MAX_LIQUIDATION_CLOSE_FACTOR;
vars.maxLiquidatableDebt = vars.userTotalDebt.percentMul(vars.closeFactor);
同样,在清算时也会考虑emode的情况,如果在同一个emode catagory,那么就更新清算奖励为该catagory的奖励,并且将抵押资产的价格来源标记为清算资产的价格来源:
if (params.userEModeCategory != 0) {
address eModePriceSource = eModeCategories[params.userEModeCategory].priceSource;
if (
EModeLogic.isInEModeCategory(
params.userEModeCategory,
collateralReserve.configuration.getEModeCategory()
)
) {
vars.liquidationBonus = eModeCategories[params.userEModeCategory].liquidationBonus;
if (eModePriceSource != address(0)) {
vars.collateralPriceSource = eModePriceSource;
}
}
// when in eMode, debt will always be in the same eMode category, can skip matching category check
if (eModePriceSource != address(0)) {
vars.debtPriceSource = eModePriceSource;
}
}
其余的逻辑和之前版本的大同小异,这里不再赘述。
跨链桥逻辑
由于涉及到跨链资产的转移,基础逻辑是在源网络销毁Atoken然后在目的网络生成Atoken,但是在目的网络上生成的Atoken并没有抵押物,这时这些生成的token就是unbacked Atokens。在后续的操作中,会将用户的抵押资产也转移到目标链上,然后调用executeBackUnbacked来将指定数值的unbacked Atoken设定抵押。
/**
* @notice Mint unbacked aTokens to a user and updates the unbacked for the reserve.
* @dev Essentially a supply without transferring the underlying.
* @dev Emits the `MintUnbacked` event
* @dev Emits the `ReserveUsedAsCollateralEnabled` if asset is set as collateral
* @param reservesData The state of all the reserves
* @param reservesList The addresses of all the active reserves
* @param userConfig The user configuration mapping that tracks the supplied/borrowed assets
* @param asset The address of the underlying asset to mint aTokens of
* @param amount The amount to mint
* @param onBehalfOf The address that will receive the aTokens
* @param referralCode Code used to register the integrator originating the operation, for potential rewards.
* 0 if the action is executed directly by the user, without any middle-man
**/
function executeMintUnbacked(
mapping(address => DataTypes.ReserveData) storage reservesData,
mapping(uint256 => address) storage reservesList,
DataTypes.UserConfigurationMap storage userConfig,
address asset,
uint256 amount,
address onBehalfOf,
uint16 referralCode
) external {
DataTypes.ReserveData storage reserve = reservesData[asset];
DataTypes.ReserveCache memory reserveCache = reserve.cache();
reserve.updateState(reserveCache);
ValidationLogic.validateSupply(reserveCache, amount);
uint256 unbackedMintCap = reserveCache.reserveConfiguration.getUnbackedMintCap();
uint256 reserveDecimals = reserveCache.reserveConfiguration.getDecimals();
uint256 unbacked = reserve.unbacked += amount.toUint128();
require(unbacked <= unbackedMintCap * (10**reserveDecimals), Errors.UNBACKED_MINT_CAP_EXCEEDED);
reserve.updateInterestRates(reserveCache, asset, 0, 0);
bool isFirstSupply = IAToken(reserveCache.aTokenAddress).mint(
msg.sender,
onBehalfOf,
amount,
reserveCache.nextLiquidityIndex
);
if (isFirstSupply) {
if (ValidationLogic.validateUseAsCollateral(reservesData, reservesList, userConfig, asset)) {
userConfig.setUsingAsCollateral(reserve.id, true);
emit ReserveUsedAsCollateralEnabled(asset, onBehalfOf);
}
}
emit MintUnbacked(asset, msg.sender, onBehalfOf, amount, referralCode);
}
executeBackUnbacked为unbacked atoken做担保,也就是在目标链上拿标的资产担保atoken。
/**
* @notice Back the current unbacked with `amount` and pay `fee`.
* @dev Emits the `BackUnbacked` event
* @param reserve The reserve to back unbacked for
* @param asset The address of the underlying asset to repay
* @param amount The amount to back
* @param fee The amount paid in fees
* @param protocolFeeBps The fraction of fees in basis points paid to the protocol
**/
function executeBackUnbacked(
DataTypes.ReserveData storage reserve,
address asset,
uint256 amount,
uint256 fee,
uint256 protocolFeeBps
) external {
DataTypes.ReserveCache memory reserveCache = reserve.cache();
reserve.updateState(reserveCache);
uint256 backingAmount = (amount < reserve.unbacked) ? amount : reserve.unbacked;
uint256 feeToProtocol = fee.percentMul(protocolFeeBps);
uint256 feeToLP = fee - feeToProtocol;
uint256 added = backingAmount + fee;
reserveCache.nextLiquidityIndex = reserve.cumulateToLiquidityIndex(
IERC20(reserveCache.aTokenAddress).totalSupply(),
feeToLP
);
reserve.accruedToTreasury += feeToProtocol.rayDiv(reserveCache.nextLiquidityIndex).toUint128();
reserve.unbacked -= backingAmount.toUint128();
reserve.updateInterestRates(reserveCache, asset, added, 0);
IERC20(asset).safeTransferFrom(msg.sender, reserveCache.aTokenAddress, added);
emit BackUnbacked(asset, msg.sender, backingAmount, fee);
}
}
pool & L2pool
主要逻辑与之前版本相同,具体逻辑体现在前一小节中。
其中L2pool就是在pool的基础上做了calldata的编码解码操作。
总结
AAVE项目构造了一个完整的借贷生态闭环,其经济模型实现已经成为业界内的参考标准。
由于AAVE项目管理呈现层级化结构,所以AAVE在去中心化上向着社区投票治理方向发展。不过在链上逻辑看来,其去中心化的程度并不高。
从v1到v3项目的发展看,AAVE正在寻求更广阔的市场。在经济模型不变的条件下,谋求更具包容性的市场环境。
由于次级代币市场的高波动性,AAVE对于各个市场做了更细致的划分,最直观的体现就是风险参数(利率计算参数)、emode以及isolationmode。
同时,AAVE也在V3版本中向着跨链市场进军,实现了跨链桥的操作接口。
安全性考量
由于AAVE是层级化管理结构,每一个关键合约都设定对应的管理员配置地址,所以在关键函数的处理上并没有外部用户直接操作的接口。用户操作接口只有交易池提供的基本操作,并且涉及利息操作的计算都由内部记账方式得出,很难进行利率操控。
如果攻击者操纵市场币价,AAVE的安全模型会做出响应,将质押的AAVE代币出售来进行救市,并且在V3版本中增加了预言机的时效性,防止预言机失灵的状况。
关于管理者监守自盗的rugpull,首先AAVE主体市场中AAVE代币占比份额很小,并且AAVE代币的发行量中管理方并没有占取巨大份额。
在v1和v2代码实现中,资金流只有产生利息中的一小部分流向协议费用。用户的资产由atoken保管,而atoken则由交易池管理,并没有后门函数可供管理者监守自盗。
不过在v3版本中,atoken增加了一个rescue函数,会将atoken中现存所有流动性转出。这一函数只有交易池管理者才能调用,为的是应对突发的安全性问题或者其他紧急状况。
从代码角度看,这一后门可能存在风险;从项目角度看,AAVE目前市值和地位如此之高,管理者按照常规逻辑不会将一手栽培的项目毁于一旦;从治理方面看,如果交易池管理遭到恶意管治,那么还是有可能出现意外状况。
参考资料
https://docs.aave.com/developers/getting-started/v3-overview
https://docs.aave.com/faq/liquidations
https://github.com/aave/aave-v3-core/blob/master/techpaper/Aave_V3_Technical_Paper.pdf
https://docs.aave.com/developers/v/1.0/developing-on-aave/the-protocol
https://docs.aave.com/risk/