@bananapus/omnichain-deployers-v6 0.0.61 → 0.0.62

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/omnichain-deployers-v6",
3
- "version": "0.0.61",
3
+ "version": "0.0.62",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -11,11 +11,14 @@
11
11
  3. On pay, it calls the 721 hook first, then the optional extra hook with the adjusted amount context.
12
12
  4. On cash out, it can short-circuit for suckers, otherwise it forwards into the configured hook stack in order.
13
13
  5. Mint permission queries can be granted by suckers or by the configured extra hook.
14
+ 6. Peer-chain adjusted account queries forward to the configured extra hook, but missing or malformed returns are
15
+ treated as no contribution.
14
16
 
15
17
  ## High-risk areas
16
18
 
17
19
  - Ruleset ID prediction: if the predicted ID is wrong, hook config can be stored under the wrong key.
18
20
  - Hook composition order: 721 logic runs before any extra hook, which affects both specs and accounting.
21
+ - Hook metadata decoding: split-credit metadata must satisfy the full ABI tuple minimum before it is decoded.
19
22
  - Sucker exemptions: early-return cash-out behavior is intentional and should not be removed casually.
20
23
  - Carry-forward logic: queueing rulesets without new tiers intentionally reuses the latest 721 hook.
21
24
  - Meta-transaction sender handling: salt derivation uses `_msgSender()`, not raw `msg.sender`.
@@ -557,7 +557,8 @@ contract JBOmnichainDeployer is
557
557
  // When issueTokensForSplits is true and splits exist, this holds the weight portion
558
558
  // attributable to tier splits — used to prevent split credit erasure if the extra
559
559
  // hook (e.g. buyback) returns weight=0.
560
- if (tiered721HookSpec.metadata.length >= 128) {
560
+ // The tuple's minimum ABI encoding is 160 bytes: 4 head words plus an empty `bytes` tail.
561
+ if (tiered721HookSpec.metadata.length >= 160) {
561
562
  (,,, splitCreditWeight) = abi.decode(tiered721HookSpec.metadata, (address, address, bytes, uint256));
562
563
  }
563
564
  }
@@ -709,12 +710,9 @@ contract JBOmnichainDeployer is
709
710
  (bool success, bytes memory data) = address(extraHook.dataHook)
710
711
  .staticcall(abi.encodeCall(IJBPeerChainAdjustedAccounts.peerChainAdjustedAccountsOf, (projectId)));
711
712
 
712
- // A well-formed `(uint256, JBSourceContext[])` return is at least three words: the supply, the array offset,
713
- // and the array length. Anything shorter (an empty return, a hook with no code, or a mismatched ABI) is
714
- // treated as no contribution rather than letting the decode revert.
715
- if (!success || data.length < 96) return (0, new JBSourceContext[](0));
713
+ if (!success) return (0, new JBSourceContext[](0));
716
714
 
717
- return abi.decode(data, (uint256, JBSourceContext[]));
715
+ return _peerChainAdjustedAccountsFrom(data);
718
716
  }
719
717
 
720
718
  //*********************************************************************//
@@ -1058,6 +1056,98 @@ contract JBOmnichainDeployer is
1058
1056
  return ERC2771Context._msgSender();
1059
1057
  }
1060
1058
 
1059
+ /// @notice Decodes a peer-chain adjusted accounting return, falling back to no contribution if malformed.
1060
+ /// @param data The raw return data from an extra hook's `peerChainAdjustedAccountsOf` call.
1061
+ /// @return supply The extra supply to include in `sourceTotalSupply`.
1062
+ /// @return contexts The extra per-context surplus and balance to include in the snapshot, un-valued.
1063
+ function _peerChainAdjustedAccountsFrom(bytes memory data)
1064
+ internal
1065
+ pure
1066
+ returns (uint256 supply, JBSourceContext[] memory contexts)
1067
+ {
1068
+ // `data` is a Solidity `bytes` value. Its first memory word is the byte length, and the ABI return payload
1069
+ // starts at `data + 32`.
1070
+ //
1071
+ // The payload for `(uint256, JBSourceContext[])` is:
1072
+ // word 0: supply
1073
+ // word 1: offset to the dynamic `contexts` array tail, relative to the payload start
1074
+ // tail word 0: contexts.length
1075
+ // tail words: each `JBSourceContext`, encoded as 4 ABI words.
1076
+ //
1077
+ // Anything shorter than the two tuple head words plus the array-length word cannot be decoded safely.
1078
+ if (data.length < 96) return (0, new JBSourceContext[](0));
1079
+
1080
+ uint256 contextsOffset;
1081
+ assembly ("memory-safe") {
1082
+ // Skip the `bytes` length word, then read the first two ABI words from the payload head.
1083
+ supply := mload(add(data, 32))
1084
+ contextsOffset := mload(add(data, 64))
1085
+ }
1086
+
1087
+ // The array tail must begin after the two-word tuple head, remain ABI-word aligned, and leave room for its own
1088
+ // length word. If the offset points into the head, into the middle of a word, or past the buffer, a normal
1089
+ // `abi.decode` would revert. This wrapper instead treats the optional hook contribution as absent.
1090
+ if (contextsOffset < 64 || contextsOffset % 32 != 0 || contextsOffset > data.length - 32) {
1091
+ return (0, new JBSourceContext[](0));
1092
+ }
1093
+
1094
+ uint256 contextCount;
1095
+ assembly ("memory-safe") {
1096
+ // The offset is relative to the payload start (`data + 32`), not the start of the `bytes` object.
1097
+ contextCount := mload(add(add(data, 32), contextsOffset))
1098
+ }
1099
+
1100
+ // Skip the array-length word to reach the first encoded `JBSourceContext`.
1101
+ uint256 contextsStart = contextsOffset + 32;
1102
+ // Each `JBSourceContext` has four static ABI words: token, decimals, surplus, and balance. Check the count
1103
+ // against the remaining bytes before allocating the array so a hostile length cannot force a large allocation
1104
+ // or make the loop read past the returned buffer.
1105
+ if (contextCount > (data.length - contextsStart) / 128) return (0, new JBSourceContext[](0));
1106
+
1107
+ contexts = new JBSourceContext[](contextCount);
1108
+
1109
+ for (uint256 i; i < contextCount; i++) {
1110
+ // Move to the encoded struct for this index. The offset is still payload-relative.
1111
+ uint256 contextOffset = contextsStart + i * 128;
1112
+ bytes32 token;
1113
+ uint256 decimals;
1114
+ uint256 surplus;
1115
+ uint256 contextBalance;
1116
+
1117
+ assembly ("memory-safe") {
1118
+ // Point at the first word of this encoded struct and read its four ABI words directly.
1119
+ let contextPointer := add(add(data, 32), contextOffset)
1120
+ token := mload(contextPointer)
1121
+ decimals := mload(add(contextPointer, 32))
1122
+ surplus := mload(add(contextPointer, 64))
1123
+ contextBalance := mload(add(contextPointer, 96))
1124
+ }
1125
+
1126
+ // The ABI decoder would reject values that do not fit their declared Solidity types. Because this function
1127
+ // decodes manually, it must enforce the same bounds before casting so malformed data cannot silently
1128
+ // truncate into `uint8` or `uint128`.
1129
+ if (decimals > type(uint8).max || surplus > type(uint128).max || contextBalance > type(uint128).max) {
1130
+ return (0, new JBSourceContext[](0));
1131
+ }
1132
+
1133
+ // Store the checked values using the struct's real types. At this point every read was inside the buffer
1134
+ // and every narrowed cast has been proven safe.
1135
+ // Casting to `uint8` is safe because the guard above rejected larger values.
1136
+ // forge-lint: disable-next-line(unsafe-typecast)
1137
+ uint8 checkedDecimals = uint8(decimals);
1138
+ // Casting to `uint128` is safe because the guard above rejected larger values.
1139
+ // forge-lint: disable-next-line(unsafe-typecast)
1140
+ uint128 checkedSurplus = uint128(surplus);
1141
+ // Casting to `uint128` is safe because the guard above rejected larger values.
1142
+ // forge-lint: disable-next-line(unsafe-typecast)
1143
+ uint128 checkedBalance = uint128(contextBalance);
1144
+
1145
+ contexts[i] = JBSourceContext({
1146
+ token: token, decimals: checkedDecimals, surplus: checkedSurplus, balance: checkedBalance
1147
+ });
1148
+ }
1149
+ }
1150
+
1061
1151
  /// @notice Revert unless the trusted directory records `CONTROLLER` for `projectId`.
1062
1152
  /// @dev Use `allowUnset = true` as a pre-launch check: a fresh project with no controller wired yet is accepted.
1063
1153
  /// Use `allowUnset = false` as a post-launch check: `CONTROLLER` must be live in the directory.