# 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 ---