# Jb V5 Impl

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

- Skill: `modbender/jb-v5-impl` (Agent Skill)
- Install (CLI): `npx skillmds@latest add modbender/jb-v5-impl`
- Raw SKILL.md: https://api.skillmd.com/api/skills/modbender/jb-v5-impl/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: modbender (https://skillmd.com/u/modbender)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/modbender/jb-v5-impl

---


# Juicebox V5 Implementation Deep Dive

This skill provides deep implementation knowledge for integrators, covering internal mechanics, edge cases, tradeoffs, and critical considerations.

---

## Payment Flow Internals

### JBMultiTerminal.pay() Execution

```
User calls pay()
  → _pay() internal
    → STORE.recordPaymentFrom()
      → Validates ruleset exists and !pausePay
      → Invokes data hook if useDataHookForPay
      → Calculates tokens: mulDiv(amount, weight, weightRatio)
      → Returns hook specifications
    → TOKENS.mintFor() via controller
    → Executes pay hook specifications
    → Emits Pay event
  → Validates minReturnedTokens
```

### Critical Implementation Details

**Weight Calculation**: Token minting uses fixed-point math with 18 decimals:
```solidity
tokenCount = mulDiv(amount.value, weight, 10**18)
```
The `weight` is the ruleset's configured weight (tokens per unit of currency).

**Data Hook Override**: When `useDataHookForPay` is true, the data hook can:
- Return a modified weight (affecting token count)
- Specify pay hooks to receive forwarded funds
- Pass custom metadata to pay hooks

**Hook Execution Order**: Pay hooks execute sequentially after token minting. Each hook receives its specified amount from the payment.

### Edge Cases

1. **Zero weight**: No tokens minted, but payment still accepted
2. **Data hook reverts**: Entire payment reverts
3. **Pay hook reverts**: Entire payment reverts (no partial execution)
4. **Insufficient minReturnedTokens**: Reverts after all execution (gas wasted)

### Gas Considerations

- Single payment without hooks: ~150k gas
- With data hook: +50-100k gas depending on hook complexity
- Each pay hook: +variable gas based on hook logic
- Metadata decoding adds ~5k gas per 32 bytes

---

## Cash Out (Redemption) Mechanics

### Bonding Curve Formula

The cash out calculation implements a modified bonding curve:

```
Base = (surplus × cashOutCount) / totalSupply

taxFactor = (MAX_RATE - taxRate) + (taxRate × cashOutCount / totalSupply)

reclaimAmount = base × taxFactor / MAX_RATE
```

Where `MAX_RATE = 10000` (representing 100%).

### Tax Rate Implications

| Tax Rate | Effect |
|----------|--------|
| 0% | Linear redemption: get proportional share of surplus |
| 50% | Partial penalty: larger redemptions penalized more |
| 100% | No redemption possible: returns 0 |

**Critical Insight**: The tax rate creates a curve where:
- Small redemptions get closer to proportional value
- Large redemptions (relative to supply) are penalized exponentially
- At 100% tax rate, surplus is locked forever

### Surplus Calculation

Surplus is calculated as:
```
surplus = balance - remainingPayoutLimit
```

The `useTotalSurplusForCashOuts` metadata flag determines whether to use:
- **Terminal surplus**: Only this terminal's balance minus its payout limit
- **Total surplus**: Aggregated across all project terminals

**Tradeoff**: Total surplus provides more accurate valuation but costs more gas due to multi-terminal queries.

### Edge Cases

1. **Zero surplus**: No reclaim possible regardless of token count
2. **100% tax rate**: Returns 0, tokens still burned
3. **Cash out count > total supply**: Reverts
4. **Paused cash outs**: Reverts via ruleset check

---

## Ruleset Transition Mechanics

### Approval Hook Flow

```
Queue ruleset
  → approvalHook.approvalStatusOf() called
  → If ApprovalExpected: queued but not yet active
  → If Approved: becomes current when start time reached
  → If Failed/Empty: reverts to base ruleset
```

**JBDeadline Implementation**: Requires `DURATION()` seconds between queue time and ruleset start. If queued too late, approval fails.

### Weight Cut Algorithm

Weight is cut exponentially each cycle:
```solidity
newWeight = weight × (MAX_CUT - weightCutPercent) / MAX_CUT
```

Applied iteratively for each cycle that has passed.

**Optimization**: For large cycle counts (>1000), the contract uses cached intermediate values to avoid O(n) computation:
```solidity
if (multiple > 1000) {
    // Use cached values at 50,000 interval checkpoints
}
```

### Ruleset Inheritance

When a ruleset is queued:
1. It inherits from the current (or latest approved) ruleset
2. Only changed parameters override inherited values
3. The `basedOnId` field tracks inheritance chain

**Critical Consideration**: If an approval hook rejects a ruleset, the system walks backward through the inheritance chain to find the latest approved ancestor.

### Cycle Number Derivation

```solidity
cycleNumber = 1 + (currentTime - start) / duration
```

For rulesets with `duration = 0` (infinite), cycle number stays at 1.

---

## Reserved Token Distribution

### Accumulation Model

Reserved tokens don't mint immediately. Instead:
```solidity
pendingReservedTokenBalanceOf[projectId] += (tokenCount × reservedRate) / MAX_RATE
```

Tokens accumulate until `sendReservedTokensToSplitsOf()` is called.

### Distribution Flow

```
sendReservedTokensToSplitsOf()
  → Reads pendingReservedTokenBalanceOf
  → Resets pending balance to 0
  → Mints all reserved tokens to controller
  → Distributes via _sendReservedTokensToSplitGroupOf()
    → For each split:
      → If projectId set: pay that project
      → If hook set: call hook
      → Else: transfer to beneficiary
  → Leftover sent to project owner
```

### Tradeoffs

**Batched Distribution**:
- Pro: Gas efficient (one mint operation)
- Con: Recipients wait for manual trigger
- Con: Large accumulations can hit gas limits

**Per-Payment Distribution** (alternative pattern):
- Pro: Immediate distribution
- Con: Higher per-payment gas cost

---

## Splits System Details

### Storage Packing

Splits use packed storage for gas efficiency:
```
Slot 1: percent (32) | projectId (64) | beneficiary (160)
Slot 2: preferAddToBalance (1) | lockedUntil (48) | hook (160)
```

### Locked Splits Behavior

When updating splits:
1. All currently locked splits must be included
2. Lock period can only be extended, never shortened
3. Other properties of locked splits cannot change

**Edge Case**: If a locked split's beneficiary becomes a contract that can't receive funds, those funds are stuck until lock expires.

### Split Execution

```solidity
for each split:
    amount = totalAmount × split.percent / SPLITS_TOTAL_PERCENT

    if (split.hook != address(0)):
        // Optimistically transfer to hook
        hook.processSplitWith(context)
    else if (split.projectId != 0):
        if (split.preferAddToBalance):
            terminal.addToBalanceOf(projectId, ...)
        else:
            terminal.pay(projectId, ...)
    else:
        // Direct transfer to beneficiary
```

**Critical**: Split hooks receive funds optimistically before `processSplitWith()` is called. Malicious hooks could steal funds.

---

## Buyback Hook Decision Logic

### Mint vs Swap Comparison

```
mintTokens = amount × weight / 10^18
swapTokens = TWAP_quote - slippageTolerance

if (swapTokens > mintTokens):
    route through swap
else:
    standard mint
```

### TWAP Calculation

1. Fetch oldest observation from Uniswap pool
2. If observation window < configured TWAP window, use available window
3. Calculate arithmetic mean tick over window
4. Convert tick to price quote
5. Apply slippage tolerance based on swap size vs. pool liquidity

### Slippage Tolerance Tiers

```
base = (amountIn × 10 × DENOMINATOR) / poolLiquidity

if base > 150 bps: tolerance = 12%
if base > 100 bps: tolerance = 33%
if base > 75 bps: tolerance = 5%
... (progressive reduction)
```

### Failure Handling

```solidity
try uniswapPool.swap(...) returns (int256 amount0, int256 amount1) {
    // Process successful swap
} catch {
    // Return 0, triggering fallback to standard mint
}
```

**Critical**: Failed swaps don't revert the entire payment. The hook gracefully falls back to standard minting.

### Leftover Handling

After swap execution:
```solidity
if (leftoverAmount > 0) {
    controller.mintTokensOf(projectId, leftoverAmount, beneficiary, ...)
}
```

Ensures no user funds are lost to rounding or partial swaps.

---

## 721 Hook Tier Mechanics

### Payment Processing Flow

```
afterPayRecordedWith()
  → Decode metadata for tier IDs
  → If no tiers specified: auto-select based on price
  → STORE.recordMint() validates:
    → Total tier prices ≤ payment amount
    → Tiers have remaining supply
    → Tiers are active
  → Mint NFTs to beneficiary
  → Handle leftover as credits or revert
```

### Credit System

```solidity
payCreditsOf[payer] += leftover

// On next payment:
effectiveAmount = payment + payCreditsOf[payer]
payCreditsOf[payer] = 0
```

**Tradeoff**: Credits provide flexibility but:
- Accumulate dust from rounding
- Can't be withdrawn, only used for NFTs
- Lost if hook is changed

### Cash Out Weight

Each NFT's cash out value equals its tier price:
```solidity
weight = tier.price × redemptionRate
```

The `totalCashOutWeight()` aggregates all outstanding NFT values for proportion calculations.

**Critical**: If NFT prices vary significantly, small-price NFT holders receive proportionally less than their initial payment.

---

## Fee Mechanics

### Fee Calculation

```solidity
FEE = 25  // 2.5% (out of 1000)
feeAmount = amount × FEE / (1000 + FEE)  // ~2.44% of gross
```

Note: Fee is calculated as a portion of the gross amount, not added on top.

### Fee Applicability

Fees apply to:
- Payouts to non-project addresses
- Surplus allowance usage
- Cash outs with tax rate < 100%

Fees exempt:
- Project-to-project payments
- Feeless addresses (registered in JBFeelessAddresses)
- Internal transfers

### Held Fees

When `holdFees` is true in ruleset metadata:
```solidity
heldFeesOf[projectId][token].push(fee)
// Fees held for 28 days
// Can be refunded by adding equivalent to balance
```

After 28 days, held fees can be processed to the fee beneficiary (Project #1).

---

## Integration Recommendations

### For Payment Integrators

1. **Always set reasonable minReturnedTokens** to protect against frontrunning
2. **Consider data hook gas costs** when estimating transaction costs
3. **Handle pay hook reverts** gracefully in UI
4. **Validate metadata encoding** matches hook expectations

### For Hook Developers

1. **Keep beforePayRecordedWith() view-only and light** - it runs on every payment
2. **Handle failures gracefully** in afterPayRecordedWith() - don't lock user funds
3. **Validate msg.sender** is an authorized terminal
4. **Consider reentrancy** - hooks receive funds before execution

### For Project Operators

1. **Lock critical splits** to prevent rug pulls
2. **Use approval hooks** for governance-controlled projects
3. **Monitor pending reserved tokens** and distribute regularly
4. **Set appropriate payout limits** to constrain risk

### Gas Optimization Tips

1. **Batch operations** when possible (queue multiple rulesets)
2. **Use credits** for 721 hook instead of exact payments
3. **Distribute reserved tokens** during low-gas periods
4. **Consider total vs terminal surplus** tradeoff for cash outs

---

## Common Pitfalls

1. **Setting minReturnedTokens = 0**: Vulnerable to sandwich attacks
2. **Forgetting to include locked splits**: Update transaction reverts
3. **Assuming immediate reserved distribution**: Tokens accumulate
4. **Not handling hook metadata correctly**: Silent failures or reverts
5. **Ignoring approval hook delays**: Rulesets rejected if queued late
6. **Underestimating gas for multi-hook payments**: Transaction fails
7. **Not validating surplus exists before cash out**: Wasted gas on revert

---

## Core Infrastructure Contracts

### JBDirectory

The directory manages terminal and controller assignments for projects.

#### Storage Architecture

```solidity
mapping(uint256 projectId => IJBController) public controllerOf;
mapping(uint256 projectId => IJBTerminal[]) internal _terminalsOf;
mapping(uint256 projectId => mapping(address token => IJBTerminal)) internal _primaryTerminalOf;
mapping(address => bool) public isAllowedToSetFirstController;
```

#### Terminal Management

**setTerminalsOf()** replaces the entire terminal array:
- Validates no duplicates via nested loop
- Requires `SET_TERMINALS` permission OR caller is project controller
- Checks ruleset's `setTerminalsAllowed` flag (bypassed if controller is caller)

**primaryTerminalOf()** resolution:
1. Return explicitly-set primary terminal if still valid
2. Otherwise, return first terminal accepting that token
3. Return zero address if none found

#### Controller Migration

**setControllerOf()** handles migration:
1. Validates `SET_CONTROLLER` permission OR first-time setup via allowlist
2. Checks ruleset's `setControllerAllowed` flag
3. Calls `IJBMigratable.migrate()` on old controller if interface supported

**Critical**: First controller can only be set by addresses on the `isAllowedToSetFirstController` allowlist.

---

### JBProjects

ERC-721 contract where each token represents a Juicebox project.

#### Project Creation

```solidity
function createFor(address owner) external returns (uint256 projectId) {
    projectId = ++count;
    _mint(owner, projectId);
}
```

The `count` variable acts as both total project counter and next project ID.

#### Metadata Resolution

```solidity
function tokenURI(uint256 projectId) public view override returns (string memory) {
    if (address(tokenUriResolver) == address(0)) return "";
    return tokenUriResolver.tokenUriOf(address(this), projectId);
}
```

**Graceful Degradation**: Returns empty string if no resolver set, preventing reverts.

#### ERC-2771 Support

Overrides `_msgSender()`, `_msgData()`, and `_contextSuffixLength()` for meta-transaction support via trusted forwarder.

---

### JBPermissions

Bitmap-based permission system enabling granular access control.

#### Storage Architecture

```solidity
mapping(
    address operator => mapping(
        address account => mapping(
            uint256 projectId => uint256 packedPermissions
        )
    )
) public permissionsOf;
```

Each bit in the `uint256` represents one of 256 possible permissions. Project ID `0` is the wildcard, granting permissions across all projects.

#### Permission Checking

**hasPermission()** implements hierarchical evaluation:

```
1. If includeRoot && operator has ROOT permission:
   → Check specific project OR wildcard project
   → Return true if ROOT found

2. Check specific permissionId bit on specific project

3. If includeWildcardProjectId:
   → Check specific permissionId bit on project 0
```

**hasPermissions()** (batch check):
- Returns true immediately if ROOT permission exists
- Iterates through all requested permissions
- Returns false if ANY permission missing

#### Security Constraints in setPermissionsFor()

```solidity
// Only account holder or authorized operators can modify
if (_msgSender() != account) {
    // Operators cannot grant ROOT permission
    if (permissionsData.permissionIds contains ROOT) revert;

    // ROOT operators cannot modify wildcard project
    if (projectId == 0) revert;

    // Must have ROOT on specific project to modify
    if (!hasPermission(ROOT, projectId)) revert;
}
```

**Edge Cases**:
- Permission ID 0 is reserved, cannot be set
- Permission IDs > 255 revert with `PermissionIdOutOfBounds`
- Empty permission arrays are valid (clears all permissions)

---

### JBTokens

Dual-balance system supporting both unclaimed credits and ERC-20 tokens.

#### Storage Design

```solidity
mapping(address holder => mapping(uint256 projectId => uint256)) public creditBalanceOf;
mapping(uint256 projectId => uint256) public totalCreditSupplyOf;
mapping(uint256 projectId => IJBToken) public tokenOf;
mapping(IJBToken token => uint256) public projectIdOf;
```

#### Minting Logic

**mintFor()** chooses based on ERC-20 existence:
```solidity
if (tokenOf[projectId] != address(0)) {
    token.mint(holder, count);  // Direct ERC-20 mint
} else {
    creditBalanceOf[holder][projectId] += count;  // Store as credits
    totalCreditSupplyOf[projectId] += count;
}
```

#### Burning Priority

**burnFrom()** burns credits first, then tokens:
```solidity
uint256 creditBalance = creditBalanceOf[holder][projectId];
uint256 tokensToBurn = creditBalance < count ? count - creditBalance : 0;
uint256 creditsToBurn = count - tokensToBurn;

// Burn credits
creditBalanceOf[holder][projectId] -= creditsToBurn;
totalCreditSupplyOf[projectId] -= creditsToBurn;

// Burn tokens
if (tokensToBurn > 0) token.burn(holder, tokensToBurn);
```

#### ERC-20 Deployment

**deployERC20For()** uses minimal proxy clones:
- Validates project doesn't already have token
- Deploys via `Clones.clone()` or `Clones.cloneDeterministic()`
- Links bidirectionally: `tokenOf[projectId]` and `projectIdOf[token]`

**setTokenFor()** validates external tokens:
- Token must use 18 decimals
- Token must return true from `canBeAddedTo(projectId)`
- Token must not be assigned to another project

---

### Custom ERC20 Token Integration

The JBTokens system supports custom ERC20 tokens, enabling advanced tokenomics while preserving Juicebox's payment and redemption mechanics.

#### How Custom Tokens Work

When `setTokenFor()` is called with a custom token:

```solidity
function setTokenFor(uint256 projectId, IJBToken token) external {
    // 1. Validate token is compatible
    if (token.decimals() != 18) revert JBTokens_TokensMustHave18Decimals();
    if (!token.canBeAddedTo(projectId)) revert JBTokens_TokenCannotBeAddedTo();
    if (projectIdOf[token] != 0) revert JBTokens_TokenAlreadyAssigned();

    // 2. Store bidirectional mapping
    tokenOf[projectId] = token;
    projectIdOf[token] = projectId;

    // 3. Existing credits remain claimable
    // totalCreditSupplyOf[projectId] stays unchanged
}
```

**Key Insight**: Setting a custom token doesn't migrate existing credits. Credit holders must call `claimTokensFor()` to convert credits to the ERC20.

#### Mint/Burn Flow with Custom Tokens

When payments are received:
```
pay() → mintTokensOf() → JBTokens.mintFor()
                              ↓
                         customToken.mint(holder, amount)
```

When cash outs occur:
```
cashOutTokensOf() → burnTokensOf() → JBTokens.burnFrom()
                                          ↓
                                     // Burns credits first, then tokens
                                     customToken.burn(holder, tokensToBurn)
```

**Critical**: The controller calls `mint()` and `burn()` directly on your token. Your token MUST grant these permissions to the controller address.

#### Custom Token Requirements

| Requirement | Reason |
|-------------|--------|
| **18 decimals** | All Juicebox math (weights, rates) assumes 18 decimals |
| **canBeAddedTo()** | Validates token agrees to serve this project |
| **mint(address, uint256)** | Controller must mint on payments |
| **burn(address, uint256)** | Controller must burn on cash outs |
| **Controller access** | Token must authorize JBController for mint/burn |

#### Common Custom Token Patterns

**1. Transfer Tax Token**
```solidity
function _update(address from, address to, uint256 amount) internal override {
    // Skip tax for controller operations (mints/burns)
    if (from == address(0) || to == address(0) || msg.sender == controller) {
        super._update(from, to, amount);
        return;
    }
    // Apply tax on transfers
    uint256 tax = (amount * TAX_RATE) / 10000;
    super._update(from, taxRecipient, tax);
    super._update(from, to, amount - tax);
}
```

**Tradeoff**: Tax revenue goes to `taxRecipient`, not the Juicebox treasury. Consider routing tax to the project via `addToBalanceOf()`.

**2. Rebasing Token**
```solidity
// Track shares instead of balances
mapping(address => uint256) private _shares;
uint256 public totalShares;
uint256 public rebaseIndex = 1e18; // Starts at 1:1

function balanceOf(address account) public view override returns (uint256) {
    return (_shares[account] * rebaseIndex) / 1e18;
}

function rebase(uint256 newIndex) external onlyOwner {
    rebaseIndex = newIndex;
    // All balances scale proportionally
}
```

**Tradeoff**: Cash out calculations use `totalSupply()`. Rebasing changes supply without minting, which affects redemption value.

**3. Governance Token (ERC20Votes)**
```solidity
import {ERC20Votes} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";

contract GovernanceProjectToken is ERC20Votes {
    address public controller;

    function mint(address to, uint256 amount) external {
        require(msg.sender == controller, "Only controller");
        _mint(to, amount);
    }

    // Inherits delegation, checkpointing, getPastVotes()
}
```

**Benefit**: Token holders can vote on external governance proposals while maintaining Juicebox treasury mechanics.

**4. Editable Name/Symbol Token**
```solidity
contract EditableProjectToken is ERC20 {
    string private _tokenName;
    string private _tokenSymbol;

    function name() public view override returns (string memory) { return _tokenName; }
    function symbol() public view override returns (string memory) { return _tokenSymbol; }

    function setName(string calldata newName) external onlyProjectOwner {
        _tokenName = newName;
    }

    function setSymbol(string calldata newSymbol) external onlyProjectOwner {
        _tokenSymbol = newSymbol;
    }
}
```

**Benefit**: Rebrand without redeploying token or migrating liquidity. Project owner controls metadata.

**5. Vesting Token with Per-Address Schedules**
```solidity
contract VestingProjectToken is ERC20 {
    struct VestingSchedule {
        uint256 totalAmount;     // Total tokens in this schedule
        uint256 released;        // Already released/transferred
        uint40 start;            // Vesting start timestamp
        uint40 cliff;            // Cliff end timestamp
        uint40 duration;         // Total vesting duration
    }

    mapping(address => VestingSchedule) public vestingOf;

    function vestedAmountOf(address account) public view returns (uint256) {
        VestingSchedule memory schedule = vestingOf[account];
        if (schedule.totalAmount == 0) return balanceOf(account);
        if (block.timestamp < schedule.cliff) return 0;
        if (block.timestamp >= schedule.start + schedule.duration) {
            return schedule.totalAmount;
        }
        uint256 elapsed = block.timestamp - schedule.start;
        return (schedule.totalAmount * elapsed) / schedule.duration;
    }

    function _update(address from, address to, uint256 amount) internal override {
        // Skip vesting checks for mints, burns, controller ops
        if (from == address(0) || to == address(0) || msg.sender == controller) {
            super._update(from, to, amount);
            return;
        }

        VestingSchedule storage schedule = vestingOf[from];
        if (schedule.totalAmount == 0) {
            super._update(from, to, amount);
            return;
        }

        require(block.timestamp >= schedule.cliff, "Cliff not reached");
        uint256 transferable = vestedAmountOf(from) - schedule.released;
        require(amount <= transferable, "Insufficient vested balance");
        schedule.released += amount;
        super._update(from, to, amount);
    }

    function setVestingSchedule(
        address beneficiary,
        uint256 totalAmount,
        uint40 start,
        uint40 cliffDuration,
        uint40 vestingDuration
    ) external onlyProjectOwner {
        vestingOf[beneficiary] = VestingSchedule({
            totalAmount: totalAmount,
            released: 0,
            start: start,
            cliff: start + cliffDuration,
            duration: vestingDuration
        });
    }
}
```

**Key Difference from Treasury Vesting**:
- **Treasury vesting** (payout limits): Controls when funds leave the treasury
- **Token vesting**: Controls when individual holders can transfer tokens

**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.

**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`.

#### Edge Cases and Gotchas

1. **Token assigned twice**: A token can only serve one project. `setTokenFor()` reverts if token already assigned.

2. **Credit/token split during cash out**: Burns credits first, then tokens:
   ```solidity
   // If user has 100 credits and 50 tokens, burning 120:
   // - Burns all 100 credits
   // - Burns 20 tokens
   ```

3. **Transfer restrictions**: If your token blocks certain transfers, ensure controller operations (mint/burn) are always allowed.

4. **Pausable tokens**: Pausing transfers will break cash outs if tokens can't be burned.

5. **Fee-on-transfer tokens**: Not directly supported. The minted amount must equal the amount the controller requested.

6. **Approval requirements**: JBTokens calls `burn()` directly. If your token requires approval for burns, this will fail. Use `burnFrom` pattern that allows controller without approval.

#### Integration Checklist

Before using a custom token:

- [ ] Token uses exactly 18 decimals
- [ ] `canBeAddedTo(projectId)` returns true for your project
- [ ] Controller address has mint permission
- [ ] Controller address has burn permission (without approval)
- [ ] Token not assigned to another project
- [ ] Mint/burn don't have unexpected side effects (fees, rebasing)
- [ ] Transfer restrictions exempt controller operations
- [ ] Considered interaction with cash out tax rate
- [ ] Tested credit → token claiming works
- [ ] Verified `totalSupply()` reflects actual redeemable tokens

---

### JBFundAccessLimits

Packed storage for payout limits and surplus allowances.

#### Bit Packing

```solidity
// Bits 0-223: amount (up to ~2^224 wei)
// Bits 224-255: currency (32-bit identifier)
uint256 packed = uint256(amount) | (uint256(currency) << 224);
```

#### Currency Ordering Enforcement

**setFundAccessLimitsFor()** requires strictly increasing currency order:
```solidity
for (uint256 i = 1; i < payoutLimits.length; i++) {
    if (payoutLimits[i].currency <= payoutLimits[i-1].currency) {
        revert JBFundAccessLimits_InvalidPayoutLimitCurrencyOrdering();
    }
}
```

This prevents duplicates and enables O(n) iteration during lookups.

#### Zero Filtering

Zero-amount limits are filtered during storage, not stored:
```solidity
if (limit.amount > 0) {
    _packedPayoutLimitsDataOf[projectId][rulesetId][terminal][token].push(packed);
}
```

---

## Fund Access Limit Lifecycle

Understanding the lifecycle of payout limits vs surplus allowances is critical for project design. **This is a common source of confusion.**

### Payout Limits: Reset Each Cycle

Payout limits reset when a new cycle begins:

```
Cycle 1: Payout limit = 10 ETH
  → Team sends 10 ETH payouts
  → Remaining limit = 0

Cycle 2: Payout limit = 10 ETH (RESET!)
  → Team can send another 10 ETH
  → This continues every cycle
```

**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.

### Surplus Allowance: One-Time Per Ruleset

Surplus allowance does NOT reset each cycle:

```
Cycle 1: Surplus allowance = 20 ETH
  → Team uses 15 ETH from surplus
  → Remaining allowance = 5 ETH

Cycle 2: Surplus allowance = 5 ETH (NOT reset!)
  → Still only 5 ETH available
  → Allowance only resets if NEW RULESET is queued
```

**Key Insight**: Surplus allowance is a one-time budget per ruleset configuration. It's designed for **discretionary treasury access**, not recurring distributions.

### How Remaining Limits Are Tracked

The terminal store tracks used amounts separately from configured limits:

```solidity
// JBTerminalStore
mapping(address terminal =>
    mapping(uint256 projectId =>
        mapping(address token =>
            mapping(uint256 rulesetCycleNumber => uint256)
        )
    )
) public usedPayoutLimitOf;

mapping(address terminal =>
    mapping(uint256 projectId =>
        mapping(address token =>
            mapping(uint256 rulesetId => uint256)
        )
    )
) public usedSurplusAllowanceOf;
```

Notice the key difference:
- **Payout limit**: Keyed by `rulesetCycleNumber` → resets each cycle
- **Surplus allowance**: Keyed by `rulesetId` → persists across cycles

### Surplus Calculation

Surplus (redeemable amount) is calculated as:
```
surplus = balance - remainingPayoutLimit
```

Where `remainingPayoutLimit` is the configured limit minus used amount for the current cycle.

**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.

### Design Pattern: Vesting via Native Mechanics

Combine both limit types for sophisticated treasury management:

| Mechanism | Behavior | Use Case |
|-----------|----------|----------|
| Payout Limit | Resets each cycle | Recurring vesting/salaries |
| Surplus Allowance | One-time per ruleset | Emergency fund / discretionary |

**Example: 12-Month Vesting with Treasury Reserve**

```solidity
// Single cycling ruleset (NOT 12 separate rulesets!)
JBRulesetConfig({
    duration: 30 days,  // Monthly cycles
    // ...
    fundAccessLimitGroups: [
        JBFundAccessLimitGroup({
            terminal: address(TERMINAL),
            token: JBConstants.NATIVE_TOKEN,
            payoutLimits: [
                JBCurrencyAmount({
                    amount: 6.67 ether,   // Monthly vesting (80 ETH / 12)
                    currency: nativeCurrency
                })
            ],
            surplusAllowances: [
                JBCurrencyAmount({
                    amount: 20 ether,     // One-time treasury access
                    currency: nativeCurrency
                })
            ]
        })
    ]
});
```

**Result over 12 months:**
- Month 1: Team can withdraw 6.67 ETH (payout) + up to 20 ETH (surplus allowance)
- Month 2-12: Team can withdraw 6.67 ETH/month (payout limit resets each cycle)
- Surplus allowance does NOT reset - once used, it's gone until new ruleset

### Common Mistakes

1. **Queueing 12 rulesets for 12-month vesting**
   - Wrong: Queue separate ruleset for each month
   - Right: One cycling ruleset with monthly payout limit

2. **Expecting surplus allowance to reset**
   - Wrong: Plan to use 10 ETH each month from surplus allowance
   - Right: Use payout limits for recurring distributions

3. **Not understanding surplus protection**
   - Payout limits protect funds from redemption
   - Funds within the payout limit are NOT part of surplus

---

### JBPrices

Hierarchical price feed system with inverse calculation support.

#### Price Resolution Order

**pricePerUnitOf()** follows this order:
```
1. Same currency? Return 10^decimals (1:1 ratio)
2. Direct feed exists? Use feed.currentUnitPrice()
3. Inverse feed exists? Calculate reciprocal
4. Project-specific not found? Try default (projectId=0)
5. Nothing found? Revert with PriceFeedNotFound
```

#### Inverse Calculation

```solidity
// If only priceFeedFor[project][B][A] exists, calculate A→B:
price = mulDiv(10**decimals, 10**decimals, inverseFeed.currentUnitPrice(decimals));
```

#### Feed Immutability

Price feeds are append-only:
- Cannot modify existing feeds
- Cannot remove feeds
- Validates both direct and inverse don't already exist before adding

**Access Control**:
- Default feeds (projectId=0): Owner only
- Project-specific feeds: Controller only

---

## Hook & Terminal Implementations

### JBSwapTerminal Deep Dive

Wraps Uniswap V3 swaps into a Juicebox terminal interface.

#### Payment Flow

```
pay()/addToBalanceOf()
  → _acceptFundsFor() // Transfer or Permit2
  → _handleTokenTransfersAndSwap()
    → _beforeTransferFor() // Wrap ETH if needed
    → _swap() // Execute Uniswap swap
  → Forward to primary terminal for output token
```

#### TWAP Integration

```solidity
function _getQuote(IUniswapV3Pool pool, uint256 amountIn) internal view returns (uint256) {
    uint32 secondsAgo = OracleLibrary.getOldestObservationSecondsAgo(address(pool));

    // Cap at configured window (2 min to 2 days)
    if (secondsAgo > twapWindow) secondsAgo = twapWindow;

    (int24 arithmeticMeanTick,) = OracleLibrary.consult(pool, secondsAgo);
    return OracleLibrary.getQuoteAtTick(arithmeticMeanTick, amountIn, tokenIn, tokenOut);
}
```

#### Pool Configuration

**addDefaultPool()** setup:
1. Validates pool deployed by stored factory
2. Increases observation cardinality to minimum 10
3. Stores accounting context (decimals, etc.)

#### Hierarchical Defaults

Project-specific configs override global defaults:
```solidity
pool = poolOf[projectId][tokenIn][tokenOut];
if (pool == address(0)) {
    pool = poolOf[DEFAULT_PROJECT_ID][tokenIn][tokenOut];
}
```

---

### JB721TiersHook Deep Dive

Tiered NFT system with integrated payment processing and cash out support. Uses JBOwnable for flexible ownership.

#### Architecture

The 721 hook implements three hook interfaces:
- **IJBRulesetDataHook**: `beforePayRecordedWith()`, `beforeCashOutRecordedWith()`
- **IJBPayHook**: `afterPayRecordedWith()`
- **IJBCashOutHook**: `afterCashOutRecordedWith()`

Plus extends **JBOwnable** for project-based or EOA ownership with permission delegation.

#### Tier Storage (JB721TiersHookStore)

```solidity
struct JB721Tier {
    uint104 price;              // Price in terminal token
    uint32 initialSupply;       // Starting supply
    uint32 remainingSupply;     // Current available
    uint16 votingUnits;         // Governance weight
    uint16 reserveFrequency;    // Reserved mint ratio (1 in N)
    uint24 category;            // Grouping identifier
    bool transfersPausable;     // Can transfers be paused
    bool cannotBeRemoved;       // Permanent tier flag
}
```

#### Payment Processing (`_processPayment`)

```
afterPayRecordedWith()
  → _processPayment()
    → Normalize payment to tier pricing currency via JBPrices
    → Add existing payCreditsOf[beneficiary] if payer == beneficiary
    → Decode metadata for tier IDs to mint
    → If no tiers specified: auto-select best fit tiers
    → Call STORE.recordMint() for each tier
      → Validates tier active and has supply
      → Handles reserved mints (1 per reserveFrequency)
    → Mint NFTs via _mint()
    → Handle leftover:
      → If allowOverspending: add to payCreditsOf
      → Else: revert if leftover exists
```

#### Auto-Tier Selection

When payer doesn't specify tiers, the hook selects automatically:
```solidity
function _selectAutoTiers(uint256 amount) internal view returns (uint256[] tiers) {
    // Iterate tiers by price descending
    // Select highest-priced tier that fits remaining amount
    // Repeat until amount exhausted or no tier fits
}
```

**Tradeoff**: Auto-selection may not match user intent; explicit tier selection preferred for UX.

#### Credit System Implementation

```solidity
mapping(address payer => uint256) public payCreditsOf;

// On payment:
if (payer == beneficiary) {
    effectiveAmount = payment.amount + payCreditsOf[payer];
    payCreditsOf[payer] = 0;  // Use all credits
}

// After minting:
if (leftover > 0 && allowOverspending) {
    payCreditsOf[beneficiary] += leftover;
    emit AddPayCredits(beneficiary, leftover, ...);
}
```

**Edge Cases**:
- Credits lost if hook is replaced
- Credits cannot be withdrawn, only spent on NFTs
- Accumulated dust from rounding

#### Cash Out Weight Calculation

Each NFT's value equals its tier price:
```solidity
function cashOutWeightOf(uint256[] tokenIds) public view returns (uint256 weight) {
    for (uint256 i; i < tokenIds.length; i++) {
        JB721Tier tier = STORE.tierOfTokenId(tokenIds[i]);
        weight += tier.price;
    }
}
```

Total outstanding value:
```solidity
function totalCashOutWeight() public view returns (uint256) {
    return STORE.totalCashOutWeight(address(this));
}
```

**Cash Out Proportion**:
```
userShare = (userNFTWeight / totalCashOutWeight) × surplus × (1 - taxRate)
```

#### Reserved Minting

Tiers can reserve NFTs for the project:
```solidity
// If reserveFrequency = 10, every 10th mint goes to reserved beneficiary
if (mintCount % tier.reserveFrequency == 0) {
    _mint(reservedBeneficiary, tokenId);
} else {
    _mint(payer, tokenId);
}
```

Reserved NFTs are minted inline during payment, not accumulated.

#### Ownership via JBOwnable

The hook extends JBOwnable for flexible access control:
```solidity
// Project-based ownership (common pattern):
constructor(..., uint256 projectId, ...) {
    _transferOwnership(projectId);  // Owner = project NFT holder
}

// Permission delegation:
// Owner can grant ADJUST_721_TIERS to operators via JBPermissions
// Operators can then call adjustTiers() without being owner
```

**Permission IDs used**:
- `ADJUST_721_TIERS` (17): Add/remove tiers
- `SET_721_METADATA` (18): Update tier metadata
- `MINT_721` (19): Manual minting
- `SET_721_DISCOUNT_PERCENT` (27): Adjust pricing

#### Tier Adjustment

```solidity
function adjustTiers(JB721TierConfig[] tiersToAdd, uint256[] tierIdsToRemove) external {
    // Requires ADJUST_721_TIERS permission or owner

    // Remove tiers (if not cannotBeRemoved)
    for (uint256 id : tierIdsToRemove) {
        require(!tier.cannotBeRemoved, "TIER_LOCKED");
        STORE.recordRemoveTierOf(id);
    }

    // Add new tiers
    for (JB721TierConfig config : tiersToAdd) {
        STORE.recordAddTier(config);
    }
}
```

#### Metadata Encoding

Payer metadata structure for specifying tiers:
```solidity
bytes4 constant METADATA_ID = bytes4(keccak256("JB721TiersHook"));

// Encoded as: [METADATA_ID][allowOverspending (bool)][tierIds (uint16[])]
bytes memory metadata = abi.encode(
    true,                    // allowOverspending
    [uint16(1), uint16(3)]   // Mint tier 1 and tier 3
);
```

---

## Extending 721-Hook: Dynamic Cash Out Weights

When 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.

### Why Extend vs. Use Resolver Only

| Need | Resolver | Extended Hook |
|------|----------|---------------|
| Custom artwork/metadata | ✅ | ✅ |
| Dynamic cash out weights | ❌ | ✅ |
| First-owner tracking | ❌ | ✅ |
| Phase-based restrictions | ❌ | ✅ |
| Governor integration | ❌ | ✅ |

**Rule of thumb**: If you need to change how money flows, extend the hook. If you o

…(truncated)
