1

Quick Look DeFi Contract Testing With Foundry

 1 year ago
source link: https://medium.com/taipei-ethereum-meetup/quick-look-defi-contract-testing-with-foundry-24361c23107c
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.

Quick Look DeFi Contract Testing With Foundry

最近開始學習 Foundry 和 DeFi,乾脆摻在一起做成一篇文章。

Table of Contents

  • Intro.
  • Cast an eye over the “Testing with Foundry”
  • “Brewing” Time — Basic Defi Project
  • “Tasting” Time — Unit Testing
  • Tipsy — Coclusion & Reference

Synchronization Link Tree

Intro.

前兩個月使用過的 Foundry 變的越來越潮了,所以想來跟大家分享一下這個新穎的測試工具!

這次的主要流程為:先寫一個簡單的 DeFi Project,有 Staking 的功能,之後用 Foundry 對其進行測試。本來想要挑現行的有名 Project 來測試的,但找到的 DeFi Project 都有一點點巨應該值得更長的篇幅特別分享!

Foundry 更新的速度遠比我想像中快,不過是一個月沒看而已,很多東西都完全長得不一樣了,所以大家如果遇到任何問題可以先參考一下 Reference 中的官方文件們。

Cast an eye over the “Testing with Foundry”

Testing Type

Unit Testing

  • Unit Testing 通常是指完整、獨立地測試每一個部件,在給定各種輸入的情況下和預期的輸出要相符。在測試的過程中通常不會考慮其他部件的影響,在 Solidity 撰寫的 Smat Contract 中,部件通常指的是每一個 Function。需要注意的輸入有:
  • 邊際測資:空字串、bound(e.g. 0, min, max, 2²⁵⁶, -2¹²⁸…)
  • 極端測資:超長的輸入
  • 特殊測資:含有特殊字元的輸入

Integration Testing

  • 將許多個 Unit 組合之後一起進行測試,確保這些部件無論是:
  • 在一起隨機運作、有特定目的運作,或甚至模擬特定情況的運作,都是正確無誤的。
  • 除了隨機交互作用之外,模擬的情況可能有:Owner Operation、WhiteList Operation、User Operation 等。

Regression Testing

  • 以迴歸的方式來對版本重測,確定舊版本 Bug 於修正後不會在新版本中出現。

Stress Testing

  • 會嘗試去模擬現實世界的運行流,通常會有多個使用者隨機操作產品,也會有不同地方的 Provider,這樣可以去測試系統中是否有 Deadlock 的發生,或者不正常甚至不斷重複呼叫某一個功能的情況。
0*hsNXEtxmT4YTRVUH.jpg
範例圖片出處

Security Testing

  • 針對特定攻擊或者目的進行預防測試,例如重送攻擊、閃電貸等。

What features can we use in Foundry

Foundry 由以下兩者組成:

  • Forge: 和我們平常使用的其他開發工具一樣,是一個 Ethereum 的測試框架。
  • Cast:支援多種客戶端功能,像是與 EVM 智能合約互動、傳遞交易、取得鏈上資訊等,就如同一把瑞士刀(官方文件寫的)。

來自官方的 Foundry 特性:

  1. 快速且彈性的編譯 Pipeline
  • 自動偵測並下載 Solidity 不同版本的編譯器(under ~/.svm)
  • 增量編譯和緩存: 只有被修改的檔案會被重新編譯
  • 支援非特定的目錄結構(e.g. Hardhat repos)

2. 以 Solidity 撰寫測試

3. 快速的 Fuzz testing,能夠收斂到最小的輸入,並輸出其反例

4. 快速的遠端 RPC 分岔模式, 利用類似 tokio 的 Rust 異步運行架構

5. 彈性的 debug 紀錄輸出(logging),例如:Dapptools-style 的 DsTest’s emitted logs 和 Hardhat-style 的 console.sol contract

6. 非常輕量(5–10MB),不需要 Nix 之類的套件管理器

7. 能利用 foundry-toolchain 使用 Foundry GitHub Action 快速的 CI(持續性整合)

Preparation

如果作業系統是 Linux 或 macOS 最簡單的方法就是使用以下方法下載 Foundry:

curl -L https://foundry.paradigm.xyz | bash
foundryup

下載完成之後再執行一次 foundryup 會將 Foundry 更新至最新版本,如果想要返回到指定版本,則使用指令 foundryup -v $VERSION

然而我自己是使用 Windows,下載的方式如下。

在下載 Foundry 之前得先擁有 Rust 和 Cargo,首先到 rustup.rs 下載 rust,然後執行:

rustup-init

這樣就能同時準備好 Rust 和 Cargo,最後打開 CMD 使用以下指令安裝 Foundry。

cargo install --git https://github.com/foundry-rs/foundry foundry-cli anvil --bins --locked

下載成功以後在電腦的某個地方使用 init 初始化一個專案。

$ forge init defi-testing

forge CLI 將會創建兩個檔案目錄:libsrc

1. lib 利用了 git submodules 來管理 dependencies,包含:

  • ds-test 中的 testing contract (lib/ds-test/src/test.sol)
  • 各式各樣測試合約的實作 demo(lib/ds-test/demo/demo.sol)
  • 和其他我們下載的 dependencies,例如:forge-stdweird-erc20solmate

2. src 放了我們寫的智能合約和測試的原始碼

.
├── foundry.toml
├── lib
│ ├─ds-test
│ │ ├─demo
│ │ └─src
│ └── forge-std
│ ├── lib
│ ├── LICENSE-APACHE
│ ├── LICENSE-MIT
│ ├── README.md
│ └── src
└── src
├── Contract.sol
└── test
└──Contract.t.sol

之後一樣在終端機的部分,輸入指令:

$ forge install OpenZeppelin/openzeppelin-contracts
>
Installing openzeppelin-contracts in "C:\\Users\\qazws\\Desktop\\Blockchain\\defi-testing\\lib\\openzeppelin-contracts", (url: https://github.com/OpenZeppelin/openzeppelin-contracts, tag: None)

便可以在 lib 中看見 OpenZeppelin 的合約們。

foundry.toml 裡面決定 Foundry 的運行設定,包含 Remap 我們 import 或執行命令的路徑,以下列出一些常用的參數:

[default]
src = 'src'
test = 'test'
out = 'out'
libs = ['lib']
remappings = ['ds-test/=lib/ds-test/src/',
"@openzeppelin/=lib/openzeppelin-contracts/"]
evm_version = 'london'
#solc_version = '0.8.10'
sender = '0xB42faBF7BCAE8bc5E368716B568a6f8Fdf3F84ec'
tx_origin = '0x00a329c0648769a73afac7f9381e08fb43dbea72'
initial_balance = '0xffffffffffffffffffffffff'
gas_limit = 9223372036854775807
gas_price = 0
block_timestamp = 0
gas_reports = ["*"]

更多詳細內容可查看以下連結

“Brewing” Time — Basic Defi Project

全文的原始碼在此

Implementation — Simple Staking Contract

三個平行合約輔以 ERC20OwnablesafeMath 等函式庫的陽春 DeFi Staking。基本上就是 User 可以透過抵押 stableCoin,換取抵押時間計算而得的 holdToken 收益。

Contract.sol:

StableCoin.sol:

LP.sol:

“Tasting” Time — Unit Testing

Initialization & First Testing

開始 Foundry 的測試時,setUp() 會是測試開始的切入點,每一個「測試函式開始前」都會特別為其「設置」一個初始狀態,也就是 setUp() 中的內容。

這邊主要測試 Owner 和兩個 ERC-20 合約的運作是否正常。

$ forge test
>
[⠆] Compiling...
[⠃] Compiling 4 files with 0.8.10
[⠊] Solc 0.8.10 finished in 2.79s
Compiler run successful (with warnings)[PASS] testMockContractInit() (gas: 21400)
[PASS] testMockContractTransferFrom(uint256) (runs: 256, μ: 95003, ~: 107435)
[PASS] testOwner() (gas: 9852)Test result: ok. 3 passed; 0 failed; finished in 0.64s

需要注意的點有:

  1. FarmTest 裡面我們在 setup()new 宣告合約之後,可取得 LP 和 StableCoin 的地址作為 FarmConstructor 參數傳入
  2. address(this) 是測試合約本身,也就是 FarmTest
  3. setup() 佈署 LP、StableCoin 和 Farm 這些合約的 Deployer 是 FarmTest 這個測試合約本身:<contract_deployer> == address(this)
  4. 使用 Solidity 來寫測試時,我們並不是透過 EOA 來 sign 一個合約(這個情況下 signer 是 msg.sender),所以 msg.sender 會需要是一個預設的值:msg.sender == <sender_in_foundry.toml>
  5. <contract_deployer> != msg.sender

在 Foundry 中如果需要 Gas Report 可以使用以下指令:

$ forge test --gas-report

More features can use

Foundry 同樣也支持 Fuzzing 測試。因為當我們一個一個函式都進行測試時,即便全部都成功 PASS,但在邊際測資中其實也很有可能會出現一些問題,導致 Under/Overflow 或其他 RuntimeError/Memory Leak 之類的錯誤。

我們在測試函式中增加參數之後,Fuzzing 能夠讓 Solidity test runner 隨機選擇大量的參數輸入我們的函式。

在以上例子中 fuzzer 會自動地對 x 嘗試各種隨機數,如果他發現當前輸入會導致測試失敗,便會回傳錯誤,這時候就可以開始 debug 啦!

進行測試:

$ forge test
>
[⠆]Compiling...
[⠆]Compiling 1 files with 0.8.10
Compiler run successfulRunning 3 tests for FooTest.json:FooTest
[PASS] testDouble() (gas: 9384)
[FAIL. Reason: Arithmetic over/underflow. Counterexample: calldata=0xc80b36b68000000000000000000000000000000000000000000000000000000000000000, args=[57896044618658097711785492504343953926634992332820282019728792003956564819968]][0m testDoubleWithFuzzing(uint256) (runs: 4, μ: 2867, ~: 3823)
[PASS] testFailDouble() (gas: 9290)Failed tests:
[FAIL. Reason: Arithmetic over/underflow. Counterexample: calldata=0xc80b36b68000000000000000000000000000000000000000000000000000000000000000, args=[57896044618658097711785492504343953926634992332820282019728792003956564819968]][0m testDoubleWithFuzzing(uint256) (runs: 4, μ: 2867, ~: 3823)Encountered a total of [31m1[0m failing tests, [32m2[0m tests succeeded

從以上錯誤會發現當參數輸入為 57896044618658097711785492504343953926634992332820282019728792003956564819968 之後會出現錯誤,來到 wolframe 貼上這個數字會發現其為 5.789 * 10^76 ~= 2^255

聽起來十分合理因為 x 的型態是 uint256,所以如果將其乘於 2 以後就會超過 uint256 的型態範圍!

未來 Foundry 除了 Fuzz Testing 之外,還會支援:

  • Invariant Testing
  • Symbolic Execution
  • Mutation Testing

New Features 可以在這兩個 Repo 找到:forge packageCLI README.

Cheating with Standard Library

$ forge install foundry-rs/forge-std

下載了 Standard Library 之後在 Contract.t.sol 我們就改繼承 Test.sol 不用 ds-testtest.sol

以下節錄自 forge-std/Test.sol 的原始碼,可以發現已經實作了 ds-testVm.solconsole.sol 這些我們需要的部分。

Vm public constant vm = Vm(HEVM_ADDRESS);

在宣告以後便可以使用 vm,他是 Foundry 中的 CheatingCode,可用於模擬「該 Test Function 中」的 EVM 和區塊鏈狀況,例如 :

  • vm.deal 可用於預設一個地址擁有一定數量的代幣(例如 deal(address(dai), address(alice), 10000e18);
  • vm.warp 可以指定 block.timestamp 等。

msg.sender in Foundry

msg.sender 在 Foundry 中是一個特別重要的存在,是過往用其他語言寫測試比較少注意到的部分。

大家還記得之前的 foundry.toml 嗎!如果我們在裡面加上參數 sender 就可以指定在合約測試時預設的 msg.sender

[default]
src = 'src'
out = 'out'
libs = ['lib']
remappings = ['forge-std/=lib/forge-std/src/','ds-test/=lib/ds-test/src/', "@openzeppelin/=lib/openzeppelin-contracts/"]
sender = '0xB42faBF7BCAE8bc5E368716B568a6f8Fdf3F84ec'
block_timestamp = 0# See more config options https://github.com/gakonst/foundry/tree/master/config

從官方文件整理的函式比較表:

1*KliV3PP9-SMeAQrt7i84CA.png
Link: OriginalCheatcode, related Forge-STD

在 STD 中也可以使用 console.log 的模擬環境,需要注意的是 prank() 只適用於下一個 external call,而 console.log 並不是 external call 所以沒辦法印出我們想像中的地址。

舉例來說在測試開始前的 setup() 中,我們想要假裝由一個 EOA(address(deployer)) 來 Deploy 合約,而不是和上面 FarmTest 一樣的使用 FarmTest 這個合約本身來 Deploy:

以上這個小例子的結果是:對 MyContract 這個合約來說,他的 Constructor 會認定 msg.sender,也就是 address(deployer) 是他的 owner。

大致了解了 vm.prank() 的功能之後,我們可以回到 FarmTest 來看第一個 Prank 測試:

這個測試的目的在觀察合約 Owner 被轉換之後,用 prank 的方式觀察「最一開始的 Deployer」是否還是能成功送出 transferOwnership。我們的預期是不行,因為 Ownership 已經被轉移給別人,所以我們使用 vm.expectRevert

再來看看另外一個測試:

msg.sender 在測試檔案中是一個會呼叫每個 test function 的 EOA。而 prank() 是一個 test function 中的函式,目的是去「改變 external call 的 caller」,並不會向外影響到整個測試檔的 msg.sender,因此沒辦法影響到不是 external call 的對象,例如:assertEq

Unit Testing

超級陽春的把四個函式測試一次,基本上這裡沒有實作太花俏的內容,比較需要注意的點是關於兩種代幣的模型,在設計上包含供給量得特別注意,但由於是 Mock 來示範的所以沒有特別著墨。

testStaking

合約原始碼:

相對應的測試:

特別注意模擬使用者情境的時候該在 Dapp 實作的部分要補上,例如 approve()。關於 Approve 和 Transfer 的用法與使用時機參考:

此外由於要符合我們代幣模型的設計,使用 vm.assume 來指定變數性質,例如以下測試中我們規定 testing 時輸入的參數大小不可以超過供應量(不然就沒有足夠的 balance 啦)。

testCalYield

合約原始碼:

相對應的測試:

testWithdrawYield

合約原始碼:

相對應的測試:

testUnStaking

合約原始碼:

相對應的測試:

1*9S6PhIPKdUxNb_xB2v7zvQ.png

大家也可以使用多個 mockAddress 來做交互測試,不一定要用 test Contract 本身當 receiver 或 sender,但本文並沒有實作這個測試方法。

Tipsy — Coclusion & Reference

Conclusion

因為最近要期末考(可憐大學生),所以 Integration Test、Stress Test、Security Test 等其他測試我留到下一篇再來分享!

這篇文章真的非常感謝 Nic 老師Cyan 老師智程老師陳品老師、傳鈞老師、狸貓老師給予我許多十分有幫助的建議!感謝老師們願意花許多寶貴的時間細心看過後給予指導😥

如果對 Foundry 有一些小問題,或者是想了解一些我其他的補充內容可以看這裡

Reference & Citation

Foundry Official Doc./Github

Foundry Resources

DeFi Testing

最後歡迎大家拍打餵食大學生0x2b83c71A59b926137D3E1f37EF20394d0495d72d


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK