@bananapus/suckers-v6 0.0.49 → 0.0.50

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.
package/CHANGELOG.md CHANGED
@@ -15,6 +15,41 @@ This file describes the verified change from `nana-suckers-v5` to the current `n
15
15
  - `JBCeloSucker`
16
16
  - the deployers, structs, and interfaces under `src/`
17
17
 
18
+ ## Unreleased — `JBLeaf.metadata` attribution field
19
+
20
+ The merkle leaf now carries a fifth field: a `bytes32 metadata` payload that travels inside the leaf hash but is
21
+ opaque to the sucker protocol itself.
22
+
23
+ **What changed**
24
+
25
+ - `JBLeaf` struct: new trailing `bytes32 metadata` field.
26
+ - `IJBSucker.prepare`: new trailing `bytes32 metadata` parameter. The metadata is included in the leaf hash, so it's
27
+ covered by the merkle root — receivers can trust it once the claim's merkle proof verifies.
28
+ - `_buildTreeHash` hashes 128 bytes (was 96): `keccak256(projectTokenCount || terminalTokenAmount || beneficiary || metadata)`.
29
+ - `_insertIntoTree`, `_validate`, `_validateBranchRoot`, `_validateForEmergencyExit` all thread `metadata` through.
30
+ - Events `InsertToOutboxTree` and `Claimed` carry the field so off-chain indexers can read it directly without
31
+ cracking the leaf.
32
+ - SVM leaf encoding widens from 96 bytes to 128 bytes; the new 32-byte suffix is the `metadata` field. The
33
+ `_svmBuildTreeHash` interop test mirrors the layout exactly so EVM↔SVM hash equality is preserved.
34
+
35
+ **Intended use**
36
+
37
+ The original motivator is the cross-chain referral split hook (`nana-referral-split-hook-v6`): when a referrer
38
+ on chain Y earns credit for fee-paying activity on chain X, the hook on X uses the fee project's sucker to
39
+ bridge the entitled fee-project tokens. The leaf's `metadata` carries `(originChainId, referralProjectId)` so the
40
+ sibling hook on chain Y can atomically claim, re-pay the fee project locally, and push to the local distributor
41
+ for the right referrer — all under the merkle proof's authentication, no off-chain coordination needed.
42
+
43
+ The field is generic: any future leaf consumer (NFT split hooks, buyback hooks, etc.) can use it for its own
44
+ attribution scheme without further sucker changes. Pass `bytes32(0)` for ordinary bridges that don't need it.
45
+
46
+ **Risk surface**
47
+
48
+ Zero new trust paths. The bridge protocol stays leaf-in-leaf-out; we just put one more 32-byte field under the
49
+ same root. Existing claim, emergency-exit, and root-relay flows behave identically when `metadata == bytes32(0)`.
50
+ The leaf-hash domain changes (96 → 128 bytes), but since nothing is deployed yet there's no on-chain
51
+ compatibility concern.
52
+
18
53
  ## 0.0.46 — Bump nana-core-v6 to 0.0.53
19
54
 
20
55
  `@bananapus/core-v6@0.0.53` ([nana-core-v6 PR #145](https://github.com/Bananapus/nana-core-v6/pull/145)) drops the `via_ir` requirement on `JBCashOutHookSpecsLib`, which lets this package consume the cross-project cashout work (`payAfterCashOutTokensOf` / `addToBalanceAfterCashOutTokensOf`) without needing `via_ir = true` in its own foundry profile.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/suckers-v6",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@arbitrum/nitro-contracts": "3.2.0",
33
- "@bananapus/core-v6": "^0.0.57",
33
+ "@bananapus/core-v6": "^0.0.59",
34
34
  "@bananapus/permission-ids-v6": "^0.0.26",
35
35
  "@chainlink/contracts-ccip": "1.6.4",
36
36
  "@chainlink/local": "0.2.7",
package/src/JBSucker.sol CHANGED
@@ -294,12 +294,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
294
294
  /// project tokens for the beneficiary, and deposits the terminal tokens into the project's local balance.
295
295
  /// @param claimData The terminal token, merkle tree leaf, and proof for the claim.
296
296
  function claim(JBClaim calldata claimData) public virtual override {
297
- // Attempt to validate the proof against the inbox tree for the terminal token.
297
+ // Attempt to validate the proof against the inbox tree for the terminal token. The leaf hash includes
298
+ // `claimData.leaf.metadata` so the proof is only valid for the exact (amount, beneficiary, metadata) tuple the
299
+ // origin committed to.
298
300
  _validate({
299
301
  projectTokenCount: claimData.leaf.projectTokenCount,
300
302
  terminalToken: claimData.token,
301
303
  terminalTokenAmount: claimData.leaf.terminalTokenAmount,
302
304
  beneficiary: claimData.leaf.beneficiary,
305
+ metadata: claimData.leaf.metadata,
303
306
  index: claimData.leaf.index,
304
307
  leaves: claimData.proof
305
308
  });
@@ -310,6 +313,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
310
313
  projectTokenCount: claimData.leaf.projectTokenCount,
311
314
  terminalTokenAmount: claimData.leaf.terminalTokenAmount,
312
315
  index: claimData.leaf.index,
316
+ metadata: claimData.leaf.metadata,
313
317
  caller: _msgSender()
314
318
  });
315
319
 
@@ -330,7 +334,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
330
334
  uint256 _projectId = projectId();
331
335
 
332
336
  _requirePermissionFrom({
333
- account: PROJECTS.ownerOf(_projectId), projectId: _projectId, permissionId: JBPermissionIds.SUCKER_SAFETY
337
+ account: _ownerOf(_projectId), projectId: _projectId, permissionId: JBPermissionIds.SUCKER_SAFETY
334
338
  });
335
339
 
336
340
  // Enable the emergency hatch for each token.
@@ -352,12 +356,14 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
352
356
  /// @param claimData The terminal token, merkle tree leaf, and proof for the claim.
353
357
  function exitThroughEmergencyHatch(JBClaim calldata claimData) external override {
354
358
  // Does all the needed validation to ensure that the claim is valid *and* that claiming through the emergency
355
- // hatch is allowed.
359
+ // hatch is allowed. The leaf hash covers `metadata` so a remote-attribution leaf is only exitable if the
360
+ // emergency exiter knows the exact `metadata` value the origin committed to.
356
361
  _validateForEmergencyExit({
357
362
  projectTokenCount: claimData.leaf.projectTokenCount,
358
363
  terminalToken: claimData.token,
359
364
  terminalTokenAmount: claimData.leaf.terminalTokenAmount,
360
365
  beneficiary: claimData.leaf.beneficiary,
366
+ metadata: claimData.leaf.metadata,
361
367
  index: claimData.leaf.index,
362
368
  leaves: claimData.proof
363
369
  });
@@ -532,7 +538,8 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
532
538
  uint256 projectTokenCount,
533
539
  bytes32 beneficiary,
534
540
  uint256 minTokensReclaimed,
535
- address token
541
+ address token,
542
+ bytes32 metadata
536
543
  )
537
544
  external
538
545
  override
@@ -564,12 +571,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
564
571
  projectToken: projectToken, count: projectTokenCount, token: token, minTokensReclaimed: minTokensReclaimed
565
572
  });
566
573
 
567
- // Insert the item into the outbox tree for the terminal `token`.
574
+ // Insert the item into the outbox tree for the terminal `token`. The `metadata` field travels inside the leaf
575
+ // hash so receivers can read attribution context from a proven claim — the sucker protocol itself never
576
+ // inspects it.
568
577
  _insertIntoTree({
569
578
  projectTokenCount: projectTokenCount,
570
579
  token: token,
571
580
  terminalTokenAmount: terminalTokenAmount,
572
- beneficiary: beneficiary
581
+ beneficiary: beneficiary,
582
+ metadata: metadata
573
583
  });
574
584
  }
575
585
 
@@ -586,9 +596,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
586
596
 
587
597
  // The caller must be the project owner or have the `SET_SUCKER_DEPRECATION` permission from them.
588
598
  _requirePermissionFrom({
589
- account: PROJECTS.ownerOf(_projectId),
590
- projectId: _projectId,
591
- permissionId: JBPermissionIds.SET_SUCKER_DEPRECATION
599
+ account: _ownerOf(_projectId), projectId: _projectId, permissionId: JBPermissionIds.SET_SUCKER_DEPRECATION
592
600
  });
593
601
 
594
602
  // This is the earliest time for when the sucker can be considered deprecated.
@@ -996,16 +1004,20 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
996
1004
  uint256 projectTokenCount,
997
1005
  address token,
998
1006
  uint256 terminalTokenAmount,
999
- bytes32 beneficiary
1007
+ bytes32 beneficiary,
1008
+ bytes32 metadata
1000
1009
  )
1001
1010
  internal
1002
1011
  {
1003
1012
  // Guard against amounts that would overflow uint128 on SVM (INTEROP-5).
1004
1013
  if (terminalTokenAmount > type(uint128).max) revert JBSucker_AmountExceedsUint128(terminalTokenAmount);
1005
1014
  if (projectTokenCount > type(uint128).max) revert JBSucker_AmountExceedsUint128(projectTokenCount);
1006
- // Build a hash based on the token amounts and the beneficiary.
1015
+ // Build a hash based on the token amounts, the beneficiary, and the attribution metadata.
1007
1016
  bytes32 hashed = _buildTreeHash({
1008
- projectTokenCount: projectTokenCount, terminalTokenAmount: terminalTokenAmount, beneficiary: beneficiary
1017
+ projectTokenCount: projectTokenCount,
1018
+ terminalTokenAmount: terminalTokenAmount,
1019
+ beneficiary: beneficiary,
1020
+ metadata: metadata
1009
1021
  });
1010
1022
 
1011
1023
  // Get the outbox in storage.
@@ -1023,6 +1035,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1023
1035
  root: _computeOutboxRoot(outbox.tree),
1024
1036
  projectTokenCount: projectTokenCount,
1025
1037
  terminalTokenAmount: terminalTokenAmount,
1038
+ metadata: metadata,
1026
1039
  caller: _msgSender()
1027
1040
  });
1028
1041
  }
@@ -1064,7 +1077,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1064
1077
 
1065
1078
  // The registry can map during authorized deployment. Otherwise, require the project's mapping permission.
1066
1079
  _requirePermissionAllowingOverrideFrom({
1067
- account: PROJECTS.ownerOf(_projectId),
1080
+ account: _ownerOf(_projectId),
1068
1081
  projectId: _projectId,
1069
1082
  permissionId: JBPermissionIds.MAP_SUCKER_TOKEN,
1070
1083
  alsoGrantAccessIf: _msgSender() == address(REGISTRY)
@@ -1170,7 +1183,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1170
1183
  // Record the balance before the cash out for the sanity check.
1171
1184
  uint256 balanceBefore = _balanceOf({token: token, addr: address(this)});
1172
1185
 
1173
- // Cash out the project tokens for terminal tokens.
1186
+ // Cash out the project tokens for terminal tokens. Suckers are a transparent value-mover (the bridge
1187
+ // accounting is the entirety of their function) — they're not a fee-paying entry point for any referrer,
1188
+ // so `referralProjectId: 0` is correct.
1174
1189
  reclaimedAmount = terminal.cashOutTokensOf({
1175
1190
  holder: address(this),
1176
1191
  projectId: cachedProjectId,
@@ -1178,7 +1193,8 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1178
1193
  tokenToReclaim: token,
1179
1194
  minTokensReclaimed: minTokensReclaimed,
1180
1195
  beneficiary: payable(address(this)),
1181
- metadata: bytes("")
1196
+ metadata: bytes(""),
1197
+ referralProjectId: 0
1182
1198
  });
1183
1199
 
1184
1200
  // Sanity check to make sure we received the expected amount.
@@ -1289,6 +1305,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1289
1305
  address terminalToken,
1290
1306
  uint256 terminalTokenAmount,
1291
1307
  bytes32 beneficiary,
1308
+ bytes32 metadata,
1292
1309
  uint256 index,
1293
1310
  bytes32[_TREE_DEPTH] calldata leaves
1294
1311
  )
@@ -1312,6 +1329,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1312
1329
  projectTokenCount: projectTokenCount,
1313
1330
  terminalTokenAmount: terminalTokenAmount,
1314
1331
  beneficiary: beneficiary,
1332
+ metadata: metadata,
1315
1333
  index: index,
1316
1334
  leaves: leaves
1317
1335
  });
@@ -1331,6 +1349,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1331
1349
  uint256 projectTokenCount,
1332
1350
  uint256 terminalTokenAmount,
1333
1351
  bytes32 beneficiary,
1352
+ bytes32 metadata,
1334
1353
  uint256 index,
1335
1354
  bytes32[_TREE_DEPTH] calldata leaves
1336
1355
  )
@@ -1341,7 +1360,10 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1341
1360
  // Delegates to JBSuckerLib (via DELEGATECALL) to keep MerkleLib.branchRoot bytecode out of each sucker.
1342
1361
  bytes32 root = JBSuckerLib.computeBranchRoot({
1343
1362
  item: _buildTreeHash({
1344
- projectTokenCount: projectTokenCount, terminalTokenAmount: terminalTokenAmount, beneficiary: beneficiary
1363
+ projectTokenCount: projectTokenCount,
1364
+ terminalTokenAmount: terminalTokenAmount,
1365
+ beneficiary: beneficiary,
1366
+ metadata: metadata
1345
1367
  }),
1346
1368
  branch: leaves,
1347
1369
  index: index
@@ -1385,6 +1407,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1385
1407
  address terminalToken,
1386
1408
  uint256 terminalTokenAmount,
1387
1409
  bytes32 beneficiary,
1410
+ bytes32 metadata,
1388
1411
  uint256 index,
1389
1412
  bytes32[_TREE_DEPTH] calldata leaves
1390
1413
  )
@@ -1435,6 +1458,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1435
1458
  projectTokenCount: projectTokenCount,
1436
1459
  terminalTokenAmount: terminalTokenAmount,
1437
1460
  beneficiary: beneficiary,
1461
+ metadata: metadata,
1438
1462
  index: index,
1439
1463
  leaves: leaves
1440
1464
  });
@@ -1482,24 +1506,27 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1482
1506
  /// @param projectTokenCount The number of project tokens to cash out.
1483
1507
  /// @param terminalTokenAmount The amount of terminal tokens to reclaim from the cash out.
1484
1508
  /// @param beneficiary The beneficiary which will receive the project tokens (bytes32 for cross-VM compatibility).
1509
+ /// @param metadata Opaque caller-defined attribution payload travelling inside the leaf hash.
1485
1510
  /// @return hash The keccak256 hash of the leaf data.
1486
1511
  function _buildTreeHash(
1487
1512
  uint256 projectTokenCount,
1488
1513
  uint256 terminalTokenAmount,
1489
- bytes32 beneficiary
1514
+ bytes32 beneficiary,
1515
+ bytes32 metadata
1490
1516
  )
1491
1517
  internal
1492
1518
  pure
1493
1519
  returns (bytes32 hash)
1494
1520
  {
1495
- // All three arguments are 32 bytes — hash from free memory to avoid abi.encode allocation overhead.
1521
+ // All four arguments are 32 bytes — hash from free memory to avoid abi.encode allocation overhead.
1496
1522
  // forge-lint: disable-next-line(asm-keccak256)
1497
1523
  assembly {
1498
1524
  let ptr := mload(0x40)
1499
1525
  mstore(ptr, projectTokenCount)
1500
1526
  mstore(add(ptr, 0x20), terminalTokenAmount)
1501
1527
  mstore(add(ptr, 0x40), beneficiary)
1502
- hash := keccak256(ptr, 0x60)
1528
+ mstore(add(ptr, 0x60), metadata)
1529
+ hash := keccak256(ptr, 0x80)
1503
1530
  }
1504
1531
  }
1505
1532
 
@@ -1543,6 +1570,20 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1543
1570
  return ERC2771Context._msgSender();
1544
1571
  }
1545
1572
 
1573
+ /// @notice Resolve the current owner of the project this sucker belongs to.
1574
+ /// @dev `PROJECTS.ownerOf(...)` is the source of truth for "project owner" permission checks; we hit it from
1575
+ /// every permission-gated entrypoint (`enableEmergencyHatchFor`, `setDeprecation`, `_mapToken`). Routing all
1576
+ /// three through this internal helper emits the abi-encode + STATICCALL + return-decode sequence once in the
1577
+ /// child contract's bytecode instead of inlining it at each call site, which is what keeps `JBSwapCCIPSucker`
1578
+ /// under the EIP-170 limit after the leaf-`metadata` thread-through landed.
1579
+ /// @param forProjectId The project ID to look up — always the sucker's own `projectId()`, but accepted as a
1580
+ /// parameter so callers can pass the cached local they already computed (avoiding a redundant `projectId()`
1581
+ /// call against the read-only registry).
1582
+ /// @return owner The address currently registered as the project's ERC-721 holder.
1583
+ function _ownerOf(uint256 forProjectId) internal view returns (address owner) {
1584
+ return PROJECTS.ownerOf(forProjectId);
1585
+ }
1586
+
1546
1587
  /// @notice Retain a failed `toRemoteFee` payment for later caller refund.
1547
1588
  /// @param account The account that can reclaim the retained fee.
1548
1589
  /// @param amount The retained fee amount.
@@ -30,6 +30,7 @@ interface IJBSucker is IERC165 {
30
30
  uint256 projectTokenCount,
31
31
  uint256 terminalTokenAmount,
32
32
  uint256 index,
33
+ bytes32 metadata,
33
34
  address caller
34
35
  );
35
36
 
@@ -50,6 +51,7 @@ interface IJBSucker is IERC165 {
50
51
  bytes32 root,
51
52
  uint256 projectTokenCount,
52
53
  uint256 terminalTokenAmount,
54
+ bytes32 metadata,
53
55
  address caller
54
56
  );
55
57
 
@@ -191,11 +193,16 @@ interface IJBSucker is IERC165 {
191
193
  /// @param beneficiary The beneficiary on the remote chain (bytes32 for cross-VM compatibility).
192
194
  /// @param minTokensReclaimed The minimum terminal tokens to receive from the cash out.
193
195
  /// @param token The terminal token to cash out into.
196
+ /// @param metadata Opaque caller-defined attribution payload included in the leaf hash. The sucker protocol does
197
+ /// not inspect this value — it's covered by the merkle root, so the destination contract that consumes the claim
198
+ /// can
199
+ /// trust it once the proof verifies. Pass `bytes32(0)` for an ordinary bridge with no attribution context.
194
200
  function prepare(
195
201
  uint256 projectTokenCount,
196
202
  bytes32 beneficiary,
197
203
  uint256 minTokensReclaimed,
198
- address token
204
+ address token,
205
+ bytes32 metadata
199
206
  )
200
207
  external;
201
208
 
@@ -6,9 +6,15 @@ pragma solidity ^0.8.0;
6
6
  /// @custom:member beneficiary The beneficiary of the leaf.
7
7
  /// @custom:member projectTokenCount The number of project tokens to claim.
8
8
  /// @custom:member terminalTokenAmount The amount of terminal tokens to claim.
9
+ /// @custom:member metadata Opaque, caller-defined attribution payload that travels with the leaf inside the merkle
10
+ /// root. Use cases include cross-chain referral split hooks tagging a leaf with `(originChainId, referralProjectId)`
11
+ /// so the destination contract can settle the bridged value atomically. The sucker protocol itself never inspects
12
+ /// this field — it's covered by the leaf hash, so receivers can trust the value once the merkle proof verifies.
13
+ /// Pass `bytes32(0)` when no attribution context is needed.
9
14
  struct JBLeaf {
10
15
  uint256 index;
11
16
  bytes32 beneficiary;
12
17
  uint256 projectTokenCount;
13
18
  uint256 terminalTokenAmount;
19
+ bytes32 metadata;
14
20
  }