@bananapus/omnichain-deployers-v6 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ # Administration
2
+
3
+ Admin privileges and their scope in nana-omnichain-deployers-v6.
4
+
5
+ ## Roles
6
+
7
+ | Role | How Assigned | Scope |
8
+ |------|-------------|-------|
9
+ | Project owner | Holds the project's ERC-721 (minted by `JBProjects`) | Per-project. Can delegate via `JBPermissions`. |
10
+ | Permitted operator | Granted specific permission IDs by the project owner through `JBPermissions` | Per-project, per-permission. ROOT (255) grants all. Wildcard projectId=0 grants across all projects. |
11
+ | Registered sucker | Deployed via `JBSuckerRegistry.deploySuckersFor` (requires DEPLOY_SUCKERS permission) | Per-project. Gets 0% cash-out tax and mint permission automatically. |
12
+ | JBSuckerRegistry | Set at construction, granted MAP_SUCKER_TOKEN for all projects (projectId=0 wildcard) | Protocol-wide. Maps tokens for sucker bridging. |
13
+
14
+ ## Privileged Functions
15
+
16
+ ### JBOmnichainDeployer
17
+
18
+ | Function | Required Role | Permission ID | Scope | What It Does |
19
+ |----------|--------------|---------------|-------|--------------|
20
+ | `deploySuckersFor` | Project owner or operator | `DEPLOY_SUCKERS` | Per-project | Deploys new cross-chain suckers for an existing project via the sucker registry. |
21
+ | `launch721RulesetsFor` | Project owner or operator | `QUEUE_RULESETS` + `SET_TERMINALS` | Per-project | Deploys a 721 tiers hook, launches new rulesets with terminal configuration for an existing project. |
22
+ | `launchRulesetsFor` | Project owner or operator | `QUEUE_RULESETS` + `SET_TERMINALS` | Per-project | Launches new rulesets with terminal configuration for an existing project (no 721 hook). |
23
+ | `queue721RulesetsOf` | Project owner or operator | `QUEUE_RULESETS` | Per-project | Deploys a 721 tiers hook and queues new rulesets for an existing project. |
24
+ | `queueRulesetsOf` | Project owner or operator | `QUEUE_RULESETS` | Per-project | Queues new rulesets for an existing project (no 721 hook). |
25
+
26
+ ### Permissionless Functions
27
+
28
+ | Function | Who Can Call | What It Does |
29
+ |----------|-------------|--------------|
30
+ | `launchProjectFor` | Anyone | Creates a new project with suckers. The ERC-721 is minted to the specified `owner`. |
31
+ | `launch721ProjectFor` | Anyone | Creates a new project with a 721 tiers hook and suckers. The ERC-721 is minted to the specified `owner`. |
32
+ | `beforePayRecordedWith` | JBMultiTerminal (via controller) | View function: forwards pay data to the stored data hook, or passes through if none configured. |
33
+ | `beforeCashOutRecordedWith` | JBMultiTerminal (via controller) | View function: returns 0% cash-out tax for registered suckers, otherwise forwards to stored data hook. |
34
+ | `hasMintPermissionFor` | JBController | View function: returns true for registered suckers, otherwise forwards to stored data hook. |
35
+ | `dataHookOf` | Anyone | View function: returns the stored data hook config for a project/ruleset pair. |
36
+
37
+ ## Deployment Administration
38
+
39
+ **Who can deploy omnichain projects:** Anyone. The `launchProjectFor` and `launch721ProjectFor` functions are permissionless. The caller specifies an `owner` address that receives the project ERC-721.
40
+
41
+ **Deployment flow:**
42
+ 1. The deployer temporarily owns the project ERC-721 (minted to `address(this)`).
43
+ 2. It configures rulesets, sets itself as the data hook wrapper, and optionally deploys suckers.
44
+ 3. It transfers the project ERC-721 to the specified `owner`.
45
+
46
+ **Configurable parameters at deployment:**
47
+ - Ruleset configurations (duration, weight, decay, approval hooks, splits, fund access limits, metadata flags).
48
+ - Terminal configurations (which terminals accept which tokens).
49
+ - 721 tiers hook configuration (tier pricing, supply, metadata, categories).
50
+ - Sucker deployment configuration (which chains, which deployers, token mappings).
51
+ - Salt for deterministic cross-chain address matching.
52
+
53
+ ## Cross-Chain Controls
54
+
55
+ | Action | Who | Mechanism |
56
+ |--------|-----|-----------|
57
+ | Deploy suckers for existing project | Project owner or DEPLOY_SUCKERS operator | `deploySuckersFor` calls `SUCKER_REGISTRY.deploySuckersFor` |
58
+ | Deploy suckers during project launch | Project deployer (anyone) | Included in `launchProjectFor` / `launch721ProjectFor` if `salt != bytes32(0)` |
59
+ | Map sucker tokens | JBSuckerRegistry | Granted MAP_SUCKER_TOKEN at construction with projectId=0 wildcard |
60
+ | Grant 0% cash-out tax to suckers | Automatic | `beforeCashOutRecordedWith` checks `SUCKER_REGISTRY.isSuckerOf` |
61
+ | Grant mint permission to suckers | Automatic | `hasMintPermissionFor` checks `SUCKER_REGISTRY.isSuckerOf` |
62
+
63
+ **Cross-chain determinism:** The salt for sucker deployment is combined with `_msgSender()` (`keccak256(abi.encode(salt, _msgSender()))`). Deploying from the same sender address with the same salt on each chain produces matching sucker addresses.
64
+
65
+ ## Immutable Configuration
66
+
67
+ These values are set at deployment and cannot be changed:
68
+
69
+ | Property | Type | What It Is |
70
+ |----------|------|-----------|
71
+ | `PROJECTS` | `IJBProjects` | The ERC-721 contract for project ownership. |
72
+ | `HOOK_DEPLOYER` | `IJB721TiersHookDeployer` | The deployer for 721 tiers hooks. |
73
+ | `SUCKER_REGISTRY` | `IJBSuckerRegistry` | The registry for deploying and tracking suckers. |
74
+ | `PERMISSIONS` | `IJBPermissions` | The permissions contract (inherited from JBPermissioned). |
75
+ | Trusted forwarder | `address` | The ERC-2771 trusted forwarder for meta-transactions. |
76
+ | MAP_SUCKER_TOKEN grant | Permission | Granted to SUCKER_REGISTRY at construction for all projects (projectId=0). Cannot be revoked by this contract. |
77
+
78
+ **Data hook mappings** (`_dataHookOf[projectId][rulesetId]`) are write-once per ruleset ID. They are set during `_setup` and never updated or deleted.
79
+
80
+ ## Admin Boundaries
81
+
82
+ What admins **cannot** do:
83
+
84
+ - **Cannot upgrade the deployer.** JBOmnichainDeployer has no upgrade mechanism, proxy pattern, or self-destruct.
85
+ - **Cannot change immutable references.** PROJECTS, HOOK_DEPLOYER, SUCKER_REGISTRY, PERMISSIONS, and the trusted forwarder are all immutable.
86
+ - **Cannot modify stored data hooks.** Once a ruleset's data hook config is stored in `_dataHookOf`, it cannot be changed. New rulesets can use different hooks, but existing mappings are permanent.
87
+ - **Cannot bypass permission checks.** All post-deployment admin functions require JBPermissions verification against the project owner.
88
+ - **Cannot revoke sucker privileges.** Once a sucker is registered in JBSuckerRegistry, it automatically gets 0% cash-out tax and mint permission for its project. Revocation must happen at the registry level.
89
+ - **Cannot set the deployer as its own data hook.** The `_setup` function explicitly reverts with `JBOmnichainDeployer_InvalidHook` if `metadata.dataHook == address(this)`.
90
+ - **Cannot use a controller that doesn't match the project.** `_validateController` reverts with `JBOmnichainDeployer_ControllerMismatch` if the provided controller is not the project's actual controller in the directory.
91
+ - **Cannot steal project ownership during deployment.** The deployer holds the project ERC-721 only transiently and transfers it to the specified owner in the same transaction.
92
+ - **Cannot drain funds.** The deployer never holds or manages token balances. It only orchestrates configuration.
@@ -0,0 +1,69 @@
1
+ # nana-omnichain-deployers-v6 — Architecture
2
+
3
+ ## Purpose
4
+
5
+ Omnichain project deployer for Juicebox V6. Wraps the project deployment flow to automatically configure cross-chain suckers, acting as a data hook that gives suckers 0% cash-out tax (bridging privilege) and mint permission.
6
+
7
+ ## Contract Map
8
+
9
+ ```
10
+ src/
11
+ ├── JBOmnichainDeployer.sol — Deploys projects with sucker integration, acts as data hook
12
+ ├── interfaces/
13
+ │ └── IJBOmnichainDeployer.sol — Interface
14
+ └── structs/
15
+ ├── JBDeployerHookConfig.sol — Hook configuration for deployment
16
+ └── JBSuckerDeploymentConfig.sol — Sucker deployment parameters
17
+ ```
18
+
19
+ ## Key Data Flows
20
+
21
+ ### Omnichain Project Deployment
22
+ ```
23
+ Deployer → JBOmnichainDeployer.deployProjectFor()
24
+ → Launch JB project via JB721TiersHookProjectDeployer
25
+ → Set JBOmnichainDeployer as data hook
26
+ → Deploy suckers via JBSuckerRegistry
27
+ → Configure sucker permissions (mint, 0% cashout tax)
28
+ → Transfer project ownership back to deployer
29
+ ```
30
+
31
+ ### Data Hook Behavior
32
+ ```
33
+ Payment → JBOmnichainDeployer.beforePayRecordedWith()
34
+ → Pass through (no modification to pay behavior)
35
+ → Return empty pay hook specifications
36
+
37
+ Cash Out → JBOmnichainDeployer.beforeCashOutRecordedWith()
38
+ → If caller is a registered sucker: return 0% cash-out tax
39
+ → Otherwise: return configured cash-out tax rate
40
+ ```
41
+
42
+ ### Ruleset Management
43
+ ```
44
+ Owner → JBOmnichainDeployer.queueRulesetsOf()
45
+ → Queue new rulesets via JBController
46
+ → Maintains deployer as data hook
47
+ → Supports adding/removing suckers
48
+
49
+ Owner → JBOmnichainDeployer.launchRulesetsFor()
50
+ → Launch rulesets for an existing project
51
+ → Configure sucker integration
52
+ ```
53
+
54
+ ## Extension Points
55
+
56
+ | Point | Interface | Purpose |
57
+ |-------|-----------|---------|
58
+ | Data hook (pay) | `IJBRulesetDataHook.beforePayRecordedWith` | Pass-through for payments |
59
+ | Data hook (cashout) | `IJBRulesetDataHook.beforeCashOutRecordedWith` | 0% tax for suckers |
60
+ | Sucker registry | `IJBSuckerRegistry` | Sucker deployment and discovery |
61
+ | 721 hook deployer | `IJB721TiersHookDeployer` | Optional NFT tier deployment |
62
+
63
+ ## Dependencies
64
+ - `@bananapus/core-v6` — Core protocol (controller, directory, permissions)
65
+ - `@bananapus/721-hook-v6` — NFT tier deployment
66
+ - `@bananapus/ownable-v6` — JB-aware ownership
67
+ - `@bananapus/permission-ids-v6` — Permission constants
68
+ - `@bananapus/suckers-v6` — Cross-chain sucker registry
69
+ - `@openzeppelin/contracts` — ERC2771, ERC721Receiver
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Juicebox Omnichain Deployers
2
2
 
3
- Deploy Juicebox projects with cross-chain suckers and optional 721 tiers hooks in a single transaction. Acts as a transparent data hook wrapper that gives suckers tax-free cash outs and on-demand mint permission -- without interfering with any custom data hook the project uses.
3
+ Deploy Juicebox projects with cross-chain suckers and optional 721 tiers hooks in a single transaction. Acts as a transparent data hook wrapper that gives suckers tax-free cash outs and on-demand mint permission -- without interfering with any custom data hook the project uses. Supports composing a 721 tiers hook alongside a custom data hook (e.g., a buyback hook) so both run on every payment.
4
4
 
5
5
  [Docs](https://docs.juicebox.money) | [Discord](https://discord.gg/juicebox)
6
6
 
@@ -8,12 +8,13 @@ Deploy Juicebox projects with cross-chain suckers and optional 721 tiers hooks i
8
8
 
9
9
  Launching a cross-chain Juicebox project normally takes several steps: deploy the project, configure rulesets, set up terminals, deploy suckers, and wire up a data hook that exempts suckers from cash out taxes. `JBOmnichainDeployer` collapses all of this into one transaction.
10
10
 
11
- It works by inserting itself as the data hook on every ruleset it touches, storing whatever hook the project actually wants in a mapping keyed by `(projectId, rulesetId)`. When the protocol calls data hook functions during payments and cash outs, the deployer:
11
+ It works by inserting itself as the data hook on every ruleset it touches, storing the project's custom data hook in a mapping keyed by `(projectId, rulesetId)` and any 721 tiers hook separately in `tiered721HookOf`. When the protocol calls data hook functions during payments and cash outs, the deployer:
12
12
 
13
- - **Checks if the holder is a sucker** -- if so, returns 0% cash out tax and grants mint permission. This early return means suckers can always bridge tokens without interference, even if the project's real data hook would revert.
14
- - **Forwards everything else** to the real data hook, or returns default values if none was set.
13
+ - **Checks if the holder is a sucker** -- if so, returns 0% cash out tax and grants mint permission. This early return means suckers can always bridge tokens without interference, even if the project's hooks would revert.
14
+ - **Composes the 721 hook and custom data hook** for payments -- the 721 hook is called first to get its specs (including split fund amounts), then the custom data hook is called with a reduced amount context (payment minus split amount) so it only considers the available funds. The deployer adjusts the returned weight proportionally for splits, ensuring the terminal only mints tokens for the amount that actually enters the project treasury. For cash outs, the 721 hook takes priority if present.
15
+ - **Forwards to the custom data hook** if no 721 hook is set, or returns default values if neither is set.
15
16
 
16
- This wrapping is invisible to the project and its users. The project's custom hook (buyback hook, 721 hook, etc.) works exactly as configured.
17
+ This wrapping is invisible to the project and its users. The project's hooks (buyback hook, 721 hook, etc.) work exactly as configured, and can be composed together.
17
18
 
18
19
  ### How It Works
19
20
 
@@ -47,10 +48,14 @@ sequenceDiagram
47
48
  Deployer->>Registry: isSuckerOf(projectId, holder)?
48
49
  alt Holder is a sucker
49
50
  Deployer-->>Terminal: 0% tax (early return)
50
- else Not a sucker
51
+ else 721 hook exists
52
+ Deployer->>721Hook: beforeCashOutRecordedWith(context)
53
+ 721Hook-->>Deployer: taxRate, count, supply, specs
54
+ Deployer-->>Terminal: forward 721 hook response
55
+ else Custom data hook exists
51
56
  Deployer->>Hook: beforeCashOutRecordedWith(context)
52
57
  Hook-->>Deployer: taxRate, count, supply, specs
53
- Deployer-->>Terminal: forward hook response
58
+ Deployer-->>Terminal: forward custom hook response
54
59
  end
55
60
  ```
56
61
 
@@ -59,9 +64,12 @@ sequenceDiagram
59
64
  The `launch721*` and `queue721*` variants deploy a tiered ERC-721 hook alongside the project. The deployer:
60
65
 
61
66
  1. Deploys the 721 hook via `HOOK_DEPLOYER`
62
- 2. Converts 721-specific ruleset configs (`JBPayDataHookRulesetConfig`) to standard configs, enforcing `useDataHookForPay = true` and `allowSetCustomToken = false`
63
- 3. Wraps the 721 hook with itself (as above)
64
- 4. Transfers hook ownership to the project via `JBOwnable.transferOwnershipToProject()`
67
+ 2. Stores the 721 hook in `tiered721HookOf[projectId]` (separate from the custom data hook)
68
+ 3. Converts 721-specific ruleset configs (`JBPayDataHookRulesetConfig`) to standard configs, enforcing `useDataHookForPay = true` and `allowSetCustomToken = false`
69
+ 4. Stores the optional custom data hook (e.g., buyback hook) in `_dataHookOf[projectId][rulesetId]`
70
+ 5. Transfers hook ownership to the project via `JBOwnable.transferOwnershipToProject()`
71
+
72
+ This separation means a project can have both a 721 hook (for NFT minting on payments) and a custom data hook (for buyback, custom weight logic, etc.) running simultaneously. During payments, both hooks' specifications are merged. During cash outs, the 721 hook takes priority.
65
73
 
66
74
  ### Deterministic Cross-Chain Addresses
67
75
 
@@ -92,7 +100,7 @@ The `queueRulesetsOf` and `queue721RulesetsOf` functions guard against predictio
92
100
 
93
101
  | Type | Description |
94
102
  |------|-------------|
95
- | `JBDeployerHookConfig` | Per-ruleset config storing the real data hook address and its `useDataHookForPay`/`useDataHookForCashOut` flags. |
103
+ | `JBDeployerHookConfig` | Per-ruleset config storing the custom data hook address and its `useDataHookForPay`/`useDataHookForCashOut` flags. The 721 hook is stored separately in `tiered721HookOf`. |
96
104
  | `JBSuckerDeploymentConfig` | Wraps an array of `JBSuckerDeployerConfig` with a `bytes32` salt for deterministic cross-chain addresses. |
97
105
  | `IJBOmnichainDeployer` | Interface for all deployer entry points and the `dataHookOf` view. |
98
106
 
@@ -150,6 +158,9 @@ test/
150
158
  JBOmnichainDeployer.t.sol # Unit tests
151
159
  JBOmnichainDeployerGuard.t.sol # Ruleset ID prediction tests
152
160
  OmnichainDeployerAttacks.t.sol # Adversarial security tests
161
+ Tiered721HookComposition.t.sol # 721 hook + custom hook composition tests
162
+ regression/
163
+ H20_HookOwnershipTransfer.t.sol # Hook ownership transfer regression
153
164
  script/
154
165
  Deploy.s.sol # Sphinx deployment script
155
166
  helpers/
package/RISKS.md ADDED
@@ -0,0 +1,35 @@
1
+ # nana-omnichain-deployers-v6 — Risks
2
+
3
+ ## Trust Assumptions
4
+
5
+ 1. **JBOmnichainDeployer as Data Hook** — Acts as data hook for all projects it deploys. A bug in the deployer affects every omnichain project's cash-out behavior.
6
+ 2. **Sucker Registry** — Trusts JBSuckerRegistry to correctly track registered suckers. The deployer grants 0% cash-out tax to any address the registry identifies as a sucker.
7
+ 3. **Project Owner** — Can queue new rulesets, deploy additional suckers, and manage project configuration through the deployer.
8
+ 4. **Core Protocol** — Relies on JBController, JBDirectory, and JBMultiTerminal for correct operation.
9
+ 5. **Bridge Infrastructure** — Inherits all sucker trust assumptions (bridge liveness, remote peer authentication).
10
+
11
+ ## Known Risks
12
+
13
+ | Risk | Description | Mitigation |
14
+ |------|-------------|------------|
15
+ | Sucker privilege abuse | Any registered sucker gets 0% cashout tax | Sucker registration requires DEPLOY_SUCKERS permission |
16
+ | Data hook centralization | Deployer is the data hook for all omnichain projects | Simple pass-through logic minimizes attack surface |
17
+ | Controller mismatch | Reverts if provided controller doesn't match project's actual controller | Explicit validation via `JBOmnichainDeployer_ControllerMismatch` |
18
+ | Invalid self-hook | Reverts if someone tries to set deployer as hook for deployer itself | `JBOmnichainDeployer_InvalidHook` check |
19
+ | Ownership transfer | Project ownership transferred during deployment | Ownership returned to caller after setup |
20
+
21
+ ## Privileged Roles
22
+
23
+ | Role | Capabilities | Scope |
24
+ |------|-------------|-------|
25
+ | Project owner | Queue rulesets, deploy suckers, manage configuration | Per-project |
26
+ | Registered suckers | 0% cash-out tax on token bridging | Per-project |
27
+ | JBSuckerRegistry | Determines which addresses are valid suckers | Protocol-wide |
28
+
29
+ ## Reentrancy Considerations
30
+
31
+ | Function | Protection | Risk |
32
+ |----------|-----------|------|
33
+ | `deployProjectFor` | Ownership transferred after all setup complete | LOW |
34
+ | `beforeCashOutRecordedWith` | View-like function, returns data only | NONE |
35
+ | `beforePayRecordedWith` | View-like function, returns data only | NONE |
package/SKILLS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- Single-transaction deployment of Juicebox projects with cross-chain suckers and optional 721 tiers hooks. Wraps the project's data hook to give suckers tax-free cash outs and mint permission without interfering with custom hooks.
5
+ Single-transaction deployment of Juicebox projects with cross-chain suckers and optional 721 tiers hooks. Wraps the project's data hook to give suckers tax-free cash outs and mint permission without interfering with custom hooks. Supports composing a 721 hook alongside a custom data hook (e.g., buyback hook) — both run on every payment.
6
6
 
7
7
  ## Contracts
8
8
 
@@ -17,26 +17,27 @@ Single-transaction deployment of Juicebox projects with cross-chain suckers and
17
17
  | Function | What it does |
18
18
  |----------|-------------|
19
19
  | `launchProjectFor(owner, projectUri, rulesetConfigs, terminalConfigs, memo, suckerConfig, controller)` | Creates a new project with rulesets, terminals, and suckers in one tx. Temporarily holds the project NFT. Returns `(projectId, suckers)`. |
20
- | `launch721ProjectFor(owner, deployTiersHookConfig, launchProjectConfig, salt, suckerConfig, controller)` | Same as above but also deploys a 721 tiers hook and transfers its ownership to the project. Returns `(projectId, hook, suckers)`. |
20
+ | `launch721ProjectFor(owner, deployTiersHookConfig, launchProjectConfig, suckerConfig, controller, dataHook, salt)` | Same as above but also deploys a 721 tiers hook. Pass a custom `dataHook` (e.g., buyback hook) to compose alongside the 721 hook, or `address(0)` for none. Returns `(projectId, hook, suckers)`. |
21
21
  | `launchRulesetsFor(projectId, rulesetConfigs, terminalConfigs, memo, controller)` | Launches new rulesets + terminals for an existing project. Requires `QUEUE_RULESETS` + `SET_TERMINALS`. |
22
- | `launch721RulesetsFor(projectId, deployTiersHookConfig, launchRulesetsConfig, controller, salt)` | Launches rulesets with a new 721 tiers hook. Requires `QUEUE_RULESETS` + `SET_TERMINALS`. |
22
+ | `launch721RulesetsFor(projectId, deployTiersHookConfig, launchRulesetsConfig, controller, dataHook, salt)` | Launches rulesets with a new 721 tiers hook + optional custom data hook. Requires `QUEUE_RULESETS` + `SET_TERMINALS`. |
23
23
  | `queueRulesetsOf(projectId, rulesetConfigs, memo, controller)` | Queues future rulesets. Requires `QUEUE_RULESETS`. Reverts if rulesets were already queued in the same block. |
24
- | `queue721RulesetsOf(projectId, deployTiersHookConfig, queueRulesetsConfig, controller, salt)` | Queues rulesets with a new 721 tiers hook. Same same-block guard. |
24
+ | `queue721RulesetsOf(projectId, deployTiersHookConfig, queueRulesetsConfig, controller, dataHook, salt)` | Queues rulesets with a new 721 tiers hook + optional custom data hook. Same same-block guard. |
25
25
  | `deploySuckersFor(projectId, suckerConfig)` | Deploys new suckers for an existing project. Requires `DEPLOY_SUCKERS`. |
26
26
 
27
27
  ### Data Hook (IJBRulesetDataHook)
28
28
 
29
29
  | Function | What it does |
30
30
  |----------|-------------|
31
- | `beforePayRecordedWith(context)` | Forwards to the stored real data hook if set and `useDataHookForPay` is true. Otherwise returns the original weight. |
32
- | `beforeCashOutRecordedWith(context)` | If holder is a sucker: returns 0% tax immediately (never calls real hook). Otherwise forwards to the real hook, or returns original values if none set. |
33
- | `hasMintPermissionFor(projectId, ruleset, addr)` | Returns `true` for registered suckers. Otherwise forwards to real hook, or returns `false` if none set. |
31
+ | `beforePayRecordedWith(context)` | Calls the 721 hook first for its specs (including split amounts), then calls the custom data hook with a reduced amount context (payment minus split amount) for weight + specs. Adjusts the returned weight proportionally so the terminal only mints tokens for the amount entering the project (`weight = mulDiv(weight, amount - splitAmount, amount)`). Merges both (721 hook specs first, then custom hook specs). |
32
+ | `beforeCashOutRecordedWith(context)` | If holder is a sucker: returns 0% tax immediately. If 721 hook exists: delegates to it (takes priority). Otherwise forwards to the custom data hook, or returns defaults. |
33
+ | `hasMintPermissionFor(projectId, ruleset, addr)` | Returns `true` for registered suckers, OR if the custom data hook grants permission, OR if the 721 hook grants permission. Returns `false` only if none grant it. |
34
34
 
35
35
  ### Views
36
36
 
37
37
  | Function | What it does |
38
38
  |----------|-------------|
39
- | `dataHookOf(projectId, rulesetId)` | Returns the stored `(useDataHookForPay, useDataHookForCashOut, dataHook)` for a given project and ruleset. |
39
+ | `dataHookOf(projectId, rulesetId)` | Returns the stored `(useDataHookForPay, useDataHookForCashOut, dataHook)` for a given project and ruleset. This is the custom data hook, not the 721 hook. |
40
+ | `tiered721HookOf(projectId)` | Returns the project's 721 tiers hook, stored separately from the custom data hook. Returns `address(0)` if no 721 hook was deployed. |
40
41
  | `supportsInterface(interfaceId)` | Returns `true` for `IJBOmnichainDeployer`, `IJBRulesetDataHook`, `IERC721Receiver`, `IERC165`. |
41
42
  | `onERC721Received(...)` | Accepts project NFTs from `PROJECTS` only. Reverts for any other NFT contract. |
42
43
 
@@ -55,7 +56,7 @@ Single-transaction deployment of Juicebox projects with cross-chain suckers and
55
56
 
56
57
  | Struct | Key Fields | Used In |
57
58
  |--------|------------|---------|
58
- | `JBDeployerHookConfig` | `bool useDataHookForPay`, `bool useDataHookForCashOut`, `IJBRulesetDataHook dataHook` | `_dataHookOf` mapping keyed by `(projectId, rulesetId)` |
59
+ | `JBDeployerHookConfig` | `bool useDataHookForPay`, `bool useDataHookForCashOut`, `IJBRulesetDataHook dataHook` | `_dataHookOf` mapping keyed by `(projectId, rulesetId)`. Stores the custom data hook only — the 721 hook is in `tiered721HookOf`. |
59
60
  | `JBSuckerDeploymentConfig` | `JBSuckerDeployerConfig[] deployerConfigurations`, `bytes32 salt` | All launch and deploy functions |
60
61
 
61
62
  ## Permission IDs
@@ -85,8 +86,8 @@ Single-transaction deployment of Juicebox projects with cross-chain suckers and
85
86
  4. Sucker deployment salts are hashed with `_msgSender()`: `keccak256(abi.encode(salt, _msgSender()))`. Cross-chain deterministic addresses require using the **same sender** on each chain. For `launch721ProjectFor`, the 721 hook salt uses `keccak256(abi.encode(_msgSender(), salt))` (reversed order).
86
87
  5. `salt = bytes32(0)` **skips sucker deployment entirely**. Use a nonzero salt to deploy suckers.
87
88
  6. The deployer **always forces `useDataHookForCashOut = true`** on every ruleset it touches, even if the original config had it as `false`. This is required so the deployer can intercept cash outs to check for suckers.
88
- 7. Suckers get an **early return** in `beforeCashOutRecordedWith` -- they bypass the real data hook entirely. This means suckers can cash out even if the real hook would revert.
89
- 8. If no real data hook is stored (or `address(0)`), `hasMintPermissionFor` returns `false` for non-suckers. It does **not** return the default `true`.
89
+ 7. Suckers get an **early return** in `beforeCashOutRecordedWith` -- they bypass both the 721 hook and custom data hook entirely. This means suckers can cash out even if either hook would revert.
90
+ 8. If no hooks are stored, `hasMintPermissionFor` returns `false` for non-suckers. It does **not** return the default `true`. Both the custom data hook and 721 hook are checked — either one can grant permission.
90
91
  9. 721 ruleset config conversion enforces `useDataHookForPay = true` and `allowSetCustomToken = false`. These cannot be overridden.
91
92
  10. Hook ownership is transferred to the **project** (not the owner) via `JBOwnable.transferOwnershipToProject(projectId)`. The project owner controls the hook through project ownership.
92
93
  11. The deployer holds the project NFT temporarily during launch. If the controller's `launchProjectFor` reverts, the entire transaction reverts -- no stuck NFTs.
@@ -95,6 +96,11 @@ Single-transaction deployment of Juicebox projects with cross-chain suckers and
95
96
  14. Setting a ruleset's `dataHook` to `address(this)` (the deployer itself) reverts with `JBOmnichainDeployer_InvalidHook`. This prevents infinite forwarding loops.
96
97
  15. `onERC721Received` only accepts NFTs from the `PROJECTS` contract. Sending any other ERC-721 to the deployer will revert.
97
98
  16. ERC2771 meta-transaction support allows gasless deployments via a trusted forwarder. Salt hashing uses `_msgSender()` (not `msg.sender`), so forwarder-relayed transactions use the original sender's address for deterministic sucker addresses.
99
+ 17. **Prefer `launch721ProjectFor` over `launchProjectFor` even with empty tiers.** Using `launch721ProjectFor` with an empty tiers array wires up the 721 hook from the start, so the project owner can add and sell NFTs later without needing to reconfigure the data hook in a new ruleset. `launchProjectFor` skips hook deployment entirely.
100
+ 18. The 721 hook is stored **per-project** in `tiered721HookOf[projectId]`, not per-ruleset. It persists across all rulesets. The custom data hook is stored **per-ruleset** in `_dataHookOf[projectId][rulesetId]`.
101
+ 19. For payments, `beforePayRecordedWith` calls the 721 hook first to get its specs (including split fund amounts and tier metadata), then calls the custom data hook with a reduced amount context (payment minus split amount) so the buyback hook only considers the available amount. The deployer then adjusts the weight proportionally for splits (`weight = mulDiv(weight, amount - splitAmount, amount)`). The 721 hook's specs come first in the merged result.
102
+ 20. For cash outs, the 721 hook **takes priority** over the custom data hook. If a 721 hook exists, `beforeCashOutRecordedWith` delegates entirely to it, ignoring the custom data hook.
103
+ 21. The `launch721*` and `queue721*` functions now accept a `dataHook` parameter (type `address`) for the custom data hook to compose alongside the 721 hook. Pass `address(0)` for no custom hook.
98
104
 
99
105
  ## Example Integration
100
106
 
@@ -133,15 +139,17 @@ address[] memory newSuckers = omnichainDeployer.deploySuckersFor({
133
139
  suckerDeploymentConfiguration: suckerConfig
134
140
  });
135
141
 
136
- // --- Queue new rulesets with a 721 hook ---
142
+ // --- Queue new rulesets with a 721 hook + buyback hook ---
137
143
 
138
144
  // Requires QUEUE_RULESETS permission. Must be called in a different block
139
145
  // than any previous ruleset queue for this project.
146
+ // Pass the buyback hook as the custom data hook to compose alongside the 721 hook.
140
147
  (uint256 rulesetId, IJB721TiersHook hook) = omnichainDeployer.queue721RulesetsOf({
141
148
  projectId: projectId,
142
149
  deployTiersHookConfig: tiersHookConfig,
143
150
  queueRulesetsConfig: queueConfig,
144
151
  controller: controller,
152
+ dataHook: address(buybackHook), // custom data hook (or address(0) for none)
145
153
  salt: bytes32("my-hook-salt")
146
154
  });
147
155
  ```