@bananapus/router-terminal-v6 0.0.28 → 0.0.29
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
package/src/JBRouterTerminal.sol
CHANGED
|
@@ -1177,30 +1177,34 @@ contract JBRouterTerminal is
|
|
|
1177
1177
|
// Walk the cashout path hop by hop until we reach a directly acceptable destination asset or exhaust the
|
|
1178
1178
|
// bounded iteration limit.
|
|
1179
1179
|
for (uint256 i; i < _MAX_CASHOUT_ITERATIONS;) {
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
1182
|
-
if (
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1180
|
+
// Skip the destination check on the first iteration if we have a credit override — the forced
|
|
1181
|
+
// cashout must happen before any early return.
|
|
1182
|
+
if (sourceProjectIdOverride == 0) {
|
|
1183
|
+
// When a preferred token was supplied, check whether the route can already finish into it
|
|
1184
|
+
// before taking another cashout hop.
|
|
1185
|
+
if (preferredToken != address(0)) {
|
|
1186
|
+
if (_hasSameRoutingAsset({tokenA: token, tokenB: preferredToken})) {
|
|
1187
|
+
// Ask the directory for the terminal that accepts the caller's preferred concrete asset.
|
|
1188
|
+
destTerminal = _usablePrimaryTerminalOf({projectId: destProjectId, token: preferredToken});
|
|
1189
|
+
|
|
1190
|
+
// If a real external terminal accepts that preferred asset, align into it and finish the
|
|
1191
|
+
// route.
|
|
1192
|
+
if (address(destTerminal) != address(0)) {
|
|
1193
|
+
(token, amount) = _alignTokenToPreferredToken({
|
|
1194
|
+
token: token, amount: amount, preferredToken: preferredToken
|
|
1195
|
+
});
|
|
1196
|
+
return (destTerminal, token, amount);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
} else {
|
|
1200
|
+
// Ask the directory whether the destination already has a usable primary terminal for the
|
|
1201
|
+
// current token.
|
|
1202
|
+
destTerminal = _usablePrimaryTerminalOf({projectId: destProjectId, token: token});
|
|
1188
1203
|
if (address(destTerminal) != address(0)) {
|
|
1189
|
-
(token, amount) =
|
|
1190
|
-
_alignTokenToPreferredToken({token: token, amount: amount, preferredToken: preferredToken});
|
|
1191
1204
|
return (destTerminal, token, amount);
|
|
1192
1205
|
}
|
|
1193
1206
|
}
|
|
1194
1207
|
}
|
|
1195
|
-
// Skip the destination check on the first iteration if we have a credit override.
|
|
1196
|
-
else if (sourceProjectIdOverride == 0) {
|
|
1197
|
-
// Ask the directory whether the destination already has a usable primary terminal for the current
|
|
1198
|
-
// token.
|
|
1199
|
-
destTerminal = _usablePrimaryTerminalOf({projectId: destProjectId, token: token});
|
|
1200
|
-
if (address(destTerminal) != address(0)) {
|
|
1201
|
-
return (destTerminal, token, amount);
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
1208
|
|
|
1205
1209
|
// Use the override if provided, otherwise look up the project ID from the token.
|
|
1206
1210
|
uint256 sourceProjectId =
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {IJBCashOutTerminal} from "@bananapus/core-v6/src/interfaces/IJBCashOutTerminal.sol";
|
|
7
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
8
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
9
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
10
|
+
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
11
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.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 {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
15
|
+
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
16
|
+
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
|
17
|
+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
18
|
+
|
|
19
|
+
import {JBRouterTerminal} from "../../src/JBRouterTerminal.sol";
|
|
20
|
+
import {IWETH9} from "../../src/interfaces/IWETH9.sol";
|
|
21
|
+
|
|
22
|
+
/// @title M26_CreditCashoutPreferredTokenBypass
|
|
23
|
+
/// @notice Before the fix, when sourceProjectIdOverride != 0 and preferredToken matched the
|
|
24
|
+
/// current token, the _cashOutLoop would short-circuit on the first iteration — returning
|
|
25
|
+
/// without cashing out. This test verifies the fix: the forced first cashout must happen.
|
|
26
|
+
contract M26_CreditCashoutPreferredTokenBypass is Test {
|
|
27
|
+
JBRouterTerminal routerTerminal;
|
|
28
|
+
|
|
29
|
+
IJBDirectory mockDirectory;
|
|
30
|
+
IJBTokens mockTokens;
|
|
31
|
+
|
|
32
|
+
address payer = makeAddr("payer");
|
|
33
|
+
address controller = makeAddr("controller");
|
|
34
|
+
address mockDestTerminal = makeAddr("destTerminal");
|
|
35
|
+
address mockCashOutTerminal = makeAddr("cashOutTerminal");
|
|
36
|
+
|
|
37
|
+
uint256 destProjectId = 1;
|
|
38
|
+
uint256 sourceProjectId = 2;
|
|
39
|
+
uint256 creditAmount = 100e18;
|
|
40
|
+
|
|
41
|
+
function setUp() public {
|
|
42
|
+
mockDirectory = IJBDirectory(makeAddr("mockDirectory"));
|
|
43
|
+
vm.etch(address(mockDirectory), hex"00");
|
|
44
|
+
mockTokens = IJBTokens(makeAddr("mockTokens"));
|
|
45
|
+
vm.etch(address(mockTokens), hex"00");
|
|
46
|
+
vm.etch(controller, hex"00");
|
|
47
|
+
vm.etch(mockDestTerminal, hex"00");
|
|
48
|
+
vm.etch(mockCashOutTerminal, hex"00");
|
|
49
|
+
|
|
50
|
+
IPermit2 mockPermit2 = IPermit2(makeAddr("mockPermit2"));
|
|
51
|
+
vm.etch(address(mockPermit2), hex"00");
|
|
52
|
+
IWETH9 mockWeth = IWETH9(makeAddr("mockWeth"));
|
|
53
|
+
vm.etch(address(mockWeth), hex"00");
|
|
54
|
+
IUniswapV3Factory mockFactory = IUniswapV3Factory(makeAddr("mockFactory"));
|
|
55
|
+
vm.etch(address(mockFactory), hex"00");
|
|
56
|
+
IPoolManager mockPoolManager = IPoolManager(makeAddr("mockPoolManager"));
|
|
57
|
+
vm.etch(address(mockPoolManager), hex"00");
|
|
58
|
+
|
|
59
|
+
routerTerminal = new JBRouterTerminal(
|
|
60
|
+
mockDirectory,
|
|
61
|
+
mockTokens,
|
|
62
|
+
mockPermit2,
|
|
63
|
+
mockWeth,
|
|
64
|
+
mockFactory,
|
|
65
|
+
mockPoolManager,
|
|
66
|
+
address(0),
|
|
67
|
+
address(0),
|
|
68
|
+
address(0)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// @notice With the fix, credit cashout with sourceProjectIdOverride and a preferred token
|
|
73
|
+
/// that matches the token must still perform at least one cashout hop.
|
|
74
|
+
function test_creditCashout_forcedHopWithPreferredToken() public {
|
|
75
|
+
// Build metadata: cashOutSource with sourceProjectId + creditAmount,
|
|
76
|
+
// and cashOutPreferredToken with NATIVE_TOKEN.
|
|
77
|
+
bytes memory metadata;
|
|
78
|
+
{
|
|
79
|
+
bytes4 cashOutSourceId = JBMetadataResolver.getId("cashOutSource", address(routerTerminal));
|
|
80
|
+
metadata = JBMetadataResolver.addToMetadata("", cashOutSourceId, abi.encode(sourceProjectId, creditAmount));
|
|
81
|
+
|
|
82
|
+
bytes4 preferredTokenId = JBMetadataResolver.getId("cashOutPreferredToken", address(routerTerminal));
|
|
83
|
+
metadata =
|
|
84
|
+
JBMetadataResolver.addToMetadata(metadata, preferredTokenId, abi.encode(JBConstants.NATIVE_TOKEN));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Mock: controller lookup for sourceProjectId.
|
|
88
|
+
vm.mockCall(
|
|
89
|
+
address(mockDirectory), abi.encodeCall(IJBDirectory.controllerOf, (sourceProjectId)), abi.encode(controller)
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Mock: controller.transferCreditsFrom.
|
|
93
|
+
vm.mockCall(
|
|
94
|
+
controller,
|
|
95
|
+
abi.encodeCall(
|
|
96
|
+
IJBController.transferCreditsFrom, (payer, sourceProjectId, address(routerTerminal), creditAmount)
|
|
97
|
+
),
|
|
98
|
+
abi.encode()
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Dest project accepts NATIVE_TOKEN.
|
|
102
|
+
vm.mockCall(
|
|
103
|
+
address(mockDirectory),
|
|
104
|
+
abi.encodeCall(IJBDirectory.primaryTerminalOf, (destProjectId, JBConstants.NATIVE_TOKEN)),
|
|
105
|
+
abi.encode(mockDestTerminal)
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Source project's terminal list.
|
|
109
|
+
{
|
|
110
|
+
IJBTerminal[] memory sourceTerminals = new IJBTerminal[](1);
|
|
111
|
+
sourceTerminals[0] = IJBTerminal(mockCashOutTerminal);
|
|
112
|
+
vm.mockCall(
|
|
113
|
+
address(mockDirectory),
|
|
114
|
+
abi.encodeCall(IJBDirectory.terminalsOf, (sourceProjectId)),
|
|
115
|
+
abi.encode(sourceTerminals)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Mock supportsInterface for IJBCashOutTerminal.
|
|
120
|
+
vm.mockCall(
|
|
121
|
+
mockCashOutTerminal,
|
|
122
|
+
abi.encodeCall(IERC165.supportsInterface, (type(IJBCashOutTerminal).interfaceId)),
|
|
123
|
+
abi.encode(true)
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Accounting context: source project terminal accepts NATIVE_TOKEN.
|
|
127
|
+
{
|
|
128
|
+
JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
|
|
129
|
+
contexts[0] = JBAccountingContext({
|
|
130
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
131
|
+
});
|
|
132
|
+
vm.mockCall(
|
|
133
|
+
mockCashOutTerminal,
|
|
134
|
+
abi.encodeCall(IJBTerminal.accountingContextsOf, (sourceProjectId)),
|
|
135
|
+
abi.encode(contexts)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Mock cashOutTokensOf: returns 60 ETH reclaimed.
|
|
140
|
+
vm.mockCall(
|
|
141
|
+
mockCashOutTerminal,
|
|
142
|
+
abi.encodeWithSelector(IJBCashOutTerminal.cashOutTokensOf.selector),
|
|
143
|
+
abi.encode(uint256(60e18))
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// The cashout terminal sends ETH to the router.
|
|
147
|
+
vm.deal(address(routerTerminal), 60e18);
|
|
148
|
+
|
|
149
|
+
// Mock dest terminal pay.
|
|
150
|
+
vm.mockCall(mockDestTerminal, abi.encodeWithSelector(IJBTerminal.pay.selector), abi.encode(uint256(500)));
|
|
151
|
+
|
|
152
|
+
// The key assertion: cashOutTokensOf MUST be called on the source terminal.
|
|
153
|
+
// Before the fix, the preferred-token short-circuit would skip this call entirely.
|
|
154
|
+
vm.expectCall(mockCashOutTerminal, abi.encodeWithSelector(IJBCashOutTerminal.cashOutTokensOf.selector));
|
|
155
|
+
|
|
156
|
+
vm.prank(payer);
|
|
157
|
+
uint256 result = routerTerminal.pay(destProjectId, JBConstants.NATIVE_TOKEN, 0, payer, 0, "", metadata);
|
|
158
|
+
|
|
159
|
+
assertEq(result, 500, "pay should return dest terminal token count");
|
|
160
|
+
}
|
|
161
|
+
}
|