Foundry入门
Foundry
开始
使用 Foundry 框架来开发或测试智能合约时,第一步需要在空白文件夹中使用命令:
forge init
然后,Foundry 会在项目目录下生成一些文件,这些文件有一些是用于测试合约的 Solidity 脚本库。
注意,在 Foundry 框架中,所有的“真正的”合约要放在 src
目录中,脚本合约要放在script
目录中,测试合约要放在test
目录中。同时,Foundry 还规定,脚本合约的后缀名为.s.sol
,测试合约的后缀名为.t.sol
使用 Foundry 进行编译
在 Foundry 框架中:
forge compile(或者 forge build)
使用该命令后,会在项目目录里生成两个新的目录:out
,·cache
在本地区块链上进行合约测试
- Foundry 内置 anvil
- Ganache
使用 Foundry 对合约进行部署
在命令行
forge create "合约名称" --rpc-url "URL" --interactive (私钥,测试时可以,注意堤防私钥泄露)
部署合约后,会在工作目录中生成两个新的目录
使用 solidity 脚本部署
使用 Solidity 脚本部署合约,需要在script
目录中新建一个.sol
文件,以下面的例子举例:
需要部署的文件为SimpleStorage.sol
(位于 src
目录下)合约代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// pragma solidity ^0.8.0;
// pragma solidity >=0.8.0 <0.9.0;
contract SimpleStorage {
uint256 myFavoriteNumber;
struct Person {
uint256 favoriteNumber;
string name;
}
// uint256[] public anArray;
Person[] public listOfPeople;
mapping(string => uint256) public nameToFavoriteNumber;
function store(uint256 _favoriteNumber) public {
myFavoriteNumber = _favoriteNumber;
}
function retrieve() public view returns (uint256) {
return myFavoriteNumber;
}
function addPerson(string memory _name, uint256 _favoriteNumber) public {
listOfPeople.push(Person(_favoriteNumber, _name));
nameToFavoriteNumber[_name] = _favoriteNumber;
}
}
Solidity 脚本书写
而要使用 Solidity 脚本部署这个合约,可以在script
目录下新建一个合约(我将其命名为DeploySimpleStorage.s.sol
注意,后缀名最好为.s.sol
,因为这是一个脚本文件。DeploySimpleStorage.s.sol
代码如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {Script} from "forge-std/Script.sol";
import {SimpleStorage} from "../src/SimpleStorage.sol";
contract DeploySimpleStorage is Script {
function run() external returns(SimpleStorage) {
vm.startBroadcast();
// 要部署的合约代码
SimpleStorage simplestprage = new SimpleStorage();
vm.stopBroadcast();
return (simplestprage);
}
}
上述脚本代码中,vm.startBroadcast()
和vm.stopBroadcast()
是一块出现的,中间的部分即为要部署的合约,可以 new 关键字新建合约,同时,记得使用 return
关键字返回部署的合约的地址。
Solidity 脚本运行
要运行上述脚本,需要在 bash 中使用下面的指令:
forge script script/DeploySimpleStorage.s.sol
这样会将合约部署在一个临时的 anvil 链上。如果想要模拟真正的交易,需要传递一个 rpc-url:
forge script script/DeploySimpleStorage.s.sol --rpc-url "rpc"
上面的指令会模拟部署到 anvil 链上,同时还创建了一个新的目录,他会存放我们之前部署的信息
如果想要广播这个消息,需要加上--broadcast
:
forge script script/DeploySimpleStorage.s.sol --rpc-url "rpc" --broadcast --private-key "key"
上述的命令中DeploySimpleStorage.s.sol
可以替换为其他部署合约的脚本。
使用上述命令部署合约后同样在目录 broadcast
下,存储部署这个合约时的信息。不同的是,没使用--broadcast
选项的信息会存储在broadcast
的子文件夹dry-run
下。
使用 thiedweb 部署合约
npx thirdweb deploy
cast命令
cast --to-base
将十六进制转换为十进制
cast --to-base 0x714c2 dec
返回结果:464066
cast send 发送签名和交易(调用非只读函数)
Foundry 内置工具 cast 中有一个可以使用的命令send
用于签名和发布交易:
cast send "合约地址" "函数签名" "参数值" --rpc-url "rpc" --private-key "privatekey"
比如:
cast send 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 "store(uint256)" 123 --rpc-url $rpc --private-key $key0
cast call
从区块中读取数据(调用只读函数)
call 命令类似于 Remix 中的蓝色按钮,只能调用函数,不能发送交易
cast call "合约地址" "函数名/变量名" --rpc-url "rpc"
cast call 0xf0a005d0697a6518a09A378f371f0058a69F5c3B "retrieve()" --rpc-url $rpc
加快与合约的交互方法(仅在开发调试时使用,实际生产中不可取)
在文件根目录新建一个.env
文件。该文件格式可类似为:
key0=0x517e11ba636227ffbdb3a4c8e2f2db11c9cc7fef4c290f6c88f28f76f5c9f789
rpc=http://127.0.0.1:7545
key1=0xc4c1bae53fb65f8549a2034f8dcc92f5be0840981912db5701435def7167099a
创建这个文件后,打开.gitignore
文件,确保.env
文件在其中被忽略。
在.env
中,我们可以设置“环境变量”。设置后,在终端运行:
source .env
在测试网上模拟在主网中的操作
部署合约到测试网
要将合约部署到测试网上,需要有测试网的 rpc ,这里使用 Alchemy 来构建一个在 Sepolia 测试网上的项目,便会得到一个我们自己的在 Sepolia 测试网上的 rpc,同时,我们部署了合约后,可以在 Alchemy 面板上查看我们部署的合约。
而部署的过程和刚刚在本地部署的过程没有很大的区别,唯一的区别就是需要把参数中的rpc
和private-key
换为我们在 Alchemy 生成的 rpc ,自己Meta Mask钱包中拥有 Sepolia ETH 的私钥。注意,如果要把自己的 Meta Mask 钱包的私钥放进 .env
环境变量之前,确保这个私钥在 ETH 主网上没有余额!!!
合约验证
手动验证
在 Etherscan 或者其他区块链浏览器上进行验证。
点击上方的合约验证
按照上述要求进行验证
继续操作,即可验证成功。
验证成功后,可以在 Etherscan 上看到我们的合约源码,并且可以在区块链浏览器上与合约进行交互
清理项目
当我们的项目基本上完成后,我们就可以对我们的项目进行最后一步的加工
格式化 solidity 代码
可以通过在终端使用forge fmt
命令对 solidity 代码进行格式化。**这是一个可以格式化我们所有的 solidity 代码的命令。
创建 README.md
创建 README.md 文件对我们的项目进行描述,可以包含:
项目信息、使用说明、联系方式等。
Foundry 调试
在 Foundry 中进行测试时,如果不指定链,Foundry 会自动启动一个 anvil 链。
解决在 Remix 导入合约报错的问题
在 Remix 编译器中,可以通过使用"@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol"
这种方式导入合约,但是使用其他编译器时编译器会报错,原因在于:在 Remix 收纳柜使用@chainlink/contracts
时,Remix 会自动在 npm 包存储库中查找,而 Foundry 需要明确地知道从哪里获取依赖项。所以,我们要直接从 Github 下载,我们使用forge install
命令安装这个依赖项:
forge install smartcontractkit/chainlink-brownie-contracts
在大多数情况下,还需要添加--no-commit
命令,同时还可以使用@
来选择版本,所以最终的代码为:
forge install smartcontractkit/chainlink-brownie-contracts@0.6.1 --no-commit
接下来要在 foundry.toml
文件中创建一个叫做remappings
的重映射:
在profile.dedfault
下面创建一个叫做remappings
的新部分:
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
remappings = ["@chainlink/contracts/=lib/chainlink-brownie-contracts/contracts"]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
最好的做法是,在命名错误时,使用合约名称加上两个下划线,这样当遇到这个错误时,可以很容易地知道是哪个合约出了问题
forge test 参数
forge test # 进行所有的测试
forge test -vv # 改变可见性,v越多可见性越高
# 如果我们在测试的时候没有为其提供 RPC URL 时,他会启动一个全新的空白 anvil 链进行测试
fork test --fork-url "url"
# 上述命令中,anvil 被启动,但他会使用 Sepolia RPC URL 的副本来运行,他会启动一个 anvil 链,并模拟所有的交易,就像它们在 Sepolia 链上运行一样,他会假装部署和读取 Sepolia 链上的内容,而不是一个完全空白的链
--fail-fast 快速失败,第一次失败后停止测试
--gas-report 打印 gas 报告
--match-test <正则表达式>,仅运行与指定正则表达式匹配的测试函数,(别名-mt)
--no-match-test <正则表达式>,仅运行与指定正则表达式不匹配的合约测试,(别名-nmt)
--match-contract <正则表达式>,仅运行与指定正则表达式匹配的合约测试,(别名-mc)
--NO-match-contract <正则表达式>,仅运行与指定正则表达式不匹配的合约测试,(别名-nmc)
-f
--fork-url <URL> 通过远程端点获取状态而不是从一个空的状态
--fork-block-number <块> 通过远程从特定块号获取状态端点。
-v EVM 的详细程度。
多次传递以增加详细程度(例如 -v、-vv、-vvv)。
详细程度:
- 2:打印所有测试的日志
- 3:打印失败测试的执行跟踪
- 4:打印所有测试的执行跟踪以及设置跟踪对于失败的测试
- 5:打印所有测试的执行和设置跟踪
forge coverage : 可以查看实际测试套件中实际测试了多少代码
基本测试
在 Foundry 框架中,有一些内置库和工具可以十分方便的测试合约,所以在编写一个测试合约的时候大致框架如下:
pragma solidity ^0.8.0;
import {Test} from "forge-std/Test.sol"
contract Name is Test {
function setUp() external {}
}
// 我们可以进行以下四种测试:
// 1. 单元测试
// - 测试代码中的一个特定部分
// 2. 集成测试
// - 测试多个不同合约是否能够正确地协同工作
// 3. Fork测试(可以认为时单元/集成测试的一部分)
// - 模拟在真实环境中对代码进行测试
// 4. Staging 测试
// - 将代码部署到测试网或者主网之类的环境中,并在这个真实环境中运行所有测试以确保事情能够正常工作
简单的测试
在所有的测试中,第一件都是设置 setUp() 函数,在这里部署合约,然后就可以写需要进行的测试,举例:
pragma solidity ^0.8.0;
import {Test} from "forge-std/Test.sol"
contract Name is Test {
uint number = 1;
function setUp() external {
number = 2;
}
function testDemo() public {
assertEq(number, 2);
}
}
测试结果:
console.log 测试、调试
另一种测试和调试的方法是使用console.log
,Test是一个很大的库,还有另一个库叫做 console
:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
contract FundMeTest is Test {
uint number = 1;
function setUp() external {
number = 2;
}
function testDemo() external {
console.log(number);
console.log("Hello!");
assertEq(number, 2);
}
}
测试结果:
对 FundMe 合约进行简单的调试
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
import {FundMe} from "../src/FundMe.sol";
contract FundMeTest is Test {
FundMe fundMe; // 定义 FundMe 合约变量
function setUp() external {
fundMe = new FundMe(); // 实例化一个 FundMe 合约 fundme
}
function testMininumDollarIsFive() public {
assertEq(fundMe.MINIMUM_USD(), 5e18);
// 对 FundMe 合约进行调试
}
}
检验合约所有者是否为 msg.sender
可以这样写:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
import {FundMe} from "../src/FundMe.sol";
contract FundMeTest is Test {
FundMe fundMe; // 定义 FundMe 合约变量
function setUp() external {
fundMe = new FundMe(); // 实例化一个 FundMe 合约 fundme
}
function testMininumDollarIsFive() public {
assertEq(fundMe.MINIMUM_USD(), 5e18);
// 对 FundMe 合约进行调试
}
function testOwnerIsMsgSender() public {
assertEq(fundMe.i_owner(), msg.sender);
}
}
经过 forge 的调试,我们发现msg.sender
不是合约的所有者,我们可以对上面的程序进行进一步的调整:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
import {FundMe} from "../src/FundMe.sol";
contract FundMeTest is Test {
FundMe fundMe; // 定义 FundMe 合约变量
function setUp() external {
fundMe = new FundMe(); // 实例化一个 FundMe 合约 fundme
}
function testMininumDollarIsFive() public {
assertEq(fundMe.MINIMUM_USD(), 5e18);
// 对 FundMe 合约进行调试
}
function testOwnerIsMsgSender() public {
console.log(fundMe.i_owner());
console.log(msg.sender);
assertEq(fundMe.i_owner(), msg.sender);
}
}
经过测试发现:我 -> FundMeTest -> FundMe,我们不应该检验我们和是否为所有者,而是检测 FundMeTest 是不是所有者,所以将上面的testOwnerIsMsgSender
函数修改为下面的形式即可:
function testOwnerIsMsgSender() public {
console.log(fundMe.i_owner());
console.log(msg.sender);
assertEq(fundMe.i_owner(), address(this));
}
检验版本(使用—fork-url 模拟实际链上的内容)
function testPriceFeedVersionIsAccurate() public { //
uint256 version = fundMe.getVersion();
assertEq(version, 4);
}
对这个函数进行测试时会报错,原因在于,FundMe 合约中的参数地址是 Sepolia 链上的地址,而我们在 Foundry 上测试其实是部署在一个新建的 anvil 链上,并在经过测试和自动关闭 anvil 链的。所以,这里就需要用到了 --fork-url
选项,详见forge test参数
然后,执行下面的bash命令会发现程序成功进行:
forge test --match-test testPriceFeedVersionIsAccurate --fork-url https://eth-sepolia.g.alchemy.com/v2/htfp6AGYmR20uiC5S-rZfe7IZvuxManb
当我们使用 Frok-URL 时,我们会模拟实际链上的内容,这是一个在实际网络上轻松测试合约的一个好方法。但是,这些 Fork 的缺点是会对 Alchemy 节点进行大量的 API 调用,这可能会产生更多的费用,所以尽可能多的进行测试而不使用 Fork 是很重要的,当然除了一些必须在 Fork 上运行的测试。或者使用模拟来确保我们有足够的覆盖率来测试所有的合约。
测试一个是否发生回滚
有些时候我们需要交易回滚,所以我们可以通过Foundry
来书写测试脚本来检查这个合约是否回滚:
function testFundFailsWithoutEnoughETH() public{
vm.expectRevert(); // 这个函数的下一行应该回滚
// 等价于: assert(This tx fails/reverts)
uint256 cat = 1;
}
在 bash 中 对上面这个函数进行测试:
$ forge test --mt testFundFailsWithoutEnoughETH
[⠢] Compiling...
[⠃] Compiling 1 files with 0.8.21
[⠊] Solc 0.8.21 finished in 670.25ms
Compiler run successful with warnings:
Warning (2072): Unused local variable.
--> test/FundeMeTest.t.sol:44:9:
|
44 | uint256 cat = 1;
| ^^^^^^^^^^^
Running 1 test for test/FundeMeTest.t.sol:FundMeTest
[FAIL. Reason: Call did not revert as expected] testFundFailsWithoutEnoughETH() (gas: 3047)
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 1.32ms
Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/FundeMeTest.t.sol:FundMeTest
[FAIL. Reason: Call did not revert as expected] testFundFailsWithoutEnoughETH() (gas: 3047)
Encountered a total of 1 failing tests, 0 tests succeeded
如上所示,上面的测试失败,因为vm.expectRevert()
的下一行语句没有发生回滚。
function testFundFailsWithoutEnoughETH() public{
vm.expectRevert(); // the next line, should revert!
// 等价于: assert(This tx fails/reverts)
fundMe.fund(); // send 0 value,根据fundme合约中的fund函数,会发生回滚。
}
结果如下:
$ forge test --mt testFundFailsWithoutEnoughETH
[⠢] Compiling...
[⠊] Compiling 1 files with 0.8.21
[⠒] Solc 0.8.21 finished in 727.78ms
Compiler run successful!
Running 1 test for test/FundeMeTest.t.sol:FundMeTest
[PASS] testFundFailsWithoutEnoughETH() (gas: 13305)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.92ms
Ran 1 test suites: 1 tests passed, 0 failed, 0 skipped (1 total tests)
交易发生回滚,是我们预期的结果。所以测试通过。
Alchemy
Alchemy 简述
Alchemy 是一个提供非托管的 Web3 开发者工具和平台,为 Web3 和 Web2 应用程序提供服务。
Alchemy 可以理解为是一个 Web3 的云服务器(类似于 华为云,阿里云),它提供了 API SDK 和库,可以让开发者有更好的体验。
它通过三点实现:
- 超级节点:
他是一个基于我的节点的负载均衡器,可以确保始终从区块链中获取到最新的可用数据。在超级节点上,Alchemy 构建了公告 API (Announced APIs:公告 API 是一组 API,可以方便地从区块链中提取数据)
开始构建
登录 Alchemy 官网,选择登录,进入页面就可以看到自己账号的所有项目(前提是已经创建完了)。 点击创建应用程序即可创建新的项目
创建成功后,点击我们创建的项目即可进入应用程序特定的仪表盘
Alchemy 是一个在实际开发 Web3 应用时很实用的一个工具,其余内容可查看 Alchemy 官方文档
源码
// FundMe.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
import {PriceConverter} from "./PriceConverter.sol";
error FundMe_NotOwner();
contract FundMe {
using PriceConverter for uint256;
mapping(address => uint256) public addressToAmountFunded;
address[] public funders;
// Could we make this constant? /* hint: no! We should make it immutable! */
address public /* immutable */ i_owner;
uint256 public constant MINIMUM_USD = 5 * 10 ** 18;
constructor() {
i_owner = msg.sender;
}
function fund() public payable {
require(msg.value.getConversionRate() >= MINIMUM_USD, "You need to spend more ETH!");
// require(PriceConverter.getConversionRate(msg.value) >= MINIMUM_USD, "You need to spend more ETH!");
addressToAmountFunded[msg.sender] += msg.value;
funders.push(msg.sender);
}
function getVersion() public view returns (uint256){
AggregatorV3Interface priceFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306);
return priceFeed.version();
}
modifier onlyOwner {
// require(msg.sender == owner);
if (msg.sender != i_owner) revert FundMe_NotOwner();
_;
}
function withdraw() public onlyOwner {
for (uint256 funderIndex=0; funderIndex < funders.length; funderIndex++){
address funder = funders[funderIndex];
addressToAmountFunded[funder] = 0;
}
funders = new address[](0);
// // transfer
// payable(msg.sender).transfer(address(this).balance);
// // send
// bool sendSuccess = payable(msg.sender).send(address(this).balance);
// require(sendSuccess, "Send failed");
// call
(bool callSuccess, ) = payable(msg.sender).call{value: address(this).balance}("");
require(callSuccess, "Call failed");
}
// Explainer from: https://solidity-by-example.org/fallback/
// Ether is sent to contract
// is msg.data empty?
// / \
// yes no
// / \
// receive()? fallback()
// / \
// yes no
// / \
//receive() fallback()
fallback() external payable {
fund();
}
receive() external payable {
fund();
}
}
// Concepts we didn't cover yet (will cover in later sections)
// 1. Enum
// 2. Events
// 3. Try / Catch
// 4. Function Selector
// 5. abi.encode / decode
// 6. Hash with keccak256
// 7. Yul / Assembly
// PriceConverter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
// Why is this a library and not abstract?
// Why not an interface?
library PriceConverter {
// We could make this public, but then we'd have to deploy it
function getPrice() internal view returns (uint256) {
// Sepolia ETH / USD Address
// https://docs.chain.link/data-feeds/price-feeds/addresses
AggregatorV3Interface priceFeed = AggregatorV3Interface(
0x694AA1769357215DE4FAC081bf1f309aDC325306
);
(, int256 answer, , , ) = priceFeed.latestRoundData();
// ETH/USD rate in 18 digit
return uint256(answer * 10000000000);
}
// 1000000000
function getConversionRate(
uint256 ethAmount
) internal view returns (uint256) {
uint256 ethPrice = getPrice();
uint256 ethAmountInUsd = (ethPrice * ethAmount) / 1000000000000000000;
// the actual ETH/USD conversion rate, after adjusting the extra 0s.
return ethAmountInUsd;
}
}
// FundMeTest.t.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol";
import {FundMe} from "../src/FundMe.sol";
contract FundMeTest is Test {
FundMe fundMe;
function setUp() external {
fundMe = new FundMe(0x694AA1769357215DE4FAC081bf1f309aDC325306);
}
function testMininumDollarIsFive() public {
assertEq(fundMe.MINIMUM_USD(), 5e18);
}
function testOwnerIsMsgSender() public {
console.log(fundMe.i_owner());
console.log(msg.sender);
assertEq(fundMe.i_owner(), address(this));
}
// 我们可以进行以下四种测试:
// 1. 单元测试
// - 测试代码中的一个特定部分
// 2. 集成测试
// - 测试多个不同合约是否能够正确地协同工作
// 3. Fork测试(可以认为时单元/集成测试的一部分)
// - 模拟在真实环境中对代码进行测试
// 4. Staging 测试
// - 将代码部署到测试网或者主网之类的环境中,并在这个真实环境中运行所有测试以确保事情能够正常工作
function testPriceFeedVersionIsAccurate() public { //
uint256 version = fundMe.getVersion();
assertEq(version, 4);
}
}