@bananapus/router-terminal-v6 0.0.19 → 0.0.21
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/ARCHITECTURE.md +2 -2
- package/AUDIT_INSTRUCTIONS.md +2 -2
- package/CHANGE_LOG.md +8 -1
- package/RISKS.md +2 -2
- package/SKILLS.md +2 -2
- package/STYLE_GUIDE.md +1 -1
- package/package.json +4 -3
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/RouterTerminalDeploymentLib.sol +1 -1
- package/src/JBRouterTerminal.sol +4 -4
- package/src/JBRouterTerminalRegistry.sol +1 -1
- package/src/libraries/JBSwapLib.sol +1 -1
- package/test/RouterTerminal.t.sol +1 -1
- package/test/RouterTerminalCashOutFork.t.sol +1 -1
- package/test/RouterTerminalCreditCashout.t.sol +1 -1
- package/test/RouterTerminalERC2771.t.sol +1 -1
- package/test/RouterTerminalFeeCashOutFork.t.sol +1 -1
- package/test/RouterTerminalFork.t.sol +1 -1
- package/test/RouterTerminalMultihopFork.t.sol +1 -1
- package/test/RouterTerminalPreviewFork.t.sol +1 -1
- package/test/RouterTerminalReentrancy.t.sol +1 -1
- package/test/RouterTerminalRegistry.t.sol +1 -1
- package/test/RouterTerminalSandwichFork.t.sol +1 -1
- package/test/TestAuditGaps.sol +1 -1
- package/test/audit/LeftoverRefund.t.sol +424 -0
- package/test/audit/PayerTrackerRefund.t.sol +3 -3
- package/test/audit/Permit2AllowanceFailed.t.sol +1 -1
- package/test/audit/RefundToBeneficiary.t.sol +104 -0
- package/test/audit/{CodexRegistryAddToBalancePartialFill.t.sol → RegistryAddToBalancePartialFill.t.sol} +2 -2
- package/test/fork/V4QuoteAndSettlementFork.t.sol +1 -1
- package/test/invariant/RouterTerminalInvariant.t.sol +1 -1
- package/test/regression/CashOutLoopLimit.t.sol +1 -1
- package/test/regression/LockTerminalRace.t.sol +1 -1
- package/test/regression/RouterTerminalEdgeCases.t.sol +1 -1
- package/test/regression/V4SpotPriceSlippage.t.sol +1 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -63,7 +63,7 @@ Payer → JBRouterTerminal.pay(projectId, token, amount)
|
|
|
63
63
|
│ │
|
|
64
64
|
│ ├─ If native ETH input: wrap any remaining raw ETH (partial fills)
|
|
65
65
|
│ ├─ If native ETH output: unwrap WETH → ETH (WETH.withdraw)
|
|
66
|
-
│ └─ Return leftover input tokens via
|
|
66
|
+
│ └─ Return leftover input tokens via _resolveRefundWithBackupRecipient (checks msg.sender's IJBPayerTracker.originalPayer() via try-catch, falls back to beneficiary for pay() or _msgSender() for addToBalanceOf())
|
|
67
67
|
│
|
|
68
68
|
├─ Approve destination terminal for output tokens (or set msg.value for native)
|
|
69
69
|
└─ Forward to destTerminal.pay() → return beneficiary token count
|
|
@@ -104,7 +104,7 @@ terminal source for loan sizing, debt normalization, or any other decimals-depen
|
|
|
104
104
|
|
|
105
105
|
**Why the registry pattern.** `JBRouterTerminalRegistry` lets project owners lock a specific `JBRouterTerminal` instance for their project and manage Permit2 approvals in one place. This provides a stable entry point: if the router implementation is upgraded, the registry can be pointed to the new instance without changing the project's directory entry. It also gates which router terminals are allowed, preventing untrusted implementations from being set.
|
|
106
106
|
|
|
107
|
-
**Why `IJBPayerTracker` is a separate interface.** The router terminal needs to know the original payer when called through an intermediary so it can return leftover tokens from partial swap fills to the right address. Rather than coupling the router to `IJBRouterTerminalRegistry` specifically, the refund resolution logic (`
|
|
107
|
+
**Why `IJBPayerTracker` is a separate interface.** The router terminal needs to know the original payer when called through an intermediary so it can return leftover tokens from partial swap fills to the right address. Rather than coupling the router to `IJBRouterTerminalRegistry` specifically, the refund resolution logic (`_resolveRefundWithBackupRecipient`) queries `IJBPayerTracker(msg.sender).originalPayer()` via a try-catch. This means any contract that implements `IJBPayerTracker` -- not just the registry -- can act as a forwarding intermediary. The registry inherits `IJBPayerTracker` through `IJBRouterTerminalRegistry`, keeping backward compatibility while opening the door for other intermediary patterns.
|
|
108
108
|
|
|
109
109
|
**Why liquidity-based pool selection instead of quote comparison.** Comparing actual output quotes across V3 and V4 would require executing (or simulating) swaps on both — expensive on-chain and complex for V4 where swaps must go through `PoolManager.unlock()`. Comparing in-range liquidity is a single `liquidity()` or `getLiquidity()` read per pool, is gas-cheap, and strongly correlates with execution quality for typical swap sizes.
|
|
110
110
|
|
package/AUDIT_INSTRUCTIONS.md
CHANGED
|
@@ -252,7 +252,7 @@ forge test
|
|
|
252
252
|
|
|
253
253
|
### Foundry Configuration
|
|
254
254
|
|
|
255
|
-
- Solidity
|
|
255
|
+
- Solidity 0.8.28 (0.8.28 for JBRouterTerminalRegistry), EVM target `cancun`, optimizer 200 runs
|
|
256
256
|
- Fuzz runs: 4,096 per test
|
|
257
257
|
- Invariant runs: 1,024 with depth 100
|
|
258
258
|
- Fork tests pinned to Ethereum mainnet block 21,700,000 (post-V4 deployment)
|
|
@@ -333,7 +333,7 @@ No prior formal audit with finding IDs has been conducted on this codebase. All
|
|
|
333
333
|
|
|
334
334
|
## Compiler and Version Info
|
|
335
335
|
|
|
336
|
-
- **Solidity**:
|
|
336
|
+
- **Solidity**: 0.8.28 (0.8.28 for JBRouterTerminalRegistry)
|
|
337
337
|
- **EVM target**: Cancun
|
|
338
338
|
- **Optimizer**: via-IR, 200 runs
|
|
339
339
|
- **Dependencies**: OpenZeppelin 5.x, Uniswap V3/V4, nana-core-v6
|
package/CHANGE_LOG.md
CHANGED
|
@@ -70,7 +70,7 @@ The `SLIPPAGE_DENOMINATOR` constant was kept but changed from `uint160` to `uint
|
|
|
70
70
|
|
|
71
71
|
### 1.11 Solidity Version
|
|
72
72
|
- **v5:** `pragma solidity 0.8.23`
|
|
73
|
-
- **v6:** `pragma solidity
|
|
73
|
+
- **v6:** `pragma solidity 0.8.28` (0.8.28 for JBRouterTerminalRegistry)
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
@@ -157,6 +157,13 @@ In v6, when a Permit2 allowance call fails during `_acceptFundsFor()`, an event
|
|
|
157
157
|
|
|
158
158
|
## 3. Event Changes
|
|
159
159
|
|
|
160
|
+
### 3.0 Indexer Notes
|
|
161
|
+
|
|
162
|
+
This repo is the direct replacement for v5 swap-terminal indexing:
|
|
163
|
+
- all registry event families moved from `JBSwapTerminalRegistry_*` to `JBRouterTerminalRegistry_*`;
|
|
164
|
+
- registry events now include `caller`;
|
|
165
|
+
- route discovery is dynamic, so do not assume one fixed output token or one manually-registered default pool per project.
|
|
166
|
+
|
|
160
167
|
### 3.1 Registry Events Renamed
|
|
161
168
|
All events were renamed from `JBSwapTerminalRegistry_*` to `JBRouterTerminalRegistry_*`.
|
|
162
169
|
|
package/RISKS.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
- **JBDirectory.** Terminal resolution trusts `DIRECTORY.primaryTerminalOf()` and `DIRECTORY.terminalsOf()`. A compromised directory can redirect funds.
|
|
8
8
|
- **PERMIT2.** Used as fallback for token transfers. Permit2 approvals can be exploited if users have stale allowances.
|
|
9
9
|
- **Owner (Ownable).** Contract owner has no fund access but controls the registry terminal allowlist and default.
|
|
10
|
-
- **`IJBPayerTracker` implementers.** `
|
|
10
|
+
- **`IJBPayerTracker` implementers.** `_resolveRefundWithBackupRecipient` in the router terminal queries `IJBPayerTracker(msg.sender).originalPayer()` via try-catch. Any contract that is the `msg.sender` and implements `IJBPayerTracker` can direct leftover refunds to an arbitrary address. This is safe when the caller is a trusted intermediary (e.g. the registry), but a malicious `msg.sender` implementing `IJBPayerTracker` could redirect refunds. The risk is bounded: the caller must have already supplied the funds being routed, so it can only redirect leftovers from its own payment.
|
|
11
11
|
|
|
12
12
|
## 2. Economic / Manipulation Risks
|
|
13
13
|
|
|
@@ -78,7 +78,7 @@ Verified in `RouterTerminalReentrancy.t.sol`: re-entrant calls via both `pay()`
|
|
|
78
78
|
|
|
79
79
|
### 8.2 Router trusts `originalPayer()` from any `msg.sender` that implements it
|
|
80
80
|
|
|
81
|
-
`
|
|
81
|
+
`_resolveRefundWithBackupRecipient` calls `IJBPayerTracker(msg.sender).originalPayer()` in a try-catch. If the call succeeds and returns a non-zero address, leftover tokens from partial swap fills are sent to that address instead of the beneficiary or `_msgSender()`. The router does not verify that `msg.sender` is the registry or any specific contract -- it trusts any caller that implements the interface. This is accepted because: (1) the caller (`msg.sender`) is the entity that supplied the funds, so redirecting its own leftovers is a legitimate operation, (2) if the call reverts or returns `address(0)`, the router falls back to the normal beneficiary/`_msgSender()` logic, and (3) decoupling from the registry allows other intermediary contracts (e.g. batch payers, aggregators) to participate in refund routing without requiring changes to the router terminal.
|
|
82
82
|
|
|
83
83
|
### 8.3 Cashout loop slippage is first-hop only (accepted trade-off)
|
|
84
84
|
|
package/SKILLS.md
CHANGED
|
@@ -60,7 +60,7 @@ If `tokenIn` is a JB project token, a cashout loop runs first (up to 20 iteratio
|
|
|
60
60
|
|
|
61
61
|
## Routing Architecture
|
|
62
62
|
|
|
63
|
-
The router uses `_route` (mutative) and `_previewRoute` (view) as the top-level entry points. Both follow the same path: detect JB project tokens and `_cashOutLoop` if needed, then `_resolveTokenOut` to pick the output token, then `_convert` to execute the conversion (no-op, wrap/unwrap, or `_handleSwap`). Swap execution goes through `_pickPoolAndQuote` (discover pool, get TWAP or spot quote with sigmoid slippage), then dispatches to V3 (`uniswapV3SwapCallback`) or V4 (`unlockCallback`). Leftover input tokens from partial fills are returned
|
|
63
|
+
The router uses `_route` (mutative) and `_previewRoute` (view) as the top-level entry points. Both follow the same path: detect JB project tokens and `_cashOutLoop` if needed, then `_resolveTokenOut` to pick the output token, then `_convert` to execute the conversion (no-op, wrap/unwrap, or `_handleSwap`). Swap execution goes through `_pickPoolAndQuote` (discover pool, get TWAP or spot quote with sigmoid slippage), then dispatches to V3 (`uniswapV3SwapCallback`) or V4 (`unlockCallback`). Leftover input tokens from partial fills are returned via `_resolveRefundWithBackupRecipient`. When the caller (`msg.sender`) implements `IJBPayerTracker` and `originalPayer()` returns a non-zero address, leftovers go to that address. Otherwise, leftovers go to `beneficiary` (for `pay()`) or `_msgSender()` (for `addToBalanceOf()`).
|
|
64
64
|
|
|
65
65
|
## Integration Points
|
|
66
66
|
|
|
@@ -201,7 +201,7 @@ The router uses `_route` (mutative) and `_previewRoute` (view) as the top-level
|
|
|
201
201
|
- The `JBRouterTerminalRegistry` handles token custody during delegation -- it transfers tokens from the payer to itself, then approves and forwards to the underlying terminal.
|
|
202
202
|
- `_msgSender()` (ERC-2771) is used instead of `msg.sender` for meta-transaction compatibility in both contracts.
|
|
203
203
|
- The `JBSwapLib` library contains slippage tolerance math (sigmoid formula), price impact estimation, and V3-compatible `sqrtPriceLimitX96` calculation. It does not contain swap execution logic.
|
|
204
|
-
- **Leftover handling**: After a swap, leftover input tokens (from partial fills where the price limit was hit) are returned via `
|
|
204
|
+
- **Leftover handling**: After a swap, leftover input tokens (from partial fills where the price limit was hit) are returned via `_resolveRefundWithBackupRecipient`. When the caller (`msg.sender`) implements `IJBPayerTracker` and `originalPayer()` returns a non-zero address, leftovers go to that address. Otherwise, leftovers go to `beneficiary` (for `pay()`) or `_msgSender()` (for `addToBalanceOf()`). For native token inputs, any remaining raw ETH is wrapped to WETH first so the leftover check catches it.
|
|
205
205
|
- **`IJBPayerTracker` decoupling**: The router terminal does not import or depend on `IJBRouterTerminalRegistry` for refund resolution. It queries `IJBPayerTracker(msg.sender).originalPayer()` via try-catch. Any intermediary contract that implements `IJBPayerTracker` can forward calls to the router and have leftovers returned to the original payer.
|
|
206
206
|
- **Credit cashouts**: When using `cashOutSource` metadata, the payer must have granted `TRANSFER_CREDITS` permission (ID 13) to the router terminal for the source project. The router calls `TOKENS.transferCreditsFrom()` to pull credits.
|
|
207
207
|
- **Cashout loop depth**: The `_cashOutLoop` iterates through JB project token chains with a cap of 20 iterations (`_MAX_CASHOUT_ITERATIONS`). Exceeding this limit reverts with `JBRouterTerminal_CashOutLoopLimit()`.
|
package/STYLE_GUIDE.md
CHANGED
|
@@ -21,7 +21,7 @@ One contract/interface/struct/enum per file. Name the file after the type it con
|
|
|
21
21
|
|
|
22
22
|
```solidity
|
|
23
23
|
// Contracts — pin to exact version
|
|
24
|
-
pragma solidity
|
|
24
|
+
pragma solidity 0.8.28;
|
|
25
25
|
|
|
26
26
|
// Interfaces, structs, enums — caret for forward compatibility
|
|
27
27
|
pragma solidity ^0.8.0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/router-terminal-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-router-terminal-v6'"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@bananapus/
|
|
21
|
-
"@bananapus/
|
|
20
|
+
"@bananapus/address-registry-v6": "^0.0.16",
|
|
21
|
+
"@bananapus/core-v6": "^0.0.28",
|
|
22
|
+
"@bananapus/permission-ids-v6": "^0.0.14",
|
|
22
23
|
"@openzeppelin/contracts": "^5.6.1",
|
|
23
24
|
"@uniswap/permit2": "github:Uniswap/permit2",
|
|
24
25
|
"@uniswap/v3-core": "github:Uniswap/v3-core#0.8",
|
package/script/Deploy.s.sol
CHANGED
package/src/JBRouterTerminal.sol
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
4
|
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
5
5
|
import {IJBCashOutTerminal} from "@bananapus/core-v6/src/interfaces/IJBCashOutTerminal.sol";
|
|
@@ -224,7 +224,7 @@ contract JBRouterTerminal is
|
|
|
224
224
|
tokenIn: token,
|
|
225
225
|
amount: _acceptFundsFor({token: token, amount: amount, metadata: metadata}),
|
|
226
226
|
metadata: metadata,
|
|
227
|
-
refundTo:
|
|
227
|
+
refundTo: _resolveRefundWithBackupRecipient(payable(_msgSender()))
|
|
228
228
|
});
|
|
229
229
|
|
|
230
230
|
uint256 payValue = _beforeTransferFor({to: address(destTerminal), token: token, amount: amount});
|
|
@@ -275,7 +275,7 @@ contract JBRouterTerminal is
|
|
|
275
275
|
tokenIn: token,
|
|
276
276
|
amount: _acceptFundsFor({token: token, amount: amount, metadata: metadata}),
|
|
277
277
|
metadata: metadata,
|
|
278
|
-
refundTo:
|
|
278
|
+
refundTo: _resolveRefundWithBackupRecipient(payable(beneficiary))
|
|
279
279
|
});
|
|
280
280
|
|
|
281
281
|
uint256 payValue = _beforeTransferFor({to: address(destTerminal), token: token, amount: amount});
|
|
@@ -518,7 +518,7 @@ contract JBRouterTerminal is
|
|
|
518
518
|
/// go to the true payer rather than the intermediary.
|
|
519
519
|
/// @param fallback_ The default refund address to use when no original payer is available.
|
|
520
520
|
/// @return The address to refund partial-fill leftovers to.
|
|
521
|
-
function
|
|
521
|
+
function _resolveRefundWithBackupRecipient(address payable fallback_) internal view returns (address payable) {
|
|
522
522
|
// Only attempt the call if msg.sender is a contract (EOAs have no code and would revert).
|
|
523
523
|
if (msg.sender.code.length > 0) {
|
|
524
524
|
// Check if the caller implements IJBPayerTracker and has an original payer set.
|
package/test/TestAuditGaps.sol
CHANGED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
8
|
+
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
9
|
+
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
10
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
11
|
+
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
12
|
+
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
13
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
14
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
15
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
16
|
+
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
17
|
+
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
|
18
|
+
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
|
19
|
+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
20
|
+
|
|
21
|
+
import {JBRouterTerminal} from "../../src/JBRouterTerminal.sol";
|
|
22
|
+
import {IWETH9} from "../../src/interfaces/IWETH9.sol";
|
|
23
|
+
|
|
24
|
+
contract AuditLeftoverMockERC20 {
|
|
25
|
+
mapping(address => uint256) public balanceOf;
|
|
26
|
+
mapping(address => mapping(address => uint256)) public allowance;
|
|
27
|
+
|
|
28
|
+
function mint(address to, uint256 amount) external {
|
|
29
|
+
balanceOf[to] += amount;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function approve(address spender, uint256 amount) external returns (bool) {
|
|
33
|
+
allowance[msg.sender][spender] = amount;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function transfer(address to, uint256 amount) external returns (bool) {
|
|
38
|
+
balanceOf[msg.sender] -= amount;
|
|
39
|
+
balanceOf[to] += amount;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
|
|
44
|
+
uint256 allowed = allowance[from][msg.sender];
|
|
45
|
+
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
|
|
46
|
+
balanceOf[from] -= amount;
|
|
47
|
+
balanceOf[to] += amount;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
contract AuditLeftoverMockWETH is IWETH9 {
|
|
53
|
+
uint256 public totalSupply;
|
|
54
|
+
mapping(address => uint256) public balanceOf;
|
|
55
|
+
mapping(address => mapping(address => uint256)) public allowance;
|
|
56
|
+
|
|
57
|
+
function approve(address spender, uint256 amount) external returns (bool) {
|
|
58
|
+
allowance[msg.sender][spender] = amount;
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function transfer(address to, uint256 amount) external returns (bool) {
|
|
63
|
+
balanceOf[msg.sender] -= amount;
|
|
64
|
+
balanceOf[to] += amount;
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
|
|
69
|
+
uint256 allowed = allowance[from][msg.sender];
|
|
70
|
+
if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amount;
|
|
71
|
+
balanceOf[from] -= amount;
|
|
72
|
+
balanceOf[to] += amount;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function deposit() external payable override {
|
|
77
|
+
balanceOf[msg.sender] += msg.value;
|
|
78
|
+
totalSupply += msg.value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function withdraw(uint256 amount) external override {
|
|
82
|
+
balanceOf[msg.sender] -= amount;
|
|
83
|
+
totalSupply -= amount;
|
|
84
|
+
payable(msg.sender).transfer(amount);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
receive() external payable {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
contract AuditLeftoverDestTerminal is IJBTerminal {
|
|
91
|
+
uint256 public lastAmount;
|
|
92
|
+
|
|
93
|
+
function addAccountingContextsFor(uint256, JBAccountingContext[] calldata) external override {}
|
|
94
|
+
|
|
95
|
+
function addToBalanceOf(
|
|
96
|
+
uint256,
|
|
97
|
+
address,
|
|
98
|
+
uint256 amount,
|
|
99
|
+
bool,
|
|
100
|
+
string calldata,
|
|
101
|
+
bytes calldata
|
|
102
|
+
)
|
|
103
|
+
external
|
|
104
|
+
payable
|
|
105
|
+
override
|
|
106
|
+
{
|
|
107
|
+
lastAmount = amount;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function accountingContextForTokenOf(
|
|
111
|
+
uint256,
|
|
112
|
+
address token
|
|
113
|
+
)
|
|
114
|
+
external
|
|
115
|
+
pure
|
|
116
|
+
override
|
|
117
|
+
returns (JBAccountingContext memory)
|
|
118
|
+
{
|
|
119
|
+
return JBAccountingContext({token: token, decimals: 18, currency: uint32(uint160(token))});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function accountingContextsOf(uint256) external pure override returns (JBAccountingContext[] memory contexts) {
|
|
123
|
+
contexts = new JBAccountingContext[](1);
|
|
124
|
+
contexts[0] = JBAccountingContext({token: address(1), decimals: 18, currency: 1});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {}
|
|
128
|
+
|
|
129
|
+
function migrateBalanceOf(uint256, address, IJBTerminal) external pure override returns (uint256) {}
|
|
130
|
+
|
|
131
|
+
function pay(
|
|
132
|
+
uint256,
|
|
133
|
+
address,
|
|
134
|
+
uint256 amount,
|
|
135
|
+
address,
|
|
136
|
+
uint256,
|
|
137
|
+
string calldata,
|
|
138
|
+
bytes calldata
|
|
139
|
+
)
|
|
140
|
+
external
|
|
141
|
+
payable
|
|
142
|
+
override
|
|
143
|
+
returns (uint256)
|
|
144
|
+
{
|
|
145
|
+
lastAmount = amount;
|
|
146
|
+
return amount;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function previewPayFor(
|
|
150
|
+
uint256,
|
|
151
|
+
address,
|
|
152
|
+
uint256 amount,
|
|
153
|
+
address,
|
|
154
|
+
bytes calldata
|
|
155
|
+
)
|
|
156
|
+
external
|
|
157
|
+
pure
|
|
158
|
+
override
|
|
159
|
+
returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory hookSpecifications)
|
|
160
|
+
{
|
|
161
|
+
hookSpecifications = new JBPayHookSpecification[](0);
|
|
162
|
+
return (JBRuleset(0, 0, 0, 0, 0, 0, 0, IJBRulesetApprovalHook(address(0)), 0), amount, 0, hookSpecifications);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function supportsInterface(bytes4) external pure override returns (bool) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
contract AuditLeftoverPartialFillPool is IUniswapV3Pool {
|
|
171
|
+
address internal immutable _token0;
|
|
172
|
+
address internal immutable _token1;
|
|
173
|
+
AuditLeftoverMockERC20 internal immutable _outputToken;
|
|
174
|
+
uint24 public immutable override fee = 3000;
|
|
175
|
+
uint128 public immutable override liquidity = 1_000_000;
|
|
176
|
+
|
|
177
|
+
uint256 public immutable amountInUsed;
|
|
178
|
+
uint256 public immutable amountOutGiven;
|
|
179
|
+
|
|
180
|
+
constructor(
|
|
181
|
+
address token0_,
|
|
182
|
+
address token1_,
|
|
183
|
+
AuditLeftoverMockERC20 outputToken_,
|
|
184
|
+
uint256 amountInUsed_,
|
|
185
|
+
uint256 amountOutGiven_
|
|
186
|
+
) {
|
|
187
|
+
_token0 = token0_;
|
|
188
|
+
_token1 = token1_;
|
|
189
|
+
_outputToken = outputToken_;
|
|
190
|
+
amountInUsed = amountInUsed_;
|
|
191
|
+
amountOutGiven = amountOutGiven_;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function swap(
|
|
195
|
+
address recipient,
|
|
196
|
+
bool zeroForOne,
|
|
197
|
+
int256,
|
|
198
|
+
uint160,
|
|
199
|
+
bytes calldata data
|
|
200
|
+
)
|
|
201
|
+
external
|
|
202
|
+
override
|
|
203
|
+
returns (int256 amount0, int256 amount1)
|
|
204
|
+
{
|
|
205
|
+
if (zeroForOne) {
|
|
206
|
+
JBRouterTerminal(payable(msg.sender))
|
|
207
|
+
.uniswapV3SwapCallback(int256(amountInUsed), -int256(amountOutGiven), data);
|
|
208
|
+
_outputToken.mint(recipient, amountOutGiven);
|
|
209
|
+
return (int256(amountInUsed), -int256(amountOutGiven));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
JBRouterTerminal(payable(msg.sender)).uniswapV3SwapCallback(-int256(amountOutGiven), int256(amountInUsed), data);
|
|
213
|
+
_outputToken.mint(recipient, amountOutGiven);
|
|
214
|
+
return (-int256(amountOutGiven), int256(amountInUsed));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function token0() external view override returns (address) {
|
|
218
|
+
return _token0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function token1() external view override returns (address) {
|
|
222
|
+
return _token1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function tickSpacing() external pure override returns (int24) {
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function slot0() external pure override returns (uint160, int24, uint16, uint16, uint16, uint8, bool) {
|
|
230
|
+
return (0, 0, 0, 0, 0, 0, false);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function feeGrowthGlobal0X128() external pure override returns (uint256) {}
|
|
234
|
+
function feeGrowthGlobal1X128() external pure override returns (uint256) {}
|
|
235
|
+
function protocolFees() external pure override returns (uint128, uint128) {}
|
|
236
|
+
function ticks(int24)
|
|
237
|
+
external
|
|
238
|
+
pure
|
|
239
|
+
override
|
|
240
|
+
returns (uint128, int128, uint256, uint256, int56, uint160, uint32, bool)
|
|
241
|
+
{}
|
|
242
|
+
function tickBitmap(int16) external pure override returns (uint256) {}
|
|
243
|
+
function positions(bytes32) external pure override returns (uint128, uint256, uint256, uint128, uint128) {}
|
|
244
|
+
function observations(uint256) external pure override returns (uint32, int56, uint160, bool) {}
|
|
245
|
+
function observe(uint32[] calldata) external pure override returns (int56[] memory, uint160[] memory) {}
|
|
246
|
+
function snapshotCumulativesInside(int24, int24) external pure override returns (int56, uint160, uint32) {}
|
|
247
|
+
function increaseObservationCardinalityNext(uint16) external override {}
|
|
248
|
+
function mint(address, int24, int24, uint128, bytes calldata) external pure override returns (uint256, uint256) {}
|
|
249
|
+
function collect(address, int24, int24, uint128, uint128) external pure override returns (uint128, uint128) {}
|
|
250
|
+
function burn(int24, int24, uint128) external pure override returns (uint256, uint256) {}
|
|
251
|
+
function flash(address, uint256, uint256, bytes calldata) external override {}
|
|
252
|
+
function initialize(uint160) external override {}
|
|
253
|
+
function setFeeProtocol(uint8, uint8) external override {}
|
|
254
|
+
function collectProtocol(address, uint128, uint128) external pure override returns (uint128, uint128) {}
|
|
255
|
+
|
|
256
|
+
function factory() external pure override returns (address) {
|
|
257
|
+
return address(0);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function maxLiquidityPerTick() external pure override returns (uint128) {
|
|
261
|
+
return type(uint128).max;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
contract LeftoverRefundTest is Test {
|
|
266
|
+
JBRouterTerminal internal router;
|
|
267
|
+
IJBDirectory internal directory;
|
|
268
|
+
IJBPermissions internal permissions;
|
|
269
|
+
IJBProjects internal projects;
|
|
270
|
+
IJBTokens internal tokens;
|
|
271
|
+
IPermit2 internal permit2;
|
|
272
|
+
IUniswapV3Factory internal factory;
|
|
273
|
+
IWETH9 internal weth;
|
|
274
|
+
|
|
275
|
+
uint256 internal constant PROJECT_ID = 1;
|
|
276
|
+
|
|
277
|
+
function setUp() public {
|
|
278
|
+
directory = IJBDirectory(makeAddr("directory"));
|
|
279
|
+
permissions = IJBPermissions(makeAddr("permissions"));
|
|
280
|
+
projects = IJBProjects(makeAddr("projects"));
|
|
281
|
+
tokens = IJBTokens(makeAddr("tokens"));
|
|
282
|
+
permit2 = IPermit2(makeAddr("permit2"));
|
|
283
|
+
factory = IUniswapV3Factory(makeAddr("factory"));
|
|
284
|
+
weth = IWETH9(address(new AuditLeftoverMockWETH()));
|
|
285
|
+
|
|
286
|
+
vm.etch(address(directory), hex"00");
|
|
287
|
+
vm.etch(address(permissions), hex"00");
|
|
288
|
+
vm.etch(address(projects), hex"00");
|
|
289
|
+
vm.etch(address(tokens), hex"00");
|
|
290
|
+
vm.etch(address(permit2), hex"00");
|
|
291
|
+
vm.etch(address(factory), hex"00");
|
|
292
|
+
|
|
293
|
+
router = new JBRouterTerminal(
|
|
294
|
+
directory,
|
|
295
|
+
permissions,
|
|
296
|
+
projects,
|
|
297
|
+
tokens,
|
|
298
|
+
permit2,
|
|
299
|
+
makeAddr("owner"),
|
|
300
|
+
weth,
|
|
301
|
+
factory,
|
|
302
|
+
IPoolManager(address(0)),
|
|
303
|
+
address(0)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/// @notice Partial-fill leftovers from pay() are refunded to the beneficiary.
|
|
308
|
+
function test_payPartialFillRefundsBeneficiary() public {
|
|
309
|
+
AuditLeftoverMockERC20 tokenIn = new AuditLeftoverMockERC20();
|
|
310
|
+
AuditLeftoverMockERC20 tokenOut = new AuditLeftoverMockERC20();
|
|
311
|
+
AuditLeftoverDestTerminal destTerminal = new AuditLeftoverDestTerminal();
|
|
312
|
+
AuditLeftoverPartialFillPool pool = _deployPool(tokenIn, tokenOut, 600 ether, 100 ether);
|
|
313
|
+
|
|
314
|
+
_mockSimpleSwapRoute(tokenIn, tokenOut, destTerminal, pool);
|
|
315
|
+
|
|
316
|
+
address alice = makeAddr("alice");
|
|
317
|
+
address bob = makeAddr("bob");
|
|
318
|
+
|
|
319
|
+
tokenIn.mint(alice, 1000 ether);
|
|
320
|
+
vm.prank(alice);
|
|
321
|
+
tokenIn.approve(address(router), type(uint256).max);
|
|
322
|
+
|
|
323
|
+
vm.prank(alice);
|
|
324
|
+
router.pay(PROJECT_ID, address(tokenIn), 1000 ether, bob, 0, "", _metadata(tokenOut));
|
|
325
|
+
|
|
326
|
+
assertEq(destTerminal.lastAmount(), 100 ether, "destination only receives filled output");
|
|
327
|
+
assertEq(tokenIn.balanceOf(bob), 400 ether, "beneficiary receives leftover input");
|
|
328
|
+
assertEq(tokenIn.balanceOf(alice), 0, "payer receives nothing extra");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function test_addToBalanceRefundAlsoLeaksPreexistingRouterBalance() public {
|
|
332
|
+
AuditLeftoverMockERC20 tokenIn = new AuditLeftoverMockERC20();
|
|
333
|
+
AuditLeftoverMockERC20 tokenOut = new AuditLeftoverMockERC20();
|
|
334
|
+
AuditLeftoverDestTerminal destTerminal = new AuditLeftoverDestTerminal();
|
|
335
|
+
AuditLeftoverPartialFillPool pool = _deployPool(tokenIn, tokenOut, 600 ether, 100 ether);
|
|
336
|
+
|
|
337
|
+
_mockSimpleSwapRoute(tokenIn, tokenOut, destTerminal, pool);
|
|
338
|
+
|
|
339
|
+
address donor = makeAddr("donor");
|
|
340
|
+
address attacker = makeAddr("attacker");
|
|
341
|
+
|
|
342
|
+
tokenIn.mint(donor, 50 ether);
|
|
343
|
+
vm.prank(donor);
|
|
344
|
+
tokenIn.transfer(address(router), 50 ether);
|
|
345
|
+
|
|
346
|
+
tokenIn.mint(attacker, 1000 ether);
|
|
347
|
+
vm.prank(attacker);
|
|
348
|
+
tokenIn.approve(address(router), type(uint256).max);
|
|
349
|
+
|
|
350
|
+
vm.prank(attacker);
|
|
351
|
+
router.addToBalanceOf(PROJECT_ID, address(tokenIn), 1000 ether, false, "", _metadata(tokenOut));
|
|
352
|
+
|
|
353
|
+
assertEq(destTerminal.lastAmount(), 100 ether, "destination only receives filled output");
|
|
354
|
+
assertEq(
|
|
355
|
+
tokenIn.balanceOf(attacker), 450 ether, "attacker captures both leftover and preexisting router balance"
|
|
356
|
+
);
|
|
357
|
+
assertEq(tokenIn.balanceOf(address(router)), 0, "router balance is fully drained by the refund");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function _deployPool(
|
|
361
|
+
AuditLeftoverMockERC20 tokenIn,
|
|
362
|
+
AuditLeftoverMockERC20 tokenOut,
|
|
363
|
+
uint256 amountInUsed,
|
|
364
|
+
uint256 amountOutGiven
|
|
365
|
+
)
|
|
366
|
+
internal
|
|
367
|
+
returns (AuditLeftoverPartialFillPool pool)
|
|
368
|
+
{
|
|
369
|
+
(address token0, address token1) = address(tokenIn) < address(tokenOut)
|
|
370
|
+
? (address(tokenIn), address(tokenOut))
|
|
371
|
+
: (address(tokenOut), address(tokenIn));
|
|
372
|
+
pool = new AuditLeftoverPartialFillPool(token0, token1, tokenOut, amountInUsed, amountOutGiven);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function _metadata(AuditLeftoverMockERC20 tokenOut) internal view returns (bytes memory metadata) {
|
|
376
|
+
metadata = JBMetadataResolver.addToMetadata(
|
|
377
|
+
"", JBMetadataResolver.getId("routeTokenOut", address(router)), abi.encode(address(tokenOut))
|
|
378
|
+
);
|
|
379
|
+
metadata = JBMetadataResolver.addToMetadata(
|
|
380
|
+
metadata, JBMetadataResolver.getId("quoteForSwap", address(router)), abi.encode(100 ether)
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function _mockSimpleSwapRoute(
|
|
385
|
+
AuditLeftoverMockERC20 tokenIn,
|
|
386
|
+
AuditLeftoverMockERC20 tokenOut,
|
|
387
|
+
AuditLeftoverDestTerminal destTerminal,
|
|
388
|
+
AuditLeftoverPartialFillPool pool
|
|
389
|
+
)
|
|
390
|
+
internal
|
|
391
|
+
{
|
|
392
|
+
vm.mockCall(address(tokens), abi.encodeWithSelector(IJBTokens.projectIdOf.selector), abi.encode(uint256(0)));
|
|
393
|
+
vm.mockCall(
|
|
394
|
+
address(directory),
|
|
395
|
+
abi.encodeCall(IJBDirectory.primaryTerminalOf, (PROJECT_ID, address(tokenIn))),
|
|
396
|
+
abi.encode(address(0))
|
|
397
|
+
);
|
|
398
|
+
vm.mockCall(
|
|
399
|
+
address(directory),
|
|
400
|
+
abi.encodeCall(IJBDirectory.primaryTerminalOf, (PROJECT_ID, address(tokenOut))),
|
|
401
|
+
abi.encode(address(destTerminal))
|
|
402
|
+
);
|
|
403
|
+
vm.mockCall(
|
|
404
|
+
address(factory),
|
|
405
|
+
abi.encodeCall(IUniswapV3Factory.getPool, (address(tokenIn), address(tokenOut), uint24(3000))),
|
|
406
|
+
abi.encode(address(pool))
|
|
407
|
+
);
|
|
408
|
+
vm.mockCall(
|
|
409
|
+
address(factory),
|
|
410
|
+
abi.encodeCall(IUniswapV3Factory.getPool, (address(tokenIn), address(tokenOut), uint24(500))),
|
|
411
|
+
abi.encode(address(0))
|
|
412
|
+
);
|
|
413
|
+
vm.mockCall(
|
|
414
|
+
address(factory),
|
|
415
|
+
abi.encodeCall(IUniswapV3Factory.getPool, (address(tokenIn), address(tokenOut), uint24(10_000))),
|
|
416
|
+
abi.encode(address(0))
|
|
417
|
+
);
|
|
418
|
+
vm.mockCall(
|
|
419
|
+
address(factory),
|
|
420
|
+
abi.encodeCall(IUniswapV3Factory.getPool, (address(tokenIn), address(tokenOut), uint24(100))),
|
|
421
|
+
abi.encode(address(0))
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
4
|
import "forge-std/Test.sol";
|
|
5
5
|
|
|
@@ -28,9 +28,9 @@ contract PayerTrackerRefundHarness is JBRouterTerminal {
|
|
|
28
28
|
)
|
|
29
29
|
{}
|
|
30
30
|
|
|
31
|
-
/// @notice Public wrapper so tests can call `
|
|
31
|
+
/// @notice Public wrapper so tests can call `_resolveRefundWithBackupRecipient` directly.
|
|
32
32
|
function resolveRefundTo(address payable fallback_) external view returns (address payable) {
|
|
33
|
-
return
|
|
33
|
+
return _resolveRefundWithBackupRecipient(fallback_);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {JBRouterTerminal} from "../../src/JBRouterTerminal.sol";
|
|
7
|
+
import {IWETH9} from "../../src/interfaces/IWETH9.sol";
|
|
8
|
+
import {IJBPayerTracker} from "../../src/interfaces/IJBPayerTracker.sol";
|
|
9
|
+
|
|
10
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
11
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
12
|
+
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
13
|
+
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
14
|
+
|
|
15
|
+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
16
|
+
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
17
|
+
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
|
18
|
+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
19
|
+
|
|
20
|
+
contract RefundToBeneficiaryTest is Test {
|
|
21
|
+
uint256 internal constant LEFTOVER = 40 ether;
|
|
22
|
+
|
|
23
|
+
HarnessRouterTerminal internal router;
|
|
24
|
+
MockERC20 internal token;
|
|
25
|
+
|
|
26
|
+
address internal payer = makeAddr("payer");
|
|
27
|
+
address internal beneficiary = makeAddr("beneficiary");
|
|
28
|
+
|
|
29
|
+
function setUp() public {
|
|
30
|
+
token = new MockERC20("Input", "IN");
|
|
31
|
+
router = new HarnessRouterTerminal();
|
|
32
|
+
|
|
33
|
+
token.mint(address(router), LEFTOVER);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function test_directCallerRefundsToBeneficiary() public {
|
|
37
|
+
vm.prank(payer);
|
|
38
|
+
router.simulatePayLeftoverRefund(address(token), beneficiary, LEFTOVER);
|
|
39
|
+
|
|
40
|
+
assertEq(token.balanceOf(beneficiary), LEFTOVER, "beneficiary receives leftover");
|
|
41
|
+
assertEq(token.balanceOf(payer), 0, "payer receives nothing");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function test_registryStyleCallerRefundsToOriginalPayer() public {
|
|
45
|
+
MockPayerTracker intermediary = new MockPayerTracker(payer);
|
|
46
|
+
|
|
47
|
+
vm.prank(address(intermediary));
|
|
48
|
+
router.simulatePayLeftoverRefund(address(token), beneficiary, LEFTOVER);
|
|
49
|
+
|
|
50
|
+
assertEq(token.balanceOf(payer), LEFTOVER, "intermediary path refunds payer");
|
|
51
|
+
assertEq(token.balanceOf(beneficiary), 0, "beneficiary receives nothing");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
contract HarnessRouterTerminal is JBRouterTerminal {
|
|
56
|
+
constructor()
|
|
57
|
+
JBRouterTerminal(
|
|
58
|
+
IJBDirectory(address(0)),
|
|
59
|
+
IJBPermissions(address(0)),
|
|
60
|
+
IJBProjects(address(0)),
|
|
61
|
+
IJBTokens(address(0)),
|
|
62
|
+
IPermit2(address(0)),
|
|
63
|
+
address(this),
|
|
64
|
+
IWETH9(address(new MockWETH())),
|
|
65
|
+
IUniswapV3Factory(address(0)),
|
|
66
|
+
IPoolManager(address(0)),
|
|
67
|
+
address(0)
|
|
68
|
+
)
|
|
69
|
+
{}
|
|
70
|
+
|
|
71
|
+
function simulatePayLeftoverRefund(address token, address beneficiary, uint256 leftover) external {
|
|
72
|
+
address payable refundTo = _resolveRefundWithBackupRecipient(payable(beneficiary));
|
|
73
|
+
_transferFrom({from: address(this), to: refundTo, token: token, amount: leftover});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
contract MockPayerTracker is IJBPayerTracker {
|
|
78
|
+
address public override originalPayer;
|
|
79
|
+
|
|
80
|
+
constructor(address payer) {
|
|
81
|
+
originalPayer = payer;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
contract MockERC20 is ERC20 {
|
|
86
|
+
constructor(string memory name, string memory symbol) ERC20(name, symbol) {}
|
|
87
|
+
|
|
88
|
+
function mint(address account, uint256 amount) external {
|
|
89
|
+
_mint(account, amount);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
contract MockWETH is MockERC20, IWETH9 {
|
|
94
|
+
constructor() MockERC20("Wrapped ETH", "WETH") {}
|
|
95
|
+
|
|
96
|
+
function deposit() external payable {
|
|
97
|
+
_mint(msg.sender, msg.value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function withdraw(uint256 wad) external {
|
|
101
|
+
_burn(msg.sender, wad);
|
|
102
|
+
payable(msg.sender).transfer(wad);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
|
|
@@ -285,7 +285,7 @@ contract AuditPartialFillPool is IUniswapV3Pool {
|
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
contract
|
|
288
|
+
contract RegistryAddToBalancePartialFillTest is Test {
|
|
289
289
|
IJBDirectory directory;
|
|
290
290
|
IJBPermissions permissions;
|
|
291
291
|
IJBProjects projects;
|