官方语法文档
https://docs.soliditylang.org/en/v0.8.13/style-guide.html#introduction
一、solidity代码风格
自定义错误
前缀为合约名,然后接着错误类型的名称。
好处:报错时。清除地看到是哪个合约出错。
error ContractName_Unauthorized();
contract ContractName{
}
NatSpec
一种代码注释的风格。
使用 Doxygen 风格的注释和标签来帮助文档化我们的代码。
用工具生成文档:
solc --userdoc --devdoc ex1.sol
官方文档: https://docs.soliditylang.org/en/v0.8.13/natspec-format.html
可见性
internal、private、external、public区别
https://blog.csdn.net/weixin_42820026/article/details/131477613
二、测试Fundme合约
简述
- 使用 gas estimator 估算gas,然后调优合约、减少gas。
- 用hardhat 来自动设置我们的测试。(类似自动执行deploy\下的脚本)。
yarn hardhat test
(一)调试
断点debug(js)
1)左侧 "运行和调试" --》"javascript 调试终端"
2)运行 yarn hardhat test。
会显示 "Debugger attached",停止在断点处的脚本。
console.log
在solidity中使用
类似 js中使用
import "hardhat/console.sol";
console.log("xxxxxx %s", "error msg");
(二)、单元测试
test\unit
FundMe.test.js
一般在本地,local hoardhat 或者 forked hardhat
1.部署,合约对象获取
beforeEach(async () => {
//这种方法直接获取 hardhat.config.js 配置的accounts列表
// const accounts = await ethers.getSigners()
// deployer = accounts[0]
//这种方法获取 namedAccounts配置的deployer索引。
deployer = (await getNamedAccounts()).deployer
//部署:运行整个 deploy目录下的脚本,
//并且可以使用任意数量的tag
await deployments.fixture(["all"])
//Hardhat-deploy为 ethers 包装了一个名为getContract的函数
//该函数将获取我们告诉它的任意合约的最新部署。
fundMe = await ethers.getContract("FundMe", deployer)
mockV3Aggregator = await ethers.getContract(
"MockV3Aggregator",
deployer,
)
})
2.测试Chainlink注入合约
describe("constructor", function () {
it("sets the aggregator addresses correctly", async () => {
const response = await fundMe.getPriceFeed()
assert.equal(response, mockV3Aggregator.address)
})
})
3.测试fund 金额不足报错
用来断言 期望错误结果,但是期望单侧 是成功的。
expect( ).to.be.revertedWith(
"You need to spend more ETH!",
)
it("Fails if you don't send enough ETH", async () => {
await expect(fundMe.fund()).to.be.revertedWith(
"You need to spend more ETH!",
)
})
4.测试fund转入金额正常
it("Updates the amount funded data structure", async () => {
await fundMe.fund({ value: sendValue })
const response =
await fundMe.getAddressToAmountFunded(deployer)
assert.equal(response.toString(), sendValue.toString())
})
5.测试fund用户记录正常
it("Adds funder to array of funders", async () => {
await fundMe.fund({ value: sendValue })
const response = await fundMe.getFunder(0)
assert.equal(response, deployer)
})
6.测试转账正常
beforeEach(async () => {
await fundMe.fund({ value: sendValue })
})
it("withdraws ETH from a single funder", async () => {
// Arrange
//获取合约初始余额
const startingFundMeBalance =
await fundMe.provider.getBalance(fundMe.address)
//获取deployer 的初始余额
const startingDeployerBalance =
await fundMe.provider.getBalance(deployer)
// Act
const transactionResponse = await fundMe.withdraw()
const transactionReceipt = await transactionResponse.wait()
const { gasUsed, effectiveGasPrice } = transactionReceipt
const gasCost = gasUsed.mul(effectiveGasPrice)
//获取合约当前余额
const endingFundMeBalance = await fundMe.provider.getBalance(
fundMe.address,
)
//获取deployer 的当前余额
const endingDeployerBalance =
await fundMe.provider.getBalance(deployer)
// Assert
// Maybe clean up to understand the testing
assert.equal(endingFundMeBalance, 0)
//bigNumber 不能用+号,要用 add()
assert.equal(
startingFundMeBalance
.add(startingDeployerBalance)
.toString(),
endingDeployerBalance.add(gasCost).toString(),
)
})
(三)集成测试(staging)
新建test\staging。
FundMe.staging.test.js
一般在测试网络或者真实网络。
开发过程的最后一步。
注意:
1.不会像单元测试中去部署。
而是假设它已经被部署在了测试网络上。
// 单测中部署
await deployments.fixture(["all"])
2.编写验证脚本 :捐款,提现,断言。
3.部署
注意配置好 chainlink的pricefeed地址
在这里helper-hardhat-config.js
yarn hardhat deploy --network sepolia
4.执行测试
yarn hardhat test --network sepolia
(四)编写脚本与代码交互
前提:启动本地节点网络 localhost
1) scripts\fund.js
这个脚本将与我们的测试非常相似。|
这样,将来如果我们想快速为其中一个合约提供资金,
我们只需运行这个脚本即可。
# 连接本地节点localhost进行测试。
yarn hardhat run scripts/fund.is --network localhost
2) scripts\withdraw.js
类似。
3) package.json添加脚本
通过添加scripts节点,
可以把这些长测试浓缩为一个yarn script。
{
"scripts": {
"test": "hardhat test",
"test:staging": "hardhat test --network sepolia",
"lint": "solhint 'contracts/**/*.sol'",
"lint:fix": "solhint 'contracts/**/*.sol' --fix",
"format": "prettier --write .",
"coverage": "hardhat coverage"
}
}
#执行命令触发脚本:
# 获取scripts>test的脚本"hardhat test"并执行
yarn test
# 集成测试
yarn test:staging
#代码扫描
yarn lint
#格式化
yarn format
三、storage原理
(一)全局变量
当我们保存或存储这些全局变量或者说"storage变量时,到底发生了些什么?
你可以将“storage" 想象为…个包含我们所创建的所有变量的一个巨大的数组或列。
其中的每个变量,每个值。
都被放置在了"Storage"数组中的某个 32 字节长的槽位中。
(二)动态的变量
对于动态的变量如array,map怎么办?
它们内部的元素实际上是以一种名为“哈希函数“哈希函数“)的形式存储。
(三)constant`变量和jimmutable
constant`变量和jimmutable。变量并不占用"Storage" 的空间,
这是因为。constant~变量实际上已经成为了合约字节码其本身的一部分了
(四)数组和mapping
数组和.mapping 会点用更多的空间.
所以 Solidity 会想要确认…我们到底在哪里处理它们.是"Storage" 还是"memory" 你必须要告诉我.
我需要知道我是否需要为它们在"Storage"数据结构中分配空间.
(五)private/internal 真的有用?
ethers.provider.getStorageAt(CONTRACT_ADDRESS, index)它能让我们获取任意一个槽位内的数据.
所以:即债你将-个函数设置为"private"或者"internal',其他人也仍然可以读取它。
log("Logging storage...")
for (let i = 0; i < 10; i++) {
log(
`Location ${i}: ${await ethers.provider.getStorageAt(
funWithStorage.address,
i
)}`
)
}
四,gas优化
(一)storage
读取或者写入storage存储都会花费大量的gas费用。
gas·费用是通过操作码的 gas 成本来计算的,每个操作码都有一个在以太坊网络中预定义的固定的gas成本。
如下表格:evm-opcodes 可以看到每个操作码的费用。
最常用的操作:
sstore操作码: 在storage中存储,需要支付高达20000gas。
sload操作码: 在storeage中读取数据 成本高达800gas。所以,全局变量(存储变量)通常用s_前缀来醒目地标注出来。
(二)优化withDraw函数
一次性读入内存,注意mapping类型没法读入内存。
function cheaperWithdraw() public onlyOwner {
//一次性读入内存,注意mapping类型没法读入内存。
address[] memory funders = s_funders;
// mappings can't be in memory, sorry!
for (
uint256 funderIndex = 0;
funderIndex < funders.length;
funderIndex++
) {
address funder = funders[funderIndex];
s_addressToAmountFunded[funder] = 0;
}
s_funders = new address[](0);
// payable(msg.sender).transfer(address(this).balance);
(bool success, ) = i_owner.call{value: address(this).balance}("");
require(success);
}
(三)合理设置可见性
设置成 internal 和 private 变量会更省 gas 费。
合约所有者地址不需要对其它人或者其它合约公开,其它信息也是
通过getVarName() 函数公开。
(四)revert代替require()
因为require() ,实际上是把这么多字符串,数组存储在链上。
// 例如,缺点是,没法传递错误信息
revert FundMe__NotOwner();