Gas Cost
Khi chạy lệnh forge snapshot, sẽ có một file tên là .gas-snapshot được tạo ra và nó chứa lượng gas tiêu thụ của các hàm test:
FundMeTest:testAddsFunderToArrayOfFunders() (gas: 103844)
FundMeTest:testFundFailsWIthoutEnoughETH() (gas: 25124)
FundMeTest:testFundUpdatesFundDataStructure() (gas: 103190)
FundMeTest:testMinimumDollarIsFive() (gas: 8818)
FundMeTest:testOnlyOwnerCanWithdraw() (gas: 102824)
FundMeTest:testOwnerIsMsgSender() (gas: 9081)
FundMeTest:testPriceFeedVersionIsAccurate() (gas: 14409)
FundMeTest:testWithdrawFromASingleFunder() (gas: 89306)
FundMeTest:testWithdrawFromMultipleFunders() (gas: 559615)Lệnh forge test cũng in ra console kết quả tương tự.
Để chuyển đổi từ gas sang USD:
- Sử dụng Ethereum Gas Tracker để tính giá 1 gas: hiện tại là 5.308 gwei. Hàm
testWithdrawFromMultipleFunderssẽ tiêu thụ 559615 x 5.308 = 2970436.42 gwei. - Sử dụng Calculate and Convert Wei, Gwei, and ETH để đổi từ gwei sang ETH: 2970436.42 gwei tương đương với 0.00297043642 ETH.
- Sử dụng Ethereum price today, ETH to USD live price, marketcap and chart | CoinMarketCap để xem giá ETH: 0.00297043642 ETH với giá 1 ETH = 2,638.55 USD thì sẽ tương đương với 7.837645015991 USD.
Important
Để phục vụ cho mục đích test, gas price của Anvil là 0.
Nhằm theo dõi lượng gas tiêu thụ, ta có thể sử dụng vm.txGasPrice để gán gas price của transaction tiếp theo, gasleft() để tính lượng gas còn lại trong transaction tại thời điểm gọi hàm. Ví dụ:
function testWithdrawFromASingleFunder() public funded {
// AAA Pattern
// Arange
uint256 startingFundMeBalance = address(fundMe).balance;
uint256 startingOwnerBalance = fundMe.getOwner().balance;
vm.txGasPrice(GAS_PRICE);
uint256 gasStart = gasleft();
// Act
vm.startPrank(fundMe.getOwner());
fundMe.withdraw();
vm.stopPrank();
uint256 gasEnd = gasleft();
uint256 gasUsed = (gasStart - gasEnd) * tx.gasprice;
console.log("Withdraw consumed: %d gas", gasUsed);
// Assert
uint256 endingFundMeBalance = address(fundMe).balance;
uint256 endingOwnerBalance = fundMe.getOwner().balance;
assertEq(endingFundMeBalance, 0);
assertEq(
startingFundMeBalance + startingOwnerBalance,
endingOwnerBalance
);
}Storage Layout
Để in ra storage layout của smart contract, ta có thể sử dụng hàm sau đây:
function testPrintStorageData() public view {
for (uint256 i = 0; i < 3; i++) {
bytes32 value = vm.load(address(fundMe), bytes32(i));
console.log("Value at location", i, ":");
console.logBytes32(value);
}
console.log("PriceFeed address:", address(fundMe.getPriceFeed()));
}Giả sử thứ tự khai báo các biến là:
mapping(address => uint256) private s_addressToAmountFunded;
function getAddressToAmountFunded(
address fundingAddress
) public view returns (uint256) {
return s_addressToAmountFunded[fundingAddress];
}
address[] private s_funders;
function getFunder(uint256 index) public view returns (address) {
return s_funders[index];
}
AggregatorV3Interface private s_priceFeed;
function getPriceFeed() public view returns (AggregatorV3Interface) {
return s_priceFeed;
}Output sẽ là:
❯ forge test --mt testPrintStorageData -vv
Ran 1 test for test/FundMeTest.t.sol:FundMeTest
[PASS] testPrintStorageData() (gas: 18487)
Logs:
Value at location 0 :
0x0000000000000000000000000000000000000000000000000000000000000000
Value at location 1 :
0x0000000000000000000000000000000000000000000000000000000000000000
Value at location 2 :
0x00000000000000000000000034a1d3fff3958843c43ad80f30b94c510645c316
PriceFeed address: 0x34A1D3fff3958843C43aD80F30b94c510645C316
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.52ms (184.00µs CPU time)
Ran 1 test suite in 9.67ms (1.52ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)Giải thích:
- Slot 0 dùng để lưu
s_addressToAmountFundedcó kiểu là map - Slot 1 dùng để lưu
s_funderscó kiểu là mảng. - Slot 2 dùng để lưu address của
priceFeed.
Chúng ta cũng có thể dùng lệnh forge inpsect <CONTRACT_NAME> storgeLayout để xem storage layout của contract nếu không quan tâm đến giá trị được lưu trong từng slot.
❯ forge inspect FundMe storageLayout
╭-------------------------+--------------------------------+------+--------+-------+-----------------------╮
| Name | Type | Slot | Offset | Bytes | Contract |
+==========================================================================================================+
| s_addressToAmountFunded | mapping(address => uint256) | 0 | 0 | 32 | src/FundMe.sol:FundMe |
|-------------------------+--------------------------------+------+--------+-------+-----------------------|
| s_funders | address[] | 1 | 0 | 32 | src/FundMe.sol:FundMe |
|-------------------------+--------------------------------+------+--------+-------+-----------------------|
| s_priceFeed | contract AggregatorV3Interface | 2 | 0 | 20 | src/FundMe.sol:FundMe |
╰-------------------------+--------------------------------+------+--------+-------+-----------------------╯Còn một cách khác để xem storage layout là sử dụng cast và anvil:
- Khởi chạy Anvil network
- Deploy contract lên Anvil network:
forge script DeployFundMe --rpc-url http://127.0.0.1:8545 --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 --broadcastVới source code của DeployFundMe là:
contract DeployFundMe is Script {
function run() external returns (FundMe) {
HelperConfig helperConfig = new HelperConfig();
address priceFeedAddress = helperConfig.activeNetworkConfig();
vm.startBroadcast();
FundMe fundMe = new FundMe(priceFeedAddress);
vm.stopBroadcast();
return fundMe;
}
}Output có 2 địa chỉ smart contract tương ứng với MockV3Aggregator và FundMe:
Transaction: 0xea036ba11d5f7794502bf8ccf968f062573b2ab0108b7ae5b6ad260b49e5357e
Contract created: 0x5FbDB2315678afecb367f032d93F642f64180aa3
Gas used: 674278
Block Number: 1
Block Hash: 0x7ccbd03bc383941ef57a59e2e8e11aa61009a3b0908b045fcd8d82fa263b414e
Block Time: "Wed, 9 Jul 2025 15:25:37 +0000"
Transaction: 0xafb7031cc40d7a1ef68ecc9148c57c4eff1cf0c8df3f68c94d567412b613ce56
Contract created: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Gas used: 857320
Block Number: 2
Block Hash: 0xd140933e194a0df2d6d00492847b8445c154617db6afb7e6a62f82ad56b054c6
Block Time: "Wed, 9 Jul 2025 15:25:37 +0000"- Sử dụng
cast storage <CONTRACT_ADDRESS> <INDEX>để xem giá trị của slotINDEX. Ví dụ:
❯ cast storage 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 2
0x0000000000000000000000005fbdb2315678afecb367f032d93f642f64180aa3Opcode
Sử dụng cast code <CONTRACT_ADDRESS> để xem deployed bytecode của contract và sử dụng Bytecode to Opcode Disassembler | Etherscan để giải mã bytecode thành opcode.
Khi xem danh sách các opcode của EVM Codes - An Ethereum Virtual Machine Opcodes Interactive Reference, ta thấy rằng việc lưu và đọc storage tốn lượng gas nhiều hơn (tối thiểu là 100) so với việc lưu và đọc memory:

Bằng cách thay đổi biến storage thành biến memory, ta có thể tối ưu lượng gas tiêu thụ. Xét hàm sau:
function withdraw() public onlyOwner {
for (
uint256 funderIndex = 0;
funderIndex < s_funders.length;
funderIndex++
) {
address funder = s_funders[funderIndex];
s_addressToAmountFunded[funder] = 0;
}
s_funders = new address[](0);
(bool callSuccess, ) = payable(msg.sender).call{
value: address(this).balance
}("");
require(callSuccess, "Call failed");
}Hàm này đọc thuộc tính length của biến storage s_funders ở mỗi lần lặp. Thay vì vậy, ta có thể lưu giá trị của nó vào một biến memory:
function cheaperWithdraw() public onlyOwner {
uint256 fundersLength = s_funders.length;
for (
uint256 funderIndex = 0;
funderIndex < fundersLength;
funderIndex++
) {
address funder = s_funders[funderIndex];
s_addressToAmountFunded[funder] = 0;
}
s_funders = new address[](0);
(bool callSuccess, ) = payable(msg.sender).call{
value: address(this).balance
}("");
require(callSuccess, "Call failed");
}Việc lấy địa chỉ của từng funder và reset danh sách các funder là không thể tránh khỏi nên ta không thể tối ưu.
Sau khi có hàm mới thì gọi sử dụng nó trong test và chạy forge snapshot lại để kiểm tra lượng gas tiêu thụ:
FundMeTest:testWithdrawFromMultipleFunders() (gas: 558620)Có thể thấy, lượng gas tiêu thụ của hàm test testWithdrawFromMultipleFunders giảm từ 559615 xuống 558620. Như vậy có nghĩa là ta đã tiết kiệm được 995 gas.
Tip
Bằng việc đặt tên biến có tiền tố
s_, ta có thể nhanh chóng xác định các storage variable để tối ưu. Xem thêm convetion ở Style Guide — Solidity 0.8.4 documentation.