@bananapus/router-terminal-v6 0.0.42 → 0.0.44

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/foundry.toml CHANGED
@@ -1,5 +1,6 @@
1
1
  [profile.default]
2
2
  solc = '0.8.28'
3
+ bytecode_hash = "none"
3
4
  evm_version = 'cancun'
4
5
  optimizer_runs = 200
5
6
  via_ir = true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/router-terminal-v6",
3
- "version": "0.0.42",
3
+ "version": "0.0.44",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,10 +26,10 @@
26
26
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-router-terminal-v6'"
27
27
  },
28
28
  "dependencies": {
29
- "@bananapus/buyback-hook-v6": "^0.0.36",
30
- "@bananapus/core-v6": "^0.0.48",
29
+ "@bananapus/buyback-hook-v6": "^0.0.47",
30
+ "@bananapus/core-v6": "^0.0.54",
31
31
  "@bananapus/permission-ids-v6": "^0.0.22",
32
- "@bananapus/univ4-router-v6": "^0.0.22",
32
+ "@bananapus/univ4-router-v6": "^0.0.32",
33
33
  "@openzeppelin/contracts": "5.6.1",
34
34
  "@uniswap/permit2": "github:Uniswap/permit2#cc56ad0f3439c502c246fc5cfcc3db92bb8b7219",
35
35
  "@uniswap/v3-core": "github:Uniswap/v3-core#6562c52e8f75f0c10f9deaf44861847585fc8129",
@@ -26,6 +26,8 @@ import {IJBForwardingTerminal} from "./interfaces/IJBForwardingTerminal.sol";
26
26
  import {IJBPayerTracker} from "./interfaces/IJBPayerTracker.sol";
27
27
  import {IJBRouterTerminalRegistry} from "./interfaces/IJBRouterTerminalRegistry.sol";
28
28
 
29
+ import {JBForwardingCheck} from "./libraries/JBForwardingCheck.sol";
30
+
29
31
  import {DefaultTerminalSegment} from "./structs/DefaultTerminalSegment.sol";
30
32
 
31
33
  /// @notice A forwarding layer that lets each project choose which router terminal receives its payments, with an
@@ -306,26 +308,45 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
306
308
  return ERC2771Context._msgSender();
307
309
  }
308
310
 
309
- /// @notice Reject terminal choices that would forward the project back into this registry.
311
+ /// @notice Returns the original payer to record in transient storage. If `_msgSender()` is a
312
+ /// contract that exposes `IJBPayerTracker.originalPayer()` and that getter returns a non-zero
313
+ /// value, the upstream payer is propagated so a forwarding chain (project payer -> registry
314
+ /// -> router) refunds the true originator instead of the intermediary. Otherwise the direct
315
+ /// caller is recorded — the common direct-call case.
316
+ /// @return originalPayerOrSender The upstream payer if the resolved sender is a forwarding
317
+ /// tracker that returns a non-zero address; otherwise the resolved sender itself.
318
+ function _originalPayerOrSender() internal view returns (address originalPayerOrSender) {
319
+ // Resolve through ERC-2771 so a trusted meta-tx forwarder is transparently unwrapped.
320
+ address sender = _msgSender();
321
+
322
+ // EOAs and contracts without code can't implement IJBPayerTracker — record them directly.
323
+ if (sender.code.length == 0) return sender;
324
+
325
+ // Probe the caller for IJBPayerTracker.originalPayer() via staticcall so a reverting or
326
+ // non-conformant caller does not bubble up — fall back to the resolved sender on failure.
327
+ (bool ok, bytes memory data) = sender.staticcall(abi.encodeWithSelector(IJBPayerTracker.originalPayer.selector));
328
+
329
+ // Caller doesn't implement the interface (revert) or returned a truncated payload — treat
330
+ // it as a direct payment from the caller itself.
331
+ if (!ok || data.length < 32) return sender;
332
+
333
+ // Decode the upstream payer the caller advertised. A zero value means "no upstream tracked",
334
+ // which only happens when the caller is itself receiving a direct call — record the caller.
335
+ address upstream = abi.decode(data, (address));
336
+ return upstream == address(0) ? sender : upstream;
337
+ }
338
+
339
+ /// @notice Reject terminal choices that would forward the project back into this registry,
340
+ /// directly or transitively. Walks up to the depth limit `JBForwardingCheck` enforces so a
341
+ /// chain registry -> A -> B -> registry is caught (the previous one-hop probe missed those
342
+ /// transitive cycles, letting a project lock itself into a route that always loops).
310
343
  /// @param projectId The project to validate forwarding for.
311
344
  /// @param terminal The terminal to validate.
312
345
  function _requireNonCircularTerminalFor(uint256 projectId, IJBTerminal terminal) internal view {
313
346
  // Reject direct self-selection so the registry cannot forward a project to itself.
314
347
  if (address(terminal) == address(this)) revert JBRouterTerminalRegistry_CircularForward(terminal);
315
-
316
- // Externally owned accounts cannot implement `terminalOf`, so there is no forwarding route to inspect.
317
- if (address(terminal).code.length == 0) return;
318
-
319
- // If the candidate is another forwarding terminal, ask where this project would end up.
320
- try IJBForwardingTerminal(address(terminal)).terminalOf({projectId: projectId}) returns (
321
- IJBTerminal downstreamTerminal
322
- ) {
323
- // Reject one-hop forwarding cycles that bounce this project back into the registry.
324
- if (address(downstreamTerminal) == address(this)) {
325
- revert JBRouterTerminalRegistry_CircularForward({terminal: terminal});
326
- }
327
- } catch {
328
- // Non-forwarding terminals are valid choices; failed interface probes should not block them.
348
+ if (JBForwardingCheck.isCircularTerminal({target: address(this), projectId: projectId, terminal: terminal})) {
349
+ revert JBRouterTerminalRegistry_CircularForward({terminal: terminal});
329
350
  }
330
351
  }
331
352
 
@@ -409,8 +430,9 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
409
430
  address previousPayer = originalPayer;
410
431
 
411
432
  // Store the original payer in transient storage so downstream router terminals can refund partial-fill
412
- // leftovers to the true payer.
413
- originalPayer = _msgSender();
433
+ // leftovers to the true payer. If the immediate caller is itself a forwarding intermediary that exposes
434
+ // its own original payer, propagate that — otherwise refunds in nested forwards stop at the intermediary.
435
+ originalPayer = _originalPayerOrSender();
414
436
 
415
437
  // Reject forwards that would bounce straight back into this call's immediate caller.
416
438
  _enforceNoCircularForward(terminal);
@@ -559,8 +581,9 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
559
581
  address previousPayer = originalPayer;
560
582
 
561
583
  // Store the original payer in transient storage so downstream router terminals can refund partial-fill
562
- // leftovers to the true payer.
563
- originalPayer = _msgSender();
584
+ // leftovers to the true payer. If the immediate caller is itself a forwarding intermediary that exposes
585
+ // its own original payer, propagate that — otherwise refunds in nested forwards stop at the intermediary.
586
+ originalPayer = _originalPayerOrSender();
564
587
 
565
588
  // Reject forwards that would bounce straight back into this call's immediate caller.
566
589
  _enforceNoCircularForward(terminal);
@@ -598,6 +621,11 @@ contract JBRouterTerminalRegistry is IJBRouterTerminalRegistry, JBPermissioned,
598
621
 
599
622
  uint256 count = PROJECTS.count();
600
623
 
624
+ // Reject defaults that would route any current project back into the registry through a
625
+ // transitive forwarding chain. Probed at the current project-count snapshot since the
626
+ // default is what unconfigured (and all-future) projects will resolve to.
627
+ _requireNonCircularTerminalFor({projectId: count, terminal: terminal});
628
+
601
629
  // Snapshot the OUTGOING default so existing projects (ID <= count) continue resolving to
602
630
  // it, not to the new default. First-ever call has defaultTerminal == 0 and nothing to snapshot.
603
631
  if (address(defaultTerminal) != address(0)) {