6

Simplified Compound Protocol Design

 2 years ago
source link: https://learnblockchain.cn/article/3205
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Simplified Compound Protocol Design

Compound简化版

Compound简化版

最近学习了很多的Compound和AAVE,读了白皮书,也参加了合约分享,对经典的借贷项目感觉有一点了解,但是始终感觉不深刻,就还是有一点朦朦胧胧的。

原创!不要转载!不要转载!不要发到公众号上!

写这篇文章的目的是检验一下自己对于Compound等借贷产品的理解,以及用智能合约练练手,找找感觉。

初步的计划有:

  1. 实现Compound中提到的资金池,包含主要的业务逻辑有:存,取,借,还,清算
  2. 要保持代码编写简单,不使用compound中的错误码
  3. 要保持代码结构简单,不考虑Dai,USDT等非标ERC20, 只考虑两种ERC20, 即Test token和WMATIC
  4. 要保持业务逻辑简单,不考虑Compound中的链上治理,不考虑COMP token的分发等
  5. 要保证代码量小,不涉及前端业务调用,只部署合约,以及与合约相关的ethers调用
  6. 不使用价格语言机,或者仅仅使用chainLink的价格预言机
  7. 合约代码部署到Matic链上,后期也可以交互
  8. 利率模型就使用直线型的利率模型,而非后期的折线
  9. 使用代理合约模式,具体代理过程如下:
    1. cTestProxy -> cToken
    2. unicontrollerProxy -> comptroller
  10. TestToken是一个ERC20 Token,用于测试用。WMatic是matic链上的真实Token
  11. 使用openzeppelin的合约模板以及Hardhat开发环境

合约结构设计:

在合约结构上,主要还是参考compound自身的合约结构来进行解耦合。主合约是cToken合约,所有的业务逻辑都在cToken中实现。针对具体的Test token,部署一个代理合约cTestProxy来存储所有的数据,该代理合约指向cToken. 在cToken中,需要使用到价格,这里就调用一个获取价格的接口合约priceOracleInterface,传入underlying Token的地址来获取价格。为保证cToken合约的尽可能简单,将cToken合约中的所有管理类的方法都抽象到comptroller合约中。同时设计一个comptrollerInterface的接口合约,用于定义这些管理类方法的接口。

在关于comptorller合约的设计时,一个关键的问题是comptorller合约是否需要存储状态?comptorller合约的定义是针对不同的cToken中的所有的管理类方法的一个集合。既然是不同的cToken,那么每一种cToken的具体管理参数应该是不一样的,故comptorller合约应该是要存储状态的。同时为了将存储和逻辑分开,这里肯定是将状态存储到代理合约上,具体逻辑写在实现合约中。那么这里肯定是要将针对所有的cToken的管理类参数都要写在这个代理合约unitrollerProxy里面。需要一个map,类似于compound中定义的markets概念。

合约逻辑设计:

compound里面最主要的经济模型都在cToken合约里面实现,所以最主要的还是要设计cToken的逻辑

cToken的逻辑设计:

全局变量的设计

cToken是一个ERC20的合约,其应该继承自openzeppelin的ERC20.除开普通的ERC20部分所涉及到的全局变量,还应该有自身的一些全局变量。

资金池的衡量指标:

对于一个资金池,最重要的指标是资金利用率 U:

当前资金池的资金利用率 U, 其中资金利用率是一个可以通过totalBorrows,cash,totalReserves实时计算出的一个数值,故不需要作为一个全局变量

当前资金池的总借贷:totalBorrows -> 总借贷的值应该作为一个全局变量,且应该是计算复利后的值

当前资金池的总流动性:cash -> 这个值可以通过实时取当前资金池在对应的underlying token的balance来得到,也不需要作为一个全局变量

当前资金池的总储备金:totalReserves

当前cToken的总供给:totalSupply, 来源于cToken,需要作为一个全局变量

还有一个参数需要考虑,即将利息转换成储备金的比例,这个参数也应该是一个全局变量

uint public totalBorrows;
uint public totalReserves;
uint public totalSupply;
uint public reserveFactorMantissa;

对于资金池的资金利用率指标,与之直接关联的是借贷指数因子Index:

借贷指数因子的概念是将FV=PV(1+x)t公式在借贷中一次应用,其反应的是这个资金池在单位资金量下的,经过一段时间和利率波动后,资金的未来价值与当前价值的比例。具体设计时,一个Index的计算为IndexB​=IndexA​+IndexA​×BorrowRateA​×ΔBlocks,即Index值应该也是一个全局变量,更新这个全局变量需要用到ΔBlocks和BorrowRateA, 对于ΔBlocks,需要用到一个全局变量, 来记录之前的区块高度

uint public borrowIndex;
uint public accrualBlockNumber;

对于BorrowRate,需要根据资金利用率来实时的计算,故不需要作为一个全局变量。

用户的数据结构设计:

债务记账方面:

因为compound是给用户记账来表明用户实际的借款,那么记账模式中,需要把用户的当前最新总债务principal和记账时刻的index记录到一个用户中。即为每一个用户都构建一个结构体:

struct BorrowSnapshot {
        uint principal;
        uint interestIndex;
}
maping(address=>BorrowSnapshot) internal accountBorrows;

设计用户的数据结构时,需要考虑的一个问题是:是否应该把区块高度也设计到用户的数据结构体中?因为最简单的想法肯定是把用户的当前累计债务含利息,用户的利率指数,以及当前的区块高度一起写进去。但是需要考虑到的问题是利率指数事实上已经包含了时间这一信息,当计算用户的新的债务时,只需要把Index_B/Index_A即可。不再需要时间。故不应该把blocknumber写到用户的数据结构中。

权益方面:

针对用户存款获取相应的权益,事实上就是用户收到的cToken。这里对于用户的cToken记账,可以采取最简单的ERC20记账模式,即:

mapping(address=>uint256) internal accountTokens;
汇率相关的指标:

汇率是一个可以通过如下公式来进行实时计算的一个值,但是对于每一个资金池的初始添加流动性时,需要用到一个汇率的初始值,这个初始值应该设计成一个全局变量:

汇率计算公式:

汇率=underlyingToken总数cToken总数​

uint internal initialExchangeRateMantissa;
ERC20相关的全局变量:

cToken是一个ERC20的token,它应该包含有普通的ERC20所应该有的全局变量:

string public name;
string public symbol;
uint8 public decimals;
mapping (address => mapping (address => uint)) internal transferAllowances;
uint public totalSupply;

accrueInterest函数

借贷产品如Compound中,最核心的一个概念是借贷指数,即accrueInterest, 如前文所言,借贷指数事实上是一个复利公式,即:

FV=PV×(1+x)t=PV×(1+xt), 这里在compound中的具体设计应为:IndexB​=IndexA​+IndexA​×BorrowRateA​×ΔBlocks

accrueInterest中涉及到的全局变量:
accrualBlockNumber -> 用于存放上一次更新的区块高度
borrowIndex -> 借贷指数,即Index_a
totalBorrows -> 资金池的总债务,含利息
具体函数设计逻辑:
function accrueInterest() public returns  {
//index应该是全局变量,这个函数的最终目的是更新Index的值
//它应该是按照块来更新的,即delta Blocks不应该为0,如果delta Blocks为0,则应该直接返回
//需要计算delta Blocks,也就是需要用到当前的区块高度和上一次的区块高度。Index_A是未更新Index之前的数值,borrowRateA应该是此时的利用率对应的借贷利率。需要思考的是这里的区块高度应该是一个全局变量还是一个写在用户数据里的一个结构体变量?这里应该是一个全局变量
//    
}

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 1天前
  • 阅读 ( 60 )
  • 学分 ( 5 )
  • 分类:智能合约

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK