这是一个 DeFi 系列教程,在动手实践的过程中,学习和理解 DeFi 相关的概念与原理:
我们已经有了两个 ERC-20 代币 USDC 与 WETH,有了 AMM 合约,有了 Oracle 合约。接下来利用之前的合约,尝试和理解一下借贷相关的合约逻辑。
借贷合约要注意的地方是,在计算用户能借出多少资产的逻辑中,需要用到代币的价格。这里的代币价格,来自 Oracle 的报价,而不是 AMM 合约的价格。Oracle 的报价一般基于 AMM 的价格波动,如果 Oracle 遭受攻击,借贷合约也会相应受到影响。
合约代码源文件在仓库:smallyunet/[email protected]
Oracle 使用的合约是 SimpleLending.sol,先克隆仓库:
git clone https://github.com/smallyunet/defi-invariant-lab/
git switch v0.0.3
cd defi-invariant-lab
部署合约:
forge create \
--rpc-url $RPC_URL \
--private-key $PK_HEX \
--broadcast \
contracts/lending/SimpleLending.sol:SimpleLending \
--constructor-args $WETH_ADDR $USDC_ADDR $ORACLE_ADDR
部署地址:0xd4bbFbCe71038b7f306319996aBbe3ed751E9A1C
验证合约:
forge verify-contract \
--chain-id 11155111 \
0xd4bbFbCe71038b7f306319996aBbe3ed751E9A1C \
contracts/lending/SimpleLending.sol:SimpleLending \
--constructor-args $(cast abi-encode "constructor(address,address,address)" $WETH_ADDR $USDC_ADDR $ORACLE_ADDR) \
--etherscan-api-key $ETHERSCAN_API_KEY
给借贷合约挖 10 万个 USDC,作为初始可以借贷的资产:
export LEND_ADDR=0xd4bbFbCe71038b7f306319996aBbe3ed751E9A1C
cast send $USDC_ADDR "mint(address,uint256)" $LEND_ADDR 100000000000 \
--rpc-url $RPC_URL --private-key $PK_HEX
存入 1 个 WETH 作为抵押物:
cast send $WETH_ADDR "approve(address,uint256)" $LEND_ADDR \
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \
--rpc-url $RPC_URL --private-key $PK_HEX
cast send $LEND_ADDR "deposit(uint256)" 1000000000000000000 \
--rpc-url $RPC_URL --private-key $PK_HEX
调用 borrow
函数借出 USDC,借出额度的计算是:
function borrow(uint256 amt) external {
_accrue();
require(_value(coll[msg.sender]) * LTV_BPS / 10_000 >= borrows[msg.sender] + amt, "exceeds LTV");
borrows[msg.sender] += amt;
totalBorrows += amt;
debt.transfer(msg.sender, amt);
}
我们抵押了 1 个 WETH,按照 2000 USDC/WETH 的价格,合约设定 LTV 最高 70%,也就是可以借出 2000*0.7=1400
个USDC。
来用实际交易试一下,这次借出 1400 个 USDC:
cast send $LEND_ADDR "borrow(uint256)" 1400000000 \
--rpc-url $RPC_URL --private-key $PK_HEX
查看借出 USDC 后的余额、负债、健康度:
cast call $USDC_ADDR "balanceOf(address)(uint256)" $MY_ADDR --rpc-url $RPC_URL
# 799400000000 [7.99e11]
cast call $LEND_ADDR "borrows(address)(uint256)" $MY_ADDR --rpc-url $RPC_URL
# 1400000000 [1e9]
cast call $LEND_ADDR "health(address)(uint256)" $MY_ADDR --rpc-url $RPC_URL
# 1904761904285714285 [1.904e18]
这里的健康度,指是否有可能触发清算。当查询结果大于 1,则比较安全。当健康度小于 1,则可以被清算机器人、套利者清算掉。
如果想还债的话,调用 repay
函数就可以了。
现在要体验一次清算逻辑,我们之前抵押了 1 WETH,价值 2000 USDC,借出了 1400 USDC,此时 LTV=1400/2000=70%,正好是 70%,处于安全状态。
当价格下跌到 1000 USDC/WETH,此时的 LTV=1400/1000=140%,已经超过 70% 的安全值,也超过了 75% 的清算阈值。
我们修改下在预言机里的价格,让借贷合约感知到 WETH 价格下跌了(这也就是预言机的主要作用,决定了链上的报价):
cast send $ORACLE "post(uint256[])" \
"[99900000000,100000000000,100000000000,100000000000,100100000000]" \
--rpc-url $RPC_URL --private-key $PK_HEX
再查一下健康度:
cast call $LEND_ADDR "health(address)(uint256)" $MY_ADDR --rpc-url $RPC_URL
# 952380952142857142 [9.523e17]
这里的健康度实际上是 0.9 1e18,已经小于 1 了,处于可以被清算的状态。
任何人都可以执行清算,执行清算成功后,可以获得 10% 的清算奖励,这就是很多人需要抢跑交易、优先执行清算的原因。10% 的清算奖励是指,假如你替抵押者还债 200 USDC,让他的仓位健康度大于 1,那么这个时候,按理你可以清算(部分清算)得到 0.2 WETH,由于 10% 的清算奖励,你实际上得到了 0.22 WETH。
我们现在执行交易还债 200 USDC:
cast send $USDC_ADDR "approve(address,uint256)" $LEND_ADDR \
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \
--rpc-url $RPC_URL --private-key $PK_HEX
cast send $LEND_ADDR "liquidate(address,uint256)" $MY_ADDR 300000000 \
--rpc-url $RPC_URL --private-key $PK_HEX
查看执行清算后,一些数据的变化:
cast call $LEND_ADDR "borrows(address)(uint256)" $MY_ADDR --rpc-url $RPC_URL
# 1200000000 [1.2e9]
cast call $LEND_ADDR "coll(address)(uint256)" $MY_ADDR --rpc-url $RPC_URL
# 999999999780000000 [9.999e17]
cast call $WETH_ADDR "balanceOf(address)(uint256)" $MY_ADDR --rpc-url $RPC_URL
# 899987136079723472734 [8.999e20]
可以看到,Defi 的借贷就是在玩这些金钱的数字游戏。
DeFi 开发的难点在于,需要理解一大堆金融相关的公式,看懂合约代码背后表达的业务含义,计算利息、负债率什么的。这个方向对金融行业从业者更友好一点。
Solidity 语言只是表达金融公式的工具,Solidity 的语法本身很简单,普通的开发人员很快就可以掌握。但是掌握 Solidity 语法,不代表能够理解金融体系,不代表能看懂金融公式。