官方語法文檔
https://docs.soliditylang.org/en/v0.8.13/style-guide.html#introduction
一、solidity代碼風(fēng)格
自定義錯誤
前綴為合約名,然后接著錯誤類型的名稱。
好處:報錯時巫员。清除地看到是哪個合約出錯。
error ContractName_Unauthorized();
contract ContractName{
}
NatSpec
一種代碼注釋的風(fēng)格捎琐。
使用 Doxygen 風(fēng)格的注釋和標(biāo)簽來幫助文檔化我們的代碼虱咧。
用工具生成文檔:
solc --userdoc --devdoc ex1.sol
官方文檔: https://docs.soliditylang.org/en/v0.8.13/natspec-format.html
可見性
internal耕拷、private汹族、external萧求、public區(qū)別
https://blog.csdn.net/weixin_42820026/article/details/131477613
二、測試Fundme合約
簡述
- 使用 gas estimator 估算gas顶瞒,然后調(diào)優(yōu)合約夸政、減少gas。
- 用hardhat 來自動設(shè)置我們的測試榴徐。(類似自動執(zhí)行deploy\下的腳本)秒梳。
yarn hardhat test
(一)調(diào)試
斷點debug(js)
1)左側(cè) "運行和調(diào)試" --》"javascript 調(diào)試終端"
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目錄下的腳本疚顷,
//并且可以使用任意數(shù)量的tag
await deployments.fixture(["all"])
//Hardhat-deploy為 ethers 包裝了一個名為getContract的函數(shù)
//該函數(shù)將獲取我們告訴它的任意合約的最新部署虐拓。
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 金額不足報錯
用來斷言 期望錯誤結(jié)果,但是期望單側(cè) 是成功的。
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轉(zhuǎn)入金額正常
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.測試轉(zhuǎn)賬正常
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)
//獲取合約當(dāng)前余額
const endingFundMeBalance = await fundMe.provider.getBalance(
fundMe.address,
)
//獲取deployer 的當(dāng)前余額
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
一般在測試網(wǎng)絡(luò)或者真實網(wǎng)絡(luò)杨耙。
開發(fā)過程的最后一步衬廷。
注意:
1.不會像單元測試中去部署。
而是假設(shè)它已經(jīng)被部署在了測試網(wǎng)絡(luò)上。
// 單測中部署
await deployments.fixture(["all"])
2.編寫驗證腳本 :捐款西潘,提現(xiàn)卷玉,斷言。
3.部署
注意配置好 chainlink的pricefeed地址
在這里helper-hardhat-config.js
yarn hardhat deploy --network sepolia
4.執(zhí)行測試
yarn hardhat test --network sepolia
(四)編寫腳本與代碼交互
前提:啟動本地節(jié)點網(wǎng)絡(luò) localhost
1) scripts\fund.js
這個腳本將與我們的測試非常相似肛捍。|
這樣铃肯,將來如果我們想快速為其中一個合約提供資金,
我們只需運行這個腳本即可遣妥。
# 連接本地節(jié)點localhost進(jìn)行測試。
yarn hardhat run scripts/fund.is --network localhost
2) scripts\withdraw.js
類似寝并。
3) package.json添加腳本
通過添加scripts節(jié)點,
可以把這些長測試濃縮為一個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"
}
}
#執(zhí)行命令觸發(fā)腳本:
# 獲取scripts>test的腳本"hardhat test"并執(zhí)行
yarn test
# 集成測試
yarn test:staging
#代碼掃描
yarn lint
#格式化
yarn format
三衬潦、storage原理
(一)全局變量
當(dāng)我們保存或存儲這些全局變量或者說"storage變量時,到底發(fā)生了些什么?
你可以將“storage" 想象為…個包含我們所創(chuàng)建的所有變量的一個巨大的數(shù)組或列。
其中的每個變量馏谨,每個值别渔。
都被放置在了"Storage"數(shù)組中的某個 32 字節(jié)長的槽位中。
(二)動態(tài)的變量
對于動態(tài)的變量如array惧互,map怎么辦?
它們內(nèi)部的元素實際上是以一種名為“哈希函數(shù)“哈希函數(shù)“)的形式存儲哎媚。
(三)constant`變量和jimmutable
constant`變量和jimmutable。變量并不占用"Storage" 的空間,
這是因為喊儡。constant~變量實際上已經(jīng)成為了合約字節(jié)碼其本身的一部分了
(四)數(shù)組和mapping
數(shù)組和.mapping 會點用更多的空間.
所以 Solidity 會想要確認(rèn)…我們到底在哪里處理它們.是"Storage" 還是"memory" 你必須要告訴我.
我需要知道我是否需要為它們在"Storage"數(shù)據(jù)結(jié)構(gòu)中分配空間.
(五)private/internal 真的有用拨与?
ethers.provider.getStorageAt(CONTRACT_ADDRESS, index)它能讓我們獲取任意一個槽位內(nèi)的數(shù)據(jù).
所以:即債你將-個函數(shù)設(shè)置為"private"或者"internal',其他人也仍然可以讀取它艾猜。
log("Logging storage...")
for (let i = 0; i < 10; i++) {
log(
`Location ${i}: ${await ethers.provider.getStorageAt(
funWithStorage.address,
i
)}`
)
}
四买喧,gas優(yōu)化
(一)storage
讀取或者寫入storage存儲都會花費大量的gas費用。
gas·費用是通過操作碼的 gas 成本來計算的匆赃,每個操作碼都有一個在以太坊網(wǎng)絡(luò)中預(yù)定義的固定的gas成本淤毛。
如下表格:evm-opcodes 可以看到每個操作碼的費用。
最常用的操作:
sstore操作碼: 在storage中存儲算柳,需要支付高達(dá)20000gas低淡。
sload操作碼: 在storeage中讀取數(shù)據(jù) 成本高達(dá)800gas。所以瞬项,全局變量(存儲變量)通常用s_前綴來醒目地標(biāo)注出來蔗蹋。
(二)優(yōu)化withDraw函數(shù)
一次性讀入內(nèi)存,注意mapping類型沒法讀入內(nèi)存囱淋。
function cheaperWithdraw() public onlyOwner {
//一次性讀入內(nèi)存猪杭,注意mapping類型沒法讀入內(nèi)存。
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);
}
(三)合理設(shè)置可見性
設(shè)置成 internal 和 private 變量會更省 gas 費妥衣。
合約所有者地址不需要對其它人或者其它合約公開皂吮,其它信息也是
通過getVarName() 函數(shù)公開戒傻。
(四)revert代替require()
因為require() ,實際上是把這么多字符串涮较,數(shù)組存儲在鏈上稠鼻。
// 例如,缺點是狂票,沒法傳遞錯誤信息
revert FundMe__NotOwner();