@bananapus/suckers-v6 0.0.73 → 0.0.75

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/suckers-v6",
3
- "version": "0.0.73",
3
+ "version": "0.0.75",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,8 +27,8 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@arbitrum/nitro-contracts": "3.2.0",
30
- "@bananapus/core-v6": "^0.0.82",
31
- "@bananapus/permission-ids-v6": "^0.0.30",
30
+ "@bananapus/core-v6": "^0.0.86",
31
+ "@bananapus/permission-ids-v6": "^0.0.31",
32
32
  "@chainlink/contracts-ccip": "1.6.4",
33
33
  "@openzeppelin/contracts": "5.6.1",
34
34
  "@prb/math": "4.1.2",
@@ -37,8 +37,8 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@chainlink/local": "0.2.9",
40
+ "@sphinx-labs/plugins": "0.33.3",
40
41
  "@uniswap/v3-core": "1.0.1",
41
- "@uniswap/v3-periphery": "1.4.4",
42
- "@sphinx-labs/plugins": "0.33.3"
42
+ "@uniswap/v3-periphery": "1.4.4"
43
43
  }
44
44
  }
@@ -0,0 +1,111 @@
1
+ # Sucker Entry Points
2
+
3
+ Use this file when you already know the task is in `nana-suckers-v6` and need the concrete contract/function surface to open next.
4
+
5
+ > V6 is testnet-only. Deployed sucker, registry, and deployer addresses live in `deploy-all-v6`'s address output, not here.
6
+
7
+ ## Purpose
8
+
9
+ A sucker bridges a Juicebox project's token economy between two chains. Cashed-out positions are committed to a local outbox merkle tree, the root is relayed to the peer sucker on the remote chain, and beneficiaries prove inclusion against the inbox root to recreate their position on the destination chain. The `JBSucker` base owns the shared flow; chain-specific subclasses (`JBArbitrumSucker`, `JBOptimismSucker`, `JBCCIPSucker`, `JBBaseSucker`) own transport delivery and verification.
10
+
11
+ ## Contracts
12
+
13
+ | Contract | Role |
14
+ |----------|------|
15
+ | `JBSucker` | Abstract base. Owns the prepare/relay/claim/token-mapping/deprecation/emergency flow and dual merkle (outbox/inbox) state. |
16
+ | `JBArbitrumSucker` | Arbitrum bridge transport. |
17
+ | `JBOptimismSucker` | OP Stack bridge transport. |
18
+ | `JBCCIPSucker` | Chainlink CCIP transport. |
19
+ | `JBBaseSucker` | Base/OP Stack transport. |
20
+ | `JBSuckerRegistry` | Project-to-sucker inventory, deployer allowlist, shared `toRemote` fee, deprecation removal, cross-chain surplus/supply aggregation. |
21
+
22
+ `IJBSucker` is the minimal interface; `IJBSuckerExtended` adds the deprecation, emergency-hatch, and retained-fee surface.
23
+
24
+ ## Structs
25
+
26
+ ### `JBTokenMapping` (argument to `mapToken` / `mapTokens`)
27
+
28
+ | Field | Type | Meaning |
29
+ |-------|------|---------|
30
+ | `localToken` | `address` | The local token address. |
31
+ | `minGas` | `uint32` | The minimum gas to use when bridging this token. |
32
+ | `remoteToken` | `bytes32` | The remote token address (bytes32 for cross-VM compatibility). |
33
+
34
+ ### `JBClaim` (argument to `claim` / `exitThroughEmergencyHatch`)
35
+
36
+ | Field | Type | Meaning |
37
+ |-------|------|---------|
38
+ | `token` | `address` | The local terminal token to claim. |
39
+ | `leaf` | `JBLeaf` | The leaf to claim from (see below). |
40
+ | `proof` | `bytes32[32]` | The merkle proof. Must be of length `JBSucker._TREE_DEPTH` (32). |
41
+
42
+ ### `JBLeaf` (the `leaf` field of `JBClaim`)
43
+
44
+ | Field | Type | Meaning |
45
+ |-------|------|---------|
46
+ | `index` | `uint256` | The leaf's index in the tree. |
47
+ | `beneficiary` | `bytes32` | The beneficiary (bytes32 for cross-VM compatibility). |
48
+ | `projectTokenCount` | `uint256` | The number of project tokens to claim. |
49
+ | `terminalTokenAmount` | `uint256` | The amount of terminal tokens to claim. |
50
+ | `metadata` | `bytes32` | Opaque, caller-defined payload covered by the leaf hash. `bytes32(0)` when no extra context. |
51
+
52
+ ## Key functions
53
+
54
+ ### JBSucker — bridge flow (`IJBSucker`)
55
+
56
+ | Function | What it does |
57
+ |----------|--------------|
58
+ | `prepare(uint256 projectTokenCount, bytes32 beneficiary, uint256 minTokensReclaimed, address token, bytes32 metadata)` | Cash out `projectTokenCount` project tokens into `token` and insert a leaf for `beneficiary` into the outbox tree for bridging. `minTokensReclaimed` bounds slippage; `metadata` is an opaque attribution payload carried in the leaf hash (`bytes32(0)` for a plain bridge). |
59
+ | `toRemote(address token) payable` | Send the current outbox tree root and bridged assets for `token` to the remote peer through the chain-specific transport. `payable` to fund the transport message and the registry's `toRemoteFee`. |
60
+ | `claim(JBClaim calldata claimData)` | Claim bridged project tokens for the leaf's beneficiary by proving inclusion against the inbox root. |
61
+ | `claim(JBClaim[] calldata claims)` | Claim multiple leaves in one call. Each leaf is routed through an external `this.claim` sub-call, so one failing leaf emits `ClaimFailed` and is reverted in isolation while the rest of the batch proceeds; the failed leaf stays claimable later. |
62
+ | `mapToken(JBTokenMapping calldata map) payable` | Map a single local token to a remote token for bridging. Mappings are immutable once the outbox tree has entries (can only be disabled, not remapped). Requires `MAP_SUCKER_TOKEN` permission (initial mappings are applied at deploy under `DEPLOY_SUCKERS`). |
63
+ | `mapTokens(JBTokenMapping[] calldata maps) payable` | Map multiple local tokens to remote tokens in one call. |
64
+
65
+ ### JBSucker — deprecation & emergency (`IJBSuckerExtended`)
66
+
67
+ | Function | What it does |
68
+ |----------|--------------|
69
+ | `setDeprecation(uint40 timestamp)` | Set or update the deprecation timestamp. Drives the `JBSuckerState` lifecycle: `ENABLED` → `DEPRECATION_PENDING` → `SENDING_DISABLED` → `DEPRECATED`. Requires `SET_SUCKER_DEPRECATION` permission. |
70
+ | `enableEmergencyHatchFor(address[] calldata tokens)` | Open the emergency hatch for the given tokens, allowing direct claims without bridging when transport is unavailable. Requires `SUCKER_SAFETY` permission (project owner). |
71
+ | `exitThroughEmergencyHatch(JBClaim calldata claimData)` | Claim a leaf directly through an open emergency hatch when bridging cannot complete. |
72
+ | `claimRetainedToRemoteFee(address payable beneficiary)` | Withdraw ETH from a `toRemote` fee payment that previously failed and was retained for the caller. |
73
+ | `claimRetainedTransportPaymentRefund(address payable beneficiary)` | Withdraw ETH from a transport-payment refund that previously failed and was retained for the caller. |
74
+
75
+ ### JBSucker — key views (`IJBSucker` / `IJBSuckerExtended`)
76
+
77
+ | Function | What it does |
78
+ |----------|--------------|
79
+ | `state()` | Returns the current `JBSuckerState` (deprecation lifecycle stage). |
80
+ | `peer()` | Returns the peer sucker address on the remote chain (bytes32). |
81
+ | `peerChainId()` | Returns the remote peer chain ID. |
82
+ | `projectId()` | Returns the local project ID this sucker serves. |
83
+ | `isMapped(address token)` | Whether a token has been mapped for bridging. |
84
+ | `remoteTokenFor(address token)` | The `JBRemoteToken` info a local token maps to. |
85
+ | `inboxOf(address token)` | The inbox merkle tree root (`JBInboxTreeRoot`) for a token. |
86
+ | `outboxOf(address token)` | The outbox merkle tree (`JBOutboxTree`) for a token. |
87
+ | `amountToAddToBalanceOf(address token)` | Tokens received from bridging that are waiting to be added to the project's terminal balance. |
88
+ | `executedLeafHashOf(address token, uint256 index)` | The committed leaf hash at `(token, index)`, or `bytes32(0)` if unexecuted. Beneficiary contracts re-derive this to authenticate a settlement that a front-runner's direct `claim` already executed. |
89
+ | `peerChainTotalSupply()` | The last-known peer-chain total token supply (used by data hooks to compute effective cross-chain supply). |
90
+ | `peerChainContextsOf()` | Per-context raw surplus and balance from the latest peer snapshot, with chain ID and freshness key. Un-valued (each context in its own currency/decimals). |
91
+ | `retainedToRemoteFeeOf(address account)` | ETH owed to `account` from a failed `toRemote` fee payment. |
92
+ | `retainedTransportPaymentRefundOf(address account)` | ETH owed to `account` from a failed transport-payment refund. |
93
+
94
+ ### JBSuckerRegistry (`IJBSuckerRegistry`)
95
+
96
+ | Function | What it does |
97
+ |----------|--------------|
98
+ | `deploySuckersFor(uint256 projectId, bytes32 salt, JBSuckerDeployerConfig[] calldata configurations)` | Deploy one or more suckers for a project and apply each config's initial token mappings. Requires `DEPLOY_SUCKERS`. Returns the deployed sucker addresses. |
99
+ | `removeDeprecatedSucker(uint256 projectId, address sucker)` | Remove a fully deprecated sucker from a project's inventory. |
100
+ | `allowSuckerDeployer(address deployer)` / `allowSuckerDeployers(address[] calldata deployers)` | Add deployer(s) to the allowlist. Owner-only. |
101
+ | `removeSuckerDeployer(address deployer)` | Remove a deployer from the allowlist. Owner-only. |
102
+ | `setToRemoteFee(uint256 fee)` | Set the ETH fee (wei) paid into the fee project on each `toRemote` call. Reverts if `fee > MAX_TO_REMOTE_FEE`. Owner-only. |
103
+ | `suckersOf(uint256 projectId)` | All active suckers for a project. |
104
+ | `allSuckersOf(uint256 projectId)` | Every sucker ever registered for a project, including deprecated ones. |
105
+ | `suckerPairsOf(uint256 projectId)` | The local/remote sucker pairs (`JBSuckersPair[]`) for a project. |
106
+ | `isSuckerOf(uint256 projectId, address addr)` | Whether `addr` is a registry-deployed sucker for the project. |
107
+ | `suckerDeployerIsAllowed(address deployer)` | Whether a deployer is on the allowlist. |
108
+ | `remoteTotalSupplyOf(uint256 projectId)` | Combined peer-chain total supply across all remote chains (dedups same-peer suckers by freshest snapshot). |
109
+ | `totalRemoteSurplusOf(uint256 projectId, uint256 currency, uint256 decimals)` | Combined peer-chain surplus valued into `currency`. Matching-currency contexts taken at par; missing cross-currency feed skips that sucker (conservative). |
110
+ | `totalRemoteBalanceOf(uint256 projectId, uint256 currency, uint256 decimals)` | Combined peer-chain balance valued into `currency`, same valuation rules as above. |
111
+ | `toRemoteFee()` / `MAX_TO_REMOTE_FEE()` | The current `toRemote` fee and its hardcoded ceiling. |
package/src/JBSucker.sol CHANGED
@@ -143,6 +143,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
143
143
  /// @notice Thrown when `msg.value` is sent for an action that expects none.
144
144
  error JBSucker_UnexpectedMsgValue(uint256 value);
145
145
 
146
+ /// @notice Thrown when an ERC-20 terminal balance does not decrease by the amount added to the project balance.
147
+ error JBSucker_UnexpectedTokenBalance(address token, uint256 expectedBalance, uint256 actualBalance);
148
+
146
149
  /// @notice Thrown when a required beneficiary address is the zero address.
147
150
  error JBSucker_ZeroBeneficiary(bytes32 beneficiary);
148
151
 
@@ -648,42 +651,34 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
648
651
  /// @param maps A list of local and remote terminal token addresses to map, and minimum amount/gas limits for
649
652
  /// bridging them.
650
653
  function mapTokens(JBTokenMapping[] calldata maps) external payable override {
651
- uint256 numberToDisable;
654
+ uint256 disableCandidates;
652
655
 
653
- // Loop over the number of mappings and increase numberToDisable to correctly set transportPaymentValue.
654
- // Note: if all mappings are enable-only (no disables), `numberToDisable` stays 0 and `transportPaymentValue`
655
- // is set to 0 for each call. Any ETH sent with the transaction is refunded after the second loop.
656
+ // Count mappings that currently need a final outbox flush. This is an upper bound because duplicated disables
657
+ // in the same batch can become no-ops after the first one updates `numberOfClaimsSent`.
656
658
  for (uint256 h; h < maps.length;) {
657
659
  JBOutboxTree storage _outbox = _outboxOf[maps[h].localToken];
658
660
  if (maps[h].remoteToken == bytes32(0) && _outbox.numberOfClaimsSent != _outbox.tree.count) {
659
- numberToDisable++;
661
+ ++disableCandidates;
660
662
  }
661
663
  unchecked {
662
664
  ++h;
663
665
  }
664
666
  }
665
667
 
666
- // Perform each token mapping.
668
+ // Split the attached value across disable candidates, then refund any value not actually used by a final
669
+ // outbox flush. Enable-only and duplicate/no-op disable entries do not consume transport payment.
670
+ uint256 transportPaymentValue = disableCandidates == 0 ? 0 : msg.value / disableCandidates;
671
+ uint256 transportPaymentSpent;
667
672
  for (uint256 i; i < maps.length;) {
668
- _mapToken({map: maps[i], transportPaymentValue: numberToDisable > 0 ? msg.value / numberToDisable : 0});
673
+ transportPaymentSpent += _mapToken({map: maps[i], transportPaymentValue: transportPaymentValue});
669
674
  unchecked {
670
675
  ++i;
671
676
  }
672
677
  }
673
678
 
674
- // If no tokens were disabled, the full `msg.value` is unused refund it.
675
- if (numberToDisable == 0) {
676
- if (msg.value > 0) {
677
- _sendNativeTo({beneficiary: payable(_msgSender()), amount: msg.value});
678
- }
679
- } else {
680
- // Refund any remainder from integer division so dust wei isn't stuck in the contract.
681
- uint256 remainder = msg.value % numberToDisable;
682
- if (remainder > 0) {
683
- // Best-effort refund — don't revert if caller can't accept ETH.
684
- (bool _ok,) = _msgSender().call{value: remainder}("");
685
- _ok; // Silence unused-variable warning; failure is intentionally ignored.
686
- }
679
+ // Return enable-only value, duplicate/no-op disable value, and integer-division dust to the caller.
680
+ if (msg.value > transportPaymentSpent) {
681
+ _sendNativeTo({beneficiary: payable(_msgSender()), amount: msg.value - transportPaymentSpent});
687
682
  }
688
683
  }
689
684
 
@@ -1123,8 +1118,14 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1123
1118
  });
1124
1119
 
1125
1120
  if (isErc20) {
1126
- // Sanity check: catches fee-on-transfer / non-conforming ERC-20s that move less than `amount`.
1127
- assert(IERC20(token).balanceOf(address(this)) == balanceBefore - amount);
1121
+ // The terminal must pull exactly `amount`; fee-on-transfer or non-conforming tokens are unsupported.
1122
+ uint256 expectedBalance = balanceBefore - amount;
1123
+ uint256 actualBalance = IERC20(token).balanceOf(address(this));
1124
+ if (actualBalance != expectedBalance) {
1125
+ revert JBSucker_UnexpectedTokenBalance({
1126
+ token: token, expectedBalance: expectedBalance, actualBalance: actualBalance
1127
+ });
1128
+ }
1128
1129
  }
1129
1130
  }
1130
1131
 
@@ -1229,7 +1230,14 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1229
1230
  /// @param map The local and remote terminal token addresses to map, and minimum amount/gas limits for bridging
1230
1231
  /// them.
1231
1232
  /// @param transportPaymentValue The amount of `msg.value` to send for the token mapping.
1232
- function _mapToken(JBTokenMapping calldata map, uint256 transportPaymentValue) internal {
1233
+ /// @return transportPaymentSpent The amount of transport payment used by a final outbox flush.
1234
+ function _mapToken(
1235
+ JBTokenMapping calldata map,
1236
+ uint256 transportPaymentValue
1237
+ )
1238
+ internal
1239
+ returns (uint256 transportPaymentSpent)
1240
+ {
1233
1241
  address token = map.localToken;
1234
1242
  JBRemoteToken memory currentMapping = _remoteTokenFor[token];
1235
1243
 
@@ -1288,6 +1296,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
1288
1296
  // _sendRoot uses the `currentMapping` parameter, not storage, so this is safe.
1289
1297
  _remoteTokenFor[token].enabled = false;
1290
1298
  _sendRoot({transportPayment: transportPaymentValue, token: token, remoteToken: currentMapping});
1299
+ transportPaymentSpent = transportPaymentValue;
1291
1300
  }
1292
1301
 
1293
1302
  // Update the reverse reservation if an unused local token is being remapped to a new remote token.
@@ -45,8 +45,19 @@ library JBRelayBeneficiary {
45
45
  return beneficiary;
46
46
  }
47
47
 
48
- // Decode the relay beneficiary address.
49
- address relayBeneficiary = abi.decode(data, (address));
48
+ // Load the first metadata word directly so malformed trailing bytes still fall back instead of reverting the
49
+ // surrounding payment.
50
+ uint256 relayBeneficiaryWord;
51
+ assembly ("memory-safe") {
52
+ relayBeneficiaryWord := mload(add(data, 32))
53
+ }
54
+ if (relayBeneficiaryWord > type(uint160).max) {
55
+ return beneficiary;
56
+ }
57
+
58
+ // The range guard above ensures the address cast cannot truncate.
59
+ // forge-lint: disable-next-line(unsafe-typecast)
60
+ address relayBeneficiary = address(uint160(relayBeneficiaryWord));
50
61
  if (relayBeneficiary == address(0)) {
51
62
  return beneficiary;
52
63
  }