diff --git a/defi/Morpho/pictures/adjustAPY.png b/defi/Morpho/pictures/adjustAPY.png new file mode 100644 index 000000000..eaf5ce3b7 Binary files /dev/null and b/defi/Morpho/pictures/adjustAPY.png differ diff --git a/defi/Morpho/pictures/borrowAPY2LendAPY.png b/defi/Morpho/pictures/borrowAPY2LendAPY.png new file mode 100644 index 000000000..e24e942ca Binary files /dev/null and b/defi/Morpho/pictures/borrowAPY2LendAPY.png differ diff --git a/defi/Morpho/pictures/borrowCapacity.png b/defi/Morpho/pictures/borrowCapacity.png new file mode 100644 index 000000000..39e85af1e Binary files /dev/null and b/defi/Morpho/pictures/borrowCapacity.png differ diff --git a/defi/Morpho/pictures/borrowDelta.png b/defi/Morpho/pictures/borrowDelta.png new file mode 100644 index 000000000..48460a29f Binary files /dev/null and b/defi/Morpho/pictures/borrowDelta.png differ diff --git a/defi/Morpho/pictures/borrowFlow.png b/defi/Morpho/pictures/borrowFlow.png new file mode 100644 index 000000000..aa35bdd45 Binary files /dev/null and b/defi/Morpho/pictures/borrowFlow.png differ diff --git a/defi/Morpho/pictures/borrowOverFlow.png b/defi/Morpho/pictures/borrowOverFlow.png new file mode 100644 index 000000000..ef95e3e31 Binary files /dev/null and b/defi/Morpho/pictures/borrowOverFlow.png differ diff --git a/defi/Morpho/pictures/borrowUnmatch.png b/defi/Morpho/pictures/borrowUnmatch.png new file mode 100644 index 000000000..8f58d45ff Binary files /dev/null and b/defi/Morpho/pictures/borrowUnmatch.png differ diff --git a/defi/Morpho/pictures/compoundAPY.png b/defi/Morpho/pictures/compoundAPY.png new file mode 100644 index 000000000..59c71a2f9 Binary files /dev/null and b/defi/Morpho/pictures/compoundAPY.png differ diff --git a/defi/Morpho/pictures/depositFlow.png b/defi/Morpho/pictures/depositFlow.png new file mode 100644 index 000000000..bd18415a0 Binary files /dev/null and b/defi/Morpho/pictures/depositFlow.png differ diff --git a/defi/Morpho/pictures/morphoAPY.png b/defi/Morpho/pictures/morphoAPY.png new file mode 100644 index 000000000..49685bb97 Binary files /dev/null and b/defi/Morpho/pictures/morphoAPY.png differ diff --git a/defi/Morpho/pictures/morphoBorrow.png b/defi/Morpho/pictures/morphoBorrow.png new file mode 100644 index 000000000..5ee5ee281 Binary files /dev/null and b/defi/Morpho/pictures/morphoBorrow.png differ diff --git a/defi/Morpho/pictures/morphoSupply.png b/defi/Morpho/pictures/morphoSupply.png new file mode 100644 index 000000000..5ea28b644 Binary files /dev/null and b/defi/Morpho/pictures/morphoSupply.png differ diff --git a/defi/Morpho/pictures/repayBalance.png b/defi/Morpho/pictures/repayBalance.png new file mode 100644 index 000000000..ef95e3e31 Binary files /dev/null and b/defi/Morpho/pictures/repayBalance.png differ diff --git a/defi/Morpho/pictures/repayBorrowDelta.png b/defi/Morpho/pictures/repayBorrowDelta.png new file mode 100644 index 000000000..86e9eaaff Binary files /dev/null and b/defi/Morpho/pictures/repayBorrowDelta.png differ diff --git a/defi/Morpho/pictures/repayP2P.png b/defi/Morpho/pictures/repayP2P.png new file mode 100644 index 000000000..4fe0f7cb1 Binary files /dev/null and b/defi/Morpho/pictures/repayP2P.png differ diff --git a/defi/Morpho/pictures/repaySupplyDelta.png b/defi/Morpho/pictures/repaySupplyDelta.png new file mode 100644 index 000000000..48460a29f Binary files /dev/null and b/defi/Morpho/pictures/repaySupplyDelta.png differ diff --git a/defi/Morpho/pictures/replayMatch.png b/defi/Morpho/pictures/replayMatch.png new file mode 100644 index 000000000..838739b31 Binary files /dev/null and b/defi/Morpho/pictures/replayMatch.png differ diff --git a/defi/Morpho/pictures/supplyBorrowBalance.png b/defi/Morpho/pictures/supplyBorrowBalance.png new file mode 100644 index 000000000..838739b31 Binary files /dev/null and b/defi/Morpho/pictures/supplyBorrowBalance.png differ diff --git a/defi/Morpho/pictures/supplyDelta.png b/defi/Morpho/pictures/supplyDelta.png new file mode 100644 index 000000000..48460a29f Binary files /dev/null and b/defi/Morpho/pictures/supplyDelta.png differ diff --git a/defi/Morpho/pictures/supplyMatch.png b/defi/Morpho/pictures/supplyMatch.png new file mode 100644 index 000000000..9e3c667ff Binary files /dev/null and b/defi/Morpho/pictures/supplyMatch.png differ diff --git a/defi/Morpho/pictures/targetAPY.png b/defi/Morpho/pictures/targetAPY.png new file mode 100644 index 000000000..49685bb97 Binary files /dev/null and b/defi/Morpho/pictures/targetAPY.png differ diff --git a/defi/Morpho/readme.md b/defi/Morpho/readme.md new file mode 100644 index 000000000..e18453036 --- /dev/null +++ b/defi/Morpho/readme.md @@ -0,0 +1,424 @@ +## Morpho 是什么 +Morpho是一个基于以太坊的借贷池优化器,通过点对点的匹配,为贷款人和借款人提供更好的利率。 +在 Morpho 的白皮书中,Morpho 描述自己的产品定位是 “流动性协议优化器” + +- 以 Compound 上 ETH 收益为例 + + +- 对应的曲线 +可以看到 Borrow 的平均年化收益率 为 0.1%,Supply 平均年化收益率为 2.7% + + +- Morpho 的目标就是降低 Borrow 的年化利率,提高 Supply 的年化利率,就是中间这条蓝色的曲线 + + +- 具体操作如下 + - 对于 Supplier + Step1: Supplier 存入资金到 Morpho + Step2: Morpho 把资金转存到 Compound + Step3: Compound mint cToken 给 Morpho + + + - 对于 Borrower + Step4: 存入抵押品,这一步可以视为 上面 Supplier 的操作 + Step5、6: Morpho 使用 cToken 换取 Supplier 存入的资金 + Step7: 把从 Compound 提取出来的资金借给 Borrower + + + 以上就是 Morpho 的整体流程,像是没有 Supplier 的时候,如果有用户进行 Borrow 需要怎么处理 ?或是 Supplier 和 Borrower 匹配成功后,Supplier 进行 withdraw 的时候需要怎么处理 ? 这些都是在 deposit, borrow, withdraw, repay , liquidate 接口进行的更加细致话的处理 + +## Deposit +Deposit 的接口流程伪代码如下 +``` +supply(user, amount): + update_Index() // 更新 Index + transfer(user, Morpho, amount) // 把用户资金转入到 Morpho 中 + + // Peer-to-peer supply + //// Promote borrowers + matched = matchBorrowers(amount) // 作为 Supplier, 寻找对手方 Borrower 进行匹配, + // 返回匹配到的最大资金量 + + + supply_p2p(user) += matched / index_p2p // 更新用户的 p2p supply 基数 + + + amount -= matched // 更新 amount + + + repay(Morpho, matched) // 把 matched 的资金偿还给 compound pool,因为这部分资金 + // 开始是 Borrower 从 Compound 池中借出的, + // 现在直接和 Supplier 匹配了,就需要偿还这部分资金 + + // Pool supply + supply_pool(user) += amount / index_pool // 更新用户的 pool supply 基数 + supply(Morpho, amount) // 把剩余的,未匹配的 amount 存入到 compound 中 +``` + + + +### 关键点解析 +#### Compound 中对于 Supplier 使用汇率的方式计算 Supplier 的本息总额 + $汇率 = \frac {资产 token 总数} {cToken总数}$ + $资产token总数 = underlyingBalance + totalBorrowBalance - reserves$ + $cToken总数 = cTokenSupply$ + $underlyingBalance : 池子中现货 token 的数量$ + $totalBalance: 池子中总共借出去的 token 数量,含利息$ + $reserve: 池子自己的储备金$ + Compound Supplier 的计算具体可参考 [Compound白皮书的思考](https://learnblockchain.cn/article/3153) + + +#### Compound 中 BorrowerIndex 、totalBorrow 的计算方式 + $Amount = 用户初始输入的借贷金额$ + $latestBorrowIndex = borrowIndexInit * (1 + borrowRate(\Delta Blocks) )^{n}$ + ( 注: 为了简化理解,上述式子中的 borrowIndexInit 可以取 1 ) + $BorrowAmount = \frac {Amount} {latestBorrowIndex}$ + $totalBorrow = BorrowAmount * latestBorrowIndex$ + $n = 经过的块高或是时长( second )$ + Compound Index 计算具体可参考 [Compound 合约部署](https://github.com/Dapp-Learning-DAO/Dapp-Learning/blob/main/defi/Compound/contract/Compound%E5%90%88%E7%BA%A6%E9%83%A8%E7%BD%B2.md) + +#### update_Index 逻辑 +前面说过,Morpho 的目标是求取 Borrow APY ( 代码中使用 poolBorrowGrowthIndex 表示 )、 Supply APY ( 代码中使用 poolSupplyGrowthIndex 表示 ) 以及 target APY ( 代码中使用 p2pGrowthIndex 表示 ) ,以便 Borrower 和 Supplier 都受益。 Morpho 中的 index 就是这个 target APY。 +后面使用 targetIndex 描述 target APY + + + +对于每一个借贷 Token,会设置一个 p2pIndexCursor 和 reserveFactor。 +- p2pIndexCursor 用于计算 p2pGrowthIndex +- reserveFactor 设置 Morpho 的 p2p 借贷手续费,这个手续费只在用户进行 p2p 借贷的时候才会收取,如果直接向 compound 借贷是不会收取这个手续费 +``` +struct MarketParameters { + uint16 reserveFactor; // Proportion of the interest earned by users sent to the DAO for each market, in basis point (100% = 10 000). The value is set at market creation. + uint16 p2pIndexCursor; // Position of the peer-to-peer rate in the pool's spread. Determine the weights of the weighted arithmetic average in the indexes computations ((1 - p2pIndexCursor) * r^S + p2pIndexCursor * r^B) (in basis point). + } +``` +$p2pGrowthIndex = (1 - p2pIndexCursor) * poolSupplyGrowthIndex + p2pIndexCursor * poolBorrowGrowthIndex$ + +$p2pBorrowGrowthIndex = p2pIndex + (poolBorrowGrowthIndex - p2pIndex ) * reserveFactor$ + +$p2pSupplyGrowthIndex = p2pIndex - (p2pIndex - poolSupplyGrowthIndex ) * reserveFactor$ +$p2pSupplyIndex = p2pIndex - (p2pIndex - poolSupplyIndex ) * reserveFactor$ + + + +最后,得到 p2pSupplyGrowthIndex 和 p2pBorrowGrowthIndex 后,就可以计算 +$p2pSupplyIndex = p2pSupplyIndex * (1 + p2pSupplyGrowthIndex )$ +$p2pBorrowIndex = p2pBorrowIndex * ( 1 + p2pBorrowGrowthIndex )$ + + +#### Index 计算方式,和 $Supply_{p2p}$ , $Supply_{pool}$ 的关系 +- Index 在 Morpho 中用于计算借款利率,贷款利率 和 Compound 中的 borrowIndex 类似 +- 在 Morpho 的黄皮书中, 使用 λ 进行描述,这里为了便于理解统一使用 Index 描述。而且在 Morpho 的合约中,实际也是使用 Index 进行标识的 +- $Supply_{p2p}$ , $Supply_{pool}$ 分别保存用户的 p2p 贷款基数,和 pool 贷款基数 + +假设 Index 以每天 20% 的速率递增 +| | 操作 | Morpho 状态 | +| --- | --- | --- | +| 第 0 天 | | Index = 1 | +| 第一天 | $User_{A}$ 存入 100 Dai | Index = 1 * ( 1 + 0.2 ) = 1.2 ( 首先更新 Index 后再进行其他计算 ) +$Supply_{pool}[User_{A}]$ = $\frac {100} {1.2}$ = 83.333 | +| 第二天 | | Index = 1.2 * ( 1 + 0.2 ) = 1.44 ( 首先更新 Index 后再进行其他计算 ) +$Supply_{pool}[User_{A}]$ = 83.333 | +| 第三天 | $User_{B}$ 存入 200 Dai | Index = 1.44 * ( 1 + 0.2 ) = 1.728 ( 首先更新 Index 后再进行其他计算 ) +$Supply_{pool}[User_{A}]$ = 83.333 +$Supply_{pool}[User_{B}]$ = $\frac {200} {1.728}$ = 115.7407 | + +#### Supplier 和 Borrower 队列更新逻辑 +队列按照 Supplier ( 或是 Borrower ) 尚未匹配 p2p 的资金大小进行排序 +| Head | Second | Third | Fourth | +| --- | --- | --- | --- | +| User_A ( 100 Dai ) | User_B ( 80 Dai ) | User_C ( 60 Dai ) | User_D ( 20 Dai ) | + +队列具体更新逻辑可以参考 [DoubleLinkedList.sol](https://etherscan.io/address/0xe3d7a242614174ccf9f96bd479c42795d666fc81#code) + + +#### 在匹配回路内允许消耗的最大的 gas +查看用户 deposit 时的接口代码,可以看到,在接口输入中有一个 _maxGasForMatching 参数,用于设置在匹配 Borrower 时允许消耗的 gas 大小 + +``` +/// @dev Implements supply logic. + /// @param _poolToken The address of the pool token the user wants to interact with. + /// @param _from The address of the account sending funds. + /// @param _onBehalf The address of the account whose positions will be updated. + /// @param _amount The amount of token (in underlying). + /// @param _maxGasForMatching The maximum amount of gas to consume within a matching engine loop. + function supplyLogic( + address _poolToken, + address _from, + address _onBehalf, + uint256 _amount, + uint256 _maxGasForMatching + ) +``` + +## Borrow +Borrow 的接口流程伪代码如下 + +``` +borrow(user, amount): + update_Index() + if amount * pθ > borrow_capacity(user): // 判断借款金额是否超过抵押品价值 + return + + transfer(Morpho, user, initialAmount) // 转入用户资金到 Morpho + initialAmount = amount + + // Peer-to-peer borrow + //// Promote suppliers + matched = matchSuppliers(amount) // 作为 Borrow 寻找匹配的 Supplier 对手方 + // 返回匹配到的最大资金量 + + borrow_p2p(user) += matched / index_p2p // 更新用户的 p2p borrow 基数 + + amount -= matched // 更新 amount + + withdraw(Morpho, matched) // 把 matched 的资金从 compound pool 中提取出来, + + // Pool borrow + borrow_pool(user) += amount / index_pool // 更新用户的 pool borrow 数据 + borrow(Morpho, amount) // 从 pool 中借出对应的金额 +``` + + +### 关键点解析 +- 判断用户的借款金额是否超出它的抵押品价值 +这里在 borrow_capacity 进行判断是否允许用户再次进行 + +- Borrow 流程 +整体的 Borrow 流程和 Supply 类似,但是中间有一个特殊的处理 +但是随着交易的进行会出现 $P2P_{supply}$ 大于 $P2P_{borrow}$ 的情况,在下面的 repay 接口中会描述如何产生这种情况。 +在这种情况下,这部分 $\Delta {P2P_{supply}}$ 是 Morpho 按照 p2p 的 supply 提供给 supplier 的, 但是实际这部资金 Morpho 又会存入到 Compound 中,按照 pool supply 的利率从 Compound 中收取利息。所以这里就会存在一个利率差 + + + +用户在 Borrow 的时候,Morpho 会首先把这部分资金匹配给 Borrower,以减少损失 + +## Withdraw +withdraw 的接口流程伪代码如下 +``` +withdraw(user, amount): + update_index() + if amount * Fθ * pθ > borrow_capacity(user): // 判断 withdraw 金额是否合法 + return + initialAmount = amount + + // Pool withdraw + withdrawnFromPool = min(amount, supply_pool(user) * index_pool ) //优先从 compound pool 中 withdraw + + supply_pool(user) -= withdrawnFromPool / index_pool // 更新用户的 pool supply 记录 + + amount -= withdrawnFromPool //更新需要从 p2p pool 中提取的资金 + + withdraw(Morpho, withdrawnFromPool) // 从 compound pool 中提取资金 + + // Promote suppliers + matched = matchSuppliers(amount) // 如果当前 user 作为 supplier 已经匹配了对应的 borrower + // 则使用其他的 supplier 替换当前的 user 作为 p2p 的 supplier + + supply_p2p(user) -= matched / index_p2p // 更新用户的 p2p supply 值 + + amount -= matched + + withdraw(Morpho, matched) // 提取匹配到的 replace 当前 user 的 supplier 的资金 + + // Demote borrowers + unmatched = unmatchBorrowers(amount) // 剩余 p2p supply 资金无法找到 replace supplier + // 只能把一些 p2p borrower 降级为 pool borrower + + supply_p2p (user) -= unmatched / index_pool // 更新 user 的 p2p supply + + borrow(Morpho, unmatched) // 从 compound 中提取 剩余 p2p supply + + transfer(Morpho, user, initialAmount) // 转移资金给用户 +``` + +### 关键点解析 +资金 withdraw 顺序 +假设当前进行 withdraw 的用户为 User_A +- 初始状态时,Morpho 池子处于平衡状态,即所有的 Borrow 都匹配了 p2p + + +- 用户 User_A 有部分资金在 p2p 中,部分资金在 pool 中 +- 当 User_A 进行 withdraw 时,首先提取 User_A 在 pool 中的资金,此时所有的 Borrow 依然全部匹配 p2p +- 在讲解 Borrow 接口的时候,我们讲到会存在 $\Delta P2P_{supply}$ 的情况,那么用户在 withdraw 的时候,会优先提取这部分的资金,相当于补偿 Morpho protocl 的损失 + + +- 之后,User_A 提取剩余 p2p 的资金 ,这里假设为 100 Dai ,同时从 pool 中寻中替代的 supplier 提供 p2p 贷款,此时所有的 Borrow 依然全部匹配 p2p +- 如果在上一个寻找替代的 p2p supplier 时,只找到 20 Dai ,那么从 Compound 中提取这 20 Dai 还给用户,对于剩余的 80 Dai 进行如下处理。 + + +- 尝试对 80 Dai 对应的 Borrower 做 unmatch 处理,即改变这些 borrower 的借款利率,使之从 p2p 模式变为 pool 模式 + + +- 在 unmatch 接口中,同样需要传入一个 _maxGasForMatching,用于计算在 unmatch 过程中允许消耗的最大 gas。 + + ```solidity + /// @notice Unmatches borrowers' liquidity in peer-to-peer for the given `_amount` and moves it to Compound. + /// @dev Note: This function expects and peer-to-peer indexes to have been updated. + /// @param _poolToken The address of the market from which to unmatch borrowers. + /// @param _amount The amount to unmatch (in underlying). + /// @param _maxGasForMatching The maximum amount of gas to consume within a matching engine loop. + /// @return The amount unmatched (in underlying). + function _unmatchBorrowers( + address _poolToken, + uint256 _amount, + uint256 _maxGasForMatching + ) internal returns (uint256) { + ``` + +- 如果传入一个很小的值,那么会造成只 unmatch 部分 p2p 资金 ,这里假设 unmatch 了 20 Dai , 那么还剩余 80 - 20 = 60 Dai 的 p2p 资金没有 unmatch。这个时候 Morpho 就会让这 60 Dai 对应的 borrower 继续保持 p2p 借贷的利率,同时记录 delta.p2pBorrowDelta 作为没有匹配到 p2p 的 borrow 资金。 + + +- 最后,从 Comound 中借出剩余的 80 Dai 还给用户。这里可以看到,实际这部分 delta.p2pBorrowDelta 是 Morpho 从 Compound 借出来,但是按照 p2p 的利率提供给 Borrow 的,这样中间就会产生一个利率差 + +## Repay +repay 的接口流程伪代码如下 +``` +repay(user, amount): + update_Index() // 更新 Index + if amount > borrow_p2p(user) * index_p2p + borrow_pool(user) * index_pool: + return + + transfer(user, Morpho, amount) // 准入用户 repay 资金到 Morpho + + // Pool repay + repaidOnPool = min(amount, b_pool(user) * index_pool) // 优先偿还 pool 的借贷资金 + borrow_pool(user) -= repaidOnPool / index_pool + amount -= repaidOnPool + repay(Morpho, repaidOnPool) + + // Promote borrowers + matched = matchBorrowers(amount) // 如果当前 user 是 p2p 匹配用户 + borrow_p2p(user) -= matched / index_p2p // 则寻找 replace borrower + amount -= matched + repay(Morpho, matched) + + // Demote suppliers + unmatched = unmatchSuppliers(amount) // 对于剩余的 p2p borrower 资金, + borrow_p2p(user) -= unmatched / index_p2p // 如果未找到 replace borrower, + supply(Morpho, unmatched) // 则尝试把对应的 p2p supplier 降级为 pool supplier +``` + +### 关键点解析 +#### 资金 repay 顺序 + +- 假设用户 pool borrow 资金为 20 Dai, p2p borrow 资金为 80 Dai。那么用户首先需要偿还 pool borrow 部分的 20 Dai 资金 + + + +- pool borrow 资金偿还后,用户剩下的 borrow 资金就是 p2p 资金,这里为 80 Dai + + + +- 更新 borrower 的 p2p 资金。 + + 这里需要注意的是,borrower 的 p2p 资金更新是在 repay p2p 之前的,因为后面更新 Morpho p2p borrow amount 的时候还需要额外计算一部分利息 + + ```solidity + borrowerBorrowBalance.inP2P -= Math.min( + borrowerBorrowBalance.inP2P, + vars.remainingToRepay.div(vars.p2pBorrowIndex) + ); + ``` + +- 在讲 withdraw 接口的时候,我们提到在 Borrow 这边会存在 $\Delta P2P_{borrow}$ 的情况。那么这里用户进行 repay 的时候,Morpho 会检测是否存在 $\Delta P2P_{borrow}$ ,如果存在,则用户需要优先支付这部分的 borrow 资金 ,这里假设 $\Delta P2P_{borrow}$ 为 10 Dai, 那么剩余需要 repay 的资金为 80 - 10 = 70 Dai。 + + 需要注意的是,这里用户偿还 $\Delta P2P_{borrow}$ 时,实际是在补偿 Morpho protocl 的亏损,而不是用户自己的 + + + +- 更新 Morhpo protocl 的 p2pBorrowAmount,p2pSupplyAmount。 + + 主意这里更新全局的 p2pBorrowAmount 的时候,会把 $\Delta P2P_{supply}$ 产生的利息差计算进去进行更新 + + ```solidity + + // Fee = (p2pBorrowAmount - p2pBorrowDelta) - (p2pSupplyAmount - p2pSupplyDelta). + // No need to subtract p2pBorrowDelta as it is zero. + vars.feeToRepay = Math.zeroFloorSub( + delta.p2pBorrowAmount.mul(vars.p2pBorrowIndex), + delta.p2pSupplyAmount.mul(vars.p2pSupplyIndex).zeroFloorSub( + delta.p2pSupplyDelta.mul(ICToken(_poolToken).exchangeRateStored()) + ) + ); + + if (vars.feeToRepay > 0) { + uint256 feeRepaid = Math.min(vars.feeToRepay, vars.remainingToRepay); + vars.remainingToRepay -= feeRepaid; + delta.p2pBorrowAmount -= feeRepaid.div(vars.p2pBorrowIndex); + emit P2PAmountsUpdated(_poolToken, delta.p2pSupplyAmount, delta.p2pBorrowAmount); + } + ``` + + + +- 到这一步的时候,Morpho 池子中消除了原始存在的 $\Delta P2P_{supply}$ 和 $\Delta P2P_{borrow}$ 。 + + 和 withdraw 的时候类似,这个时候就是在 $Pool_{borrow}$ 中寻找替代者。 + + 但是如果找不到可以提到的 borrower 的时候,又会产生新的 $\Delta P2P_{supplyNew}$ + + + + - 对于这个 $\Delta P2P_{supplyNew}$ 继续进行处理,最直接的方式就是把这部分的 supplier 降级。这里使用 _unmatchSuppliers 进行降级处理。 + + 跟 _unmatchBorrowers 接口一样,调用时会传入一个 _maxGasForMatching,用于计算在 unmatch 过程中允许消耗的最大 gas。所以依然存在 $\Delta P2P_{supplyNew}$ 无法完全消除的情况 + + ```solidity + /// @notice Unmatches suppliers' liquidity in peer-to-peer up to the given `_amount` and moves it to Compound. + /// @dev Note: This function expects Compound's exchange rate and peer-to-peer indexes to have been updated. + /// @param _poolToken The address of the market from which to unmatch suppliers. + /// @param _amount The amount to search for (in underlying). + /// @param _maxGasForMatching The maximum amount of gas to consume within a matching engine loop. + /// @return The amount unmatched (in underlying). + function _unmatchSuppliers( + address _poolToken, + uint256 _amount, + uint256 _maxGasForMatching + ) internal returns (uint256) { + ``` + + - $\Delta P2P_{supplyNew}$ 无法完全消除时,更新 p2pSupplyDelta 的值,然后把剩余的 repay 资金转入 Compound 并结束此次处理 + + ```solidity + if (vars.remainingToRepay > 0) { + uint256 unmatched = _unmatchSuppliers( + _poolToken, + vars.remainingToRepay, + vars.remainingGasForMatching + ); + + // Increase the peer-to-peer supply delta. + if (unmatched < vars.remainingToRepay) { + delta.p2pSupplyDelta += (vars.remainingToRepay - unmatched).div( + ICToken(_poolToken).exchangeRateStored() // Exchange rate has already been updated. + ); + emit P2PSupplyDeltaUpdated(_poolToken, delta.p2pSupplyDelta); + } + + delta.p2pSupplyAmount -= unmatched.div(vars.p2pSupplyIndex); + delta.p2pBorrowAmount -= vars.remainingToRepay.div(vars.p2pBorrowIndex); + emit P2PAmountsUpdated(_poolToken, delta.p2pSupplyAmount, delta.p2pBorrowAmount); + + _supplyToPool(_poolToken, underlyingToken, vars.remainingToRepay); // Reverts on error. + } + ``` + + +## Liquidate + +liquidate 的接口流程伪代码如下 + +```solidity +liquiate(user, amount): + update_Index() // 更新 Index + liquidationAllowed() // 判断是否可以被清算 + + _unsafeRepay() // 调用内部 repay 接口进行 repay + _unsafeWithdraw() // 调用内部 withdraw 接口进行 withdraw +``` + +### 关键点解析 + +- 清算逻辑 +Morpho 使用和 Compound 同样的 oracle,closeFactor 和 liquidationIncentive 进行清算,即保持和 Compound 完全相同的清算处理 \ No newline at end of file diff --git "a/defi/instadapp/InstaDapp\345\220\210\347\272\246\350\247\243\346\236\220.md" "b/defi/instadapp/InstaDapp\345\220\210\347\272\246\350\247\243\346\236\220.md" new file mode 100644 index 000000000..c5987f42b --- /dev/null +++ "b/defi/instadapp/InstaDapp\345\220\210\347\272\246\350\247\243\346\236\220.md" @@ -0,0 +1,348 @@ +## InstaDapp 是什么 +Instadapp 是 DeFi 的中间件平台,是一个面向用户的去中心化资产管理协议,提供了针对 MakerDAO, Compound, AAVE , Uniswap 等协议 的高效的资产管理功能,其目标是简化 DeFi 的复杂性,最终成为 DeFi 的统一前端。 + +## InstaDapp 的两个产品 +针对不同的用户群体,推出了两个不同的产品,分别是 InstaDapp Lite 和 InstaDapp Pro. + +个人感觉,这两个产品主要的区别在 分险系数 、用户对 Defi 认知度的水平 ( 或用户操作的频率 ) + +### [InstaDapp Lite](https://lite.instadapp.io/) + + + +低分险 ( 相对于 InstaDapp Pro ),不频繁操作的用户或是对 Defi 认知水平较低的可以选择这个产品。 + +根据 [官方文档](https://lite.guides.instadapp.io/getting-started/what-is-instadapp-lite) 的介绍,InstaDapp Lite 有如下特点: + +1) POS 奖励放大 + +Lite 只有一个主要收益策略,那就是通过在 [Lido](https://lido.fi/) 上 stake ETH 赚取收益。就算用户 Deposit Dai, Lite 也会把 Dai 转换为 stETH。“POS 奖励放大” 其实也就是通过集中散户的资金,在 Lido 中达到较高的占比,从而获取更高的收益。 + +另一个次要收益策略是把 stETH 放到 Compound、AAVE 等借贷协议中进行放贷来赚取双重收益。 + + + +2) 简化 DeFi 策略 + +利用 DSA ( DeFi Smart Accounts, 后面 InstaDapp Pro 会详细介绍 ) 账户简化和各个 Defi 协议的操作 + +3) 最小化交易和节省 Gas 成本 + +这个也是利用了 DSA 的便利性 + +### [InstaDapp Pro](https://defi.instadapp.io/) + +针对这个产品,用户有更多的操作空间,可以利用不同的策略进行杠杆、再融资和转移头寸,同时通过自动化实现回报最大化。 + +当然这些要求用户具有更加专业的 Defi 知识,能自己控制识别其中的风险。 + +Pro 页面提供了 Simulation 功能,用户可以在正式使用前,进行模拟操作,熟悉掌握 Pro 的用法 + + + +1) Pro 目前在 Ethereum Mainnet 上集成了 Maker、Compound、Aave V2、Aave V3、Uniswap V3、Liquity、Savings Dai、Morpho、Morpho V3、Spark 这些协议,在其他 L2 上集成的协议较少。比如 Polygon 上, 就只集成了 Aave V2、Aave V3、Compound V3、Uniswap V3 + +2) 对于用户的账户资金,可以进行如下操作: + +- 资产跨链 ( 集成 [Hop.Exchange](http://Hop.Exchange) 实现 ) +- 资产交易 ( swap ) +- Deposit、Withdraw + +3)对于用户的杠杆、头寸的操作,Pro 定义为 Strategies,总共有如下 Strategies: + +- Refinance , 比如把 Aave V2 的仓位转移到 到 Morpho +- Leverage / Max mining,最大化杠杆,比如在 Aave 中存入 ETH, 借出 Dai 后再转换为 ETH, 然后在存入 Aave 中,这样循环操作 +- Unwind / Deleverage,降低杠杆,比如 withdraw Aave 中的 ETH 用来偿还从 Aave 中借出的 Dai +- Collateral Swap, 抵押品转换,例如 withdraw 部分 Aave 中的抵押品 ( ETH ),然后把这部分的 ETH 换成 USDT 后在存入到 Aave 中 +- Debt Swap, 债务转换,比如已经从 Aave 中借出 10 Dai, 那么现在再从 Aave 中借出 5 USDT,把这 5 USDT 转换为 Dai, 偿还 Aave 中的 Dai, 那么最后用户就从 10 Dai 的债务变为 5 Dai、5 USDT 的债务 +- Deposit & Borrow , 存入和借款,就是在一笔交易中完成存入和借款的操作 +- Payback & Withdraw, 偿还和提取,在一笔交易中完成债务偿还和取款的操作 + +总结,从上面我们可以看到,Pro 的这些 Strategies 其实就是组合了多笔操作,把原先用户需要 2 -3 笔交易才能完成的操作合并为 1 笔交易,同时操作的主要对象还是当前仓位对应的 Defi + +## InstaDapp 架构 + +对于 Lite 和 Pro 都是采用同一套架构,其中主要分为了 3 个角色 + +- Dapp + + 和 DeFi Smart Layer (DSL) 交互的 Dap,包括 InstaDapp、其他 Dapp 、Wallet ( 如 Metamask 等 ) + +- DeFi Smart Layer (DSL) + + DeFi 智能层,也是 InstaDapp 的核心逻辑层,这一层又可以细分为 3 个部分 + + - Authority: 只有授权的地址才可以操作 **Smart Accounts ,使用 Smart Accounts 中的资金进行操作** + - Defi Smart Accounts ( DSA )**:智能账户层 ( 其实就是 solidity 合约 )。**InstaDapp **单独创建一个 solidity 合约作为用户的 Account ,优势在于 InstaDapp 可以在合约中插件式组合各种操作,从而在一笔交易中完成复杂的操作,而这是原生的 address 无法完成的** + - Connectors: 连接器层。**Smart Accounts 没有直接和各个 Defi 协议进行交易,而是通过 Connector 进行操作,实现了账户和操作分离的架构。各个开发者或是项目方可以通过实现特定的 Connector ,完美的接入 InstaDapp 中** + + + 各个链各个 Defi Protocol 对应的 connector 对应官网如下:https://docs.instadapp.io/connectors/mainnet/aave-v2 + +- Defi Protocol + + 协议层,对应的就是各个 Defi 协议 + + + + + 参考来源:https://blog.instadapp.io/introducing-defi-smart-layer/ + +## Defi Smart Layer 实现 + +下面将分析 Defi Smart Layer 的具体实现逻辑。 + +### 创建 Smart Account + +从 Defi Smart Layer 的结构中可以看到,用户必须要有一个 Smart Account 才能和各个 Defi 进行交互。而且当用户初次进入 InstaDapp Pro 的时候,会强制要求用户创建一个 DSA + + + +创建 Smart Account 的具体过程如下: + +- 用户调用 InstaIndex 合约的 build 接口 + +``` +function build( + address _owner, + uint accountVersion, + address _origin + ) public returns (address _account) { + require(accountVersion != 0 && accountVersion <= versionCount, "not-valid-account"); + _account = createClone(accountVersion); // 通过 clone 的 方式生成 Smart Account + ListInterface(list).init(_account); + AccountInterface(_account).enable(_owner); + emit LogAccountCreated(msg.sender, _owner, _account, _origin); + } +``` + + InstaIndex 合约实现可参考:https://polygonscan.com/address/0xa9b99766e6c676cf1975c0d3166f96c0848ff5ad#code + +- InstaIndex 在 build 接口内部通过 clone 的方式生成 Smart Account + +``` +function createClone(uint version) internal returns (address result) { + bytes20 targetBytes = bytes20(account[version]); + // solium-disable-next-line security/no-inline-assembly + assembly { + let clone := mload(0x40) + mstore(clone, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000) + mstore(add(clone, 0x14), targetBytes) + mstore(add(clone, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000) + result := create(0, clone, 0x37) +``` + +Smart Account 合约实现可参考: https://polygonscan.com/address/0x51cbc90528bf960a6d5728306f0e9fae3cce38ed#code + +至此,Smart Account 就创建完成 + +### Smart Account 的 delegate + +Smart Account 内部代码很简洁,所有过来的用户调用最终通过 delegate 的方式调用到真正的业务合约,这里的 delegate 的目标地址为 implementation 地址 + +``` +contract InstaAccountV2 { + + AccountImplementations public immutable implementations; + + constructor(address _implementations) { + implementations = AccountImplementations(_implementations); + } + + ................... + + function _fallback(bytes4 _sig) internal { + address _implementation = implementations.getImplementation(_sig); + require(_implementation != address(0), "InstaAccountV2: Not able to find _implementation"); + _delegate(_implementation); + } + + fallback () external payable { + _fallback(msg.sig); + } + +} +``` + +- _delegate 具体实现如下 + +``` +function _delegate(address implementation) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } +``` + +Smart Account 处理流程图: + + + +### Implementation 的实现 + +从如下 Smart Account 的 fallback 接口中可以看到,根据用户的 sig, 可以选择不同的 implementation 实现,这里以其中的一个实现 InstaImplementationM1 为例进行分析 + +``` +function _fallback(bytes4 _sig) internal { + address _implementation = implementations.getImplementation(_sig); + require(_implementation != address(0), "InstaAccountV2: Not able to find _implementation"); + _delegate(_implementation); + } +``` + +InstaImplementationM1 合约实现可参考: + +https://etherscan.io/address/0x8a3462a50e1a9fe8c9e7d9023cacbd9a98d90021 + +- cast 调用 + +InstaImplementationM1 中的入口只有一个,那就 cast 接口。 + +cast 处理过程如下: + +- 根据传入的 _targetNames ( 这个参数是个数组 )获取对应的 connectors ( 返回的 connectors 也是个数组 ),connectors 在代码中对应为 _targets +- 然后对每个 _target 调用 spell ,其实就是 delegate call + +```solidity + function cast( + string[] calldata _targetNames, + bytes[] calldata _datas, + address _origin + ) + external + payable + returns (bytes32) // Dummy return to fix instaIndex buildWithCast function + { + .............. + + string[] memory eventNames = new string[](_length); + bytes[] memory eventParams = new bytes[](_length); + + (bool isOk, address[] memory _targets) = ConnectorsInterface(connectorsM1).isConnectors(_targetNames); + + require(isOk, "1: not-connector"); + + for (uint i = 0; i < _length; i++) { + bytes memory response = spell(_targets[i], _datas[i]); + (eventNames[i], eventParams[i]) = decodeEvent(response); + } + + .............. + } +``` + +- 如下查看 spell 代码,可以发现里面使用的就是 delegate call 到 connector + +``` +function spell(address _target, bytes memory _data) internal returns (bytes memory response) { + require(_target != address(0), "target-invalid"); + assembly { + let succeeded := delegatecall(gas(), _target, add(_data, 0x20), mload(_data), 0, 0) + let size := returndatasize() + + response := mload(0x40) + mstore(0x40, add(response, and(add(add(size, 0x20), 0x1f), not(0x1f)))) + mstore(response, size) + returndatacopy(add(response, 0x20), 0, size) + + switch iszero(succeeded) + case 1 { + // throw if delegatecall failed + returndatacopy(0x00, 0x00, size) + revert(0x00, size) + } + } + } +``` + +- connector 中就是和具体的 Defi protocol 交互的逻辑,比如 Aave, Uniswap, Compound + +ConnectV2AaveV2Polygon **可参考如下:** + +https://polygonscan.com/address/0x14272cf069a5ce1b97c9999b8c368cf3704acd0b#code + +### Smart Account 整体调用流程 + +总结起来,Smart Account 调用的整体流程如下图 + + + +### Smart Account 调用实例 + +下面以在 InstaDapp Pro 上,对 Aave V2 的仓位进行 Leverage / Max mining 为例进行复盘分析 + + + +- 交易如下 + +传入的参数 _targetNames 中,存在三个元素 + + + +重放交易,可以看到其中具体的跳转 + + + +交易分析工具: https://dashboard.tenderly.co/ + +### Authority 角色赋权 + +Defi Smart Layer 中的一个重要角色就是 Authority ( user ) + +一个 Smart Account 可以有多个 Authorities + + + +Authority 的管理涉及到两个合约,InstaList 和 InstaIndex ,这两个合约记录了 Smart Account 和各个 Authority 的对应关系 + +**InstaList 合约代码:** https://polygonscan.com/address/0x839c2D3aDe63DF5b0b8F3E57D5e145057Ab41556#code + +**InstaIndex 合约代码:** https://polygonscan.com/address/0xa9b99766e6c676cf1975c0d3166f96c0848ff5ad#code + +- 初次创建 Smart Account 时添加 Authority + +在前文中讲到,在创建 Smart Account 的时候,需要调用 InstaIndex 的 build 接口。在这个接口中,会把 user 添加到新创建的 Smart Account 的 Authority 里面, + +```solidity +function build( + address _owner, + uint accountVersion, + address _origin + ) public returns (address _account) { + require(accountVersion != 0 && accountVersion <= versionCount, "not-valid-account"); + _account = createClone(accountVersion); + ListInterface(list).init(_account); + AccountInterface(_account).enable(_owner); // 添加 owner 为 Authority + emit LogAccountCreated(msg.sender, _owner, _account, _origin); + } +``` + +- 添加其他 Authority + +在界面手工添加另一个 Authority, 在交易参数中,可以看到 _targetNames 传入的参数值为 AUTHORITY-A + + + +重放这笔交易,可以看到最终时调用的 InstaList 的 addAuth + + + +最后吐糟下,官方的 repo 的只需要看 dsa-contract 这个 repo 就可以了 + +官方的 doc 文档确实最新的,所以最好是直接对着 doc 文档,然后发笔交易进行 debug \ No newline at end of file diff --git a/defi/instadapp/pictures/instaDapp1.png b/defi/instadapp/pictures/instaDapp1.png new file mode 100644 index 000000000..2d2c20599 Binary files /dev/null and b/defi/instadapp/pictures/instaDapp1.png differ diff --git a/defi/instadapp/pictures/instaDapp10.png b/defi/instadapp/pictures/instaDapp10.png new file mode 100644 index 000000000..ce0d86dbf Binary files /dev/null and b/defi/instadapp/pictures/instaDapp10.png differ diff --git a/defi/instadapp/pictures/instaDapp11.png b/defi/instadapp/pictures/instaDapp11.png new file mode 100644 index 000000000..81428253a Binary files /dev/null and b/defi/instadapp/pictures/instaDapp11.png differ diff --git a/defi/instadapp/pictures/instaDapp12.png b/defi/instadapp/pictures/instaDapp12.png new file mode 100644 index 000000000..18013221d Binary files /dev/null and b/defi/instadapp/pictures/instaDapp12.png differ diff --git a/defi/instadapp/pictures/instaDapp13.png b/defi/instadapp/pictures/instaDapp13.png new file mode 100644 index 000000000..8f1a3cf61 Binary files /dev/null and b/defi/instadapp/pictures/instaDapp13.png differ diff --git a/defi/instadapp/pictures/instaDapp2.png b/defi/instadapp/pictures/instaDapp2.png new file mode 100644 index 000000000..a4b5d0d98 Binary files /dev/null and b/defi/instadapp/pictures/instaDapp2.png differ diff --git a/defi/instadapp/pictures/instaDapp3.png b/defi/instadapp/pictures/instaDapp3.png new file mode 100644 index 000000000..0fa36c399 Binary files /dev/null and b/defi/instadapp/pictures/instaDapp3.png differ diff --git a/defi/instadapp/pictures/instaDapp4.png b/defi/instadapp/pictures/instaDapp4.png new file mode 100644 index 000000000..5e812823a Binary files /dev/null and b/defi/instadapp/pictures/instaDapp4.png differ diff --git a/defi/instadapp/pictures/instaDapp5.png b/defi/instadapp/pictures/instaDapp5.png new file mode 100644 index 000000000..94d2cf5a4 Binary files /dev/null and b/defi/instadapp/pictures/instaDapp5.png differ diff --git a/defi/instadapp/pictures/instaDapp6.png b/defi/instadapp/pictures/instaDapp6.png new file mode 100644 index 000000000..c2b8bd524 Binary files /dev/null and b/defi/instadapp/pictures/instaDapp6.png differ diff --git a/defi/instadapp/pictures/instaDapp7.png b/defi/instadapp/pictures/instaDapp7.png new file mode 100644 index 000000000..dba9ab6ee Binary files /dev/null and b/defi/instadapp/pictures/instaDapp7.png differ diff --git a/defi/instadapp/pictures/instaDapp8.png b/defi/instadapp/pictures/instaDapp8.png new file mode 100644 index 000000000..5170e6ea9 Binary files /dev/null and b/defi/instadapp/pictures/instaDapp8.png differ diff --git a/defi/instadapp/pictures/instaDapp9.png b/defi/instadapp/pictures/instaDapp9.png new file mode 100644 index 000000000..77332d9f1 Binary files /dev/null and b/defi/instadapp/pictures/instaDapp9.png differ