用 Python 開發 DeFi 去中心化借貸應用
本文最後更新於:2022年1月29日 凌晨
利用Solidity和Python在Aave測試鏈上進行交易的Defi應用
最近投入大部分的寒假時間來學習,第一次把專案用比較詳細的文字敘述記錄下來,收穫頗多!
也剛好是因為已經有好心人整理要點,自己在思考過程中順暢很多(也省掉碼字的力氣)!
一直在反思要怎麼更好的吸收學習的東西,目前是這樣子的想法:
「先寫筆記,再回頭code」。兩件事情混在一起,對大腦認知負擔會太大
總之拉回正題,今天的專案是「利用Solidity和Python在Aave測試鏈上進行交易的Defi應用」
總目標:
- 抵押
- 將ETH換成 WETH
- 把一些ETH存入aave
- 使用存入的ETH(抵押品Collateral)來借入一些資產assets
- 做空Short Selling(把借來的資產賣出)
- 歸還Repay所有債務
- 拿出貸款
- 償還貸款
工具:
- brownie
- Aave
- Python
0. 前置工作
brownie init
這個專案我們不會自己創建合約,因為都是要使用外部鏈上已經發布的˙合約
- 把ETH存進WETH 合約會讓你拿到WETH Token
- 所以我們需要先呼叫WETH 合約的deposit()
- 如何呼叫合約呢? 透過合約地址和合約abi
- 怎麼取得合約地址和合約abi? abi = interface(address)
- 透過「本地自定或config」(地址)以及「 WETH 的 Interface (
IWeth.sol
)」(abi)! - 先取得address,再利用address取得abi
IWeth.sol
裡面包含了ERC20
合約的所有方法
- 透過「本地自定或config」(地址)以及「 WETH 的 Interface (
先使用
Koven
上的weth_token address(==WETH 在Koven上的合約地址)測試
線上+integration test : koven
本地+ unit test : mainnet-fork
- 因為不使用oracle,所以也不需要 部屬mocking contract,不過還是可以用
- 直接把mainnet上的資料都抓下來
- 要先把
mainnet-fork
加入「development」的鏈上(用brownie list true
查看)- 先移除在原本的 「ETHEREUM」鏈上的
- 再加入「development」的鏈上
- 要先把
預設的測試網路其實是「development + mocking」
不過如果你不會用到oracle,你可以直接使用mainnet-fork
1 |
|
超級常用的get_account()
1 |
|
accounts.load
目標: 為了從script或console存取local account,首先必須要「解鎖」
鑰匙本人: accounts.load
,解完鎖就可以在Accounts
container.中使用
1 |
|
accounts / local account
- 唯一差別:
local account
的私鑰是直接被放置的- so is not found in
web3.eth.accounts
.
- so is not found in
Resetting the RPC client will delete all
LocalAccount
objects from theAccount
container.
1 |
|
dotenv= .env
1 |
|
1. 將ETH換成 WETH(By AAVE)
我們使用
./scripts/get_weth.py
腳本中的get_weth
函數完成了此操作。
測試網上的第一筆交易!
- 為了與 Aave 協議交互,把我們的 ETH 換成 ERC20 版本的 ETH,稱為 WETH(也稱為wrapped ETH)。它使我們更容易與 AAVE 協議交互。
- ERC20 是以太坊上的通用代幣標準。
- 您應該會看到您的 MetaMask 餘額減少。這是因為我們正在將 ETH 換成 WETH。
- 為了查看我們的新餘額,進入 MetaMask,然後點擊添加令牌。然後,在自定義令牌下輸入地址 0xd0a1e359811322d97991e03f863a0c30c2cf029c。
- 這是 Kovan 測試網上 Wrapped Ether 代幣的合約地址。
- 你會注意到現在總共不到 2 個 ETH (1 + 0.99 != 2)。這是因為每次在區塊鏈上進行交易時,都需要支付一點 gas。
- gas 是一種向礦工和驗證者支付少量交易費用的方式。有兩種 gas:
- TransactionGas2、
- OracleGas
- 在這個專案中,我們只需要關注 transaction gas。
- gas 是一種向礦工和驗證者支付少量交易費用的方式。有兩種 gas:
- 就是你在測試網上的第一筆交易!
1 |
|
我們本身有一個儲存ETH的帳戶,要和WETH的合約進行交互
- 我們需要獲取 WETH 合約對象,以便與該合約對象進行交互。
- 我們想將 ETH 存入合約,所以它會為我們鑄造相同數量的 WETH。
- 我們可以隨時使用此合約將 WETH 轉換回 ETH。(使用
withdraw()
)
- 我們可以隨時使用此合約將 WETH 轉換回 ETH。(使用
- 所有 ERC20 代幣(如 LINK、WETH、AAVE 等)本身都是鏈上合約。
- 我們想將 ETH 存入合約,所以它會為我們鑄造相同數量的 WETH。
- 要與合約交互,我們需要兩個東西。
- 合約ABI/接口
- 合約地址
- 我們可以使用該interfaces 接口,因為它可以編譯為 ABI。
- 編譯後的項目將合約放入 build 文件夾中。
- 如果我們查看 build 下的 interfaces 文件夾,我們可以看到一個名為 WethInterface.json 的文件。在該文件夾中,有一個名為 abi 的密鑰。
- ABI 代表 APP 二進制接口,是程序了解如何與合約交互的標準方式,簡單來說,就是每個合約的「使用手冊」
- 我們可以通過創建一個這樣的 weth 變數來將地址和 ABI 添加到一個對像中以供我們交互
1 |
|
- 我們再一次從配置文件中獲取了 weth_token 地址。你會注意到不同的代幣有不同的地址,這取決於你正在處理的鏈。
- 如果我們要
print(type(weth))
,我們會得到:
- 如果我們要
1 |
|
Contract
對象就像一個類,它代錶鍊上的合約。然後我們可以在鏈上調用該合約的函數。- 因此,我們調用
weth
合約上的存款函數deposit
,並回傳該次transactiontx
:
- 因此,我們調用
1 |
|
補充原理
1. 我要如何在以太坊上進行交易或調用?
以太坊上交易(transact)==修改區塊鍊的狀態,調用(call)只是查看狀態
如果您想修改區塊鏈的狀態,您必須始終 from一個帳戶。
應用:
- 我們正在修改區塊鏈的狀態,因為我們將修改我們的 ETH 和 WETH 餘額。
每當我們進行函數調用並修改區塊鏈的狀態時,我們就會進行交易。
1
2Transaction sent:
0x888bb9d6657b1de2e5eec465bf9641b401647a61a2bd428b51d8a95d5a3e329a然後,您可以將此交易哈希複製到區塊瀏覽器中以查看該交易的詳細信息。
2.何謂interface接口?
- 接口就是一組「可以面向外部的共同的調用方法」
- 對於外部程序來說,如果繼承了這個接口,那麼這個合約一定包含接口中的方法和實現
1 |
|
3.函式的modifier宣告的順序
- Visibility
- Mutability
- Virtual
- Override
- Custom modifiers
4.Constructor
怎麼用?
- 用來初始化該合約的state variable
- 是選擇性使用的
- 有用
- 每個合約只能有一個
- 當該合約被建立,
Constructor
就會執行
- 沒用
- 一個default constructor 會自動出現在合約
- 有用
- 執行完畢,
final code
會被部屬到鏈上- YES:
public
function +code reachable through public functions
- NO:
Constructor code
+any internal method used only by constructor
- YES:
- 可見度:
public
或internal
- 如果是
internal
,代表這是「抽象合約」
- 如果是
- 如果繼承來的合約(父合約)的
Constructor
有參數,那「子合約」也要輸入參數- 有兩種方法可以使用
- 注意: (父合約)本人不能用這兩種方法來初始化
1 |
|
- 若「子合約」沒有輸入參數,會被視為「抽象合約」
- 如何建立可以在腳本中使用的帳戶? 如何用PYTHON獲得交易所需的帳戶?
- 位於
brownie-config.yaml
。使用我們的PRIVATE_KEY
環境變量。
- 位於
1 |
|
- 現在不用擔心 MNEMONIC。我们將該帳戶添加到我們的Brownie accounts列表中:
1 |
|
- 如何透過「對方合約的地址和ABI」和對方合約進行交互?
- 如何使用函式來「調用Call」並傳送交易
Brownie的功能
- accounts
- network
- config
- interface
2. 把一些ETH存入aave
運行 aave_borrow.py 腳本,它做了 4 件事:
- 將抵押品(Collateral,此處是ETH)存入 Aave 貸款池
- 獲得了我們的抵押品和另一種資產之間的匯率
- 使用該抵押品借入不同的資產(貸款)
- 還清了貸款
我们看一下main
函數的開頭:
1 |
|
1. 從配置文件中獲取我們的地址
- 這在 get_account 函數中定義
- 如果我們在測試網上,它會再次從我們的配置中提取
- 如果您在本地鏈上進行測試,我們只使用它生成的第一個帳戶。
2. 我們也從配置文件中獲取 WETH 令牌的地址
- 如果我們在本地鏈上,我們調用之前調用的相同 get_weth 函數。
- 這是為了確保如果我們使用本地鏈,我們的錢包中有 WETH。
3. 我們得到了lending_pool的地址
- lending_pool是管理借貸功能的鏈上智能合約。
- 您可以在 Aave 文檔中查看功能,或者您可以直接在鏈上查看合約。
lending_pool
我們在這個合約中看到了許多有用的功能:
- 借貸
- 抵押
- 獲取用戶帳戶數據
- 償還
請記住,鏈上智能合約與傳統軟件工程中的對像object或類class相同,每個都有與之相關的功能。
如果您要print(type(lending_pool)),我們會得到這種類型
1 |
|
這意味著借貸池是一個合約,就像 WETH 一樣!
get_lending_pool()
函數
- 我們的
get_lending_pool()
函數實際上做了一些額外的步驟來獲取貸款池合約lending_pool
:
1 |
|
- 我們需要兩個東西才能與智能合約進行交互:ABI/接口+ 地址
- 我們有位於interfaces文件夾中的借貸池的接口。
- 補充: 你會注意到一個流行的接口約定是讓它們以字母“I”開頭。我們在這個例子中有一個mix,表示它不必以“I”開頭。
- 為了讓我們獲得借貸池的地址,我們必須透過 LendingPoolAddressesProvider 的地址。
- 這是一個鏈上合約,其中包含 Aave 借貸池的所有地址。
- 有時,出借池地址發生變化,但出借池提供者地址永遠不會改變,因此我們始終可以使用該合約的地址來獲取出借池地址。
- 你會看到我們在
lenting_pool_addresses_provider
變量上調用了getLendingPool
函數
- 這是一個鏈上合約,其中包含 Aave 借貸池的所有地址。
存入抵押品
讓我們回到主函數。在我們得到了貸款池合約的地址後,我們要先存入抵押品,才能獲得貸款。
存入抵押品將使我們獲得:
收益: 該協議向將抵押品存入平台的用戶支付報酬。
取出貸款: 我們只有在有足夠的抵押品的情況下才能貸款/借入。
- 超額抵押貸款
- Aave 有一個名為
Liquidationcall
的函數,如果您沒有足夠的抵押品,任何人都可以調用該函數並獲得獎勵。抵押品的價值必須保持大於貸款的價值,
- Aave 有一個名為
- 超額抵押貸款
流動性挖礦 = 用戶存入抵押品增加流動性,該合約會提供獎勵
- 也稱為收益耕作,讓用戶能夠以治理govermance或其他類型代幣的形式獲得獎勵,以向協議提供流動性。
- 但是,為了讓我們存入一些代幣,我們必須批准智能合約才能將代幣從我們的錢包中取出。
- 我們將把我們的 WETH 作為抵押品。
approve_erc20
讓我們看看我們的approve_erc20函數。
我的account允許lending_pool_address這個合約(借貸池地址)從erc20_address這個地址領取amount這麼多的ERC20 代幣
1 |
|
- amount:允許協議使用多少 ERC20,以 wei 為單位。
- wei 是區塊鏈世界中最小的計量單位。 1 ETH == 1000000000000000000wei (10^18)
- lending_pool_address:借貸池地址。
- erc20_address:ERC20 代幣的地址。(哪一個幣種?)
- account:我們想要交易的賬戶。
原理:
在使用 ERC20 代幣時,如果我們希望從我們的錢包中“調用”另一個合約,我們必須首先批准該合約
我們調用 ERC20 合約(我們的 WETH)上的批准函數(
approve_erc20
),然後我們“等待”tx_hash.wait(1)
該交易繼續進行。現在我們已經批准了合同,我們可以將我們的抵押品存入我們的主函數中的
lending_pool
合同中(來自get_lending_pool()
)
lending_pool
借貸池合約,完成deposit!
我讓account.address存amount這麼多的erc20_address該種類代幣到借貸池
1 |
|
我們可以在這裡看到,我們在合約上調用了deposit函數,我們給了它:
- erc20_address:要存入的代幣(在我們的例子中是 WETH)
- amount:要存入的 wei 金額。
- account.address:我們想為誰貸款,在這種情況下是我們自己。
- 0:這是推薦代碼(已折舊,始終使用 0)
我們可以從 Aave 文檔中了解更多關於智能合約中的存款功能。
Etherscan 上 Kovan 測試網的例子
如果你正確地完成了這部分,你會得到一個交易哈希,它會告訴你發生了什麼。
讓我們看一個關於 Etherscan 上 Kovan 測試網的例子。
如果我們查看 Tokens Transferred 部分,我們可以看到 1 筆存款交易發生了 3 筆交易。
按順序,這些是:
- 一些 aToken 被鑄造出來供我們存入 (aWETH)
- 我們發送了 1 個 WETH 到 lending_pool 合約(這是我們的存款)
- 我們收到了一些 aToken
所以中間代幣轉移是有道理的——我們正在向借貸池發送一些 WETH。但是其他token是什麼?
屬於支付利息Interest Bearing 的代幣aToken
aToken 是一種計息代幣,在存款時鑄造並在贖回時銷毀。
- 所以當你取回你的存款時,你燒掉你的 aToken 以獲得等量的基礎資產。
- aTokens 最酷的部分是它們的價值每時每刻都在上漲!
- 餘額增加是因為人們使用 Aave 協議並藉用您的資產,因此您因此獲得報酬!
您可以查看某人的餘額並每隔幾秒鐘刷新一次,以實時查看餘額的增加情況。
每當我們想要贖回或提取我們的 WETH 時,無論我們擁有多少 aWETH,我們都會提取,而這些 aWETH 將被燒毀。
與 WETH token相同,您現在可以在您的 MetaMask 錢包中看到 aWETH。只需進入您的 MetaMask,並執行我們上面所做的添加的token部分,Kovan WETH 地址為
0x87b1f4cf9bd63f7bbd3ee1ad04e8f52540349347
3. 貸款–使用存入的ETH(抵押品Collateral)來借入一些資產assets
現在我們已經存入了一些抵押品,我們可以貸款了。現在我們為什麼要貸款?
- 無摩擦賣空
- 無需平倉即可獲得流動性
健康系數
您可以借入資產以保持對標的抵押品的敞口,同時還可以用另一種資產進行交易或支付。只要確保將您的健康係數保持在 1 以上。
- 您的健康因素是您貸款的抵押品數量。
- 假設我存入了 1 ETH,借了 0.5 ETH。如果清算閾值為 1,那麼我的健康係數將為 2。
清算門檻( liquidation threshold )是貸款被定義為抵押不足的百分比。
例如,清算門檻為 80% 意味著如果價值超過抵押品的 80%,則貸款抵押不足,可以清算。
但是假設我有 2 ETH 借入和 1 ETH 作為抵押品,1 作為清算門檻。我的健康係數是 0.5,我會被要求清算。這是清算人代表借款人償還部分或全部未償還借入金額的情況,同時獲得折扣金額的抵押品作為回報(也稱為清算“獎金”)。
清算人可以決定他們是否想要獲得等值的金額 抵押aToken,或者直接標的資產,
當清算成功完成後,增加倉位的健康係數,使健康係數在1以上。通過這種方式,來保持貸款健康
getUserAccountData
函數
目的: 了解帳戶情況( 抵押品/借貸(債務)/可借貸數量/清算門檻/貸款價值比 (Loan-to-value ratio)/健康系數)
以我們的抵押品借款,我們首先要衡量我們作為抵押品的抵押品數據。我們可以調用貸款池上的 getUserAccountData 函數來找出這一點。
- 我們將它封裝到一個名為 get_borrowable_data 的函數中
- 我們只關注 available_borrow_eth 和 total_debt_eth,這樣我們就會知道:
- 我們可以借多少錢
- 我們目前有多少債務
Web3.fromWei( )
web3.utils.fromWei(number [, unit])
從 wei
轉為指定單位
number
:接受格式字串、數字、BN,單位是wei
。uint
:接受格式字串 (非必填),預設是ether
。
1 |
|
為什麼我還沒有進行交易?
其實有兩種類型的智能合約功能,您無需進行交易即可調用。
- 視圖函數 view
- 純函數
這些函數不會修改區塊鏈的狀態,因此我們可以調用它們,因為我們只是在讀取智能合約的狀態。
請記住,如果您修改區塊鏈的狀態,它只會花費 gas。
獲取可借資產價值
目的: 借不同種類的代幣,需要得到「資產VS目標代幣的匯率」
- 現在,當我們借款時,我們會被告知我們擁有的以 ETH 表示的資產數量。然而,也許我們想借用一個不同的代幣,比如 DAI。如果我們要以 ETH 為抵押借入 DAI,我們首先要得到 ETH -> DAI 的兌換率。
Chainlink 介紹
Chainlink 是 DeFi 和智能合約生態系統中獲取數據和執行外部計算的標準,也是 Aave 用來獲取費率的標準!他們有一個價格 Oracle 部分,描述如何通過他們的智能合約 API 獲取 Chainlink 數據。如果你學會了一般地使用 Chainlink 數據,你可以跨協議使用它,即使他們沒有使用 Chainlink,或者沒有價格預言機機制!
Chainlink 通過去中心化的預言機網絡獲取數據,其中多個節點將跨多個 API 和服務的數據聚合到一個去中心化的來源,該來源經過驗證並記錄在鏈上供我們使用。我們可以在 data.chain.link 頁面或文檔上看到當前可用數據提要的列表。如您所知,數據是大多數量化系統、算法交易者和整個 DeFi 生態系統的支柱,因此您要始終確保自己是從去中心化系統中提取的,這樣您就可以獲得最準確、最可靠和最安全的數據。
此外,如果您不想要某些數據,您可以從任何 API 調用您的智能合約,獲得可證明的隨機數以及許多其他不同的服務。這些使得智能合約數據安全、可靠和去中心化。
但是讓我們回到 Python!
法1: get_asset_price
目的: 它從 dai_eth_price_feed 中讀取數據,如下所示:
1 |
|
我們從Interface接口中提取的 Chainlink 合約 AggregatorV3Interface 已存儲在我們的配置文件中。
- 自行複製AggregatorV3Interface.sol檔案並設置在config中
在該合約中,我們調用
latestRoundData
函數- 你會注意到我們也不需要為這個做一個交易,這也是一個視圖函數!
法2: 使用ENS 來獲取價格對的地址
- 請注意,ENS 僅適用於主網網絡,
- 因此您在運行 brownie 時必須使用 –network mainnet 或 –network mainnet-fork。
你會看到通過brownie獲得 Chainlink 數據價格的另一種方式是:
1 |
|
這是使用 ENS 來獲取價格對的地址。
- ENS 是一種獲取智能合約地址並使其可讀的方法。例如,您可以執行以下操作:
1 |
|
以美元為單位獲取以太坊的最新價格。
回到貸款。
每項資產都有不同的貸款借入比率,它告訴您的清算門檻,以便您借入資產。
一旦我們得出了可藉貸款量,我們就可以借了!
1 |
|
做空Short Selling(把借來的資產賣出)
將價值轉換為借用
- 現在我們有了抵押品和我們想要借入的資產之間的轉換率,
- 我們可以設置一個變量來確定我們想要借入多少。
1 |
|
- 我們得到 ==ETH/DAI 價格的倒數==,並將其乘以我們==可以借入的金額(以 ETH 為單位)==。
- 為了安全起見,我們還將其乘以 0.95。然後,我們調用借用函數
lending_pool.borrow
!
- 為了安全起見,我們還將其乘以 0.95。然後,我們調用借用函數
1 |
|
借用函數需要幾個參數:
- 資產的地址(在我們的例子中是 DAI 代幣)
- 我們要借的金額,單位為 wei
- interestRateMode,(0, 1, 2: 1 是穩定的,2 是可變的……現在不用擔心 0)
- 推薦碼(忽略)
- onBehalfOf 值,即誰將承擔債務
讓我們看一下 Etherscan 上這筆交易的樣本,以了解剛剛發生的事情。
按順序,代幣轉移的方向是:
- Aave 協議正在鑄造一些 aDAI
- 我們的錢包正在鑄造一些 Aave Stable Debt Bearing DAI
- 我們的錢包獲得了一些 DAI
重要說明:Aave 有時會更改其測試網token。通過檢查已部署的合約地址,確保您擁有正確的 testnet DAI 地址。並檢查官方 json 列表。
債務代幣
這一次,我們得到了一種不同類型的計息代幣,一種債務代幣。
- 這是我們必須償還的金額,並且與 aToken 一樣,它的價值也會增長。
- 如果你欠更多的債務(用這個債務代幣表示),有人可以清算你並拿走你所有的抵押品!所以要警惕你有多少債務。
查看狀況:
- 通過添加新的代幣地址,我們可以再次在我們的 MetaNask 中看到借入的資產。
- 我們還可以通過添加債務token地址來查看我們的債務。
- 現在最簡單的賣空方法之一就是出售我們的資產。如果資產價格下跌,我們將不得不償還比借入的更少的錢。
然而,我越來越擔心我會被清算。讓我們償還我們的債務。
4. 歸還Repay所有債務
償還我們的債務
我們想弄清楚我們欠了多少,然後償還!我們可以用這些函數做到這一點:
1 |
|
總結
我們已經完成了與 Aave 合作的所有步驟。
- 抵押
- 拿出貸款
- 償還貸款
小結:
筆記很花時間(約4~5小時),不過省下更多之後回頭重看影片的麻煩
寫完也把很多模糊的概念理清楚了,繼續精進!
資料來源:
Solidity, Blockchain, and Smart Contract Course – Beginner to Expert Python Tutorial - YouTube
用 Python 开发 DeFi 去中心化借贷应用 (qq.com)