# Gas Optimization Guidelines
## Standard Gas Optimizations
1. Use `uint256` for counters (cheaper than smaller types)
2. Pack struct variables efficiently
3. Use `calldata` for read-only array parameters
4. Cache storage variables in memory
5. Use batch operations where possible
6. Minimize storage writes
7. Use events for data that doesn't need on-chain storage
---
## PolkaVM-Specific Bytecode Size Optimizations
### Critical: Bytecode Size Limits
Every contract MUST be < 100KB compiled bytecode for PolkaVM deployment.
**Validation**:
```bash
forge build --resolc --sizes
```
### 1. Custom Errors Over String Reverts
**Impact**: ~50-100 bytes saved per error message
```solidity
// ❌ BAD - Increases bytecode
require(msg.sender == owner, "Ownable: caller is not the owner");
require(amount > 0, "Amount must be greater than zero");
require(isActive, "Contract is not active");
// ✅ GOOD - Minimal bytecode
error NotOwner();
error InvalidAmount();
error ContractNotActive();
if (msg.sender != owner) revert NotOwner();
if (amount == 0) revert InvalidAmount();
if (!isActive) revert ContractNotActive();
```
### 2. Minimize Inheritance Depth
**Impact**: 5-15KB savings depending on inherited contracts
```solidity
// ❌ BAD - Deep inheritance increases size
contract MyContract is
ERC721Upgradeable,
ERC721EnumerableUpgradeable,
ERC721URIStorageUpgradeable,
AccessControlUpgradeable,
PausableUpgradeable,
ReentrancyGuardUpgradeable
{ }
// ✅ GOOD - Minimal inheritance
contract MyContract is
ERC721Upgradeable,
AccessControlUpgradeable,
ReentrancyGuardUpgradeable
{ }
```
**Strategy**:
- Only inherit what you absolutely need
- Implement simple features inline rather than inheriting
- Avoid extension contracts (e.g., ERC721Enumerable, ERC721URIStorage)
### 3. Avoid Heavy Libraries
**Impact**: 2-10KB savings per avoided library
```solidity
// ❌ BAD - Heavy libraries
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
// ✅ GOOD - Inline minimal implementations
// Solidity ^0.8.0 has built-in overflow checks, no SafeMath needed
// Simple counter (instead of Counters library)
uint256 private _tokenIdCounter;
function _nextTokenId() internal returns (uint256) {
return ++_tokenIdCounter;
}
// Minimal string conversion (only if needed)
function uint2str(uint256 value) internal pure returns (string memory) {
if (value == 0) return "0";
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
```
### 4. Use Events for Historical Data
**Impact**: Variable bytecode savings + significant gas savings
```solidity
// ❌ BAD - Stores everything on-chain
struct Metadata {
string uri;
uint256 version;
uint256 timestamp;
}
mapping(uint256 => Metadata[]) public metadataHistory;
function updateMetadata(uint256 tokenId, string memory uri) public {
metadataHistory[tokenId].push(Metadata(uri, version++, block.timestamp));
}
// ✅ GOOD - Emit events, query off-chain
mapping(uint256 => string) public currentMetadata;
event MetadataUpdated(uint256 indexed tokenId, string uri, uint256 version, uint256 timestamp);
function updateMetadata(uint256 tokenId, string memory uri) public {
currentMetadata[tokenId] = uri;
emit MetadataUpdated(tokenId, uri, version++, block.timestamp);
}
```
### 5. Simplify Access Control
**Impact**: 3-8KB savings
```solidity
// ❌ BAD - Full AccessControl for simple needs
import "./AccessControlUpgradeable.sol";
contract MyContract is AccessControlUpgradeable {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");
// Uses complex role hierarchy
}
// ✅ GOOD - Simple ownership if sufficient
import "./Ownable.sol";
contract MyContract is Ownable {
mapping(address => bool) public minters;
function setMinter(address account, bool status) external onlyOwner {
minters[account] = status;
}
modifier onlyMinter() {
if (!minters[msg.sender]) revert NotMinter();
_;
}
}
```
### 6. Remove View Functions (Query Off-Chain)
**Impact**: 1-5KB savings
```solidity
// ❌ BAD - Many on-chain view functions
function getAllTokens() public view returns (uint256[] memory) { }
function getTokensByOwner(address owner) public view returns (uint256[] memory) { }
function getTokenMetadata(uint256 tokenId) public view returns (Metadata memory) { }
function isTokenActive(uint256 tokenId) public view returns (bool) { }
// ✅ GOOD - Minimal view functions, query events off-chain
// Only keep essential view functions
function ownerOf(uint256 tokenId) public view returns (address) { }
// Query token lists and metadata from events using The Graph or similar
```
### 7. Optimize Struct Packing
**Impact**: Gas savings primarily, minor bytecode impact
```solidity
// ❌ BAD - Poor packing
struct Listing {
uint256 price; // 32 bytes
address seller; // 20 bytes
uint256 expiresAt; // 32 bytes
bool isActive; // 1 byte
uint8 listingType; // 1 byte
}
// ✅ GOOD - Optimal packing
struct Listing {
address seller; // 20 bytes
bool isActive; // 1 byte
uint8 listingType; // 1 byte
// 10 bytes padding
uint256 price; // 32 bytes
uint256 expiresAt; // 32 bytes
}
// Saves 1 storage slot (20,000 gas per use)
```
### 8. Inline Simple Modifiers
**Impact**: 500-2000 bytes per modifier
```solidity
// ❌ BAD - Separate modifiers increase bytecode
modifier onlyActive() {
if (!isActive) revert NotActive();
_;
}
modifier validAmount(uint256 amount) {
if (amount == 0) revert InvalidAmount();
_;
}
// ✅ GOOD - Inline simple checks
function doSomething(uint256 amount) external {
if (!isActive) revert NotActive();
if (amount == 0) revert InvalidAmount();
// ... function body
}
```
**Note**: Keep modifiers for complex, reused logic or security-critical checks (like `nonReentrant`, `onlyOwner`).
### 9. Batch Operations Over Loops
**Impact**: Bytecode savings + gas savings
```solidity
// ❌ BAD - Multiple functions with loops
function setMultipleURIs(uint256[] calldata tokenIds, string[] calldata uris) external {
for (uint256 i = 0; i < tokenIds.length; i++) {
_setTokenURI(tokenIds[i], uris[i]);
}
}
function burnMultiple(uint256[] calldata tokenIds) external {
for (uint256 i = 0; i < tokenIds.length; i++) {
_burn(tokenIds[i]);
}
}
// ✅ GOOD - Single optimized batch function
function batchOperation(
uint256[] calldata tokenIds,
string[] calldata uris,
bool shouldBurn
) external {
uint256 length = tokenIds.length;
for (uint256 i = 0; i < length; i++) {
if (shouldBurn) {
_burn(tokenIds[i]);
} else if (uris.length > i) {
_setTokenURI(tokenIds[i], uris[i]);
}
}
}
```
### 10. Avoid String Concatenation
**Impact**: 2-5KB savings
```solidity
// ❌ BAD - String operations add bytecode
import "@openzeppelin/contracts/utils/Strings.sol";
string memory fullURI = string(abi.encodePacked(baseURI, Strings.toString(tokenId), ".json"));
// ✅ GOOD - Store full URIs or compute off-chain
mapping(uint256 => string) private _tokenURIs;
```
---
## Contract Size Budget Strategy
### Size Targets by Contract
| Contract | Target | Critical Threshold |
|----------|--------|-------------------|
| IPAsset | < 95KB | 98KB |
| LicenseToken | < 98KB | 99KB |
| Marketplace | < 85KB | 90KB |
| RevenueDistributor | < 80KB | 85KB |
| GovernanceArbitrator | < 75KB | 80KB |
| DeterministicFactory | < 30KB | 35KB |
### Feature Reduction Priority
When approaching size limits, remove in this order:
1. **View/Query Functions** - Implement off-chain via event indexing
2. **Administrative Convenience Functions** - Keep only essential admin functions
3. **Optional Features** - Remove nice-to-have features
4. **Complex Event Parameters** - Simplify event data
5. **Batch Operations** - Keep single operations only if desperate
6. **Last Resort** - Split contract into multiple contracts
---
## Continuous Monitoring
### During Development
```bash
# After every significant change
forge build --resolc --sizes
# Check specific contract
forge inspect ContractName bytecode --resolc | wc -c
```
### Size Alert Thresholds
- **Green**: < 80KB - Continue development
- **Yellow**: 80-90KB - Apply optimizations proactively
- **Orange**: 90-95KB - Code review for size reduction
- **Red**: 95-100KB - Critical optimization or feature removal required
- **FAIL**: > 100KB - Cannot deploy
---
## Testing Impact
**Test contracts are exempt from size limits.**
```solidity
// test/MyContract.t.sol - NO SIZE LIMIT
import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; // ✅ OK in tests
import "forge-std/Test.sol";
contract MyContractTest is Test {
// Full testing capabilities
}
```
This allows comprehensive testing while maintaining production minimalism.
---
## References
- **PolkaVM Constraints**: `./polkavm-constraints.md`
- **Implementation Guide**: `/docs/IMPLEMENTATION_GUIDE.md`
- **Foundry-Polkadot**: https://github.com/paritytech/foundry-polkadot
---