pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {Token} from "./Token.sol";
import {Token2} from "./Token2.sol";
contract GasComparisonTest is Test {
Token internal custom;
Token2 internal oz;
address internal alice = address(0xA11CE);
address internal bob = address(0xB0B);
address internal spender = address(0x5ED);
uint256 internal amount = 1_000 ether;
function setUp() public {
custom = new Token();
oz = new Token2();
custom.mint(alice, amount);
oz.mint(alice, amount);
}
function test_gas_balanceOf() public {
uint256 gasBefore = gasleft();
uint256 balC = custom.balanceOf(alice);
uint256 gasAfter = gasleft();
uint256 gasUsedCustom = gasBefore - gasAfter;
assertEq(balC, amount);
gasBefore = gasleft();
uint256 balO = oz.balanceOf(alice);
gasAfter = gasleft();
uint256 gasUsedOZ = gasBefore - gasAfter;
assertEq(balO, amount);
emit log_named_uint("gas(balanceOf) - Custom", gasUsedCustom);
emit log_named_uint("gas(balanceOf) - OpenZeppelin", gasUsedOZ);
}
function test_gas_approve() public {
vm.startPrank(alice);
uint256 gasBefore = gasleft();
bool okC = custom.approve(spender, 123 ether);
uint256 gasAfter = gasleft();
vm.stopPrank();
require(okC, "custom approve failed");
uint256 gasUsedCustom = gasBefore - gasAfter;
vm.startPrank(alice);
gasBefore = gasleft();
bool okO = oz.approve(spender, 123 ether);
gasAfter = gasleft();
vm.stopPrank();
require(okO, "oz approve failed");
uint256 gasUsedOZ = gasBefore - gasAfter;
emit log_named_uint("gas(approve 123e18) - Custom", gasUsedCustom);
emit log_named_uint("gas(approve 123e18) - OpenZeppelin", gasUsedOZ);
}
function test_gas_transfer() public {
vm.startPrank(alice);
uint256 gasBefore = gasleft();
bool okC = custom.transfer(bob, 10 ether);
uint256 gasAfter = gasleft();
vm.stopPrank();
require(okC, "custom transfer failed");
uint256 gasUsedCustom = gasBefore - gasAfter;
vm.prank(bob);
oz.transfer(alice, oz.balanceOf(bob));
vm.startPrank(alice);
uint256 gasBefore2 = gasleft();
bool okO = oz.transfer(bob, 10 ether);
uint256 gasAfter2 = gasleft();
vm.stopPrank();
require(okO, "oz transfer failed");
uint256 gasUsedOZ = gasBefore2 - gasAfter2;
emit log_named_uint("gas(transfer 10e18) - Custom", gasUsedCustom);
emit log_named_uint("gas(transfer 10e18) - OpenZeppelin", gasUsedOZ);
}
function test_gas_allowance_nonzero() public {
vm.startPrank(alice);
custom.approve(spender, 50 ether);
vm.stopPrank();
vm.startPrank(alice);
oz.approve(spender, 50 ether);
vm.stopPrank();
uint256 gasBefore = gasleft();
uint256 aC = custom.allowance(alice, spender);
uint256 gasAfter = gasleft();
uint256 gasUsedCustom = gasBefore - gasAfter;
assertEq(aC, 50 ether);
gasBefore = gasleft();
uint256 aO = oz.allowance(alice, spender);
gasAfter = gasleft();
uint256 gasUsedOZ = gasBefore - gasAfter;
assertEq(aO, 50 ether);
emit log_named_uint("gas(allowance) - Custom", gasUsedCustom);
emit log_named_uint("gas(allowance) - OpenZeppelin", gasUsedOZ);
}
function test_gas_transferFrom() public {
vm.startPrank(alice);
custom.approve(spender, 20 ether);
vm.stopPrank();
vm.startPrank(alice);
oz.approve(spender, 20 ether);
vm.stopPrank();
vm.startPrank(spender);
uint256 gasBefore = gasleft();
bool okC = custom.transferFrom(alice, bob, 15 ether);
uint256 gasAfter = gasleft();
vm.stopPrank();
require(okC, "custom transferFrom failed");
uint256 gasUsedCustom = gasBefore - gasAfter;
vm.startPrank(spender);
uint256 gasBefore2 = gasleft();
bool okO = oz.transferFrom(alice, bob, 15 ether);
uint256 gasAfter2 = gasleft();
vm.stopPrank();
require(okO, "oz transferFrom failed");
uint256 gasUsedOZ = gasBefore2 - gasAfter2;
emit log_named_uint("gas(transferFrom 15e18) - Custom", gasUsedCustom);
emit log_named_uint("gas(transferFrom 15e18) - OpenZeppelin", gasUsedOZ);
}
}
[⠊] Compiling...
No files changed, compilation skipped
Ran 5 tests for test/poc5.t.sol:GasComparisonTest
[PASS] test_gas_allowance_nonzero() (gas: 81506)
Logs:
gas(allowance) - Custom: 2621
gas(allowance) - OpenZeppelin: 2498
[PASS] test_gas_approve() (gas: 75487)
Logs:
gas(approve 123e18) - Custom: 32745
gas(approve 123e18) - OpenZeppelin: 31003
[PASS] test_gas_balanceOf() (gas: 29382)
Logs:
gas(balanceOf) - Custom: 10253
gas(balanceOf) - OpenZeppelin: 8321
[PASS] test_gas_transfer() (gas: 95263)
Logs:
gas(transfer 10e18) - Custom: 37853
gas(transfer 10e18) - OpenZeppelin: 27776
[PASS] test_gas_transferFrom() (gas: 144864)
Logs:
gas(transferFrom 15e18) - Custom: 34287
gas(transferFrom 15e18) - OpenZeppelin: 33105
Suite result: ok. 5 passed; 0 failed; 0 skipped; finished in 2.90ms (1.38ms CPU time)
Ran 1 test suite in 15.54ms (2.90ms CPU time): 5 tests passed, 0 failed, 0 skipped (5 total tests)
- Continue emitting/special-casing events and performing storage reads/writes
- in multiple places with repeated assembly snippets.
+ 1) Factor common transfer/mint/burn logic into a single internal function
+ (similar in spirit to OpenZeppelin’s `_update`), and call it from
+ `transfer`, `transferFrom`, `_mint`, and `_burn`.
+ - Centralize storage slot computation, arithmetic, and event emission.
+ - Reduce duplicated `keccak256` computations and `sload`/`sstore` pairs.
+ - Make it easier to apply micro-optimizations once, everywhere.
+ 2) Remove unnecessary memory writes (e.g., the trailing `mstore(add(ptr, 0x20), 0)`
+ in `_balanceOf`) and avoid zero-address reverts in views (return `0` instead),
+ reducing control-flow and memory churn.
+ 3) Add explicit overflow/underflow guards only where needed, and keep the rest
+ in Solidity (0.8.x checked arithmetic) for better compiler optimization and
+ readability; use Yul selectively where it demonstrably saves gas.
+ 4) Cache frequently reused slots/hashes in memory registers, and avoid recomputing
+ the same `keccak256` mapping slot multiple times within one operation.
+ 5) Emit mint/burn `Transfer` events using the same path as normal transfers
+ (via the centralized `_update`-like function) to avoid duplicated logging code.