[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6FXK8QTPJH46SYTHNEFGETK":3,"skill-same:01M6FXK8QTPJH46SYTHNEFGETK":62},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":36,"linkOnly":37,"markdownContent":38,"markdownBody":39,"frontMatter":40,"refName":41,"commitSha":42,"skillDirectory":43,"forksCount":44,"repositoryHtmlUrl":45,"repositoryHomepageUrl":6,"ownerHtmlUrl":46,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":47,"install":48,"resources":61},"01M6FXK8QTPJH46SYTHNEFGETK","jb-v5-impl",null,"Deep implementation knowledge for Juicebox V5 protocol. Covers internal mechanics, edge cases, tradeoffs, gas considerations, and critical integration patterns. Use when reasoning about how things work internally.","kbarbel640-del\u002Fskills","kbarbel640-del","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F255315490?v=4","01M6CJPPQGCCESXMG3K7BBGQ5J","01M6CJPPX2PB2BCZERFVM2C2B3",1,"MIT","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fmejango\u002Fjuicy\u002Fjb-v5-impl\u002FSKILL.md",[],[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35],"jb v5 impl","impl","implementation","juicebox v5","about","critical","mechanics","deep","patterns","reasoning","use","gas","work","knowledge","how","edge","integration","protocol","2026-11-10 10:12:30",false,"---\nname: jb-v5-impl\ndescription: Deep implementation knowledge for Juicebox V5 protocol. Covers internal mechanics, edge cases, tradeoffs, gas considerations, and critical integration patterns. Use when reasoning about how things work internally.\n---\n\n# Juicebox V5 Implementation Deep Dive\n\nThis skill provides deep implementation knowledge for integrators, covering internal mechanics, edge cases, tradeoffs, and critical considerations.\n\n---\n\n## Payment Flow Internals\n\n### JBMultiTerminal.pay() Execution\n\n```\nUser calls pay()\n  → _pay() internal\n    → STORE.recordPaymentFrom()\n      → Validates ruleset exists and !pausePay\n      → Invokes data hook if useDataHookForPay\n      → Calculates tokens: mulDiv(amount, weight, weightRatio)\n      → Returns hook specifications\n    → TOKENS.mintFor() via controller\n    → Executes pay hook specifications\n    → Emits Pay event\n  → Validates minReturnedTokens\n```\n\n### Critical Implementation Details\n\n**Weight Calculation**: Token minting uses fixed-point math with 18 decimals:\n```solidity\ntokenCount = mulDiv(amount.value, weight, 10**18)\n```\nThe `weight` is the ruleset's configured weight (tokens per unit of currency).\n\n**Data Hook Override**: When `useDataHookForPay` is true, the data hook can:\n- Return a modified weight (affecting token count)\n- Specify pay hooks to receive forwarded funds\n- Pass custom metadata to pay hooks\n\n**Hook Execution Order**: Pay hooks execute sequentially after token minting. Each hook receives its specified amount from the payment.\n\n### Edge Cases\n\n1. **Zero weight**: No tokens minted, but payment still accepted\n2. **Data hook reverts**: Entire payment reverts\n3. **Pay hook reverts**: Entire payment reverts (no partial execution)\n4. **Insufficient minReturnedTokens**: Reverts after all execution (gas wasted)\n\n### Gas Considerations\n\n- Single payment without hooks: ~150k gas\n- With data hook: +50-100k gas depending on hook complexity\n- Each pay hook: +variable gas based on hook logic\n- Metadata decoding adds ~5k gas per 32 bytes\n\n---\n\n## Cash Out (Redemption) Mechanics\n\n### Bonding Curve Formula\n\nThe cash out calculation implements a modified bonding curve:\n\n```\nBase = (surplus × cashOutCount) \u002F totalSupply\n\ntaxFactor = (MAX_RATE - taxRate) + (taxRate × cashOutCount \u002F totalSupply)\n\nreclaimAmount = base × taxFactor \u002F MAX_RATE\n```\n\nWhere `MAX_RATE = 10000` (representing 100%).\n\n### Tax Rate Implications\n\n| Tax Rate | Effect |\n|----------|--------|\n| 0% | Linear redemption: get proportional share of surplus |\n| 50% | Partial penalty: larger redemptions penalized more |\n| 100% | No redemption possible: returns 0 |\n\n**Critical Insight**: The tax rate creates a curve where:\n- Small redemptions get closer to proportional value\n- Large redemptions (relative to supply) are penalized exponentially\n- At 100% tax rate, surplus is locked forever\n\n### Surplus Calculation\n\nSurplus is calculated as:\n```\nsurplus = balance - remainingPayoutLimit\n```\n\nThe `useTotalSurplusForCashOuts` metadata flag determines whether to use:\n- **Terminal surplus**: Only this terminal's balance minus its payout limit\n- **Total surplus**: Aggregated across all project terminals\n\n**Tradeoff**: Total surplus provides more accurate valuation but costs more gas due to multi-terminal queries.\n\n### Edge Cases\n\n1. **Zero surplus**: No reclaim possible regardless of token count\n2. **100% tax rate**: Returns 0, tokens still burned\n3. **Cash out count > total supply**: Reverts\n4. **Paused cash outs**: Reverts via ruleset check\n\n---\n\n## Ruleset Transition Mechanics\n\n### Approval Hook Flow\n\n```\nQueue ruleset\n  → approvalHook.approvalStatusOf() called\n  → If ApprovalExpected: queued but not yet active\n  → If Approved: becomes current when start time reached\n  → If Failed\u002FEmpty: reverts to base ruleset\n```\n\n**JBDeadline Implementation**: Requires `DURATION()` seconds between queue time and ruleset start. If queued too late, approval fails.\n\n### Weight Cut Algorithm\n\nWeight is cut exponentially each cycle:\n```solidity\nnewWeight = weight × (MAX_CUT - weightCutPercent) \u002F MAX_CUT\n```\n\nApplied iteratively for each cycle that has passed.\n\n**Optimization**: For large cycle counts (>1000), the contract uses cached intermediate values to avoid O(n) computation:\n```solidity\nif (multiple > 1000) {\n    \u002F\u002F Use cached values at 50,000 interval checkpoints\n}\n```\n\n### Ruleset Inheritance\n\nWhen a ruleset is queued:\n1. It inherits from the current (or latest approved) ruleset\n2. Only changed parameters override inherited values\n3. The `basedOnId` field tracks inheritance chain\n\n**Critical Consideration**: If an approval hook rejects a ruleset, the system walks backward through the inheritance chain to find the latest approved ancestor.\n\n### Cycle Number Derivation\n\n```solidity\ncycleNumber = 1 + (currentTime - start) \u002F duration\n```\n\nFor rulesets with `duration = 0` (infinite), cycle number stays at 1.\n\n---\n\n## Reserved Token Distribution\n\n### Accumulation Model\n\nReserved tokens don't mint immediately. Instead:\n```solidity\npendingReservedTokenBalanceOf[projectId] += (tokenCount × reservedRate) \u002F MAX_RATE\n```\n\nTokens accumulate until `sendReservedTokensToSplitsOf()` is called.\n\n### Distribution Flow\n\n```\nsendReservedTokensToSplitsOf()\n  → Reads pendingReservedTokenBalanceOf\n  → Resets pending balance to 0\n  → Mints all reserved tokens to controller\n  → Distributes via _sendReservedTokensToSplitGroupOf()\n    → For each split:\n      → If projectId set: pay that project\n      → If hook set: call hook\n      → Else: transfer to beneficiary\n  → Leftover sent to project owner\n```\n\n### Tradeoffs\n\n**Batched Distribution**:\n- Pro: Gas efficient (one mint operation)\n- Con: Recipients wait for manual trigger\n- Con: Large accumulations can hit gas limits\n\n**Per-Payment Distribution** (alternative pattern):\n- Pro: Immediate distribution\n- Con: Higher per-payment gas cost\n\n---\n\n## Splits System Details\n\n### Storage Packing\n\nSplits use packed storage for gas efficiency:\n```\nSlot 1: percent (32) | projectId (64) | beneficiary (160)\nSlot 2: preferAddToBalance (1) | lockedUntil (48) | hook (160)\n```\n\n### Locked Splits Behavior\n\nWhen updating splits:\n1. All currently locked splits must be included\n2. Lock period can only be extended, never shortened\n3. Other properties of locked splits cannot change\n\n**Edge Case**: If a locked split's beneficiary becomes a contract that can't receive funds, those funds are stuck until lock expires.\n\n### Split Execution\n\n```solidity\nfor each split:\n    amount = totalAmount × split.percent \u002F SPLITS_TOTAL_PERCENT\n\n    if (split.hook != address(0)):\n        \u002F\u002F Optimistically transfer to hook\n        hook.processSplitWith(context)\n    else if (split.projectId != 0):\n        if (split.preferAddToBalance):\n            terminal.addToBalanceOf(projectId, ...)\n        else:\n            terminal.pay(projectId, ...)\n    else:\n        \u002F\u002F Direct transfer to beneficiary\n```\n\n**Critical**: Split hooks receive funds optimistically before `processSplitWith()` is called. Malicious hooks could steal funds.\n\n---\n\n## Buyback Hook Decision Logic\n\n### Mint vs Swap Comparison\n\n```\nmintTokens = amount × weight \u002F 10^18\nswapTokens = TWAP_quote - slippageTolerance\n\nif (swapTokens > mintTokens):\n    route through swap\nelse:\n    standard mint\n```\n\n### TWAP Calculation\n\n1. Fetch oldest observation from Uniswap pool\n2. If observation window \u003C configured TWAP window, use available window\n3. Calculate arithmetic mean tick over window\n4. Convert tick to price quote\n5. Apply slippage tolerance based on swap size vs. pool liquidity\n\n### Slippage Tolerance Tiers\n\n```\nbase = (amountIn × 10 × DENOMINATOR) \u002F poolLiquidity\n\nif base > 150 bps: tolerance = 12%\nif base > 100 bps: tolerance = 33%\nif base > 75 bps: tolerance = 5%\n... (progressive reduction)\n```\n\n### Failure Handling\n\n```solidity\ntry uniswapPool.swap(...) returns (int256 amount0, int256 amount1) {\n    \u002F\u002F Process successful swap\n} catch {\n    \u002F\u002F Return 0, triggering fallback to standard mint\n}\n```\n\n**Critical**: Failed swaps don't revert the entire payment. The hook gracefully falls back to standard minting.\n\n### Leftover Handling\n\nAfter swap execution:\n```solidity\nif (leftoverAmount > 0) {\n    controller.mintTokensOf(projectId, leftoverAmount, beneficiary, ...)\n}\n```\n\nEnsures no user funds are lost to rounding or partial swaps.\n\n---\n\n## 721 Hook Tier Mechanics\n\n### Payment Processing Flow\n\n```\nafterPayRecordedWith()\n  → Decode metadata for tier IDs\n  → If no tiers specified: auto-select based on price\n  → STORE.recordMint() validates:\n    → Total tier prices ≤ payment amount\n    → Tiers have remaining supply\n    → Tiers are active\n  → Mint NFTs to beneficiary\n  → Handle leftover as credits or revert\n```\n\n### Credit System\n\n```solidity\npayCreditsOf[payer] += leftover\n\n\u002F\u002F On next payment:\neffectiveAmount = payment + payCreditsOf[payer]\npayCreditsOf[payer] = 0\n```\n\n**Tradeoff**: Credits provide flexibility but:\n- Accumulate dust from rounding\n- Can't be withdrawn, only used for NFTs\n- Lost if hook is changed\n\n### Cash Out Weight\n\nEach NFT's cash out value equals its tier price:\n```solidity\nweight = tier.price × redemptionRate\n```\n\nThe `totalCashOutWeight()` aggregates all outstanding NFT values for proportion calculations.\n\n**Critical**: If NFT prices vary significantly, small-price NFT holders receive proportionally less than their initial payment.\n\n---\n\n## Fee Mechanics\n\n### Fee Calculation\n\n```solidity\nFEE = 25  \u002F\u002F 2.5% (out of 1000)\nfeeAmount = amount × FEE \u002F (1000 + FEE)  \u002F\u002F ~2.44% of gross\n```\n\nNote: Fee is calculated as a portion of the gross amount, not added on top.\n\n### Fee Applicability\n\nFees apply to:\n- Payouts to non-project addresses\n- Surplus allowance usage\n- Cash outs with tax rate \u003C 100%\n\nFees exempt:\n- Project-to-project payments\n- Feeless addresses (registered in JBFeelessAddresses)\n- Internal transfers\n\n### Held Fees\n\nWhen `holdFees` is true in ruleset metadata:\n```solidity\nheldFeesOf[projectId][token].push(fee)\n\u002F\u002F Fees held for 28 days\n\u002F\u002F Can be refunded by adding equivalent to balance\n```\n\nAfter 28 days, held fees can be processed to the fee beneficiary (Project #1).\n\n---\n\n## Integration Recommendations\n\n### For Payment Integrators\n\n1. **Always set reasonable minReturnedTokens** to protect against frontrunning\n2. **Consider data hook gas costs** when estimating transaction costs\n3. **Handle pay hook reverts** gracefully in UI\n4. **Validate metadata encoding** matches hook expectations\n\n### For Hook Developers\n\n1. **Keep beforePayRecordedWith() view-only and light** - it runs on every payment\n2. **Handle failures gracefully** in afterPayRecordedWith() - don't lock user funds\n3. **Validate msg.sender** is an authorized terminal\n4. **Consider reentrancy** - hooks receive funds before execution\n\n### For Project Operators\n\n1. **Lock critical splits** to prevent rug pulls\n2. **Use approval hooks** for governance-controlled projects\n3. **Monitor pending reserved tokens** and distribute regularly\n4. **Set appropriate payout limits** to constrain risk\n\n### Gas Optimization Tips\n\n1. **Batch operations** when possible (queue multiple rulesets)\n2. **Use credits** for 721 hook instead of exact payments\n3. **Distribute reserved tokens** during low-gas periods\n4. **Consider total vs terminal surplus** tradeoff for cash outs\n\n---\n\n## Common Pitfalls\n\n1. **Setting minReturnedTokens = 0**: Vulnerable to sandwich attacks\n2. **Forgetting to include locked splits**: Update transaction reverts\n3. **Assuming immediate reserved distribution**: Tokens accumulate\n4. **Not handling hook metadata correctly**: Silent failures or reverts\n5. **Ignoring approval hook delays**: Rulesets rejected if queued late\n6. **Underestimating gas for multi-hook payments**: Transaction fails\n7. **Not validating surplus exists before cash out**: Wasted gas on revert\n\n---\n\n## Core Infrastructure Contracts\n\n### JBDirectory\n\nThe directory manages terminal and controller assignments for projects.\n\n#### Storage Architecture\n\n```solidity\nmapping(uint256 projectId => IJBController) public controllerOf;\nmapping(uint256 projectId => IJBTerminal[]) internal _terminalsOf;\nmapping(uint256 projectId => mapping(address token => IJBTerminal)) internal _primaryTerminalOf;\nmapping(address => bool) public isAllowedToSetFirstController;\n```\n\n#### Terminal Management\n\n**setTerminalsOf()** replaces the entire terminal array:\n- Validates no duplicates via nested loop\n- Requires `SET_TERMINALS` permission OR caller is project controller\n- Checks ruleset's `setTerminalsAllowed` flag (bypassed if controller is caller)\n\n**primaryTerminalOf()** resolution:\n1. Return explicitly-set primary terminal if still valid\n2. Otherwise, return first terminal accepting that token\n3. Return zero address if none found\n\n#### Controller Migration\n\n**setControllerOf()** handles migration:\n1. Validates `SET_CONTROLLER` permission OR first-time setup via allowlist\n2. Checks ruleset's `setControllerAllowed` flag\n3. Calls `IJBMigratable.migrate()` on old controller if interface supported\n\n**Critical**: First controller can only be set by addresses on the `isAllowedToSetFirstController` allowlist.\n\n---\n\n### JBProjects\n\nERC-721 contract where each token represents a Juicebox project.\n\n#### Project Creation\n\n```solidity\nfunction createFor(address owner) external returns (uint256 projectId) {\n    projectId = ++count;\n    _mint(owner, projectId);\n}\n```\n\nThe `count` variable acts as both total project counter and next project ID.\n\n#### Metadata Resolution\n\n```solidity\nfunction tokenURI(uint256 projectId) public view override returns (string memory) {\n    if (address(tokenUriResolver) == address(0)) return \"\";\n    return tokenUriResolver.tokenUriOf(address(this), projectId);\n}\n```\n\n**Graceful Degradation**: Returns empty string if no resolver set, preventing reverts.\n\n#### ERC-2771 Support\n\nOverrides `_msgSender()`, `_msgData()`, and `_contextSuffixLength()` for meta-transaction support via trusted forwarder.\n\n---\n\n### JBPermissions\n\nBitmap-based permission system enabling granular access control.\n\n#### Storage Architecture\n\n```solidity\nmapping(\n    address operator => mapping(\n        address account => mapping(\n            uint256 projectId => uint256 packedPermissions\n        )\n    )\n) public permissionsOf;\n```\n\nEach bit in the `uint256` represents one of 256 possible permissions. Project ID `0` is the wildcard, granting permissions across all projects.\n\n#### Permission Checking\n\n**hasPermission()** implements hierarchical evaluation:\n\n```\n1. If includeRoot && operator has ROOT permission:\n   → Check specific project OR wildcard project\n   → Return true if ROOT found\n\n2. Check specific permissionId bit on specific project\n\n3. If includeWildcardProjectId:\n   → Check specific permissionId bit on project 0\n```\n\n**hasPermissions()** (batch check):\n- Returns true immediately if ROOT permission exists\n- Iterates through all requested permissions\n- Returns false if ANY permission missing\n\n#### Security Constraints in setPermissionsFor()\n\n```solidity\n\u002F\u002F Only account holder or authorized operators can modify\nif (_msgSender() != account) {\n    \u002F\u002F Operators cannot grant ROOT permission\n    if (permissionsData.permissionIds contains ROOT) revert;\n\n    \u002F\u002F ROOT operators cannot modify wildcard project\n    if (projectId == 0) revert;\n\n    \u002F\u002F Must have ROOT on specific project to modify\n    if (!hasPermission(ROOT, projectId)) revert;\n}\n```\n\n**Edge Cases**:\n- Permission ID 0 is reserved, cannot be set\n- Permission IDs > 255 revert with `PermissionIdOutOfBounds`\n- Empty permission arrays are valid (clears all permissions)\n\n---\n\n### JBTokens\n\nDual-balance system supporting both unclaimed credits and ERC-20 tokens.\n\n#### Storage Design\n\n```solidity\nmapping(address holder => mapping(uint256 projectId => uint256)) public creditBalanceOf;\nmapping(uint256 projectId => uint256) public totalCreditSupplyOf;\nmapping(uint256 projectId => IJBToken) public tokenOf;\nmapping(IJBToken token => uint256) public projectIdOf;\n```\n\n#### Minting Logic\n\n**mintFor()** chooses based on ERC-20 existence:\n```solidity\nif (tokenOf[projectId] != address(0)) {\n    token.mint(holder, count);  \u002F\u002F Direct ERC-20 mint\n} else {\n    creditBalanceOf[holder][projectId] += count;  \u002F\u002F Store as credits\n    totalCreditSupplyOf[projectId] += count;\n}\n```\n\n#### Burning Priority\n\n**burnFrom()** burns credits first, then tokens:\n```solidity\nuint256 creditBalance = creditBalanceOf[holder][projectId];\nuint256 tokensToBurn = creditBalance \u003C count ? count - creditBalance : 0;\nuint256 creditsToBurn = count - tokensToBurn;\n\n\u002F\u002F Burn credits\ncreditBalanceOf[holder][projectId] -= creditsToBurn;\ntotalCreditSupplyOf[projectId] -= creditsToBurn;\n\n\u002F\u002F Burn tokens\nif (tokensToBurn > 0) token.burn(holder, tokensToBurn);\n```\n\n#### ERC-20 Deployment\n\n**deployERC20For()** uses minimal proxy clones:\n- Validates project doesn't already have token\n- Deploys via `Clones.clone()` or `Clones.cloneDeterministic()`\n- Links bidirectionally: `tokenOf[projectId]` and `projectIdOf[token]`\n\n**setTokenFor()** validates external tokens:\n- Token must use 18 decimals\n- Token must return true from `canBeAddedTo(projectId)`\n- Token must not be assigned to another project\n\n---\n\n### Custom ERC20 Token Integration\n\nThe JBTokens system supports custom ERC20 tokens, enabling advanced tokenomics while preserving Juicebox's payment and redemption mechanics.\n\n#### How Custom Tokens Work\n\nWhen `setTokenFor()` is called with a custom token:\n\n```solidity\nfunction setTokenFor(uint256 projectId, IJBToken token) external {\n    \u002F\u002F 1. Validate token is compatible\n    if (token.decimals() != 18) revert JBTokens_TokensMustHave18Decimals();\n    if (!token.canBeAddedTo(projectId)) revert JBTokens_TokenCannotBeAddedTo();\n    if (projectIdOf[token] != 0) revert JBTokens_TokenAlreadyAssigned();\n\n    \u002F\u002F 2. Store bidirectional mapping\n    tokenOf[projectId] = token;\n    projectIdOf[token] = projectId;\n\n    \u002F\u002F 3. Existing credits remain claimable\n    \u002F\u002F totalCreditSupplyOf[projectId] stays unchanged\n}\n```\n\n**Key Insight**: Setting a custom token doesn't migrate existing credits. Credit holders must call `claimTokensFor()` to convert credits to the ERC20.\n\n#### Mint\u002FBurn Flow with Custom Tokens\n\nWhen payments are received:\n```\npay() → mintTokensOf() → JBTokens.mintFor()\n                              ↓\n                         customToken.mint(holder, amount)\n```\n\nWhen cash outs occur:\n```\ncashOutTokensOf() → burnTokensOf() → JBTokens.burnFrom()\n                                          ↓\n                                     \u002F\u002F Burns credits first, then tokens\n                                     customToken.burn(holder, tokensToBurn)\n```\n\n**Critical**: The controller calls `mint()` and `burn()` directly on your token. Your token MUST grant these permissions to the controller address.\n\n#### Custom Token Requirements\n\n| Requirement | Reason |\n|-------------|--------|\n| **18 decimals** | All Juicebox math (weights, rates) assumes 18 decimals |\n| **canBeAddedTo()** | Validates token agrees to serve this project |\n| **mint(address, uint256)** | Controller must mint on payments |\n| **burn(address, uint256)** | Controller must burn on cash outs |\n| **Controller access** | Token must authorize JBController for mint\u002Fburn |\n\n#### Common Custom Token Patterns\n\n**1. Transfer Tax Token**\n```solidity\nfunction _update(address from, address to, uint256 amount) internal override {\n    \u002F\u002F Skip tax for controller operations (mints\u002Fburns)\n    if (from == address(0) || to == address(0) || msg.sender == controller) {\n        super._update(from, to, amount);\n        return;\n    }\n    \u002F\u002F Apply tax on transfers\n    uint256 tax = (amount * TAX_RATE) \u002F 10000;\n    super._update(from, taxRecipient, tax);\n    super._update(from, to, amount - tax);\n}\n```\n\n**Tradeoff**: Tax revenue goes to `taxRecipient`, not the Juicebox treasury. Consider routing tax to the project via `addToBalanceOf()`.\n\n**2. Rebasing Token**\n```solidity\n\u002F\u002F Track shares instead of balances\nmapping(address => uint256) private _shares;\nuint256 public totalShares;\nuint256 public rebaseIndex = 1e18; \u002F\u002F Starts at 1:1\n\nfunction balanceOf(address account) public view override returns (uint256) {\n    return (_shares[account] * rebaseIndex) \u002F 1e18;\n}\n\nfunction rebase(uint256 newIndex) external onlyOwner {\n    rebaseIndex = newIndex;\n    \u002F\u002F All balances scale proportionally\n}\n```\n\n**Tradeoff**: Cash out calculations use `totalSupply()`. Rebasing changes supply without minting, which affects redemption value.\n\n**3. Governance Token (ERC20Votes)**\n```solidity\nimport {ERC20Votes} from \"@openzeppelin\u002Fcontracts\u002Ftoken\u002FERC20\u002Fextensions\u002FERC20Votes.sol\";\n\ncontract GovernanceProjectToken is ERC20Votes {\n    address public controller;\n\n    function mint(address to, uint256 amount) external {\n        require(msg.sender == controller, \"Only controller\");\n        _mint(to, amount);\n    }\n\n    \u002F\u002F Inherits delegation, checkpointing, getPastVotes()\n}\n```\n\n**Benefit**: Token holders can vote on external governance proposals while maintaining Juicebox treasury mechanics.\n\n**4. Editable Name\u002FSymbol Token**\n```solidity\ncontract EditableProjectToken is ERC20 {\n    string private _tokenName;\n    string private _tokenSymbol;\n\n    function name() public view override returns (string memory) { return _tokenName; }\n    function symbol() public view override returns (string memory) { return _tokenSymbol; }\n\n    function setName(string calldata newName) external onlyProjectOwner {\n        _tokenName = newName;\n    }\n\n    function setSymbol(string calldata newSymbol) external onlyProjectOwner {\n        _tokenSymbol = newSymbol;\n    }\n}\n```\n\n**Benefit**: Rebrand without redeploying token or migrating liquidity. Project owner controls metadata.\n\n**5. Vesting Token with Per-Address Schedules**\n```solidity\ncontract VestingProjectToken is ERC20 {\n    struct VestingSchedule {\n        uint256 totalAmount;     \u002F\u002F Total tokens in this schedule\n        uint256 released;        \u002F\u002F Already released\u002Ftransferred\n        uint40 start;            \u002F\u002F Vesting start timestamp\n        uint40 cliff;            \u002F\u002F Cliff end timestamp\n        uint40 duration;         \u002F\u002F Total vesting duration\n    }\n\n    mapping(address => VestingSchedule) public vestingOf;\n\n    function vestedAmountOf(address account) public view returns (uint256) {\n        VestingSchedule memory schedule = vestingOf[account];\n        if (schedule.totalAmount == 0) return balanceOf(account);\n        if (block.timestamp \u003C schedule.cliff) return 0;\n        if (block.timestamp >= schedule.start + schedule.duration) {\n            return schedule.totalAmount;\n        }\n        uint256 elapsed = block.timestamp - schedule.start;\n        return (schedule.totalAmount * elapsed) \u002F schedule.duration;\n    }\n\n    function _update(address from, address to, uint256 amount) internal override {\n        \u002F\u002F Skip vesting checks for mints, burns, controller ops\n        if (from == address(0) || to == address(0) || msg.sender == controller) {\n            super._update(from, to, amount);\n            return;\n        }\n\n        VestingSchedule storage schedule = vestingOf[from];\n        if (schedule.totalAmount == 0) {\n            super._update(from, to, amount);\n            return;\n        }\n\n        require(block.timestamp >= schedule.cliff, \"Cliff not reached\");\n        uint256 transferable = vestedAmountOf(from) - schedule.released;\n        require(amount \u003C= transferable, \"Insufficient vested balance\");\n        schedule.released += amount;\n        super._update(from, to, amount);\n    }\n\n    function setVestingSchedule(\n        address beneficiary,\n        uint256 totalAmount,\n        uint40 start,\n        uint40 cliffDuration,\n        uint40 vestingDuration\n    ) external onlyProjectOwner {\n        vestingOf[beneficiary] = VestingSchedule({\n            totalAmount: totalAmount,\n            released: 0,\n            start: start,\n            cliff: start + cliffDuration,\n            duration: vestingDuration\n        });\n    }\n}\n```\n\n**Key Difference from Treasury Vesting**:\n- **Treasury vesting** (payout limits): Controls when funds leave the treasury\n- **Token vesting**: Controls when individual holders can transfer tokens\n\n**Use Case**: Team allocations, investor lock-ups, contributor rewards where tokens should vest per-person with individual cliffs and durations. Can be combined with treasury vesting for layered protection.\n\n**Tradeoff**: Does not prevent cash outs (controller operations bypass vesting). If you need to prevent recipients from cashing out, combine with a ruleset that has `pauseCashOut: true` during the vesting period, or set a high `cashOutTaxRate`.\n\n#### Edge Cases and Gotchas\n\n1. **Token assigned twice**: A token can only serve one project. `setTokenFor()` reverts if token already assigned.\n\n2. **Credit\u002Ftoken split during cash out**: Burns credits first, then tokens:\n   ```solidity\n   \u002F\u002F If user has 100 credits and 50 tokens, burning 120:\n   \u002F\u002F - Burns all 100 credits\n   \u002F\u002F - Burns 20 tokens\n   ```\n\n3. **Transfer restrictions**: If your token blocks certain transfers, ensure controller operations (mint\u002Fburn) are always allowed.\n\n4. **Pausable tokens**: Pausing transfers will break cash outs if tokens can't be burned.\n\n5. **Fee-on-transfer tokens**: Not directly supported. The minted amount must equal the amount the controller requested.\n\n6. **Approval requirements**: JBTokens calls `burn()` directly. If your token requires approval for burns, this will fail. Use `burnFrom` pattern that allows controller without approval.\n\n#### Integration Checklist\n\nBefore using a custom token:\n\n- [ ] Token uses exactly 18 decimals\n- [ ] `canBeAddedTo(projectId)` returns true for your project\n- [ ] Controller address has mint permission\n- [ ] Controller address has burn permission (without approval)\n- [ ] Token not assigned to another project\n- [ ] Mint\u002Fburn don't have unexpected side effects (fees, rebasing)\n- [ ] Transfer restrictions exempt controller operations\n- [ ] Considered interaction with cash out tax rate\n- [ ] Tested credit → token claiming works\n- [ ] Verified `totalSupply()` reflects actual redeemable tokens\n\n---\n\n### JBFundAccessLimits\n\nPacked storage for payout limits and surplus allowances.\n\n#### Bit Packing\n\n```solidity\n\u002F\u002F Bits 0-223: amount (up to ~2^224 wei)\n\u002F\u002F Bits 224-255: currency (32-bit identifier)\nuint256 packed = uint256(amount) | (uint256(currency) \u003C\u003C 224);\n```\n\n#### Currency Ordering Enforcement\n\n**setFundAccessLimitsFor()** requires strictly increasing currency order:\n```solidity\nfor (uint256 i = 1; i \u003C payoutLimits.length; i++) {\n    if (payoutLimits[i].currency \u003C= payoutLimits[i-1].currency) {\n        revert JBFundAccessLimits_InvalidPayoutLimitCurrencyOrdering();\n    }\n}\n```\n\nThis prevents duplicates and enables O(n) iteration during lookups.\n\n#### Zero Filtering\n\nZero-amount limits are filtered during storage, not stored:\n```solidity\nif (limit.amount > 0) {\n    _packedPayoutLimitsDataOf[projectId][rulesetId][terminal][token].push(packed);\n}\n```\n\n---\n\n## Fund Access Limit Lifecycle\n\nUnderstanding the lifecycle of payout limits vs surplus allowances is critical for project design. **This is a common source of confusion.**\n\n### Payout Limits: Reset Each Cycle\n\nPayout limits reset when a new cycle begins:\n\n```\nCycle 1: Payout limit = 10 ETH\n  → Team sends 10 ETH payouts\n  → Remaining limit = 0\n\nCycle 2: Payout limit = 10 ETH (RESET!)\n  → Team can send another 10 ETH\n  → This continues every cycle\n```\n\n**Key Insight**: The payout limit is defined per-ruleset, and when a cycling ruleset starts a new cycle, the limit refreshes. This enables **recurring distributions** without queuing multiple rulesets.\n\n### Surplus Allowance: One-Time Per Ruleset\n\nSurplus allowance does NOT reset each cycle:\n\n```\nCycle 1: Surplus allowance = 20 ETH\n  → Team uses 15 ETH from surplus\n  → Remaining allowance = 5 ETH\n\nCycle 2: Surplus allowance = 5 ETH (NOT reset!)\n  → Still only 5 ETH available\n  → Allowance only resets if NEW RULESET is queued\n```\n\n**Key Insight**: Surplus allowance is a one-time budget per ruleset configuration. It's designed for **discretionary treasury access**, not recurring distributions.\n\n### How Remaining Limits Are Tracked\n\nThe terminal store tracks used amounts separately from configured limits:\n\n```solidity\n\u002F\u002F JBTerminalStore\nmapping(address terminal =>\n    mapping(uint256 projectId =>\n        mapping(address token =>\n            mapping(uint256 rulesetCycleNumber => uint256)\n        )\n    )\n) public usedPayoutLimitOf;\n\nmapping(address terminal =>\n    mapping(uint256 projectId =>\n        mapping(address token =>\n            mapping(uint256 rulesetId => uint256)\n        )\n    )\n) public usedSurplusAllowanceOf;\n```\n\nNotice the key difference:\n- **Payout limit**: Keyed by `rulesetCycleNumber` → resets each cycle\n- **Surplus allowance**: Keyed by `rulesetId` → persists across cycles\n\n### Surplus Calculation\n\nSurplus (redeemable amount) is calculated as:\n```\nsurplus = balance - remainingPayoutLimit\n```\n\nWhere `remainingPayoutLimit` is the configured limit minus used amount for the current cycle.\n\n**Implication**: Payout limits **protect funds from cash outs**. If you set a 10 ETH payout limit, that 10 ETH cannot be cashed out by token holders until it's distributed or the cycle ends.\n\n### Design Pattern: Vesting via Native Mechanics\n\nCombine both limit types for sophisticated treasury management:\n\n| Mechanism | Behavior | Use Case |\n|-----------|----------|----------|\n| Payout Limit | Resets each cycle | Recurring vesting\u002Fsalaries |\n| Surplus Allowance | One-time per ruleset | Emergency fund \u002F discretionary |\n\n**Example: 12-Month Vesting with Treasury Reserve**\n\n```solidity\n\u002F\u002F Single cycling ruleset (NOT 12 separate rulesets!)\nJBRulesetConfig({\n    duration: 30 days,  \u002F\u002F Monthly cycles\n    \u002F\u002F ...\n    fundAccessLimitGroups: [\n        JBFundAccessLimitGroup({\n            terminal: address(TERMINAL),\n            token: JBConstants.NATIVE_TOKEN,\n            payoutLimits: [\n                JBCurrencyAmount({\n                    amount: 6.67 ether,   \u002F\u002F Monthly vesting (80 ETH \u002F 12)\n                    currency: nativeCurrency\n                })\n            ],\n            surplusAllowances: [\n                JBCurrencyAmount({\n                    amount: 20 ether,     \u002F\u002F One-time treasury access\n                    currency: nativeCurrency\n                })\n            ]\n        })\n    ]\n});\n```\n\n**Result over 12 months:**\n- Month 1: Team can withdraw 6.67 ETH (payout) + up to 20 ETH (surplus allowance)\n- Month 2-12: Team can withdraw 6.67 ETH\u002Fmonth (payout limit resets each cycle)\n- Surplus allowance does NOT reset - once used, it's gone until new ruleset\n\n### Common Mistakes\n\n1. **Queueing 12 rulesets for 12-month vesting**\n   - Wrong: Queue separate ruleset for each month\n   - Right: One cycling ruleset with monthly payout limit\n\n2. **Expecting surplus allowance to reset**\n   - Wrong: Plan to use 10 ETH each month from surplus allowance\n   - Right: Use payout limits for recurring distributions\n\n3. **Not understanding surplus protection**\n   - Payout limits protect funds from redemption\n   - Funds within the payout limit are NOT part of surplus\n\n---\n\n### JBPrices\n\nHierarchical price feed system with inverse calculation support.\n\n#### Price Resolution Order\n\n**pricePerUnitOf()** follows this order:\n```\n1. Same currency? Return 10^decimals (1:1 ratio)\n2. Direct feed exists? Use feed.currentUnitPrice()\n3. Inverse feed exists? Calculate reciprocal\n4. Project-specific not found? Try default (projectId=0)\n5. Nothing found? Revert with PriceFeedNotFound\n```\n\n#### Inverse Calculation\n\n```solidity\n\u002F\u002F If only priceFeedFor[project][B][A] exists, calculate A→B:\nprice = mulDiv(10**decimals, 10**decimals, inverseFeed.currentUnitPrice(decimals));\n```\n\n#### Feed Immutability\n\nPrice feeds are append-only:\n- Cannot modify existing feeds\n- Cannot remove feeds\n- Validates both direct and inverse don't already exist before adding\n\n**Access Control**:\n- Default feeds (projectId=0): Owner only\n- Project-specific feeds: Controller only\n\n---\n\n## Hook & Terminal Implementations\n\n### JBSwapTerminal Deep Dive\n\nWraps Uniswap V3 swaps into a Juicebox terminal interface.\n\n#### Payment Flow\n\n```\npay()\u002FaddToBalanceOf()\n  → _acceptFundsFor() \u002F\u002F Transfer or Permit2\n  → _handleTokenTransfersAndSwap()\n    → _beforeTransferFor() \u002F\u002F Wrap ETH if needed\n    → _swap() \u002F\u002F Execute Uniswap swap\n  → Forward to primary terminal for output token\n```\n\n#### TWAP Integration\n\n```solidity\nfunction _getQuote(IUniswapV3Pool pool, uint256 amountIn) internal view returns (uint256) {\n    uint32 secondsAgo = OracleLibrary.getOldestObservationSecondsAgo(address(pool));\n\n    \u002F\u002F Cap at configured window (2 min to 2 days)\n    if (secondsAgo > twapWindow) secondsAgo = twapWindow;\n\n    (int24 arithmeticMeanTick,) = OracleLibrary.consult(pool, secondsAgo);\n    return OracleLibrary.getQuoteAtTick(arithmeticMeanTick, amountIn, tokenIn, tokenOut);\n}\n```\n\n#### Pool Configuration\n\n**addDefaultPool()** setup:\n1. Validates pool deployed by stored factory\n2. Increases observation cardinality to minimum 10\n3. Stores accounting context (decimals, etc.)\n\n#### Hierarchical Defaults\n\nProject-specific configs override global defaults:\n```solidity\npool = poolOf[projectId][tokenIn][tokenOut];\nif (pool == address(0)) {\n    pool = poolOf[DEFAULT_PROJECT_ID][tokenIn][tokenOut];\n}\n```\n\n---\n\n### JB721TiersHook Deep Dive\n\nTiered NFT system with integrated payment processing and cash out support. Uses JBOwnable for flexible ownership.\n\n#### Architecture\n\nThe 721 hook implements three hook interfaces:\n- **IJBRulesetDataHook**: `beforePayRecordedWith()`, `beforeCashOutRecordedWith()`\n- **IJBPayHook**: `afterPayRecordedWith()`\n- **IJBCashOutHook**: `afterCashOutRecordedWith()`\n\nPlus extends **JBOwnable** for project-based or EOA ownership with permission delegation.\n\n#### Tier Storage (JB721TiersHookStore)\n\n```solidity\nstruct JB721Tier {\n    uint104 price;              \u002F\u002F Price in terminal token\n    uint32 initialSupply;       \u002F\u002F Starting supply\n    uint32 remainingSupply;     \u002F\u002F Current available\n    uint16 votingUnits;         \u002F\u002F Governance weight\n    uint16 reserveFrequency;    \u002F\u002F Reserved mint ratio (1 in N)\n    uint24 category;            \u002F\u002F Grouping identifier\n    bool transfersPausable;     \u002F\u002F Can transfers be paused\n    bool cannotBeRemoved;       \u002F\u002F Permanent tier flag\n}\n```\n\n#### Payment Processing (`_processPayment`)\n\n```\nafterPayRecordedWith()\n  → _processPayment()\n    → Normalize payment to tier pricing currency via JBPrices\n    → Add existing payCreditsOf[beneficiary] if payer == beneficiary\n    → Decode metadata for tier IDs to mint\n    → If no tiers specified: auto-select best fit tiers\n    → Call STORE.recordMint() for each tier\n      → Validates tier active and has supply\n      → Handles reserved mints (1 per reserveFrequency)\n    → Mint NFTs via _mint()\n    → Handle leftover:\n      → If allowOverspending: add to payCreditsOf\n      → Else: revert if leftover exists\n```\n\n#### Auto-Tier Selection\n\nWhen payer doesn't specify tiers, the hook selects automatically:\n```solidity\nfunction _selectAutoTiers(uint256 amount) internal view returns (uint256[] tiers) {\n    \u002F\u002F Iterate tiers by price descending\n    \u002F\u002F Select highest-priced tier that fits remaining amount\n    \u002F\u002F Repeat until amount exhausted or no tier fits\n}\n```\n\n**Tradeoff**: Auto-selection may not match user intent; explicit tier selection preferred for UX.\n\n#### Credit System Implementation\n\n```solidity\nmapping(address payer => uint256) public payCreditsOf;\n\n\u002F\u002F On payment:\nif (payer == beneficiary) {\n    effectiveAmount = payment.amount + payCreditsOf[payer];\n    payCreditsOf[payer] = 0;  \u002F\u002F Use all credits\n}\n\n\u002F\u002F After minting:\nif (leftover > 0 && allowOverspending) {\n    payCreditsOf[beneficiary] += leftover;\n    emit AddPayCredits(beneficiary, leftover, ...);\n}\n```\n\n**Edge Cases**:\n- Credits lost if hook is replaced\n- Credits cannot be withdrawn, only spent on NFTs\n- Accumulated dust from rounding\n\n#### Cash Out Weight Calculation\n\nEach NFT's value equals its tier price:\n```solidity\nfunction cashOutWeightOf(uint256[] tokenIds) public view returns (uint256 weight) {\n    for (uint256 i; i \u003C tokenIds.length; i++) {\n        JB721Tier tier = STORE.tierOfTokenId(tokenIds[i]);\n        weight += tier.price;\n    }\n}\n```\n\nTotal outstanding value:\n```solidity\nfunction totalCashOutWeight() public view returns (uint256) {\n    return STORE.totalCashOutWeight(address(this));\n}\n```\n\n**Cash Out Proportion**:\n```\nuserShare = (userNFTWeight \u002F totalCashOutWeight) × surplus × (1 - taxRate)\n```\n\n#### Reserved Minting\n\nTiers can reserve NFTs for the project:\n```solidity\n\u002F\u002F If reserveFrequency = 10, every 10th mint goes to reserved beneficiary\nif (mintCount % tier.reserveFrequency == 0) {\n    _mint(reservedBeneficiary, tokenId);\n} else {\n    _mint(payer, tokenId);\n}\n```\n\nReserved NFTs are minted inline during payment, not accumulated.\n\n#### Ownership via JBOwnable\n\nThe hook extends JBOwnable for flexible access control:\n```solidity\n\u002F\u002F Project-based ownership (common pattern):\nconstructor(..., uint256 projectId, ...) {\n    _transferOwnership(projectId);  \u002F\u002F Owner = project NFT holder\n}\n\n\u002F\u002F Permission delegation:\n\u002F\u002F Owner can grant ADJUST_721_TIERS to operators via JBPermissions\n\u002F\u002F Operators can then call adjustTiers() without being owner\n```\n\n**Permission IDs used**:\n- `ADJUST_721_TIERS` (17): Add\u002Fremove tiers\n- `SET_721_METADATA` (18): Update tier metadata\n- `MINT_721` (19): Manual minting\n- `SET_721_DISCOUNT_PERCENT` (27): Adjust pricing\n\n#### Tier Adjustment\n\n```solidity\nfunction adjustTiers(JB721TierConfig[] tiersToAdd, uint256[] tierIdsToRemove) external {\n    \u002F\u002F Requires ADJUST_721_TIERS permission or owner\n\n    \u002F\u002F Remove tiers (if not cannotBeRemoved)\n    for (uint256 id : tierIdsToRemove) {\n        require(!tier.cannotBeRemoved, \"TIER_LOCKED\");\n        STORE.recordRemoveTierOf(id);\n    }\n\n    \u002F\u002F Add new tiers\n    for (JB721TierConfig config : tiersToAdd) {\n        STORE.recordAddTier(config);\n    }\n}\n```\n\n#### Metadata Encoding\n\nPayer metadata structure for specifying tiers:\n```solidity\nbytes4 constant METADATA_ID = bytes4(keccak256(\"JB721TiersHook\"));\n\n\u002F\u002F Encoded as: [METADATA_ID][allowOverspending (bool)][tierIds (uint16[])]\nbytes memory metadata = abi.encode(\n    true,                    \u002F\u002F allowOverspending\n    [uint16(1), uint16(3)]   \u002F\u002F Mint tier 1 and tier 3\n);\n```\n\n---\n\n## Extending 721-Hook: Dynamic Cash Out Weights\n\nWhen building prediction games or outcome-based systems, you need to extend the 721-hook to change treasury mechanics. This section covers the key implementation patterns from Defifa.\n\n### Why Extend vs. Use Resolver Only\n\n| Need | Resolver | Extended Hook |\n|------|----------|---------------|\n| Custom artwork\u002Fmetadata | ✅ | ✅ |\n| Dynamic cash out weights | ❌ | ✅ |\n| First-owner tracking | ❌ | ✅ |\n| Phase-based restrictions | ❌ | ✅ |\n| Governor integration | ❌ | ✅ |\n\n**Rule of thumb**: If you need to change how money flows, extend the hook. If you only need to change how tokens look, use a resolver.\n\n### Dynamic Cash Out Weight Implementation\n\nStandard 721-hook uses fixed weights based on tier price:\n```solidity\n\u002F\u002F Standard: weight = tier.price\nfunction cashOutWeightOf(uint256[] tokenIds) returns (uint256 weight) {\n    for (uint256 i; i \u003C tokenIds.length; i++) {\n        weight += STORE.tierOfTokenId(tokenIds[i]).price;\n    }\n}\n```\n\nFor dynamic weights (e.g., prediction games), override with configurable weights:\n```solidity\n\u002F\u002F Extended: weight = configurable per tier\nuint256 constant TOTAL_CASH_OUT_WEIGHT = 1e18; \u002F\u002F 100% distributed among tiers\n\nmapping(uint256 tierId => uint256) public tierCashOutWeight;\n\nfunction cashOutWeightOf(uint256[] tokenIds) returns (uint256 weight) {\n    for (uint256 i; i \u003C tokenIds.length; i++) {\n        uint256 tierId = STORE.tierIdOfToken(tokenIds[i]);\n        weight += tierCashOutWeight[tierId];\n    }\n}\n\n\u002F\u002F Called by governor after outcome is known\nfunction setTierCashOutWeightsTo(DefifaTierCashOutWeight[] calldata weights) external {\n    \u002F\u002F Verify caller is authorized (governor)\n    \u002F\u002F Verify total weights sum to TOTAL_CASH_OUT_WEIGHT\n    for (uint256 i; i \u003C weights.length; i++) {\n        tierCashOutWeight[weights[i].id] = weights[i].cashOutWeight;\n    }\n}\n```\n\n### First-Owner Tracking\n\nFor games where rewards should go to original minters (not secondary buyers):\n\n```solidity\n\u002F\u002F Track original minter\nmapping(uint256 tokenId => address) public firstOwnerOf;\n\n\u002F\u002F In _processPayment() after minting:\nfunction _processPayment(JBAfterPayRecordedContext calldata context) internal override {\n    \u002F\u002F ... mint logic ...\n\n    \u002F\u002F Record first owner\n    for (uint256 i; i \u003C mintedTokenIds.length; i++) {\n        firstOwnerOf[mintedTokenIds[i]] = context.beneficiary;\n    }\n}\n\n\u002F\u002F In cash out, rewards go to first owner:\nfunction afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context) external {\n    \u002F\u002F Verify current owner initiated cash out\n    \u002F\u002F But send rewards to firstOwnerOf[tokenId]\n    address rewardRecipient = firstOwnerOf[tokenId];\n\n    \u002F\u002F Transfer rewards to original minter\n    _transferRewards(rewardRecipient, amount);\n}\n```\n\n**Trade-off**: First-owner tracking adds storage costs but ensures fair game mechanics where secondary market purchases don't steal rewards from original participants.\n\n### Phase-Based Cash Out Logic\n\nDifferent phases have different cash out rules:\n\n```solidity\nenum GamePhase { COUNTDOWN, MINT, REFUND, SCORING, COMPLETE, NO_CONTEST }\n\nfunction beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)\n    external view override\n    returns (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply, JBCashOutHookSpecification[] memory)\n{\n    GamePhase phase = currentPhase();\n\n    if (phase == GamePhase.REFUND) {\n        \u002F\u002F During refund: return mint cost (full refund)\n        return _refundCashOut(context);\n    }\n\n    if (phase == GamePhase.COMPLETE) {\n        \u002F\u002F After scoring: return weighted share of pot\n        return _scoredCashOut(context);\n    }\n\n    \u002F\u002F Other phases: no cash out allowed\n    revert CashOutNotAllowed();\n}\n```\n\n### Governor Integration Pattern\n\nThe governor contract calls into the delegate to set weights:\n\n```solidity\n\u002F\u002F In Governor contract:\nfunction ratifyScorecard(DefifaScorecard calldata scorecard) external {\n    \u002F\u002F Verify quorum reached\n    require(attestationCount[scorecard.id] >= quorum(), \"Quorum not reached\");\n\n    \u002F\u002F Set weights on delegate\n    IDefifaDelegate(delegate).setTierCashOutWeightsTo(scorecard.weights);\n\n    \u002F\u002F Emit event\n    emit ScorecardRatified(scorecard.id);\n}\n\n\u002F\u002F In Delegate contract:\nfunction setTierCashOutWeightsTo(DefifaTierCashOutWeight[] calldata weights) external {\n    \u002F\u002F Only governor can set weights\n    require(msg.sender == governor, \"Only governor\");\n\n    \u002F\u002F Only during SCORING phase\n    require(currentPhase() == GamePhase.SCORING, \"Wrong phase\");\n\n    \u002F\u002F Verify weights sum to 100%\n    uint256 total;\n    for (uint256 i; i \u003C weights.length; i++) {\n        tierCashOutWeight[weights[i].id] = weights[i].cashOutWeight;\n        total += weights[i].cashOutWeight;\n    }\n    require(total == TOTAL_CASH_OUT_WEIGHT, \"Invalid total\");\n\n    \u002F\u002F Transition to COMPLETE phase\n    _setPhase(GamePhase.COMPLETE);\n}\n```\n\n### Voting Power Calculation\n\nNFT holders vote with power proportional to their holdings:\n\n```solidity\n\u002F\u002F Voting power = (tokens owned in tier \u002F total minted in tier) * MAX_POWER_PER_TIER\nfunction getAttestationPowerOf(address account, uint256[] calldata tierIds)\n    public view returns (uint256 power)\n{\n    for (uint256 i; i \u003C tierIds.length; i++) {\n        uint256 tierId = tierIds[i];\n        uint256 owned = balanceOfTier(account, tierId);\n        uint256 totalMinted = STORE.tier(tierId).initialSupply - STORE.tier(tierId).remainingSupply;\n\n        if (totalMinted > 0) {\n            power += (owned * MAX_ATTESTATION_POWER_TIER) \u002F totalMinted;\n        }\n    }\n}\n```\n\n### Reference Implementation\n\nSee [defifa-collection-deployer-v5](https:\u002F\u002Fgithub.com\u002FBallKidz\u002Fdefifa-collection-deployer-v5) for complete implementation including:\n- `DefifaDelegate.sol` - Extended 721-hook with all patterns above\n- `DefifaGovernor.sol` - On-chain voting with tier-weighted power\n- `DefifaDeployer.sol` - Factory for deploying games\n\n---\n\n## Contract-as-Owner Pattern\n\n### REVDeployer Architecture\n\nREVDeployer owns Juicebox projects (revnets), enabling autonomous governance.\n\n#### Deployment Flow\n\n```\ndeployFor()\n  → Calculate next projectId via PROJECTS.count() + 1\n  → CONTROLLER.launchProjectFor()\n    → Creates project, transfers NFT to REVDeployer\n  → _deployRevnetFor()\n    → Deploy ERC-20 token\n    → Configure buyback hook with pools\n    → Set up split operator permissions\n    → Deploy suckers for cross-chain\n```\n\n#### Permission Delegation Model\n\nREVDeployer grants permissions to designated operators:\n```solidity\nfunction _setSplitOperatorOf(uint256 revnetId, address operator) internal {\n    uint256[] memory permissions = _splitOperatorPermissionIndexesOf(revnetId);\n\n    _setPermissionsFor(\n        operator,\n        revnetId,\n        permissions\n    );\n}\n```\n\n**Default Split Operator Permissions** (6 total):\n1. SET_SPLIT_GROUPS\n2. SET_BUYBACK_POOL\n3. SET_BUYBACK_TWAP\n4. SET_PROJECT_URI\n5. DEPLOY_SUCKERS\n6. SET_CONTROLLER (for suckers)\n\nPlus any custom permissions in `_extraOperatorPermissions[revnetId]`.\n\n#### Stage Mechanics\n\nStages define temporal revenue phases:\n```solidity\nstruct REVStageConfig {\n    uint40 startsAtOrAfter;      \u002F\u002F Stage start time\n    uint16 splitPercent;          \u002F\u002F Operator split %\n    uint16 initialIssuance;       \u002F\u002F Starting weight\n    uint40 issuanceCutFrequency; \u002F\u002F How often weight cuts\n    uint16 issuanceCutPercent;    \u002F\u002F Weight cut amount\n    uint16 cashOutTaxRate;        \u002F\u002F Cash out penalty\n}\n```\n\n**Validation Rules**:\n- Stages must have increasing start times\n- Split percent requires non-empty splits\n- Cash out tax rate must allow some cash outs (\u003C 100%)\n\n#### Auto-Issuance\n\nPre-mint tokens to beneficiaries after stage starts:\n```solidity\nfunction autoIssueFor(uint256 revnetId, uint256 stageId, address beneficiary) external {\n    uint256 amount = amountToAutoIssue[revnetId][stageId][beneficiary];\n    amountToAutoIssue[revnetId][stageId][beneficiary] = 0;\n\n    CONTROLLER.mintTokensOf(revnetId, amount, beneficiary, \"\", true);\n}\n```\n\n---\n\n### REVLoans: Token-Backed Lending\n\n#### Collateralization Model\n\nBorrowers provide revnet tokens as collateral:\n```\nborrowable = cashOutValue(collateral) - existingDebt\n```\n\nWhere `cashOutValue` considers surplus, tax rate, and total supply.\n\n#### Three-Tier Fee Structure\n\n```solidity\nuint256 constant MIN_PREPAID_FEE_PERCENT = 25;   \u002F\u002F 2.5% minimum\nuint256 constant MAX_PREPAID_FEE_PERCENT = 500;  \u002F\u002F 50% maximum\nuint256 constant REV_PREPAID_FEE_PERCENT = 10;   \u002F\u002F 1% to REV\n```\n\nHigher prepaid fees = longer interest-free periods:\n```solidity\nprepaidDuration = (prepaidFeePercent \u002F MAX_PREPAID_FEE_PERCENT) × LOAN_LIQUIDATION_DURATION\n```\n\n#### Interest After Prepaid Period\n\nLinear interpolation from 0% to 100% over remaining loan duration:\n```solidity\ntimeSincePrepaid = block.timestamp - (loan.createdAt + prepaidDuration);\nremainingDuration = LOAN_LIQUIDATION_DURATION - prepaidDuration;\n\nfeePercent = (timeSincePrepaid × 100%) \u002F remainingDuration;\n```\n\n#### Liquidation\n\nAfter 10 years (3,650 days):\n- Anyone can call `liquidateExpiredLoansFrom()`\n- ERC-721 loan token burned\n- Collateral remains burned (not returned)\n- Borrowed amount tracking decremented\n\n---\n\n## JBOwnable: Flexible Ownership\n\nShared utility for Juicebox-aware ownership. Used by JB721TiersHook, custom hooks, and any contract needing project-based access control.\n\n#### Dual Ownership Modes\n\n**Project-Based**:\n```solidity\nfunction owner() public view returns (address) {\n    if (jbOwner.projectId != 0) {\n        return PROJECTS.ownerOf(jbOwner.projectId);\n    }\n    return jbOwner.owner;\n}\n```\n\n**EOA-Based**: Direct address stored in `jbOwner.owner`.\n\n#### Permission Integration\n\nProject-based ownership enables delegation:\n```solidity\n\u002F\u002F Owner can grant permissions via JBPermissions\nPERMISSIONS.setPermissionsFor(\n    operator,\n    jbOwner.projectId,\n    [jbOwner.permissionId]\n);\n\n\u002F\u002F Operator can now call onlyOwner functions\n```\n\n---\n\n## Croptop Publisher\n\n### CTPublisher: Permissioned NFT Publishing\n\n#### Posting Flow\n\n```\nmintFrom(posts[])\n  → For each post:\n    → Validate against allowance (min\u002Fmax price, supply)\n    → Check caller in allowlist\n    → Create or reuse tier via hook.adjustTiers()\n    → Accumulate total price\n  → Pay primary project (value - fee)\n  → Pay fee project (5% via FEE_DIVISOR = 20)\n```\n\n#### Tier Deduplication\n\nIPFS URIs map to tier IDs:\n```solidity\nif (tierIdOfEncodedIPFSUri[encodedUri] != 0) {\n    \u002F\u002F Mint from existing tier\n} else {\n    \u002F\u002F Create new tier, store mapping\n    tierIdOfEncodedIPFSUri[encodedUri] = newTierId;\n}\n```\n\n#### Allowance Configuration\n\nPacked configuration per project:\n```\nBits 0-103:   minPrice\nBits 104-135: minTotalSupply\nBits 136-167: maxTotalSupply\n+ Address[] allowlist\n```\n\n---\n\n## Cross-Chain: Suckers\n\n### JBSucker Architecture\n\nBidirectional cross-chain bridge using merkle trees.\n\n#### Dual Merkle Tree System\n\n**Outbox** (local → remote):\n- Stores claims from `prepare()` calls\n- Root sent via `toRemote()`\n- Cleared after successful bridge\n\n**Inbox** (remote → local):\n- Receives roots via `fromRemote()`\n- Claims validated against stored root\n- BitMap prevents replay\n\n#### Prepare & Bridge Flow\n\n**Phase 1: prepare()**\n```solidity\nfunction prepare(uint256 projectTokenCount, address beneficiary, ...) external {\n    \u002F\u002F 1. Transfer tokens to sucker\n    TOKENS.transferCreditsFrom(msg.sender, address(this), projectId, projectTokenCount);\n\n    \u002F\u002F 2. Cash out for backing assets\n    terminal.cashOutTokensOf(projectId, projectTokenCount, ...);\n\n    \u002F\u002F 3. Insert into outbox tree\n    _insertIntoTree(\n        keccak256(abi.encode(projectTokenCount, terminalTokenAmount, beneficiary)),\n        OUTBOX\n    );\n}\n```\n\n**Phase 2: toRemote()**\n```solidity\nfunction toRemote(JBRemoteToken[] remoteTokens) external {\n    for each token:\n        \u002F\u002F Validate mapping and minimum amount\n        \u002F\u002F Send root + balance to remote peer\n        _sendRootOverAMB(peer, root, balance, minGas);\n\n        \u002F\u002F Clear outbox\n        outboxOf[token].balance = 0;\n        outboxOf[token].nonce++;\n}\n```\n\n#### Claiming on Destination\n\n```solidity\nfunction claim(JBClaim[] claims) external {\n    for each claim:\n        \u002F\u002F 1. Validate merkle proof against inbox root\n        _validate(leaf, proof, INBOX);\n\n        \u002F\u002F 2. Mark leaf as executed (prevent replay)\n        _executedOf[token][leafIndex] = true;\n\n        \u002F\u002F 3. Mint project tokens\n        CONTROLLER.mintTokensOf(projectId, tokenCount, beneficiary, ...);\n\n        \u002F\u002F 4. Optionally add backing to balance\n        if (addToBalanceMode == ON_CLAIM) {\n            terminal.addToBalanceOf(projectId, terminalAmount, ...);\n        }\n}\n```\n\n#### Emergency Exit\n\nWhen bridge fails, users can reclaim from outbox:\n```solidity\nfunction emergencyExit(JBClaim[] claims, address token) external {\n    \u002F\u002F Must be enabled for this token\n    require(emergencyHatchEnabledFor[token]);\n\n    \u002F\u002F Validate against OUTBOX tree (not inbox)\n    _validateForEmergencyExit(leaf, proof);\n\n    \u002F\u002F Return terminal tokens directly\n    terminal.pay(projectId, terminalAmount, beneficiary, ...);\n}\n```\n\n**Critical**: Once emergency hatch is enabled, it cannot be disabled.\n\n#### Deprecation Flow\n\nThree-stage sunset:\n1. **DEPRECATION_PENDING**: Warning state\n2. **SENDING_DISABLED**: Accept incoming, prevent new sends\n3. **DEPRECATED**: Reject all operations\n\nMinimum delay enforced between stages for message propagation.\n\n#### Token Mapping Constraints\n\n```solidity\nfunction mapToken(JBTokenMapping mapping) external {\n    \u002F\u002F Cannot remap if outbox has entries\n    if (outboxOf[token].tree.count > 0) revert;\n\n    \u002F\u002F Native tokens can only map to native or zero\n    if (token == JBConstants.NATIVE_TOKEN) {\n        require(remoteToken == NATIVE_TOKEN || remoteToken == address(0));\n    }\n}\n```\n\n---\n\n## Extended Integration Patterns\n\n### When to Use Contract-as-Owner\n\n**Use REVDeployer\u002FContract Owner When**:\n- Project should operate autonomously without EOA control\n- Structured access needed (split operators, loan contracts)\n- Cross-chain deployment requires coordinated setup\n- Token economics should be immutable after launch\n\n**Use EOA Owner When**:\n- Rapid iteration expected\n- Full control required for governance\n- Simple single-owner model sufficient\n\n### Cross-Chain Deployment Checklist\n\n1. Deploy suckers on each chain via REVDeployer\n2. Map tokens with appropriate minimums and gas limits\n3. Set up terminals on each chain\n4. Verify peer addresses are correct\n5. Test with small amounts before production\n\n### Loan Integration Considerations\n\n1. Collateral value depends on current surplus and supply\n2. Higher prepaid fees reduce ongoing interest burden\n3. Liquidation deadline is absolute (10 years)\n4. Collateral is burned, not locked (can't be recovered)\n\n### Croptop Integration\n\n1. Configure allowances before enabling publishing\n2. Set appropriate min\u002Fmax prices for tier creation\n3. Monitor tier creation for duplicate IPFS URIs\n4. Fee extraction is automatic (5% to fee project)\n","# Juicebox V5 Implementation Deep Dive\n\nThis skill provides deep implementation knowledge for integrators, covering internal mechanics, edge cases, tradeoffs, and critical considerations.\n\n---\n\n## Payment Flow Internals\n\n### JBMultiTerminal.pay() Execution\n\n```\nUser calls pay()\n  → _pay() internal\n    → STORE.recordPaymentFrom()\n      → Validates ruleset exists and !pausePay\n      → Invokes data hook if useDataHookForPay\n      → Calculates tokens: mulDiv(amount, weight, weightRatio)\n      → Returns hook specifications\n    → TOKENS.mintFor() via controller\n    → Executes pay hook specifications\n    → Emits Pay event\n  → Validates minReturnedTokens\n```\n\n### Critical Implementation Details\n\n**Weight Calculation**: Token minting uses fixed-point math with 18 decimals:\n```solidity\ntokenCount = mulDiv(amount.value, weight, 10**18)\n```\nThe `weight` is the ruleset's configured weight (tokens per unit of currency).\n\n**Data Hook Override**: When `useDataHookForPay` is true, the data hook can:\n- Return a modified weight (affecting token count)\n- Specify pay hooks to receive forwarded funds\n- Pass custom metadata to pay hooks\n\n**Hook Execution Order**: Pay hooks execute sequentially after token minting. Each hook receives its specified amount from the payment.\n\n### Edge Cases\n\n1. **Zero weight**: No tokens minted, but payment still accepted\n2. **Data hook reverts**: Entire payment reverts\n3. **Pay hook reverts**: Entire payment reverts (no partial execution)\n4. **Insufficient minReturnedTokens**: Reverts after all execution (gas wasted)\n\n### Gas Considerations\n\n- Single payment without hooks: ~150k gas\n- With data hook: +50-100k gas depending on hook complexity\n- Each pay hook: +variable gas based on hook logic\n- Metadata decoding adds ~5k gas per 32 bytes\n\n---\n\n## Cash Out (Redemption) Mechanics\n\n### Bonding Curve Formula\n\nThe cash out calculation implements a modified bonding curve:\n\n```\nBase = (surplus × cashOutCount) \u002F totalSupply\n\ntaxFactor = (MAX_RATE - taxRate) + (taxRate × cashOutCount \u002F totalSupply)\n\nreclaimAmount = base × taxFactor \u002F MAX_RATE\n```\n\nWhere `MAX_RATE = 10000` (representing 100%).\n\n### Tax Rate Implications\n\n| Tax Rate | Effect |\n|----------|--------|\n| 0% | Linear redemption: get proportional share of surplus |\n| 50% | Partial penalty: larger redemptions penalized more |\n| 100% | No redemption possible: returns 0 |\n\n**Critical Insight**: The tax rate creates a curve where:\n- Small redemptions get closer to proportional value\n- Large redemptions (relative to supply) are penalized exponentially\n- At 100% tax rate, surplus is locked forever\n\n### Surplus Calculation\n\nSurplus is calculated as:\n```\nsurplus = balance - remainingPayoutLimit\n```\n\nThe `useTotalSurplusForCashOuts` metadata flag determines whether to use:\n- **Terminal surplus**: Only this terminal's balance minus its payout limit\n- **Total surplus**: Aggregated across all project terminals\n\n**Tradeoff**: Total surplus provides more accurate valuation but costs more gas due to multi-terminal queries.\n\n### Edge Cases\n\n1. **Zero surplus**: No reclaim possible regardless of token count\n2. **100% tax rate**: Returns 0, tokens still burned\n3. **Cash out count > total supply**: Reverts\n4. **Paused cash outs**: Reverts via ruleset check\n\n---\n\n## Ruleset Transition Mechanics\n\n### Approval Hook Flow\n\n```\nQueue ruleset\n  → approvalHook.approvalStatusOf() called\n  → If ApprovalExpected: queued but not yet active\n  → If Approved: becomes current when start time reached\n  → If Failed\u002FEmpty: reverts to base ruleset\n```\n\n**JBDeadline Implementation**: Requires `DURATION()` seconds between queue time and ruleset start. If queued too late, approval fails.\n\n### Weight Cut Algorithm\n\nWeight is cut exponentially each cycle:\n```solidity\nnewWeight = weight × (MAX_CUT - weightCutPercent) \u002F MAX_CUT\n```\n\nApplied iteratively for each cycle that has passed.\n\n**Optimization**: For large cycle counts (>1000), the contract uses cached intermediate values to avoid O(n) computation:\n```solidity\nif (multiple > 1000) {\n    \u002F\u002F Use cached values at 50,000 interval checkpoints\n}\n```\n\n### Ruleset Inheritance\n\nWhen a ruleset is queued:\n1. It inherits from the current (or latest approved) ruleset\n2. Only changed parameters override inherited values\n3. The `basedOnId` field tracks inheritance chain\n\n**Critical Consideration**: If an approval hook rejects a ruleset, the system walks backward through the inheritance chain to find the latest approved ancestor.\n\n### Cycle Number Derivation\n\n```solidity\ncycleNumber = 1 + (currentTime - start) \u002F duration\n```\n\nFor rulesets with `duration = 0` (infinite), cycle number stays at 1.\n\n---\n\n## Reserved Token Distribution\n\n### Accumulation Model\n\nReserved tokens don't mint immediately. Instead:\n```solidity\npendingReservedTokenBalanceOf[projectId] += (tokenCount × reservedRate) \u002F MAX_RATE\n```\n\nTokens accumulate until `sendReservedTokensToSplitsOf()` is called.\n\n### Distribution Flow\n\n```\nsendReservedTokensToSplitsOf()\n  → Reads pendingReservedTokenBalanceOf\n  → Resets pending balance to 0\n  → Mints all reserved tokens to controller\n  → Distributes via _sendReservedTokensToSplitGroupOf()\n    → For each split:\n      → If projectId set: pay that project\n      → If hook set: call hook\n      → Else: transfer to beneficiary\n  → Leftover sent to project owner\n```\n\n### Tradeoffs\n\n**Batched Distribution**:\n- Pro: Gas efficient (one mint operation)\n- Con: Recipients wait for manual trigger\n- Con: Large accumulations can hit gas limits\n\n**Per-Payment Distribution** (alternative pattern):\n- Pro: Immediate distribution\n- Con: Higher per-payment gas cost\n\n---\n\n## Splits System Details\n\n### Storage Packing\n\nSplits use packed storage for gas efficiency:\n```\nSlot 1: percent (32) | projectId (64) | beneficiary (160)\nSlot 2: preferAddToBalance (1) | lockedUntil (48) | hook (160)\n```\n\n### Locked Splits Behavior\n\nWhen updating splits:\n1. All currently locked splits must be included\n2. Lock period can only be extended, never shortened\n3. Other properties of locked splits cannot change\n\n**Edge Case**: If a locked split's beneficiary becomes a contract that can't receive funds, those funds are stuck until lock expires.\n\n### Split Execution\n\n```solidity\nfor each split:\n    amount = totalAmount × split.percent \u002F SPLITS_TOTAL_PERCENT\n\n    if (split.hook != address(0)):\n        \u002F\u002F Optimistically transfer to hook\n        hook.processSplitWith(context)\n    else if (split.projectId != 0):\n        if (split.preferAddToBalance):\n            terminal.addToBalanceOf(projectId, ...)\n        else:\n            terminal.pay(projectId, ...)\n    else:\n        \u002F\u002F Direct transfer to beneficiary\n```\n\n**Critical**: Split hooks receive funds optimistically before `processSplitWith()` is called. Malicious hooks could steal funds.\n\n---\n\n## Buyback Hook Decision Logic\n\n### Mint vs Swap Comparison\n\n```\nmintTokens = amount × weight \u002F 10^18\nswapTokens = TWAP_quote - slippageTolerance\n\nif (swapTokens > mintTokens):\n    route through swap\nelse:\n    standard mint\n```\n\n### TWAP Calculation\n\n1. Fetch oldest observation from Uniswap pool\n2. If observation window \u003C configured TWAP window, use available window\n3. Calculate arithmetic mean tick over window\n4. Convert tick to price quote\n5. Apply slippage tolerance based on swap size vs. pool liquidity\n\n### Slippage Tolerance Tiers\n\n```\nbase = (amountIn × 10 × DENOMINATOR) \u002F poolLiquidity\n\nif base > 150 bps: tolerance = 12%\nif base > 100 bps: tolerance = 33%\nif base > 75 bps: tolerance = 5%\n... (progressive reduction)\n```\n\n### Failure Handling\n\n```solidity\ntry uniswapPool.swap(...) returns (int256 amount0, int256 amount1) {\n    \u002F\u002F Process successful swap\n} catch {\n    \u002F\u002F Return 0, triggering fallback to standard mint\n}\n```\n\n**Critical**: Failed swaps don't revert the entire payment. The hook gracefully falls back to standard minting.\n\n### Leftover Handling\n\nAfter swap execution:\n```solidity\nif (leftoverAmount > 0) {\n    controller.mintTokensOf(projectId, leftoverAmount, beneficiary, ...)\n}\n```\n\nEnsures no user funds are lost to rounding or partial swaps.\n\n---\n\n## 721 Hook Tier Mechanics\n\n### Payment Processing Flow\n\n```\nafterPayRecordedWith()\n  → Decode metadata for tier IDs\n  → If no tiers specified: auto-select based on price\n  → STORE.recordMint() validates:\n    → Total tier prices ≤ payment amount\n    → Tiers have remaining supply\n    → Tiers are active\n  → Mint NFTs to beneficiary\n  → Handle leftover as credits or revert\n```\n\n### Credit System\n\n```solidity\npayCreditsOf[payer] += leftover\n\n\u002F\u002F On next payment:\neffectiveAmount = payment + payCreditsOf[payer]\npayCreditsOf[payer] = 0\n```\n\n**Tradeoff**: Credits provide flexibility but:\n- Accumulate dust from rounding\n- Can't be withdrawn, only used for NFTs\n- Lost if hook is changed\n\n### Cash Out Weight\n\nEach NFT's cash out value equals its tier price:\n```solidity\nweight = tier.price × redemptionRate\n```\n\nThe `totalCashOutWeight()` aggregates all outstanding NFT values for proportion calculations.\n\n**Critical**: If NFT prices vary significantly, small-price NFT holders receive proportionally less than their initial payment.\n\n---\n\n## Fee Mechanics\n\n### Fee Calculation\n\n```solidity\nFEE = 25  \u002F\u002F 2.5% (out of 1000)\nfeeAmount = amount × FEE \u002F (1000 + FEE)  \u002F\u002F ~2.44% of gross\n```\n\nNote: Fee is calculated as a portion of the gross amount, not added on top.\n\n### Fee Applicability\n\nFees apply to:\n- Payouts to non-project addresses\n- Surplus allowance usage\n- Cash outs with tax rate \u003C 100%\n\nFees exempt:\n- Project-to-project payments\n- Feeless addresses (registered in JBFeelessAddresses)\n- Internal transfers\n\n### Held Fees\n\nWhen `holdFees` is true in ruleset metadata:\n```solidity\nheldFeesOf[projectId][token].push(fee)\n\u002F\u002F Fees held for 28 days\n\u002F\u002F Can be refunded by adding equivalent to balance\n```\n\nAfter 28 days, held fees can be processed to the fee beneficiary (Project #1).\n\n---\n\n## Integration Recommendations\n\n### For Payment Integrators\n\n1. **Always set reasonable minReturnedTokens** to protect against frontrunning\n2. **Consider data hook gas costs** when estimating transaction costs\n3. **Handle pay hook reverts** gracefully in UI\n4. **Validate metadata encoding** matches hook expectations\n\n### For Hook Developers\n\n1. **Keep beforePayRecordedWith() view-only and light** - it runs on every payment\n2. **Handle failures gracefully** in afterPayRecordedWith() - don't lock user funds\n3. **Validate msg.sender** is an authorized terminal\n4. **Consider reentrancy** - hooks receive funds before execution\n\n### For Project Operators\n\n1. **Lock critical splits** to prevent rug pulls\n2. **Use approval hooks** for governance-controlled projects\n3. **Monitor pending reserved tokens** and distribute regularly\n4. **Set appropriate payout limits** to constrain risk\n\n### Gas Optimization Tips\n\n1. **Batch operations** when possible (queue multiple rulesets)\n2. **Use credits** for 721 hook instead of exact payments\n3. **Distribute reserved tokens** during low-gas periods\n4. **Consider total vs terminal surplus** tradeoff for cash outs\n\n---\n\n## Common Pitfalls\n\n1. **Setting minReturnedTokens = 0**: Vulnerable to sandwich attacks\n2. **Forgetting to include locked splits**: Update transaction reverts\n3. **Assuming immediate reserved distribution**: Tokens accumulate\n4. **Not handling hook metadata correctly**: Silent failures or reverts\n5. **Ignoring approval hook delays**: Rulesets rejected if queued late\n6. **Underestimating gas for multi-hook payments**: Transaction fails\n7. **Not validating surplus exists before cash out**: Wasted gas on revert\n\n---\n\n## Core Infrastructure Contracts\n\n### JBDirectory\n\nThe directory manages terminal and controller assignments for projects.\n\n#### Storage Architecture\n\n```solidity\nmapping(uint256 projectId => IJBController) public controllerOf;\nmapping(uint256 projectId => IJBTerminal[]) internal _terminalsOf;\nmapping(uint256 projectId => mapping(address token => IJBTerminal)) internal _primaryTerminalOf;\nmapping(address => bool) public isAllowedToSetFirstController;\n```\n\n#### Terminal Management\n\n**setTerminalsOf()** replaces the entire terminal array:\n- Validates no duplicates via nested loop\n- Requires `SET_TERMINALS` permission OR caller is project controller\n- Checks ruleset's `setTerminalsAllowed` flag (bypassed if controller is caller)\n\n**primaryTerminalOf()** resolution:\n1. Return explicitly-set primary terminal if still valid\n2. Otherwise, return first terminal accepting that token\n3. Return zero address if none found\n\n#### Controller Migration\n\n**setControllerOf()** handles migration:\n1. Validates `SET_CONTROLLER` permission OR first-time setup via allowlist\n2. Checks ruleset's `setControllerAllowed` flag\n3. Calls `IJBMigratable.migrate()` on old controller if interface supported\n\n**Critical**: First controller can only be set by addresses on the `isAllowedToSetFirstController` allowlist.\n\n---\n\n### JBProjects\n\nERC-721 contract where each token represents a Juicebox project.\n\n#### Project Creation\n\n```solidity\nfunction createFor(address owner) external returns (uint256 projectId) {\n    projectId = ++count;\n    _mint(owner, projectId);\n}\n```\n\nThe `count` variable acts as both total project counter and next project ID.\n\n#### Metadata Resolution\n\n```solidity\nfunction tokenURI(uint256 projectId) public view override returns (string memory) {\n    if (address(tokenUriResolver) == address(0)) return \"\";\n    return tokenUriResolver.tokenUriOf(address(this), projectId);\n}\n```\n\n**Graceful Degradation**: Returns empty string if no resolver set, preventing reverts.\n\n#### ERC-2771 Support\n\nOverrides `_msgSender()`, `_msgData()`, and `_contextSuffixLength()` for meta-transaction support via trusted forwarder.\n\n---\n\n### JBPermissions\n\nBitmap-based permission system enabling granular access control.\n\n#### Storage Architecture\n\n```solidity\nmapping(\n    address operator => mapping(\n        address account => mapping(\n            uint256 projectId => uint256 packedPermissions\n        )\n    )\n) public permissionsOf;\n```\n\nEach bit in the `uint256` represents one of 256 possible permissions. Project ID `0` is the wildcard, granting permissions across all projects.\n\n#### Permission Checking\n\n**hasPermission()** implements hierarchical evaluation:\n\n```\n1. If includeRoot && operator has ROOT permission:\n   → Check specific project OR wildcard project\n   → Return true if ROOT found\n\n2. Check specific permissionId bit on specific project\n\n3. If includeWildcardProjectId:\n   → Check specific permissionId bit on project 0\n```\n\n**hasPermissions()** (batch check):\n- Returns true immediately if ROOT permission exists\n- Iterates through all requested permissions\n- Returns false if ANY permission missing\n\n#### Security Constraints in setPermissionsFor()\n\n```solidity\n\u002F\u002F Only account holder or authorized operators can modify\nif (_msgSender() != account) {\n    \u002F\u002F Operators cannot grant ROOT permission\n    if (permissionsData.permissionIds contains ROOT) revert;\n\n    \u002F\u002F ROOT operators cannot modify wildcard project\n    if (projectId == 0) revert;\n\n    \u002F\u002F Must have ROOT on specific project to modify\n    if (!hasPermission(ROOT, projectId)) revert;\n}\n```\n\n**Edge Cases**:\n- Permission ID 0 is reserved, cannot be set\n- Permission IDs > 255 revert with `PermissionIdOutOfBounds`\n- Empty permission arrays are valid (clears all permissions)\n\n---\n\n### JBTokens\n\nDual-balance system supporting both unclaimed credits and ERC-20 tokens.\n\n#### Storage Design\n\n```solidity\nmapping(address holder => mapping(uint256 projectId => uint256)) public creditBalanceOf;\nmapping(uint256 projectId => uint256) public totalCreditSupplyOf;\nmapping(uint256 projectId => IJBToken) public tokenOf;\nmapping(IJBToken token => uint256) public projectIdOf;\n```\n\n#### Minting Logic\n\n**mintFor()** chooses based on ERC-20 existence:\n```solidity\nif (tokenOf[projectId] != address(0)) {\n    token.mint(holder, count);  \u002F\u002F Direct ERC-20 mint\n} else {\n    creditBalanceOf[holder][projectId] += count;  \u002F\u002F Store as credits\n    totalCreditSupplyOf[projectId] += count;\n}\n```\n\n#### Burning Priority\n\n**burnFrom()** burns credits first, then tokens:\n```solidity\nuint256 creditBalance = creditBalanceOf[holder][projectId];\nuint256 tokensToBurn = creditBalance \u003C count ? count - creditBalance : 0;\nuint256 creditsToBurn = count - tokensToBurn;\n\n\u002F\u002F Burn credits\ncreditBalanceOf[holder][projectId] -= creditsToBurn;\ntotalCreditSupplyOf[projectId] -= creditsToBurn;\n\n\u002F\u002F Burn tokens\nif (tokensToBurn > 0) token.burn(holder, tokensToBurn);\n```\n\n#### ERC-20 Deployment\n\n**deployERC20For()** uses minimal proxy clones:\n- Validates project doesn't already have token\n- Deploys via `Clones.clone()` or `Clones.cloneDeterministic()`\n- Links bidirectionally: `tokenOf[projectId]` and `projectIdOf[token]`\n\n**setTokenFor()** validates external tokens:\n- Token must use 18 decimals\n- Token must return true from `canBeAddedTo(projectId)`\n- Token must not be assigned to another project\n\n---\n\n### Custom ERC20 Token Integration\n\nThe JBTokens system supports custom ERC20 tokens, enabling advanced tokenomics while preserving Juicebox's payment and redemption mechanics.\n\n#### How Custom Tokens Work\n\nWhen `setTokenFor()` is called with a custom token:\n\n```solidity\nfunction setTokenFor(uint256 projectId, IJBToken token) external {\n    \u002F\u002F 1. Validate token is compatible\n    if (token.decimals() != 18) revert JBTokens_TokensMustHave18Decimals();\n    if (!token.canBeAddedTo(projectId)) revert JBTokens_TokenCannotBeAddedTo();\n    if (projectIdOf[token] != 0) revert JBTokens_TokenAlreadyAssigned();\n\n    \u002F\u002F 2. Store bidirectional mapping\n    tokenOf[projectId] = token;\n    projectIdOf[token] = projectId;\n\n    \u002F\u002F 3. Existing credits remain claimable\n    \u002F\u002F totalCreditSupplyOf[projectId] stays unchanged\n}\n```\n\n**Key Insight**: Setting a custom token doesn't migrate existing credits. Credit holders must call `claimTokensFor()` to convert credits to the ERC20.\n\n#### Mint\u002FBurn Flow with Custom Tokens\n\nWhen payments are received:\n```\npay() → mintTokensOf() → JBTokens.mintFor()\n                              ↓\n                         customToken.mint(holder, amount)\n```\n\nWhen cash outs occur:\n```\ncashOutTokensOf() → burnTokensOf() → JBTokens.burnFrom()\n                                          ↓\n                                     \u002F\u002F Burns credits first, then tokens\n                                     customToken.burn(holder, tokensToBurn)\n```\n\n**Critical**: The controller calls `mint()` and `burn()` directly on your token. Your token MUST grant these permissions to the controller address.\n\n#### Custom Token Requirements\n\n| Requirement | Reason |\n|-------------|--------|\n| **18 decimals** | All Juicebox math (weights, rates) assumes 18 decimals |\n| **canBeAddedTo()** | Validates token agrees to serve this project |\n| **mint(address, uint256)** | Controller must mint on payments |\n| **burn(address, uint256)** | Controller must burn on cash outs |\n| **Controller access** | Token must authorize JBController for mint\u002Fburn |\n\n#### Common Custom Token Patterns\n\n**1. Transfer Tax Token**\n```solidity\nfunction _update(address from, address to, uint256 amount) internal override {\n    \u002F\u002F Skip tax for controller operations (mints\u002Fburns)\n    if (from == address(0) || to == address(0) || msg.sender == controller) {\n        super._update(from, to, amount);\n        return;\n    }\n    \u002F\u002F Apply tax on transfers\n    uint256 tax = (amount * TAX_RATE) \u002F 10000;\n    super._update(from, taxRecipient, tax);\n    super._update(from, to, amount - tax);\n}\n```\n\n**Tradeoff**: Tax revenue goes to `taxRecipient`, not the Juicebox treasury. Consider routing tax to the project via `addToBalanceOf()`.\n\n**2. Rebasing Token**\n```solidity\n\u002F\u002F Track shares instead of balances\nmapping(address => uint256) private _shares;\nuint256 public totalShares;\nuint256 public rebaseIndex = 1e18; \u002F\u002F Starts at 1:1\n\nfunction balanceOf(address account) public view override returns (uint256) {\n    return (_shares[account] * rebaseIndex) \u002F 1e18;\n}\n\nfunction rebase(uint256 newIndex) external onlyOwner {\n    rebaseIndex = newIndex;\n    \u002F\u002F All balances scale proportionally\n}\n```\n\n**Tradeoff**: Cash out calculations use `totalSupply()`. Rebasing changes supply without minting, which affects redemption value.\n\n**3. Governance Token (ERC20Votes)**\n```solidity\nimport {ERC20Votes} from \"@openzeppelin\u002Fcontracts\u002Ftoken\u002FERC20\u002Fextensions\u002FERC20Votes.sol\";\n\ncontract GovernanceProjectToken is ERC20Votes {\n    address public controller;\n\n    function mint(address to, uint256 amount) external {\n        require(msg.sender == controller, \"Only controller\");\n        _mint(to, amount);\n    }\n\n    \u002F\u002F Inherits delegation, checkpointing, getPastVotes()\n}\n```\n\n**Benefit**: Token holders can vote on external governance proposals while maintaining Juicebox treasury mechanics.\n\n**4. Editable Name\u002FSymbol Token**\n```solidity\ncontract EditableProjectToken is ERC20 {\n    string private _tokenName;\n    string private _tokenSymbol;\n\n    function name() public view override returns (string memory) { return _tokenName; }\n    function symbol() public view override returns (string memory) { return _tokenSymbol; }\n\n    function setName(string calldata newName) external onlyProjectOwner {\n        _tokenName = newName;\n    }\n\n    function setSymbol(string calldata newSymbol) external onlyProjectOwner {\n        _tokenSymbol = newSymbol;\n    }\n}\n```\n\n**Benefit**: Rebrand without redeploying token or migrating liquidity. Project owner controls metadata.\n\n**5. Vesting Token with Per-Address Schedules**\n```solidity\ncontract VestingProjectToken is ERC20 {\n    struct VestingSchedule {\n        uint256 totalAmount;     \u002F\u002F Total tokens in this schedule\n        uint256 released;        \u002F\u002F Already released\u002Ftransferred\n        uint40 start;            \u002F\u002F Vesting start timestamp\n        uint40 cliff;            \u002F\u002F Cliff end timestamp\n        uint40 duration;         \u002F\u002F Total vesting duration\n    }\n\n    mapping(address => VestingSchedule) public vestingOf;\n\n    function vestedAmountOf(address account) public view returns (uint256) {\n        VestingSchedule memory schedule = vestingOf[account];\n        if (schedule.totalAmount == 0) return balanceOf(account);\n        if (block.timestamp \u003C schedule.cliff) return 0;\n        if (block.timestamp >= schedule.start + schedule.duration) {\n            return schedule.totalAmount;\n        }\n        uint256 elapsed = block.timestamp - schedule.start;\n        return (schedule.totalAmount * elapsed) \u002F schedule.duration;\n    }\n\n    function _update(address from, address to, uint256 amount) internal override {\n        \u002F\u002F Skip vesting checks for mints, burns, controller ops\n        if (from == address(0) || to == address(0) || msg.sender == controller) {\n            super._update(from, to, amount);\n            return;\n        }\n\n        VestingSchedule storage schedule = vestingOf[from];\n        if (schedule.totalAmount == 0) {\n            super._update(from, to, amount);\n            return;\n        }\n\n        require(block.timestamp >= schedule.cliff, \"Cliff not reached\");\n        uint256 transferable = vestedAmountOf(from) - schedule.released;\n        require(amount \u003C= transferable, \"Insufficient vested balance\");\n        schedule.released += amount;\n        super._update(from, to, amount);\n    }\n\n    function setVestingSchedule(\n        address beneficiary,\n        uint256 totalAmount,\n        uint40 start,\n        uint40 cliffDuration,\n        uint40 vestingDuration\n    ) external onlyProjectOwner {\n        vestingOf[beneficiary] = VestingSchedule({\n            totalAmount: totalAmount,\n            released: 0,\n            start: start,\n            cliff: start + cliffDuration,\n            duration: vestingDuration\n        });\n    }\n}\n```\n\n**Key Difference from Treasury Vesting**:\n- **Treasury vesting** (payout limits): Controls when funds leave the treasury\n- **Token vesting**: Controls when individual holders can transfer tokens\n\n**Use Case**: Team allocations, investor lock-ups, contributor rewards where tokens should vest per-person with individual cliffs and durations. Can be combined with treasury vesting for layered protection.\n\n**Tradeoff**: Does not prevent cash outs (controller operations bypass vesting). If you need to prevent recipients from cashing out, combine with a ruleset that has `pauseCashOut: true` during the vesting period, or set a high `cashOutTaxRate`.\n\n#### Edge Cases and Gotchas\n\n1. **Token assigned twice**: A token can only serve one project. `setTokenFor()` reverts if token already assigned.\n\n2. **Credit\u002Ftoken split during cash out**: Burns credits first, then tokens:\n   ```solidity\n   \u002F\u002F If user has 100 credits and 50 tokens, burning 120:\n   \u002F\u002F - Burns all 100 credits\n   \u002F\u002F - Burns 20 tokens\n   ```\n\n3. **Transfer restrictions**: If your token blocks certain transfers, ensure controller operations (mint\u002Fburn) are always allowed.\n\n4. **Pausable tokens**: Pausing transfers will break cash outs if tokens can't be burned.\n\n5. **Fee-on-transfer tokens**: Not directly supported. The minted amount must equal the amount the controller requested.\n\n6. **Approval requirements**: JBTokens calls `burn()` directly. If your token requires approval for burns, this will fail. Use `burnFrom` pattern that allows controller without approval.\n\n#### Integration Checklist\n\nBefore using a custom token:\n\n- [ ] Token uses exactly 18 decimals\n- [ ] `canBeAddedTo(projectId)` returns true for your project\n- [ ] Controller address has mint permission\n- [ ] Controller address has burn permission (without approval)\n- [ ] Token not assigned to another project\n- [ ] Mint\u002Fburn don't have unexpected side effects (fees, rebasing)\n- [ ] Transfer restrictions exempt controller operations\n- [ ] Considered interaction with cash out tax rate\n- [ ] Tested credit → token claiming works\n- [ ] Verified `totalSupply()` reflects actual redeemable tokens\n\n---\n\n### JBFundAccessLimits\n\nPacked storage for payout limits and surplus allowances.\n\n#### Bit Packing\n\n```solidity\n\u002F\u002F Bits 0-223: amount (up to ~2^224 wei)\n\u002F\u002F Bits 224-255: currency (32-bit identifier)\nuint256 packed = uint256(amount) | (uint256(currency) \u003C\u003C 224);\n```\n\n#### Currency Ordering Enforcement\n\n**setFundAccessLimitsFor()** requires strictly increasing currency order:\n```solidity\nfor (uint256 i = 1; i \u003C payoutLimits.length; i++) {\n    if (payoutLimits[i].currency \u003C= payoutLimits[i-1].currency) {\n        revert JBFundAccessLimits_InvalidPayoutLimitCurrencyOrdering();\n    }\n}\n```\n\nThis prevents duplicates and enables O(n) iteration during lookups.\n\n#### Zero Filtering\n\nZero-amount limits are filtered during storage, not stored:\n```solidity\nif (limit.amount > 0) {\n    _packedPayoutLimitsDataOf[projectId][rulesetId][terminal][token].push(packed);\n}\n```\n\n---\n\n## Fund Access Limit Lifecycle\n\nUnderstanding the lifecycle of payout limits vs surplus allowances is critical for project design. **This is a common source of confusion.**\n\n### Payout Limits: Reset Each Cycle\n\nPayout limits reset when a new cycle begins:\n\n```\nCycle 1: Payout limit = 10 ETH\n  → Team sends 10 ETH payouts\n  → Remaining limit = 0\n\nCycle 2: Payout limit = 10 ETH (RESET!)\n  → Team can send another 10 ETH\n  → This continues every cycle\n```\n\n**Key Insight**: The payout limit is defined per-ruleset, and when a cycling ruleset starts a new cycle, the limit refreshes. This enables **recurring distributions** without queuing multiple rulesets.\n\n### Surplus Allowance: One-Time Per Ruleset\n\nSurplus allowance does NOT reset each cycle:\n\n```\nCycle 1: Surplus allowance = 20 ETH\n  → Team uses 15 ETH from surplus\n  → Remaining allowance = 5 ETH\n\nCycle 2: Surplus allowance = 5 ETH (NOT reset!)\n  → Still only 5 ETH available\n  → Allowance only resets if NEW RULESET is queued\n```\n\n**Key Insight**: Surplus allowance is a one-time budget per ruleset configuration. It's designed for **discretionary treasury access**, not recurring distributions.\n\n### How Remaining Limits Are Tracked\n\nThe terminal store tracks used amounts separately from configured limits:\n\n```solidity\n\u002F\u002F JBTerminalStore\nmapping(address terminal =>\n    mapping(uint256 projectId =>\n        mapping(address token =>\n            mapping(uint256 rulesetCycleNumber => uint256)\n        )\n    )\n) public usedPayoutLimitOf;\n\nmapping(address terminal =>\n    mapping(uint256 projectId =>\n        mapping(address token =>\n            mapping(uint256 rulesetId => uint256)\n        )\n    )\n) public usedSurplusAllowanceOf;\n```\n\nNotice the key difference:\n- **Payout limit**: Keyed by `rulesetCycleNumber` → resets each cycle\n- **Surplus allowance**: Keyed by `rulesetId` → persists across cycles\n\n### Surplus Calculation\n\nSurplus (redeemable amount) is calculated as:\n```\nsurplus = balance - remainingPayoutLimit\n```\n\nWhere `remainingPayoutLimit` is the configured limit minus used amount for the current cycle.\n\n**Implication**: Payout limits **protect funds from cash outs**. If you set a 10 ETH payout limit, that 10 ETH cannot be cashed out by token holders until it's distributed or the cycle ends.\n\n### Design Pattern: Vesting via Native Mechanics\n\nCombine both limit types for sophisticated treasury management:\n\n| Mechanism | Behavior | Use Case |\n|-----------|----------|----------|\n| Payout Limit | Resets each cycle | Recurring vesting\u002Fsalaries |\n| Surplus Allowance | One-time per ruleset | Emergency fund \u002F discretionary |\n\n**Example: 12-Month Vesting with Treasury Reserve**\n\n```solidity\n\u002F\u002F Single cycling ruleset (NOT 12 separate rulesets!)\nJBRulesetConfig({\n    duration: 30 days,  \u002F\u002F Monthly cycles\n    \u002F\u002F ...\n    fundAccessLimitGroups: [\n        JBFundAccessLimitGroup({\n            terminal: address(TERMINAL),\n            token: JBConstants.NATIVE_TOKEN,\n            payoutLimits: [\n                JBCurrencyAmount({\n                    amount: 6.67 ether,   \u002F\u002F Monthly vesting (80 ETH \u002F 12)\n                    currency: nativeCurrency\n                })\n            ],\n            surplusAllowances: [\n                JBCurrencyAmount({\n                    amount: 20 ether,     \u002F\u002F One-time treasury access\n                    currency: nativeCurrency\n                })\n            ]\n        })\n    ]\n});\n```\n\n**Result over 12 months:**\n- Month 1: Team can withdraw 6.67 ETH (payout) + up to 20 ETH (surplus allowance)\n- Month 2-12: Team can withdraw 6.67 ETH\u002Fmonth (payout limit resets each cycle)\n- Surplus allowance does NOT reset - once used, it's gone until new ruleset\n\n### Common Mistakes\n\n1. **Queueing 12 rulesets for 12-month vesting**\n   - Wrong: Queue separate ruleset for each month\n   - Right: One cycling ruleset with monthly payout limit\n\n2. **Expecting surplus allowance to reset**\n   - Wrong: Plan to use 10 ETH each month from surplus allowance\n   - Right: Use payout limits for recurring distributions\n\n3. **Not understanding surplus protection**\n   - Payout limits protect funds from redemption\n   - Funds within the payout limit are NOT part of surplus\n\n---\n\n### JBPrices\n\nHierarchical price feed system with inverse calculation support.\n\n#### Price Resolution Order\n\n**pricePerUnitOf()** follows this order:\n```\n1. Same currency? Return 10^decimals (1:1 ratio)\n2. Direct feed exists? Use feed.currentUnitPrice()\n3. Inverse feed exists? Calculate reciprocal\n4. Project-specific not found? Try default (projectId=0)\n5. Nothing found? Revert with PriceFeedNotFound\n```\n\n#### Inverse Calculation\n\n```solidity\n\u002F\u002F If only priceFeedFor[project][B][A] exists, calculate A→B:\nprice = mulDiv(10**decimals, 10**decimals, inverseFeed.currentUnitPrice(decimals));\n```\n\n#### Feed Immutability\n\nPrice feeds are append-only:\n- Cannot modify existing feeds\n- Cannot remove feeds\n- Validates both direct and inverse don't already exist before adding\n\n**Access Control**:\n- Default feeds (projectId=0): Owner only\n- Project-specific feeds: Controller only\n\n---\n\n## Hook & Terminal Implementations\n\n### JBSwapTerminal Deep Dive\n\nWraps Uniswap V3 swaps into a Juicebox terminal interface.\n\n#### Payment Flow\n\n```\npay()\u002FaddToBalanceOf()\n  → _acceptFundsFor() \u002F\u002F Transfer or Permit2\n  → _handleTokenTransfersAndSwap()\n    → _beforeTransferFor() \u002F\u002F Wrap ETH if needed\n    → _swap() \u002F\u002F Execute Uniswap swap\n  → Forward to primary terminal for output token\n```\n\n#### TWAP Integration\n\n```solidity\nfunction _getQuote(IUniswapV3Pool pool, uint256 amountIn) internal view returns (uint256) {\n    uint32 secondsAgo = OracleLibrary.getOldestObservationSecondsAgo(address(pool));\n\n    \u002F\u002F Cap at configured window (2 min to 2 days)\n    if (secondsAgo > twapWindow) secondsAgo = twapWindow;\n\n    (int24 arithmeticMeanTick,) = OracleLibrary.consult(pool, secondsAgo);\n    return OracleLibrary.getQuoteAtTick(arithmeticMeanTick, amountIn, tokenIn, tokenOut);\n}\n```\n\n#### Pool Configuration\n\n**addDefaultPool()** setup:\n1. Validates pool deployed by stored factory\n2. Increases observation cardinality to minimum 10\n3. Stores accounting context (decimals, etc.)\n\n#### Hierarchical Defaults\n\nProject-specific configs override global defaults:\n```solidity\npool = poolOf[projectId][tokenIn][tokenOut];\nif (pool == address(0)) {\n    pool = poolOf[DEFAULT_PROJECT_ID][tokenIn][tokenOut];\n}\n```\n\n---\n\n### JB721TiersHook Deep Dive\n\nTiered NFT system with integrated payment processing and cash out support. Uses JBOwnable for flexible ownership.\n\n#### Architecture\n\nThe 721 hook implements three hook interfaces:\n- **IJBRulesetDataHook**: `beforePayRecordedWith()`, `beforeCashOutRecordedWith()`\n- **IJBPayHook**: `afterPayRecordedWith()`\n- **IJBCashOutHook**: `afterCashOutRecordedWith()`\n\nPlus extends **JBOwnable** for project-based or EOA ownership with permission delegation.\n\n#### Tier Storage (JB721TiersHookStore)\n\n```solidity\nstruct JB721Tier {\n    uint104 price;              \u002F\u002F Price in terminal token\n    uint32 initialSupply;       \u002F\u002F Starting supply\n    uint32 remainingSupply;     \u002F\u002F Current available\n    uint16 votingUnits;         \u002F\u002F Governance weight\n    uint16 reserveFrequency;    \u002F\u002F Reserved mint ratio (1 in N)\n    uint24 category;            \u002F\u002F Grouping identifier\n    bool transfersPausable;     \u002F\u002F Can transfers be paused\n    bool cannotBeRemoved;       \u002F\u002F Permanent tier flag\n}\n```\n\n#### Payment Processing (`_processPayment`)\n\n```\nafterPayRecordedWith()\n  → _processPayment()\n    → Normalize payment to tier pricing currency via JBPrices\n    → Add existing payCreditsOf[beneficiary] if payer == beneficiary\n    → Decode metadata for tier IDs to mint\n    → If no tiers specified: auto-select best fit tiers\n    → Call STORE.recordMint() for each tier\n      → Validates tier active and has supply\n      → Handles reserved mints (1 per reserveFrequency)\n    → Mint NFTs via _mint()\n    → Handle leftover:\n      → If allowOverspending: add to payCreditsOf\n      → Else: revert if leftover exists\n```\n\n#### Auto-Tier Selection\n\nWhen payer doesn't specify tiers, the hook selects automatically:\n```solidity\nfunction _selectAutoTiers(uint256 amount) internal view returns (uint256[] tiers) {\n    \u002F\u002F Iterate tiers by price descending\n    \u002F\u002F Select highest-priced tier that fits remaining amount\n    \u002F\u002F Repeat until amount exhausted or no tier fits\n}\n```\n\n**Tradeoff**: Auto-selection may not match user intent; explicit tier selection preferred for UX.\n\n#### Credit System Implementation\n\n```solidity\nmapping(address payer => uint256) public payCreditsOf;\n\n\u002F\u002F On payment:\nif (payer == beneficiary) {\n    effectiveAmount = payment.amount + payCreditsOf[payer];\n    payCreditsOf[payer] = 0;  \u002F\u002F Use all credits\n}\n\n\u002F\u002F After minting:\nif (leftover > 0 && allowOverspending) {\n    payCreditsOf[beneficiary] += leftover;\n    emit AddPayCredits(beneficiary, leftover, ...);\n}\n```\n\n**Edge Cases**:\n- Credits lost if hook is replaced\n- Credits cannot be withdrawn, only spent on NFTs\n- Accumulated dust from rounding\n\n#### Cash Out Weight Calculation\n\nEach NFT's value equals its tier price:\n```solidity\nfunction cashOutWeightOf(uint256[] tokenIds) public view returns (uint256 weight) {\n    for (uint256 i; i \u003C tokenIds.length; i++) {\n        JB721Tier tier = STORE.tierOfTokenId(tokenIds[i]);\n        weight += tier.price;\n    }\n}\n```\n\nTotal outstanding value:\n```solidity\nfunction totalCashOutWeight() public view returns (uint256) {\n    return STORE.totalCashOutWeight(address(this));\n}\n```\n\n**Cash Out Proportion**:\n```\nuserShare = (userNFTWeight \u002F totalCashOutWeight) × surplus × (1 - taxRate)\n```\n\n#### Reserved Minting\n\nTiers can reserve NFTs for the project:\n```solidity\n\u002F\u002F If reserveFrequency = 10, every 10th mint goes to reserved beneficiary\nif (mintCount % tier.reserveFrequency == 0) {\n    _mint(reservedBeneficiary, tokenId);\n} else {\n    _mint(payer, tokenId);\n}\n```\n\nReserved NFTs are minted inline during payment, not accumulated.\n\n#### Ownership via JBOwnable\n\nThe hook extends JBOwnable for flexible access control:\n```solidity\n\u002F\u002F Project-based ownership (common pattern):\nconstructor(..., uint256 projectId, ...) {\n    _transferOwnership(projectId);  \u002F\u002F Owner = project NFT holder\n}\n\n\u002F\u002F Permission delegation:\n\u002F\u002F Owner can grant ADJUST_721_TIERS to operators via JBPermissions\n\u002F\u002F Operators can then call adjustTiers() without being owner\n```\n\n**Permission IDs used**:\n- `ADJUST_721_TIERS` (17): Add\u002Fremove tiers\n- `SET_721_METADATA` (18): Update tier metadata\n- `MINT_721` (19): Manual minting\n- `SET_721_DISCOUNT_PERCENT` (27): Adjust pricing\n\n#### Tier Adjustment\n\n```solidity\nfunction adjustTiers(JB721TierConfig[] tiersToAdd, uint256[] tierIdsToRemove) external {\n    \u002F\u002F Requires ADJUST_721_TIERS permission or owner\n\n    \u002F\u002F Remove tiers (if not cannotBeRemoved)\n    for (uint256 id : tierIdsToRemove) {\n        require(!tier.cannotBeRemoved, \"TIER_LOCKED\");\n        STORE.recordRemoveTierOf(id);\n    }\n\n    \u002F\u002F Add new tiers\n    for (JB721TierConfig config : tiersToAdd) {\n        STORE.recordAddTier(config);\n    }\n}\n```\n\n#### Metadata Encoding\n\nPayer metadata structure for specifying tiers:\n```solidity\nbytes4 constant METADATA_ID = bytes4(keccak256(\"JB721TiersHook\"));\n\n\u002F\u002F Encoded as: [METADATA_ID][allowOverspending (bool)][tierIds (uint16[])]\nbytes memory metadata = abi.encode(\n    true,                    \u002F\u002F allowOverspending\n    [uint16(1), uint16(3)]   \u002F\u002F Mint tier 1 and tier 3\n);\n```\n\n---\n\n## Extending 721-Hook: Dynamic Cash Out Weights\n\nWhen building prediction games or outcome-based systems, you need to extend the 721-hook to change treasury mechanics. This section covers the key implementation patterns from Defifa.\n\n### Why Extend vs. Use Resolver Only\n\n| Need | Resolver | Extended Hook |\n|------|----------|---------------|\n| Custom artwork\u002Fmetadata | ✅ | ✅ |\n| Dynamic cash out weights | ❌ | ✅ |\n| First-owner tracking | ❌ | ✅ |\n| Phase-based restrictions | ❌ | ✅ |\n| Governor integration | ❌ | ✅ |\n\n**Rule of thumb**: If you need to change how money flows, extend the hook. If you only need to change how tokens look, use a resolver.\n\n### Dynamic Cash Out Weight Implementation\n\nStandard 721-hook uses fixed weights based on tier price:\n```solidity\n\u002F\u002F Standard: weight = tier.price\nfunction cashOutWeightOf(uint256[] tokenIds) returns (uint256 weight) {\n    for (uint256 i; i \u003C tokenIds.length; i++) {\n        weight += STORE.tierOfTokenId(tokenIds[i]).price;\n    }\n}\n```\n\nFor dynamic weights (e.g., prediction games), override with configurable weights:\n```solidity\n\u002F\u002F Extended: weight = configurable per tier\nuint256 constant TOTAL_CASH_OUT_WEIGHT = 1e18; \u002F\u002F 100% distributed among tiers\n\nmapping(uint256 tierId => uint256) public tierCashOutWeight;\n\nfunction cashOutWeightOf(uint256[] tokenIds) returns (uint256 weight) {\n    for (uint256 i; i \u003C tokenIds.length; i++) {\n        uint256 tierId = STORE.tierIdOfToken(tokenIds[i]);\n        weight += tierCashOutWeight[tierId];\n    }\n}\n\n\u002F\u002F Called by governor after outcome is known\nfunction setTierCashOutWeightsTo(DefifaTierCashOutWeight[] calldata weights) external {\n    \u002F\u002F Verify caller is authorized (governor)\n    \u002F\u002F Verify total weights sum to TOTAL_CASH_OUT_WEIGHT\n    for (uint256 i; i \u003C weights.length; i++) {\n        tierCashOutWeight[weights[i].id] = weights[i].cashOutWeight;\n    }\n}\n```\n\n### First-Owner Tracking\n\nFor games where rewards should go to original minters (not secondary buyers):\n\n```solidity\n\u002F\u002F Track original minter\nmapping(uint256 tokenId => address) public firstOwnerOf;\n\n\u002F\u002F In _processPayment() after minting:\nfunction _processPayment(JBAfterPayRecordedContext calldata context) internal override {\n    \u002F\u002F ... mint logic ...\n\n    \u002F\u002F Record first owner\n    for (uint256 i; i \u003C mintedTokenIds.length; i++) {\n        firstOwnerOf[mintedTokenIds[i]] = context.beneficiary;\n    }\n}\n\n\u002F\u002F In cash out, rewards go to first owner:\nfunction afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context) external {\n    \u002F\u002F Verify current owner initiated cash out\n    \u002F\u002F But send rewards to firstOwnerOf[tokenId]\n    address rewardRecipient = firstOwnerOf[tokenId];\n\n    \u002F\u002F Transfer rewards to original minter\n    _transferRewards(rewardRecipient, amount);\n}\n```\n\n**Trade-off**: First-owner tracking adds storage costs but ensures fair game mechanics where secondary market purchases don't steal rewards from original participants.\n\n### Phase-Based Cash Out Logic\n\nDifferent phases have different cash out rules:\n\n```solidity\nenum GamePhase { COUNTDOWN, MINT, REFUND, SCORING, COMPLETE, NO_CONTEST }\n\nfunction beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)\n    external view override\n    returns (uint256 cashOutTaxRate, uint256 cashOutCount, uint256 totalSupply, JBCashOutHookSpecification[] memory)\n{\n    GamePhase phase = currentPhase();\n\n    if (phase == GamePhase.REFUND) {\n        \u002F\u002F During refund: return mint cost (full refund)\n        return _refundCashOut(context);\n    }\n\n    if (phase == GamePhase.COMPLETE) {\n        \u002F\u002F After scoring: return weighted share of pot\n        return _scoredCashOut(context);\n    }\n\n    \u002F\u002F Other phases: no cash out allowed\n    revert CashOutNotAllowed();\n}\n```\n\n### Governor Integration Pattern\n\nThe governor contract calls into the delegate to set weights:\n\n```solidity\n\u002F\u002F In Governor contract:\nfunction ratifyScorecard(DefifaScorecard calldata scorecard) external {\n    \u002F\u002F Verify quorum reached\n    require(attestationCount[scorecard.id] >= quorum(), \"Quorum not reached\");\n\n    \u002F\u002F Set weights on delegate\n    IDefifaDelegate(delegate).setTierCashOutWeightsTo(scorecard.weights);\n\n    \u002F\u002F Emit event\n    emit ScorecardRatified(scorecard.id);\n}\n\n\u002F\u002F In Delegate contract:\nfunction setTierCashOutWeightsTo(DefifaTierCashOutWeight[] calldata weights) external {\n    \u002F\u002F Only governor can set weights\n    require(msg.sender == governor, \"Only governor\");\n\n    \u002F\u002F Only during SCORING phase\n    require(currentPhase() == GamePhase.SCORING, \"Wrong phase\");\n\n    \u002F\u002F Verify weights sum to 100%\n    uint256 total;\n    for (uint256 i; i \u003C weights.length; i++) {\n        tierCashOutWeight[weights[i].id] = weights[i].cashOutWeight;\n        total += weights[i].cashOutWeight;\n    }\n    require(total == TOTAL_CASH_OUT_WEIGHT, \"Invalid total\");\n\n    \u002F\u002F Transition to COMPLETE phase\n    _setPhase(GamePhase.COMPLETE);\n}\n```\n\n### Voting Power Calculation\n\nNFT holders vote with power proportional to their holdings:\n\n```solidity\n\u002F\u002F Voting power = (tokens owned in tier \u002F total minted in tier) * MAX_POWER_PER_TIER\nfunction getAttestationPowerOf(address account, uint256[] calldata tierIds)\n    public view returns (uint256 power)\n{\n    for (uint256 i; i \u003C tierIds.length; i++) {\n        uint256 tierId = tierIds[i];\n        uint256 owned = balanceOfTier(account, tierId);\n        uint256 totalMinted = STORE.tier(tierId).initialSupply - STORE.tier(tierId).remainingSupply;\n\n        if (totalMinted > 0) {\n            power += (owned * MAX_ATTESTATION_POWER_TIER) \u002F totalMinted;\n        }\n    }\n}\n```\n\n### Reference Implementation\n\nSee [defifa-collection-deployer-v5](https:\u002F\u002Fgithub.com\u002FBallKidz\u002Fdefifa-collection-deployer-v5) for complete implementation including:\n- `DefifaDelegate.sol` - Extended 721-hook with all patterns above\n- `DefifaGovernor.sol` - On-chain voting with tier-weighted power\n- `DefifaDeployer.sol` - Factory for deploying games\n\n---\n\n## Contract-as-Owner Pattern\n\n### REVDeployer Architecture\n\nREVDeployer owns Juicebox projects (revnets), enabling autonomous governance.\n\n#### Deployment Flow\n\n```\ndeployFor()\n  → Calculate next projectId via PROJECTS.count() + 1\n  → CONTROLLER.launchProjectFor()\n    → Creates project, transfers NFT to REVDeployer\n  → _deployRevnetFor()\n    → Deploy ERC-20 token\n    → Configure buyback hook with pools\n    → Set up split operator permissions\n    → Deploy suckers for cross-chain\n```\n\n#### Permission Delegation Model\n\nREVDeployer grants permissions to designated operators:\n```solidity\nfunction _setSplitOperatorOf(uint256 revnetId, address operator) internal {\n    uint256[] memory permissions = _splitOperatorPermissionIndexesOf(revnetId);\n\n    _setPermissionsFor(\n        operator,\n        revnetId,\n        permissions\n    );\n}\n```\n\n**Default Split Operator Permissions** (6 total):\n1. SET_SPLIT_GROUPS\n2. SET_BUYBACK_POOL\n3. SET_BUYBACK_TWAP\n4. SET_PROJECT_URI\n5. DEPLOY_SUCKERS\n6. SET_CONTROLLER (for suckers)\n\nPlus any custom permissions in `_extraOperatorPermissions[revnetId]`.\n\n#### Stage Mechanics\n\nStages define temporal revenue phases:\n```solidity\nstruct REVStageConfig {\n    uint40 startsAtOrAfter;      \u002F\u002F Stage start time\n    uint16 splitPercent;          \u002F\u002F Operator split %\n    uint16 initialIssuance;       \u002F\u002F Starting weight\n    uint40 issuanceCutFrequency; \u002F\u002F How often weight cuts\n    uint16 issuanceCutPercent;    \u002F\u002F Weight cut amount\n    uint16 cashOutTaxRate;        \u002F\u002F Cash out penalty\n}\n```\n\n**Validation Rules**:\n- Stages must have increasing start times\n- Split percent requires non-empty splits\n- Cash out tax rate must allow some cash outs (\u003C 100%)\n\n#### Auto-Issuance\n\nPre-mint tokens to beneficiaries after stage starts:\n```solidity\nfunction autoIssueFor(uint256 revnetId, uint256 stageId, address beneficiary) external {\n    uint256 amount = amountToAutoIssue[revnetId][stageId][beneficiary];\n    amountToAutoIssue[revnetId][stageId][beneficiary] = 0;\n\n    CONTROLLER.mintTokensOf(revnetId, amount, beneficiary, \"\", true);\n}\n```\n\n---\n\n### REVLoans: Token-Backed Lending\n\n#### Collateralization Model\n\nBorrowers provide revnet tokens as collateral:\n```\nborrowable = cashOutValue(collateral) - existingDebt\n```\n\nWhere `cashOutValue` considers surplus, tax rate, and total supply.\n\n#### Three-Tier Fee Structure\n\n```solidity\nuint256 constant MIN_PREPAID_FEE_PERCENT = 25;   \u002F\u002F 2.5% minimum\nuint256 constant MAX_PREPAID_FEE_PERCENT = 500;  \u002F\u002F 50% maximum\nuint256 constant REV_PREPAID_FEE_PERCENT = 10;   \u002F\u002F 1% to REV\n```\n\nHigher prepaid fees = longer interest-free periods:\n```solidity\nprepaidDuration = (prepaidFeePercent \u002F MAX_PREPAID_FEE_PERCENT) × LOAN_LIQUIDATION_DURATION\n```\n\n#### Interest After Prepaid Period\n\nLinear interpolation from 0% to 100% over remaining loan duration:\n```solidity\ntimeSincePrepaid = block.timestamp - (loan.createdAt + prepaidDuration);\nremainingDuration = LOAN_LIQUIDATION_DURATION - prepaidDuration;\n\nfeePercent = (timeSincePrepaid × 100%) \u002F remainingDuration;\n```\n\n#### Liquidation\n\nAfter 10 years (3,650 days):\n- Anyone can call `liquidateExpiredLoansFrom()`\n- ERC-721 loan token burned\n- Collateral remains burned (not returned)\n- Borrowed amount tracking decremented\n\n---\n\n## JBOwnable: Flexible Ownership\n\nShared utility for Juicebox-aware ownership. Used by JB721TiersHook, custom hooks, and any contract needing project-based access control.\n\n#### Dual Ownership Modes\n\n**Project-Based**:\n```solidity\nfunction owner() public view returns (address) {\n    if (jbOwner.projectId != 0) {\n        return PROJECTS.ownerOf(jbOwner.projectId);\n    }\n    return jbOwner.owner;\n}\n```\n\n**EOA-Based**: Direct address stored in `jbOwner.owner`.\n\n#### Permission Integration\n\nProject-based ownership enables delegation:\n```solidity\n\u002F\u002F Owner can grant permissions via JBPermissions\nPERMISSIONS.setPermissionsFor(\n    operator,\n    jbOwner.projectId,\n    [jbOwner.permissionId]\n);\n\n\u002F\u002F Operator can now call onlyOwner functions\n```\n\n---\n\n## Croptop Publisher\n\n### CTPublisher: Permissioned NFT Publishing\n\n#### Posting Flow\n\n```\nmintFrom(posts[])\n  → For each post:\n    → Validate against allowance (min\u002Fmax price, supply)\n    → Check caller in allowlist\n    → Create or reuse tier via hook.adjustTiers()\n    → Accumulate total price\n  → Pay primary project (value - fee)\n  → Pay fee project (5% via FEE_DIVISOR = 20)\n```\n\n#### Tier Deduplication\n\nIPFS URIs map to tier IDs:\n```solidity\nif (tierIdOfEncodedIPFSUri[encodedUri] != 0) {\n    \u002F\u002F Mint from existing tier\n} else {\n    \u002F\u002F Create new tier, store mapping\n    tierIdOfEncodedIPFSUri[encodedUri] = newTierId;\n}\n```\n\n#### Allowance Configuration\n\nPacked configuration per project:\n```\nBits 0-103:   minPrice\nBits 104-135: minTotalSupply\nBits 136-167: maxTotalSupply\n+ Address[] allowlist\n```\n\n---\n\n## Cross-Chain: Suckers\n\n### JBSucker Architecture\n\nBidirectional cross-chain bridge using merkle trees.\n\n#### Dual Merkle Tree System\n\n**Outbox** (local → remote):\n- Stores claims from `prepare()` calls\n- Root sent via `toRemote()`\n- Cleared after successful bridge\n\n**Inbox** (remote → local):\n- Receives roots via `fromRemote()`\n- Claims validated against stored root\n- BitMap prevents replay\n\n#### Prepare & Bridge Flow\n\n**Phase 1: prepare()**\n```solidity\nfunction prepare(uint256 projectTokenCount, address beneficiary, ...) external {\n    \u002F\u002F 1. Transfer tokens to sucker\n    TOKENS.transferCreditsFrom(msg.sender, address(this), projectId, projectTokenCount);\n\n    \u002F\u002F 2. Cash out for backing assets\n    terminal.cashOutTokensOf(projectId, projectTokenCount, ...);\n\n    \u002F\u002F 3. Insert into outbox tree\n    _insertIntoTree(\n        keccak256(abi.encode(projectTokenCount, terminalTokenAmount, beneficiary)),\n        OUTBOX\n    );\n}\n```\n\n**Phase 2: toRemote()**\n```solidity\nfunction toRemote(JBRemoteToken[] remoteTokens) external {\n    for each token:\n        \u002F\u002F Validate mapping and minimum amount\n        \u002F\u002F Send root + balance to remote peer\n        _sendRootOverAMB(peer, root, balance, minGas);\n\n        \u002F\u002F Clear outbox\n        outboxOf[token].balance = 0;\n        outboxOf[token].nonce++;\n}\n```\n\n#### Claiming on Destination\n\n```solidity\nfunction claim(JBClaim[] claims) external {\n    for each claim:\n        \u002F\u002F 1. Validate merkle proof against inbox root\n        _validate(leaf, proof, INBOX);\n\n        \u002F\u002F 2. Mark leaf as executed (prevent replay)\n        _executedOf[token][leafIndex] = true;\n\n        \u002F\u002F 3. Mint project tokens\n        CONTROLLER.mintTokensOf(projectId, tokenCount, beneficiary, ...);\n\n        \u002F\u002F 4. Optionally add backing to balance\n        if (addToBalanceMode == ON_CLAIM) {\n            terminal.addToBalanceOf(projectId, terminalAmount, ...);\n        }\n}\n```\n\n#### Emergency Exit\n\nWhen bridge fails, users can reclaim from outbox:\n```solidity\nfunction emergencyExit(JBClaim[] claims, address token) external {\n    \u002F\u002F Must be enabled for this token\n    require(emergencyHatchEnabledFor[token]);\n\n    \u002F\u002F Validate against OUTBOX tree (not inbox)\n    _validateForEmergencyExit(leaf, proof);\n\n    \u002F\u002F Return terminal tokens directly\n    terminal.pay(projectId, terminalAmount, beneficiary, ...);\n}\n```\n\n**Critical**: Once emergency hatch is enabled, it cannot be disabled.\n\n#### Deprecation Flow\n\nThree-stage sunset:\n1. **DEPRECATION_PENDING**: Warning state\n2. **SENDING_DISABLED**: Accept incoming, prevent new sends\n3. **DEPRECATED**: Reject all operations\n\nMinimum delay enforced between stages for message propagation.\n\n#### Token Mapping Constraints\n\n```solidity\nfunction mapToken(JBTokenMapping mapping) external {\n    \u002F\u002F Cannot remap if outbox has entries\n    if (outboxOf[token].tree.count > 0) revert;\n\n    \u002F\u002F Native tokens can only map to native or zero\n    if (token == JBConstants.NATIVE_TOKEN) {\n        require(remoteToken == NATIVE_TOKEN || remoteToken == address(0));\n    }\n}\n```\n\n---\n\n## Extended Integration Patterns\n\n### When to Use Contract-as-Owner\n\n**Use REVDeployer\u002FContract Owner When**:\n- Project should operate autonomously without EOA control\n- Structured access needed (split operators, loan contracts)\n- Cross-chain deployment requires coordinated setup\n- Token economics should be immutable after launch\n\n**Use EOA Owner When**:\n- Rapid iteration expected\n- Full control required for governance\n- Simple single-owner model sufficient\n\n### Cross-Chain Deployment Checklist\n\n1. Deploy suckers on each chain via REVDeployer\n2. Map tokens with appropriate minimums and gas limits\n3. Set up terminals on each chain\n4. Verify peer addresses are correct\n5. Test with small amounts before production\n\n### Loan Integration Considerations\n\n1. Collateral value depends on current surplus and supply\n2. Higher prepaid fees reduce ongoing interest burden\n3. Liquidation deadline is absolute (10 years)\n4. Collateral is burned, not locked (can't be recovered)\n\n### Croptop Integration\n\n1. Configure allowances before enabling publishing\n2. Set appropriate min\u002Fmax prices for tier creation\n3. Monitor tier creation for duplicate IPFS URIs\n4. Fee extraction is automatic (5% to fee project)\n",{"name":5,"description":7},"main","e7e336ac628c3e3bd771aff1aaa469c93778f94a","skills\u002Fmejango\u002Fjuicy\u002Fjb-v5-impl",0,"https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del","2026-08-23 22:39:37",{"source":8,"skillName":5,"commands":49,"prompt":59,"zipAvailable":60},[50,53,56],{"tool":51,"command":52},"npx","npx skills add kbarbel640-del\u002Fskills --skill jb-v5-impl",{"tool":54,"command":55},"bunx","bunx skills add kbarbel640-del\u002Fskills --skill jb-v5-impl",{"tool":57,"command":58},"pnpm","pnpm dlx skills add kbarbel640-del\u002Fskills --skill jb-v5-impl","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fmejango\u002Fjuicy\u002Fjb-v5-impl\u002FSKILL.md",true,[],[63,94,122,143,149,169],{"skillNo":64,"name":65,"nameZh":6,"description":66,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":67,"categories":68,"keywords":74,"publishedAt":36},"01M6G8J55967D6281ACMKAKTEQ","crowd-prompting","A marketplace where AI agents improve prompts, system instructions, tool descriptions, and other text-based content with domain expertise from real-world operations — and earn tokens for valuable contributions.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzzadrian\u002Fcrowd-prompting\u002FSKILL.md",[69],{"categoryNo":70,"categoryName":71,"taxonomyType":72,"taxonomyTypeDesc":73},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93],"crowd prompting","prompting","api","with","tokens","domain","world","prompts","operations","instructions","text","tool","system","improve","productivity","marketplace","https","content","ai agents",{"skillNo":95,"name":96,"nameZh":6,"description":97,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":98,"categories":99,"keywords":105,"publishedAt":36},"01M6G8J0PKPAYVET6BWE8W4C26","dilemm-ai","Compete in DilemmAI, the prisoner's dilemma AI arena at dilemm.ai. Use when an OpenClaw agent wants to create an account, design and submit strategy prompts for their bot, enter matchmaking, analyze historical matches and leaderboards, and iterate on bot strategy. Requires headless browser automation (no API available yet).","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzyori\u002Fdilemm-ai\u002FSKILL.md",[100],{"categoryNo":101,"categoryName":102,"taxonomyType":103,"taxonomyTypeDesc":104},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[106,77,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,82,28],"dilemm ai","arena","analyze","browser","openclaw agent","create","automation","browser automation","bot","openclaw","design","strategy","headless","compete","submit","matchmaking",{"skillNo":123,"name":124,"nameZh":6,"description":125,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":126,"categories":127,"keywords":131,"publishedAt":36},"01M6G8HWAF3K9SW7NP3Z83CPJ6","arxiv-paper-reviews","Interact with arXiv Crawler API to fetch papers, read reviews, and submit comments. Use when working with arXiv papers, fetching paper lists by date\u002Fcategory\u002Finterest, viewing paper details with comments, or submitting paper reviews via API at http:\u002F\u002F150.158.152.82:8000.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Fweak-accept\u002FSKILL.md",[128],{"categoryNo":129,"categoryName":130,"taxonomyType":103,"taxonomyTypeDesc":104},"01JBSEEDCAT000000000000021","资料检索与研究",[132,133,134,135,136,137,138,78,139,120,28,140,77,141,142],"arxiv paper reviews","reviews","arxiv","paper","crawler","papers","comments","fetch","read","http","via",{"skillNo":144,"name":124,"nameZh":6,"description":125,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":145,"categories":146,"keywords":148,"publishedAt":36},"01M6G8HQJ7N7WMY6YV7D7SSJ16","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzxrys\u002Farxiv-paper-reviews\u002FSKILL.md",[147],{"categoryNo":129,"categoryName":130,"taxonomyType":103,"taxonomyTypeDesc":104},[135,134,133,132,142,137,138,78,136,120,28,139,140,77,141],{"skillNo":150,"name":151,"nameZh":6,"description":152,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":153,"categories":154,"keywords":155,"publishedAt":36},"01M6G8HM95Y64JNVFC2HN4E2BG","git-summary","Get a quick summary of the current Git repository including status, recent commits, branches, and contributors.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzweack\u002Fgit-summary\u002FSKILL.md",[],[156,157,158,159,160,161,162,163,164,165,166,167,168],"git","summary","git summary","commits","repository","win32","status","linux","contributors","true","quick","get","branches",{"skillNo":170,"name":171,"nameZh":6,"description":172,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":173,"categories":174,"keywords":179,"publishedAt":36},"01M6G8HGEGF05CNEJCGVYFSC4R","agent-constitution","Interact with AgentConstitution governance contracts on Base Sepolia. Check compliance, read rules, log actions, query governance state.","https:\u002F\u002Fgithub.com\u002Fkbarbel640-del\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fztsalexey\u002Fagent-constitution\u002FSKILL.md",[175,176],{"categoryNo":101,"categoryName":102,"taxonomyType":103,"taxonomyTypeDesc":104},{"categoryNo":177,"categoryName":178,"taxonomyType":72,"taxonomyTypeDesc":73},"01JBSEEDCAT000000000000037","律师与法务",[180,181,182,183,78,184,185,41,186,140,187,188,189,190,191,91,192,193,194],"agent constitution","constitution","contracts","tree","query","base","log","check","Rules","projects","compliance","actions","state","governance","github"]