Nano Banana Pro
Agent skill for nano-banana-pro
> **Note**: For quick reference, see [AGENTS.md](./AGENTS.md) which contains the essential guidelines. This file provides deeper explanations and context.
Sign in to like and favorite skills
Note: For quick reference, see AGENTS.md which contains the essential guidelines. This file provides deeper explanations and context.
AGENTS.md provides concise, actionable guidelines for AI assistants. This document (CLAUDE.md) provides:
When to reference each:
Problem: Smart contracts cannot be upgraded without changing address
Solution: Proxy pattern with fallback mechanism
Why separate Extensions and Adapters?
Extensions need chain-specific constructor params (oracle feeds, SpokePool addresses)
Adapters are protocol integrations that may need updates
Why namespaced storage?
keccak256("unique.namespace") for isolationCritical implementation detail:
// Slot calculation bytes32 slot = keccak256("pool.proxy.virtual.balances") - 1; // Why -1? // Prevents collision with compiler-assigned slots at keccak256(value) // Compiler uses keccak256 for mappings/arrays, so we offset by -1
Testing gotcha - ERC-7201 with explicit structs: When you define storage as:
library VirtualStorageLib { struct VirtualBalances { mapping(address => int256) balances; int256 supply; } function _storage() private pure returns (VirtualBalances storage $) { bytes32 slot = VIRTUAL_BALANCES_SLOT; assembly { $.slot := slot } } }
Each contract calling this library accesses its own storage at that slot. Tests calling library functions access the test contract's storage, not the pool's storage. This is why we cannot directly manipulate pool storage from tests - we must use actual pool operations.
The Problem:
Pool on Arbitrum has 100 USDC (NAV = 100) Transfer 50 USDC to Optimism - Arbitrum: 50 USDC left (NAV drops to 50) ❌ - Optimism: 50 USDC received (NAV = 50) - Total NAV dropped from 100 to 100? No, it's 150! ❌
The Solution - Virtual Balances:
Transfer 50 USDC from Arbitrum to Optimism (Transfer mode) Arbitrum: - Physical: 50 USDC - Virtual: +50 USDC (in base token units) - NAV calculation: (50 + 50) = 100 ✓ Optimism: - Physical: 50 USDC - Virtual: -50 USDC (in base token units) - NAV calculation: (50 - 50) = 0 ✓ Total NAV: 100 + 0 = 100 ✓
Why two systems (Virtual Supply AND Virtual Balances)?
Virtual Supply handles edge case:
Pool deployed on Arbitrum, totalSupply = 100 tokens User bridges 20 pool tokens to Optimism - Arbitrum: totalSupply = 100, virtualSupply = 0 - Optimism: totalSupply = 20, virtualSupply = -20 - Net global supply: 100 + 0 + 20 + (-20) = 100 ✓
Without virtual supply:
Transfer USDC from Arbitrum to Optimism Optimism has totalSupply = 20 tokens How much virtual balance to create? Need: baseValue / currentNav * 10^poolDecimals But if we always used virtual balances, couldn't track cross-chain supply!
When to use which:
Both are used together to maintain NAV integrity while tracking true economic position.
Why SafeTransferLib?
Standard ERC20 has inconsistencies:
// USDT approve() reverts if allowance > 0 token.approve(spender, 100); // OK token.approve(spender, 200); // REVERTS! ❌ // Some tokens don't return bool token.transfer(to, amount); // No return value // Some tokens return false instead of reverting bool success = token.transfer(to, amount); if (!success) { /* need to check! */ }
SafeTransferLib handles all cases:
token.safeApprove(spender, amount); // Resets to 0 first if needed token.safeTransfer(to, amount); // Reverts on failure regardless of return
When to call updateUnitaryValue()?
NAV is calculated as:
NAV = (total asset value) / totalSupply
It changes when:
Automatic updates:
deposit() / withdraw() - Always updates NAVManual updates needed:
// ❌ Wrong - may read stale NAV uint256 nav = ISmartPoolState(address(this)).getPoolTokens().unitaryValue; // ✓ Correct - ensure current NAV ISmartPoolActions(address(this)).updateUnitaryValue(); uint256 nav = ISmartPoolState(address(this)).getPoolTokens().unitaryValue;
Why have Transfer AND Sync modes?
Initial design: Only Transfer mode (always NAV-neutral)
Problem: Solver fills with extra tokens (surplus) - Destination NAV increases - But we applied negative virtual balance - Result: NAV appears LOWER than reality ❌
Solution: Two modes
Why not always use Sync?
Why must handler verify msg.sender?
Extension called via delegatecall from pool:
Malicious actor → Pool.fallback() → delegatecall ECrosschain.handleV3AcrossMessage()
In delegatecall context:
msg.sender is preserved (the attacker)Security requirement:
if (msg.sender != _ACROSS_SPOKE_POOL) revert UnauthorizedCaller();
Only Across SpokePool can legitimately call this after fill.
Problem encountered: Test tried to directly set virtual balance:
// In test VirtualStorageLib._setVirtualBalance(poolAddress, token, amount); // Doesn't work! Updates test contract's storage, not pool's storage
Why? Library uses ERC-7201 storage with explicit struct:
function _storage() private pure returns (VirtualBalances storage $) { bytes32 slot = VIRTUAL_BALANCES_SLOT; assembly { $.slot := slot } // $ = storage at this slot IN CALLING CONTRACT }
When test calls library,
$ points to test contract storage at that slot, not pool storage.
Solution: Create virtual balance through actual pool operations:
// Use actual protocol operation (donation) to create virtual balance vm.prank(address(spokePool)); ECrosschain(pool).handleV3AcrossMessage( token, amount, relayer, encodeMessage(OpType.Transfer, ...) ); // Now pool has virtual balance via legitimate operation
When to use forks:
Pattern:
uint256 ethFork = vm.createFork("ethereum", Constants.MAINNET_BLOCK); uint256 baseFork = vm.createFork("base", Constants.BASE_BLOCK); // Test cross-chain flow vm.selectFork(ethFork); // ... initiate transfer on Arbitrum bytes memory message = captureMessage(); vm.selectFork(optFbaseForkork); // ... simulate Across fill on Optimism vm.prank(address(spokePool)); pool.handleV3AcrossMessage(token, amount, relayer, message);
Critical: Use deployed addresses from Constants.sol
When libraries show as "uncovered" in codecov:
Integration tests may use library methods, but codecov needs explicit unit tests:
// Library library TransientStorage { function setDonationLock(bool locked) internal { ... } } // Integration test (uses library indirectly) pool.donate(...); // Internally calls TransientStorage.setDonationLock() // Codecov: "TransientStorage.setDonationLock() not covered" ❌ // Solution: Add explicit unit test function test_SetDonationLock() public { TransientStorage.setDonationLock(true); assertTrue(TransientStorage.getDonationLock()); } // Now codecov sees direct coverage ✓
AGENTS.md: Quick reference optimized for AI code generation
CLAUDE.md: Deeper understanding for complex decisions
Analogy:
Anti-pattern seen:
Better approach:
Rule of thumb:
/docs/<protocol>/ when complete