@bananapus/router-terminal-v6 0.0.1
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/LICENSE +21 -0
- package/README.md +145 -0
- package/SKILLS.md +101 -0
- package/foundry.toml +22 -0
- package/package.json +30 -0
- package/script/Deploy.s.sol +130 -0
- package/script/helpers/RouterTerminalDeploymentLib.sol +76 -0
- package/slither-ci.config.json +10 -0
- package/src/JBRouterTerminal.sol +1355 -0
- package/src/JBRouterTerminalRegistry.sol +466 -0
- package/src/interfaces/IJBRouterTerminal.sol +45 -0
- package/src/interfaces/IJBRouterTerminalRegistry.sol +62 -0
- package/src/interfaces/IWETH9.sol +13 -0
- package/src/libraries/JBSwapLib.sol +159 -0
- package/src/structs/PoolInfo.sol +14 -0
- package/test/RouterTerminal.t.sol +865 -0
- package/test/RouterTerminalRegistry.t.sol +333 -0
|
@@ -0,0 +1,1355 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
5
|
+
import {IJBCashOutTerminal} from "@bananapus/core-v6/src/interfaces/IJBCashOutTerminal.sol";
|
|
6
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
|
+
import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
|
|
8
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
9
|
+
import {IJBPermitTerminal} from "@bananapus/core-v6/src/interfaces/IJBPermitTerminal.sol";
|
|
10
|
+
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
11
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
12
|
+
import {IJBToken} from "@bananapus/core-v6/src/interfaces/IJBToken.sol";
|
|
13
|
+
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
14
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
15
|
+
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
16
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
17
|
+
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
18
|
+
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
19
|
+
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
20
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
21
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
22
|
+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
|
|
23
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
24
|
+
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
25
|
+
import {IAllowanceTransfer} from "@uniswap/permit2/src/interfaces/IAllowanceTransfer.sol";
|
|
26
|
+
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
27
|
+
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
|
28
|
+
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
|
29
|
+
import {IUniswapV3SwapCallback} from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol";
|
|
30
|
+
import {TickMath} from "@uniswap/v3-core/contracts/libraries/TickMath.sol";
|
|
31
|
+
import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol";
|
|
32
|
+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
33
|
+
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
|
|
34
|
+
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
|
|
35
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
36
|
+
import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
|
|
37
|
+
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
38
|
+
import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
39
|
+
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
40
|
+
import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
|
|
41
|
+
|
|
42
|
+
import {IWETH9} from "./interfaces/IWETH9.sol";
|
|
43
|
+
import {IJBRouterTerminal} from "./interfaces/IJBRouterTerminal.sol";
|
|
44
|
+
import {PoolInfo} from "./structs/PoolInfo.sol";
|
|
45
|
+
import {JBSwapLib} from "./libraries/JBSwapLib.sol";
|
|
46
|
+
|
|
47
|
+
/// @notice The `JBRouterTerminal` accepts any token and dynamically discovers what token each destination project
|
|
48
|
+
/// accepts, then routes the payment there — via direct forwarding, Uniswap swap, JB token cashout, or a combination.
|
|
49
|
+
/// Supports both Uniswap V3 and V4 pools, choosing whichever offers better liquidity.
|
|
50
|
+
/// @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM
|
|
51
|
+
contract JBRouterTerminal is
|
|
52
|
+
JBPermissioned,
|
|
53
|
+
Ownable,
|
|
54
|
+
ERC2771Context,
|
|
55
|
+
IJBTerminal,
|
|
56
|
+
IJBPermitTerminal,
|
|
57
|
+
IUniswapV3SwapCallback,
|
|
58
|
+
IUnlockCallback,
|
|
59
|
+
IJBRouterTerminal
|
|
60
|
+
{
|
|
61
|
+
// A library that adds default safety checks to ERC20 functionality.
|
|
62
|
+
using SafeERC20 for IERC20;
|
|
63
|
+
using PoolIdLibrary for PoolKey;
|
|
64
|
+
using StateLibrary for IPoolManager;
|
|
65
|
+
using BalanceDeltaLibrary for BalanceDelta;
|
|
66
|
+
|
|
67
|
+
//*********************************************************************//
|
|
68
|
+
// ------------------------- public constants ------------------------ //
|
|
69
|
+
//*********************************************************************//
|
|
70
|
+
|
|
71
|
+
/// @notice The default TWAP window used for auto-discovered pools.
|
|
72
|
+
uint256 public constant DEFAULT_TWAP_WINDOW = 10 minutes;
|
|
73
|
+
|
|
74
|
+
/// @notice The fee tiers to search when auto-discovering V3 pools, ordered by commonality.
|
|
75
|
+
/// 3000 = 0.3%, 500 = 0.05%, 10000 = 1%, 100 = 0.01%.
|
|
76
|
+
uint24[4] public FEE_TIERS = [uint24(3000), uint24(500), uint24(10_000), uint24(100)];
|
|
77
|
+
|
|
78
|
+
/// @notice The fee/tickSpacing pairings to search for V4 vanilla pools.
|
|
79
|
+
uint24[4] public V4_FEES = [uint24(3000), uint24(500), uint24(10_000), uint24(100)];
|
|
80
|
+
int24[4] public V4_TICK_SPACINGS = [int24(60), int24(10), int24(200), int24(1)];
|
|
81
|
+
|
|
82
|
+
/// @notice The denominator used for slippage tolerance basis points.
|
|
83
|
+
uint256 public constant SLIPPAGE_DENOMINATOR = 10_000;
|
|
84
|
+
|
|
85
|
+
//*********************************************************************//
|
|
86
|
+
// ---------------- public immutable stored properties --------------- //
|
|
87
|
+
//*********************************************************************//
|
|
88
|
+
|
|
89
|
+
/// @notice The directory of terminals and controllers for projects.
|
|
90
|
+
IJBDirectory public immutable DIRECTORY;
|
|
91
|
+
|
|
92
|
+
/// @notice Mints ERC-721s that represent project ownership and transfers.
|
|
93
|
+
IJBProjects public immutable PROJECTS;
|
|
94
|
+
|
|
95
|
+
/// @notice Manages minting, burning, and balances of projects' tokens and token credits.
|
|
96
|
+
IJBTokens public immutable TOKENS;
|
|
97
|
+
|
|
98
|
+
/// @notice The Uniswap V3 factory used for pool discovery and verification.
|
|
99
|
+
IUniswapV3Factory public immutable FACTORY;
|
|
100
|
+
|
|
101
|
+
/// @notice The Uniswap V4 PoolManager. Can be address(0) if V4 is not deployed on this chain.
|
|
102
|
+
IPoolManager public immutable POOL_MANAGER;
|
|
103
|
+
|
|
104
|
+
/// @notice The permit2 utility.
|
|
105
|
+
IPermit2 public immutable override PERMIT2;
|
|
106
|
+
|
|
107
|
+
/// @notice The ERC-20 wrapper for the native token.
|
|
108
|
+
IWETH9 public immutable WETH;
|
|
109
|
+
|
|
110
|
+
//*********************************************************************//
|
|
111
|
+
// -------------------------- constructor ---------------------------- //
|
|
112
|
+
//*********************************************************************//
|
|
113
|
+
|
|
114
|
+
/// @param directory A contract storing directories of terminals and controllers for each project.
|
|
115
|
+
/// @param permissions A contract storing permissions.
|
|
116
|
+
/// @param projects A contract which mints ERC-721s that represent project ownership and transfers.
|
|
117
|
+
/// @param tokens A contract managing project token balances.
|
|
118
|
+
/// @param permit2 A permit2 utility.
|
|
119
|
+
/// @param owner The owner of the contract.
|
|
120
|
+
/// @param weth A contract which wraps the native token.
|
|
121
|
+
/// @param factory The Uniswap V3 factory for pool discovery.
|
|
122
|
+
/// @param poolManager The Uniswap V4 PoolManager (address(0) if V4 not available).
|
|
123
|
+
/// @param trustedForwarder The trusted forwarder for the contract.
|
|
124
|
+
constructor(
|
|
125
|
+
IJBDirectory directory,
|
|
126
|
+
IJBPermissions permissions,
|
|
127
|
+
IJBProjects projects,
|
|
128
|
+
IJBTokens tokens,
|
|
129
|
+
IPermit2 permit2,
|
|
130
|
+
address owner,
|
|
131
|
+
IWETH9 weth,
|
|
132
|
+
IUniswapV3Factory factory,
|
|
133
|
+
IPoolManager poolManager,
|
|
134
|
+
address trustedForwarder
|
|
135
|
+
)
|
|
136
|
+
JBPermissioned(permissions)
|
|
137
|
+
ERC2771Context(trustedForwarder)
|
|
138
|
+
Ownable(owner)
|
|
139
|
+
{
|
|
140
|
+
DIRECTORY = directory;
|
|
141
|
+
PROJECTS = projects;
|
|
142
|
+
TOKENS = tokens;
|
|
143
|
+
FACTORY = factory;
|
|
144
|
+
POOL_MANAGER = poolManager;
|
|
145
|
+
PERMIT2 = permit2;
|
|
146
|
+
WETH = weth;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//*********************************************************************//
|
|
150
|
+
// ------------------------- external views -------------------------- //
|
|
151
|
+
//*********************************************************************//
|
|
152
|
+
|
|
153
|
+
/// @notice Returns a dynamic accounting context for any token with 18 decimals.
|
|
154
|
+
/// @dev The router terminal does not store accounting contexts — it constructs them on the fly because it can
|
|
155
|
+
/// accept any token.
|
|
156
|
+
/// @param token The address of the token to get the accounting context for.
|
|
157
|
+
/// @return context A `JBAccountingContext` for the specified token.
|
|
158
|
+
function accountingContextForTokenOf(
|
|
159
|
+
uint256,
|
|
160
|
+
address token
|
|
161
|
+
)
|
|
162
|
+
external
|
|
163
|
+
pure
|
|
164
|
+
override
|
|
165
|
+
returns (JBAccountingContext memory context)
|
|
166
|
+
{
|
|
167
|
+
context = JBAccountingContext({token: token, decimals: 18, currency: uint32(uint160(token))});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// @notice Returns an empty array — this terminal accepts any token dynamically.
|
|
171
|
+
/// @return contexts An empty array of `JBAccountingContext`.
|
|
172
|
+
function accountingContextsOf(uint256) external pure override returns (JBAccountingContext[] memory contexts) {
|
|
173
|
+
contexts = new JBAccountingContext[](0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// @notice This terminal holds no surplus. Always returns 0.
|
|
177
|
+
function currentSurplusOf(
|
|
178
|
+
uint256,
|
|
179
|
+
JBAccountingContext[] memory,
|
|
180
|
+
uint256,
|
|
181
|
+
uint256
|
|
182
|
+
)
|
|
183
|
+
external
|
|
184
|
+
pure
|
|
185
|
+
override
|
|
186
|
+
returns (uint256)
|
|
187
|
+
{
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// @notice Public wrapper for V3-only _discoverPool, useful for off-chain queries.
|
|
192
|
+
/// @param normalizedTokenIn The input token (wrapped if native).
|
|
193
|
+
/// @param normalizedTokenOut The output token (wrapped if native).
|
|
194
|
+
/// @return pool The V3 pool with the highest liquidity.
|
|
195
|
+
function discoverPool(
|
|
196
|
+
address normalizedTokenIn,
|
|
197
|
+
address normalizedTokenOut
|
|
198
|
+
)
|
|
199
|
+
external
|
|
200
|
+
view
|
|
201
|
+
override
|
|
202
|
+
returns (IUniswapV3Pool pool)
|
|
203
|
+
{
|
|
204
|
+
PoolInfo memory info = _discoverPool(normalizedTokenIn, normalizedTokenOut);
|
|
205
|
+
if (!info.isV4) pool = info.v3Pool;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// @notice Discover the best pool across both V3 and V4 for a token pair.
|
|
209
|
+
/// @param normalizedTokenIn The input token (wrapped if native).
|
|
210
|
+
/// @param normalizedTokenOut The output token (wrapped if native).
|
|
211
|
+
/// @return pool The best pool found.
|
|
212
|
+
function discoverBestPool(
|
|
213
|
+
address normalizedTokenIn,
|
|
214
|
+
address normalizedTokenOut
|
|
215
|
+
)
|
|
216
|
+
external
|
|
217
|
+
view
|
|
218
|
+
override
|
|
219
|
+
returns (PoolInfo memory pool)
|
|
220
|
+
{
|
|
221
|
+
return _discoverPool(normalizedTokenIn, normalizedTokenOut);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
//*********************************************************************//
|
|
225
|
+
// -------------------------- public views --------------------------- //
|
|
226
|
+
//*********************************************************************//
|
|
227
|
+
|
|
228
|
+
/// @notice Indicates if this contract adheres to the specified interface.
|
|
229
|
+
/// @dev See {IERC165-supportsInterface}.
|
|
230
|
+
/// @param interfaceId The ID of the interface to check for adherence to.
|
|
231
|
+
/// @return A flag indicating if the provided interface ID is supported.
|
|
232
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
|
233
|
+
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPermitTerminal).interfaceId
|
|
234
|
+
|| interfaceId == type(IERC165).interfaceId || interfaceId == type(IJBPermissioned).interfaceId;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
//*********************************************************************//
|
|
238
|
+
// -------------------------- internal views ------------------------- //
|
|
239
|
+
//*********************************************************************//
|
|
240
|
+
|
|
241
|
+
/// @dev `ERC-2771` specifies the context as being a single address (20 bytes).
|
|
242
|
+
function _contextSuffixLength() internal view override(ERC2771Context, Context) returns (uint256) {
|
|
243
|
+
return super._contextSuffixLength();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// @notice The calldata. Preferred to use over `msg.data`.
|
|
247
|
+
/// @return calldata The `msg.data` of this call.
|
|
248
|
+
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
249
|
+
return ERC2771Context._msgData();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// @notice The message's sender. Preferred to use over `msg.sender`.
|
|
253
|
+
/// @return sender The address which sent this call.
|
|
254
|
+
function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
|
|
255
|
+
return ERC2771Context._msgSender();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//*********************************************************************//
|
|
259
|
+
// ---------------------- external transactions ---------------------- //
|
|
260
|
+
//*********************************************************************//
|
|
261
|
+
|
|
262
|
+
/// @notice Empty implementation to satisfy the interface. Accounting contexts are determined dynamically.
|
|
263
|
+
function addAccountingContextsFor(uint256, JBAccountingContext[] calldata) external override {}
|
|
264
|
+
|
|
265
|
+
/// @notice Empty implementation to satisfy the interface. This terminal has no balance to migrate.
|
|
266
|
+
function migrateBalanceOf(uint256, address, IJBTerminal) external override returns (uint256 balance) {
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// @notice Pay a project by routing the incoming token to whatever token the project accepts.
|
|
271
|
+
/// @dev Automatically handles direct forwarding, Uniswap swaps, JB token cashouts, or combinations.
|
|
272
|
+
/// @param projectId The ID of the destination project being paid.
|
|
273
|
+
/// @param token The address of the token being paid in.
|
|
274
|
+
/// @param amount The amount of tokens being paid in.
|
|
275
|
+
/// @param beneficiary The address to receive any tokens minted by the destination project.
|
|
276
|
+
/// @param minReturnedTokens The minimum number of destination project tokens expected in return.
|
|
277
|
+
/// @param memo A memo to pass along to the emitted event.
|
|
278
|
+
/// @param metadata Bytes in `JBMetadataResolver`'s format.
|
|
279
|
+
/// @return beneficiaryTokenCount The number of tokens minted for the beneficiary.
|
|
280
|
+
function pay(
|
|
281
|
+
uint256 projectId,
|
|
282
|
+
address token,
|
|
283
|
+
uint256 amount,
|
|
284
|
+
address beneficiary,
|
|
285
|
+
uint256 minReturnedTokens,
|
|
286
|
+
string calldata memo,
|
|
287
|
+
bytes calldata metadata
|
|
288
|
+
)
|
|
289
|
+
external
|
|
290
|
+
payable
|
|
291
|
+
virtual
|
|
292
|
+
override
|
|
293
|
+
returns (uint256 beneficiaryTokenCount)
|
|
294
|
+
{
|
|
295
|
+
IJBTerminal destTerminal;
|
|
296
|
+
{
|
|
297
|
+
amount = _acceptFundsFor({token: token, amount: amount, metadata: metadata});
|
|
298
|
+
|
|
299
|
+
address tokenOut;
|
|
300
|
+
uint256 amountOut;
|
|
301
|
+
(destTerminal, tokenOut, amountOut) =
|
|
302
|
+
_route({destProjectId: projectId, tokenIn: token, amount: amount, metadata: metadata});
|
|
303
|
+
token = tokenOut;
|
|
304
|
+
amount = amountOut;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
uint256 payValue = _beforeTransferFor({to: address(destTerminal), token: token, amount: amount});
|
|
308
|
+
|
|
309
|
+
return destTerminal.pay{value: payValue}({
|
|
310
|
+
projectId: projectId,
|
|
311
|
+
token: token,
|
|
312
|
+
amount: amount,
|
|
313
|
+
beneficiary: beneficiary,
|
|
314
|
+
minReturnedTokens: minReturnedTokens,
|
|
315
|
+
memo: memo,
|
|
316
|
+
metadata: metadata
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/// @notice Add funds to a project's balance by routing the incoming token to whatever token the project accepts.
|
|
321
|
+
/// @param projectId The ID of the destination project.
|
|
322
|
+
/// @param token The address of the token being paid in.
|
|
323
|
+
/// @param amount The amount of tokens being paid in.
|
|
324
|
+
/// @param shouldReturnHeldFees Whether held fees should be returned based on the amount added.
|
|
325
|
+
/// @param memo A memo to pass along to the emitted event.
|
|
326
|
+
/// @param metadata Bytes in `JBMetadataResolver`'s format.
|
|
327
|
+
function addToBalanceOf(
|
|
328
|
+
uint256 projectId,
|
|
329
|
+
address token,
|
|
330
|
+
uint256 amount,
|
|
331
|
+
bool shouldReturnHeldFees,
|
|
332
|
+
string calldata memo,
|
|
333
|
+
bytes calldata metadata
|
|
334
|
+
)
|
|
335
|
+
external
|
|
336
|
+
payable
|
|
337
|
+
override
|
|
338
|
+
{
|
|
339
|
+
IJBTerminal destTerminal;
|
|
340
|
+
{
|
|
341
|
+
amount = _acceptFundsFor({token: token, amount: amount, metadata: metadata});
|
|
342
|
+
|
|
343
|
+
address tokenOut;
|
|
344
|
+
uint256 amountOut;
|
|
345
|
+
(destTerminal, tokenOut, amountOut) =
|
|
346
|
+
_route({destProjectId: projectId, tokenIn: token, amount: amount, metadata: metadata});
|
|
347
|
+
token = tokenOut;
|
|
348
|
+
amount = amountOut;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
uint256 payValue = _beforeTransferFor({to: address(destTerminal), token: token, amount: amount});
|
|
352
|
+
|
|
353
|
+
destTerminal.addToBalanceOf{value: payValue}({
|
|
354
|
+
projectId: projectId,
|
|
355
|
+
token: token,
|
|
356
|
+
amount: amount,
|
|
357
|
+
shouldReturnHeldFees: shouldReturnHeldFees,
|
|
358
|
+
memo: memo,
|
|
359
|
+
metadata: metadata
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// @notice The Uniswap v3 pool callback where the token transfer is expected to happen.
|
|
364
|
+
/// @dev Verifies the caller is a legitimate pool via the factory using the encoded tokenIn/tokenOut pair.
|
|
365
|
+
/// @param amount0Delta The amount of token 0 being used for the swap.
|
|
366
|
+
/// @param amount1Delta The amount of token 1 being used for the swap.
|
|
367
|
+
/// @param data Data passed in by the swap operation: abi.encode(projectId, tokenIn, tokenOut).
|
|
368
|
+
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external override {
|
|
369
|
+
// Unpack the data from the original swap config.
|
|
370
|
+
(, address tokenIn, address tokenOut) = abi.decode(data, (uint256, address, address));
|
|
371
|
+
|
|
372
|
+
// Normalize tokens (wrap native token if needed).
|
|
373
|
+
address normalizedTokenIn = tokenIn == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenIn;
|
|
374
|
+
address normalizedTokenOut = tokenOut == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenOut;
|
|
375
|
+
|
|
376
|
+
// Verify caller is a legitimate pool via the factory.
|
|
377
|
+
uint24 fee = IUniswapV3Pool(msg.sender).fee();
|
|
378
|
+
address expectedPool = FACTORY.getPool(normalizedTokenIn, normalizedTokenOut, fee);
|
|
379
|
+
if (msg.sender != expectedPool) revert JBRouterTerminal_CallerNotPool(msg.sender);
|
|
380
|
+
|
|
381
|
+
// Calculate the amount of tokens to send to the pool (the positive delta).
|
|
382
|
+
uint256 amountToSendToPool = amount0Delta < 0 ? uint256(amount1Delta) : uint256(amount0Delta);
|
|
383
|
+
|
|
384
|
+
// Wrap native tokens if needed.
|
|
385
|
+
if (tokenIn == JBConstants.NATIVE_TOKEN) WETH.deposit{value: amountToSendToPool}();
|
|
386
|
+
|
|
387
|
+
// Transfer the tokens to the pool.
|
|
388
|
+
IERC20(normalizedTokenIn).safeTransfer(msg.sender, amountToSendToPool);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/// @notice The Uniswap V4 unlock callback. Called by the PoolManager during `unlock()`.
|
|
392
|
+
/// @param data Encoded swap parameters.
|
|
393
|
+
/// @return Encoded output amount.
|
|
394
|
+
function unlockCallback(bytes calldata data) external override returns (bytes memory) {
|
|
395
|
+
if (msg.sender != address(POOL_MANAGER)) revert JBRouterTerminal_CallerNotPoolManager(msg.sender);
|
|
396
|
+
|
|
397
|
+
// Decode the swap parameters.
|
|
398
|
+
(PoolKey memory key, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, uint256 minAmountOut) =
|
|
399
|
+
abi.decode(data, (PoolKey, bool, int256, uint160, uint256));
|
|
400
|
+
|
|
401
|
+
// Execute the swap.
|
|
402
|
+
BalanceDelta delta = POOL_MANAGER.swap({
|
|
403
|
+
key: key,
|
|
404
|
+
params: SwapParams({
|
|
405
|
+
zeroForOne: zeroForOne,
|
|
406
|
+
amountSpecified: amountSpecified,
|
|
407
|
+
sqrtPriceLimitX96: sqrtPriceLimitX96
|
|
408
|
+
}),
|
|
409
|
+
hookData: ""
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Determine input/output amounts from the delta.
|
|
413
|
+
uint256 amountIn;
|
|
414
|
+
uint256 amountOut;
|
|
415
|
+
{
|
|
416
|
+
int128 delta0 = delta.amount0();
|
|
417
|
+
int128 delta1 = delta.amount1();
|
|
418
|
+
if (zeroForOne) {
|
|
419
|
+
amountIn = uint256(uint128(-delta0));
|
|
420
|
+
amountOut = uint256(uint128(delta1));
|
|
421
|
+
} else {
|
|
422
|
+
amountIn = uint256(uint128(-delta1));
|
|
423
|
+
amountOut = uint256(uint128(delta0));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (amountOut < minAmountOut) revert JBRouterTerminal_SlippageExceeded(amountOut, minAmountOut);
|
|
428
|
+
|
|
429
|
+
// Settle input (pay what we owe to the PoolManager).
|
|
430
|
+
Currency inputCurrency = zeroForOne ? key.currency0 : key.currency1;
|
|
431
|
+
_settleV4(inputCurrency, amountIn);
|
|
432
|
+
|
|
433
|
+
// Take output (receive what the PoolManager owes us).
|
|
434
|
+
Currency outputCurrency = zeroForOne ? key.currency1 : key.currency0;
|
|
435
|
+
_takeV4(outputCurrency, amountOut);
|
|
436
|
+
|
|
437
|
+
return abi.encode(amountOut);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
//*********************************************************************//
|
|
441
|
+
// ---------------------- internal transactions ---------------------- //
|
|
442
|
+
//*********************************************************************//
|
|
443
|
+
|
|
444
|
+
/// @notice Core routing logic shared by pay() and addToBalanceOf().
|
|
445
|
+
/// @dev Determines whether to forward directly, cashout JB tokens, swap via Uniswap, or a combination.
|
|
446
|
+
/// @param destProjectId The ID of the destination project.
|
|
447
|
+
/// @param tokenIn The address of the token being routed.
|
|
448
|
+
/// @param amount The amount of tokens being routed.
|
|
449
|
+
/// @param metadata Bytes in `JBMetadataResolver`'s format.
|
|
450
|
+
/// @return destTerminal The terminal to forward funds to.
|
|
451
|
+
/// @return tokenOut The token the destination project accepts.
|
|
452
|
+
/// @return amountOut The amount of tokenOut to forward.
|
|
453
|
+
function _route(
|
|
454
|
+
uint256 destProjectId,
|
|
455
|
+
address tokenIn,
|
|
456
|
+
uint256 amount,
|
|
457
|
+
bytes calldata metadata
|
|
458
|
+
)
|
|
459
|
+
internal
|
|
460
|
+
returns (IJBTerminal destTerminal, address tokenOut, uint256 amountOut)
|
|
461
|
+
{
|
|
462
|
+
// Check for credit source override.
|
|
463
|
+
uint256 sourceProjectIdOverride;
|
|
464
|
+
{
|
|
465
|
+
(bool creditExists, bytes memory creditData) = JBMetadataResolver.getDataFor({
|
|
466
|
+
id: JBMetadataResolver.getId("cashOutSource"),
|
|
467
|
+
metadata: metadata
|
|
468
|
+
});
|
|
469
|
+
if (creditExists) {
|
|
470
|
+
(sourceProjectIdOverride,) = abi.decode(creditData, (uint256, uint256));
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Check if input is a JB project token (credit or ERC-20).
|
|
475
|
+
uint256 sourceProjectId = sourceProjectIdOverride;
|
|
476
|
+
if (sourceProjectId == 0 && tokenIn != JBConstants.NATIVE_TOKEN) {
|
|
477
|
+
sourceProjectId = TOKENS.projectIdOf(IJBToken(tokenIn));
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (sourceProjectId != 0) {
|
|
481
|
+
// JB token path: cashout recursively, then maybe swap the reclaimed token.
|
|
482
|
+
(destTerminal, tokenOut, amountOut) = _cashOutLoop({
|
|
483
|
+
destProjectId: destProjectId,
|
|
484
|
+
token: tokenIn,
|
|
485
|
+
amount: amount,
|
|
486
|
+
sourceProjectIdOverride: sourceProjectIdOverride
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// If the cashout loop found a terminal that accepts the reclaimed token, we're done.
|
|
490
|
+
if (address(destTerminal) != address(0)) {
|
|
491
|
+
return (destTerminal, tokenOut, amountOut);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Cashout produced a base token the destination doesn't accept — fall through to resolve + convert.
|
|
495
|
+
tokenIn = tokenOut;
|
|
496
|
+
amount = amountOut;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Resolve what token the destination project accepts and which terminal to use.
|
|
500
|
+
(tokenOut, destTerminal) = _resolveTokenOut({projectId: destProjectId, tokenIn: tokenIn, metadata: metadata});
|
|
501
|
+
|
|
502
|
+
// Convert tokenIn -> tokenOut (no-op if they match, wrap/unwrap, or swap).
|
|
503
|
+
amountOut = _convert({
|
|
504
|
+
tokenIn: tokenIn,
|
|
505
|
+
tokenOut: tokenOut,
|
|
506
|
+
amount: amount,
|
|
507
|
+
projectId: destProjectId,
|
|
508
|
+
metadata: metadata
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/// @notice Determine what output token a project accepts for a given input token.
|
|
513
|
+
/// @dev Priority: 1) metadata override, 2) direct acceptance, 3) NATIVE/WETH equivalence, 4) pool discovery.
|
|
514
|
+
/// @param projectId The ID of the destination project.
|
|
515
|
+
/// @param tokenIn The input token being routed.
|
|
516
|
+
/// @param metadata Bytes in `JBMetadataResolver`'s format.
|
|
517
|
+
/// @return tokenOut The token the project accepts.
|
|
518
|
+
/// @return destTerminal The terminal that accepts tokenOut.
|
|
519
|
+
function _resolveTokenOut(
|
|
520
|
+
uint256 projectId,
|
|
521
|
+
address tokenIn,
|
|
522
|
+
bytes calldata metadata
|
|
523
|
+
)
|
|
524
|
+
internal
|
|
525
|
+
view
|
|
526
|
+
returns (address tokenOut, IJBTerminal destTerminal)
|
|
527
|
+
{
|
|
528
|
+
// 1. Metadata override — payer specifies tokenOut explicitly.
|
|
529
|
+
{
|
|
530
|
+
(bool exists, bytes memory routeData) = JBMetadataResolver.getDataFor({
|
|
531
|
+
id: JBMetadataResolver.getId("routeTokenOut"),
|
|
532
|
+
metadata: metadata
|
|
533
|
+
});
|
|
534
|
+
if (exists) {
|
|
535
|
+
(tokenOut) = abi.decode(routeData, (address));
|
|
536
|
+
destTerminal = DIRECTORY.primaryTerminalOf({projectId: projectId, token: tokenOut});
|
|
537
|
+
if (address(destTerminal) == address(0)) {
|
|
538
|
+
revert JBRouterTerminal_TokenNotAccepted(projectId, tokenOut);
|
|
539
|
+
}
|
|
540
|
+
return (tokenOut, destTerminal);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 2. Direct acceptance — project accepts tokenIn as-is.
|
|
545
|
+
destTerminal = DIRECTORY.primaryTerminalOf({projectId: projectId, token: tokenIn});
|
|
546
|
+
if (address(destTerminal) != address(0)) {
|
|
547
|
+
return (tokenIn, destTerminal);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// 2b. Check NATIVE_TOKEN <-> WETH equivalence.
|
|
551
|
+
if (tokenIn == JBConstants.NATIVE_TOKEN) {
|
|
552
|
+
destTerminal = DIRECTORY.primaryTerminalOf({projectId: projectId, token: address(WETH)});
|
|
553
|
+
if (address(destTerminal) != address(0)) return (address(WETH), destTerminal);
|
|
554
|
+
} else if (tokenIn == address(WETH)) {
|
|
555
|
+
destTerminal = DIRECTORY.primaryTerminalOf({projectId: projectId, token: JBConstants.NATIVE_TOKEN});
|
|
556
|
+
if (address(destTerminal) != address(0)) return (JBConstants.NATIVE_TOKEN, destTerminal);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// 3. Dynamic discovery — iterate terminals, find accepted token with best Uniswap pool.
|
|
560
|
+
(tokenOut, destTerminal) = _discoverAcceptedToken({projectId: projectId, tokenIn: tokenIn});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/// @notice Search a project's terminals for an accepted token that has a Uniswap pool with tokenIn.
|
|
564
|
+
/// @dev Falls back to the first accepted token if no pool exists (for JB token cashout paths).
|
|
565
|
+
/// @param projectId The ID of the project to search.
|
|
566
|
+
/// @param tokenIn The input token to find a pool for.
|
|
567
|
+
/// @return tokenOut The best accepted token found.
|
|
568
|
+
/// @return destTerminal The terminal that accepts tokenOut.
|
|
569
|
+
function _discoverAcceptedToken(
|
|
570
|
+
uint256 projectId,
|
|
571
|
+
address tokenIn
|
|
572
|
+
)
|
|
573
|
+
internal
|
|
574
|
+
view
|
|
575
|
+
returns (address tokenOut, IJBTerminal destTerminal)
|
|
576
|
+
{
|
|
577
|
+
address normalizedTokenIn = tokenIn == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenIn;
|
|
578
|
+
IJBTerminal[] memory terminals = DIRECTORY.terminalsOf(projectId);
|
|
579
|
+
|
|
580
|
+
uint128 bestLiquidity;
|
|
581
|
+
bool hasFallback;
|
|
582
|
+
|
|
583
|
+
for (uint256 i; i < terminals.length; i++) {
|
|
584
|
+
JBAccountingContext[] memory contexts = terminals[i].accountingContextsOf(projectId);
|
|
585
|
+
|
|
586
|
+
for (uint256 j; j < contexts.length; j++) {
|
|
587
|
+
address candidateToken = contexts[j].token;
|
|
588
|
+
address normalizedCandidate =
|
|
589
|
+
candidateToken == JBConstants.NATIVE_TOKEN ? address(WETH) : candidateToken;
|
|
590
|
+
|
|
591
|
+
if (normalizedCandidate == normalizedTokenIn) continue;
|
|
592
|
+
|
|
593
|
+
// Save first candidate as fallback.
|
|
594
|
+
if (!hasFallback) {
|
|
595
|
+
tokenOut = candidateToken;
|
|
596
|
+
destTerminal = terminals[i];
|
|
597
|
+
hasFallback = true;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Search for pool with best liquidity (V3 + V4).
|
|
601
|
+
uint128 candidateLiquidity = _bestPoolLiquidity(normalizedTokenIn, normalizedCandidate);
|
|
602
|
+
if (candidateLiquidity > bestLiquidity) {
|
|
603
|
+
bestLiquidity = candidateLiquidity;
|
|
604
|
+
tokenOut = candidateToken;
|
|
605
|
+
destTerminal = terminals[i];
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (!hasFallback) revert JBRouterTerminal_NoRouteFound(projectId, tokenIn);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/// @notice Find the highest liquidity across all V3 fee tiers and V4 pools for a token pair.
|
|
614
|
+
/// @param tokenA One token in the pair.
|
|
615
|
+
/// @param tokenB The other token in the pair.
|
|
616
|
+
/// @return bestLiquidity The highest liquidity found, or 0 if no pool exists.
|
|
617
|
+
function _bestPoolLiquidity(address tokenA, address tokenB) internal view returns (uint128 bestLiquidity) {
|
|
618
|
+
// Search V3.
|
|
619
|
+
for (uint256 i; i < 4; i++) {
|
|
620
|
+
address poolAddr = FACTORY.getPool(tokenA, tokenB, FEE_TIERS[i]);
|
|
621
|
+
if (poolAddr == address(0)) continue;
|
|
622
|
+
|
|
623
|
+
uint128 liquidity = IUniswapV3Pool(poolAddr).liquidity();
|
|
624
|
+
if (liquidity > bestLiquidity) bestLiquidity = liquidity;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Search V4.
|
|
628
|
+
uint128 v4Best = _bestV4PoolLiquidity(tokenA, tokenB);
|
|
629
|
+
if (v4Best > bestLiquidity) bestLiquidity = v4Best;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/// @notice Find the highest liquidity across V4 vanilla pools for a token pair.
|
|
633
|
+
/// @param tokenA One token in the pair.
|
|
634
|
+
/// @param tokenB The other token in the pair.
|
|
635
|
+
/// @return bestLiquidity The highest liquidity found across V4 pools.
|
|
636
|
+
function _bestV4PoolLiquidity(address tokenA, address tokenB) internal view returns (uint128 bestLiquidity) {
|
|
637
|
+
if (address(POOL_MANAGER) == address(0)) return 0;
|
|
638
|
+
|
|
639
|
+
// Convert WETH -> address(0) for V4 native ETH representation.
|
|
640
|
+
address v4A = tokenA == address(WETH) ? address(0) : tokenA;
|
|
641
|
+
address v4B = tokenB == address(WETH) ? address(0) : tokenB;
|
|
642
|
+
|
|
643
|
+
// Sort currencies (currency0 < currency1).
|
|
644
|
+
(address sorted0, address sorted1) = v4A < v4B ? (v4A, v4B) : (v4B, v4A);
|
|
645
|
+
|
|
646
|
+
for (uint256 i; i < 4; i++) {
|
|
647
|
+
PoolKey memory key = PoolKey({
|
|
648
|
+
currency0: Currency.wrap(sorted0),
|
|
649
|
+
currency1: Currency.wrap(sorted1),
|
|
650
|
+
fee: V4_FEES[i],
|
|
651
|
+
tickSpacing: V4_TICK_SPACINGS[i],
|
|
652
|
+
hooks: IHooks(address(0))
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
(uint160 sqrtPriceX96,,,) = POOL_MANAGER.getSlot0(key.toId());
|
|
656
|
+
if (sqrtPriceX96 == 0) continue;
|
|
657
|
+
|
|
658
|
+
uint128 liquidity = POOL_MANAGER.getLiquidity(key.toId());
|
|
659
|
+
if (liquidity > bestLiquidity) bestLiquidity = liquidity;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/// @notice Convert tokenIn to tokenOut. No-op if same, wrap/unwrap for NATIVE/WETH, or swap via Uniswap.
|
|
664
|
+
/// @param tokenIn The token to convert from.
|
|
665
|
+
/// @param tokenOut The token to convert to.
|
|
666
|
+
/// @param amount The amount to convert.
|
|
667
|
+
/// @param projectId The project ID (passed through to swap callback data).
|
|
668
|
+
/// @param metadata Bytes in `JBMetadataResolver`'s format (may contain quoteForSwap).
|
|
669
|
+
/// @return The amount of tokenOut produced.
|
|
670
|
+
function _convert(
|
|
671
|
+
address tokenIn,
|
|
672
|
+
address tokenOut,
|
|
673
|
+
uint256 amount,
|
|
674
|
+
uint256 projectId,
|
|
675
|
+
bytes calldata metadata
|
|
676
|
+
)
|
|
677
|
+
internal
|
|
678
|
+
returns (uint256)
|
|
679
|
+
{
|
|
680
|
+
// Exact same token — no conversion needed.
|
|
681
|
+
if (tokenIn == tokenOut) return amount;
|
|
682
|
+
|
|
683
|
+
address nIn = tokenIn == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenIn;
|
|
684
|
+
address nOut = tokenOut == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenOut;
|
|
685
|
+
|
|
686
|
+
if (nIn == nOut) {
|
|
687
|
+
// Same underlying token — just wrap or unwrap.
|
|
688
|
+
if (tokenIn == JBConstants.NATIVE_TOKEN) {
|
|
689
|
+
WETH.deposit{value: amount}();
|
|
690
|
+
} else {
|
|
691
|
+
WETH.withdraw(amount);
|
|
692
|
+
}
|
|
693
|
+
return amount;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Different tokens — swap via Uniswap (V3 or V4).
|
|
697
|
+
return _handleSwap({
|
|
698
|
+
projectId: projectId,
|
|
699
|
+
tokenIn: tokenIn,
|
|
700
|
+
tokenOut: tokenOut,
|
|
701
|
+
amount: amount,
|
|
702
|
+
metadata: metadata
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/// @notice Execute a Uniswap swap from tokenIn to tokenOut (V3 or V4).
|
|
707
|
+
/// @param projectId The project ID (included in callback data).
|
|
708
|
+
/// @param tokenIn The input token.
|
|
709
|
+
/// @param tokenOut The output token.
|
|
710
|
+
/// @param amount The amount of tokenIn to swap.
|
|
711
|
+
/// @param metadata Bytes in `JBMetadataResolver`'s format (may contain quoteForSwap).
|
|
712
|
+
/// @return amountOut The amount of tokenOut received.
|
|
713
|
+
function _handleSwap(
|
|
714
|
+
uint256 projectId,
|
|
715
|
+
address tokenIn,
|
|
716
|
+
address tokenOut,
|
|
717
|
+
uint256 amount,
|
|
718
|
+
bytes calldata metadata
|
|
719
|
+
)
|
|
720
|
+
internal
|
|
721
|
+
returns (uint256 amountOut)
|
|
722
|
+
{
|
|
723
|
+
address normalizedTokenIn = tokenIn == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenIn;
|
|
724
|
+
|
|
725
|
+
// Execute the swap in a scoped block to manage stack depth.
|
|
726
|
+
amountOut = _executeSwap({
|
|
727
|
+
normalizedTokenIn: normalizedTokenIn,
|
|
728
|
+
normalizedTokenOut: tokenOut == JBConstants.NATIVE_TOKEN ? address(WETH) : tokenOut,
|
|
729
|
+
amount: amount,
|
|
730
|
+
metadata: metadata,
|
|
731
|
+
callbackData: abi.encode(projectId, tokenIn, tokenOut)
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// Unwrap if output is native token.
|
|
735
|
+
if (tokenOut == JBConstants.NATIVE_TOKEN) WETH.withdraw(amountOut);
|
|
736
|
+
|
|
737
|
+
// Return leftover input tokens to payer.
|
|
738
|
+
uint256 leftover = IERC20(normalizedTokenIn).balanceOf(address(this));
|
|
739
|
+
if (leftover != 0) {
|
|
740
|
+
if (tokenIn == JBConstants.NATIVE_TOKEN) WETH.withdraw(leftover);
|
|
741
|
+
_transferFrom({from: address(this), to: payable(_msgSender()), token: tokenIn, amount: leftover});
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/// @notice Discover a pool, get a quote, and execute the swap (dispatches to V3 or V4).
|
|
746
|
+
/// @dev Separated from _handleSwap to manage stack depth.
|
|
747
|
+
function _executeSwap(
|
|
748
|
+
address normalizedTokenIn,
|
|
749
|
+
address normalizedTokenOut,
|
|
750
|
+
uint256 amount,
|
|
751
|
+
bytes calldata metadata,
|
|
752
|
+
bytes memory callbackData
|
|
753
|
+
)
|
|
754
|
+
internal
|
|
755
|
+
returns (uint256 amountOut)
|
|
756
|
+
{
|
|
757
|
+
(uint256 minAmountOut, PoolInfo memory pool) = _pickPoolAndQuote({
|
|
758
|
+
metadata: metadata,
|
|
759
|
+
normalizedTokenIn: normalizedTokenIn,
|
|
760
|
+
amount: amount,
|
|
761
|
+
normalizedTokenOut: normalizedTokenOut
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
if (pool.isV4) {
|
|
765
|
+
return _executeV4Swap({
|
|
766
|
+
key: pool.v4Key,
|
|
767
|
+
normalizedTokenIn: normalizedTokenIn,
|
|
768
|
+
normalizedTokenOut: normalizedTokenOut,
|
|
769
|
+
amount: amount,
|
|
770
|
+
minAmountOut: minAmountOut
|
|
771
|
+
});
|
|
772
|
+
} else {
|
|
773
|
+
return _executeV3Swap({
|
|
774
|
+
pool: pool.v3Pool,
|
|
775
|
+
normalizedTokenIn: normalizedTokenIn,
|
|
776
|
+
normalizedTokenOut: normalizedTokenOut,
|
|
777
|
+
amount: amount,
|
|
778
|
+
minAmountOut: minAmountOut,
|
|
779
|
+
callbackData: callbackData
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/// @notice Execute a swap through a V3 pool.
|
|
785
|
+
function _executeV3Swap(
|
|
786
|
+
IUniswapV3Pool pool,
|
|
787
|
+
address normalizedTokenIn,
|
|
788
|
+
address normalizedTokenOut,
|
|
789
|
+
uint256 amount,
|
|
790
|
+
uint256 minAmountOut,
|
|
791
|
+
bytes memory callbackData
|
|
792
|
+
)
|
|
793
|
+
internal
|
|
794
|
+
returns (uint256 amountOut)
|
|
795
|
+
{
|
|
796
|
+
bool zeroForOne = normalizedTokenIn < normalizedTokenOut;
|
|
797
|
+
|
|
798
|
+
(int256 amount0, int256 amount1) = pool.swap({
|
|
799
|
+
recipient: address(this),
|
|
800
|
+
zeroForOne: zeroForOne,
|
|
801
|
+
amountSpecified: int256(amount),
|
|
802
|
+
sqrtPriceLimitX96: JBSwapLib.sqrtPriceLimitFromAmounts(amount, minAmountOut, zeroForOne),
|
|
803
|
+
data: callbackData
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
amountOut = uint256(-(zeroForOne ? amount1 : amount0));
|
|
807
|
+
if (amountOut < minAmountOut) revert JBRouterTerminal_SlippageExceeded(amountOut, minAmountOut);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/// @notice Execute a swap through a V4 pool via PoolManager.unlock().
|
|
811
|
+
function _executeV4Swap(
|
|
812
|
+
PoolKey memory key,
|
|
813
|
+
address normalizedTokenIn,
|
|
814
|
+
address normalizedTokenOut,
|
|
815
|
+
uint256 amount,
|
|
816
|
+
uint256 minAmountOut
|
|
817
|
+
)
|
|
818
|
+
internal
|
|
819
|
+
returns (uint256 amountOut)
|
|
820
|
+
{
|
|
821
|
+
// Convert WETH addresses to V4's native ETH (address(0)) for currency comparison.
|
|
822
|
+
address v4In = normalizedTokenIn == address(WETH) ? address(0) : normalizedTokenIn;
|
|
823
|
+
bool zeroForOne = Currency.unwrap(key.currency0) == v4In;
|
|
824
|
+
|
|
825
|
+
uint160 sqrtPriceLimitX96 = zeroForOne
|
|
826
|
+
? TickMath.MIN_SQRT_RATIO + 1
|
|
827
|
+
: TickMath.MAX_SQRT_RATIO - 1;
|
|
828
|
+
|
|
829
|
+
bytes memory result = POOL_MANAGER.unlock(
|
|
830
|
+
abi.encode(key, zeroForOne, int256(amount), sqrtPriceLimitX96, minAmountOut)
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
amountOut = abi.decode(result, (uint256));
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/// @notice Settle the input side of a V4 swap (transfer tokens to PoolManager).
|
|
837
|
+
function _settleV4(Currency currency, uint256 amount) internal {
|
|
838
|
+
if (Currency.unwrap(currency) == address(0)) {
|
|
839
|
+
// Native ETH: contract already holds raw ETH.
|
|
840
|
+
POOL_MANAGER.settle{value: amount}();
|
|
841
|
+
} else {
|
|
842
|
+
// ERC20: sync then transfer then settle.
|
|
843
|
+
POOL_MANAGER.sync(currency);
|
|
844
|
+
IERC20(Currency.unwrap(currency)).safeTransfer(address(POOL_MANAGER), amount);
|
|
845
|
+
POOL_MANAGER.settle();
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/// @notice Take the output side of a V4 swap (receive tokens from PoolManager).
|
|
850
|
+
function _takeV4(Currency currency, uint256 amount) internal {
|
|
851
|
+
POOL_MANAGER.take(currency, address(this), amount);
|
|
852
|
+
|
|
853
|
+
// If native ETH output, wrap to WETH (downstream _handleSwap unwraps if needed).
|
|
854
|
+
if (Currency.unwrap(currency) == address(0)) {
|
|
855
|
+
WETH.deposit{value: amount}();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/// @notice Discover a pool and compute the minimum acceptable output for a swap.
|
|
860
|
+
/// @dev Uses a user-provided quote if available, otherwise falls back to TWAP (V3) or spot price (V4)
|
|
861
|
+
/// with dynamic slippage.
|
|
862
|
+
/// @param metadata Bytes in `JBMetadataResolver`'s format (may contain quoteForSwap).
|
|
863
|
+
/// @param normalizedTokenIn The normalized input token address.
|
|
864
|
+
/// @param amount The amount of tokens to swap.
|
|
865
|
+
/// @param normalizedTokenOut The normalized output token address.
|
|
866
|
+
/// @return minAmountOut The minimum acceptable output.
|
|
867
|
+
/// @return pool The pool to swap in (V3 or V4).
|
|
868
|
+
function _pickPoolAndQuote(
|
|
869
|
+
bytes calldata metadata,
|
|
870
|
+
address normalizedTokenIn,
|
|
871
|
+
uint256 amount,
|
|
872
|
+
address normalizedTokenOut
|
|
873
|
+
)
|
|
874
|
+
internal
|
|
875
|
+
view
|
|
876
|
+
returns (uint256 minAmountOut, PoolInfo memory pool)
|
|
877
|
+
{
|
|
878
|
+
// Discover the best pool across V3 and V4 fee tiers.
|
|
879
|
+
pool = _discoverPool(normalizedTokenIn, normalizedTokenOut);
|
|
880
|
+
|
|
881
|
+
// Check for a user-provided quote.
|
|
882
|
+
(bool exists, bytes memory quote) = JBMetadataResolver.getDataFor({
|
|
883
|
+
id: JBMetadataResolver.getId("quoteForSwap"),
|
|
884
|
+
metadata: metadata
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
if (exists) {
|
|
888
|
+
(minAmountOut) = abi.decode(quote, (uint256));
|
|
889
|
+
} else if (pool.isV4) {
|
|
890
|
+
minAmountOut = _getV4SpotQuote({
|
|
891
|
+
key: pool.v4Key,
|
|
892
|
+
normalizedTokenIn: normalizedTokenIn,
|
|
893
|
+
normalizedTokenOut: normalizedTokenOut,
|
|
894
|
+
amount: amount
|
|
895
|
+
});
|
|
896
|
+
} else {
|
|
897
|
+
minAmountOut = _getV3TwapQuote({
|
|
898
|
+
pool: pool.v3Pool,
|
|
899
|
+
normalizedTokenIn: normalizedTokenIn,
|
|
900
|
+
normalizedTokenOut: normalizedTokenOut,
|
|
901
|
+
amount: amount
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/// @notice Get a TWAP-based quote with dynamic slippage for a V3 pool.
|
|
907
|
+
function _getV3TwapQuote(
|
|
908
|
+
IUniswapV3Pool pool,
|
|
909
|
+
address normalizedTokenIn,
|
|
910
|
+
address normalizedTokenOut,
|
|
911
|
+
uint256 amount
|
|
912
|
+
)
|
|
913
|
+
internal
|
|
914
|
+
view
|
|
915
|
+
returns (uint256 minAmountOut)
|
|
916
|
+
{
|
|
917
|
+
uint256 feeBps = uint256(pool.fee()) / 100;
|
|
918
|
+
uint32 oldestObservation = OracleLibrary.getOldestObservationSecondsAgo(address(pool));
|
|
919
|
+
if (oldestObservation == 0) revert JBRouterTerminal_NoObservationHistory();
|
|
920
|
+
|
|
921
|
+
uint256 twapWindow = DEFAULT_TWAP_WINDOW;
|
|
922
|
+
if (oldestObservation < twapWindow) twapWindow = oldestObservation;
|
|
923
|
+
|
|
924
|
+
(int24 arithmeticMeanTick, uint128 liquidity) =
|
|
925
|
+
OracleLibrary.consult({pool: address(pool), secondsAgo: uint32(twapWindow)});
|
|
926
|
+
|
|
927
|
+
if (liquidity == 0) revert JBRouterTerminal_NoLiquidity();
|
|
928
|
+
|
|
929
|
+
{
|
|
930
|
+
uint256 slippageTolerance = _getSlippageTolerance({
|
|
931
|
+
amountIn: amount,
|
|
932
|
+
liquidity: liquidity,
|
|
933
|
+
tokenOut: normalizedTokenOut,
|
|
934
|
+
tokenIn: normalizedTokenIn,
|
|
935
|
+
arithmeticMeanTick: arithmeticMeanTick,
|
|
936
|
+
poolFeeBps: feeBps
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
if (slippageTolerance >= SLIPPAGE_DENOMINATOR) return 0;
|
|
940
|
+
|
|
941
|
+
if (amount > type(uint128).max) revert JBRouterTerminal_AmountOverflow(amount);
|
|
942
|
+
minAmountOut = OracleLibrary.getQuoteAtTick({
|
|
943
|
+
tick: arithmeticMeanTick,
|
|
944
|
+
baseAmount: uint128(amount),
|
|
945
|
+
baseToken: normalizedTokenIn,
|
|
946
|
+
quoteToken: normalizedTokenOut
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
minAmountOut -= (minAmountOut * slippageTolerance) / SLIPPAGE_DENOMINATOR;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/// @notice Get a spot-price-based quote with dynamic slippage for a V4 pool.
|
|
954
|
+
/// @dev V4 vanilla pools have no TWAP oracle. Uses spot tick with the same sigmoid slippage formula.
|
|
955
|
+
function _getV4SpotQuote(
|
|
956
|
+
PoolKey memory key,
|
|
957
|
+
address normalizedTokenIn,
|
|
958
|
+
address normalizedTokenOut,
|
|
959
|
+
uint256 amount
|
|
960
|
+
)
|
|
961
|
+
internal
|
|
962
|
+
view
|
|
963
|
+
returns (uint256 minAmountOut)
|
|
964
|
+
{
|
|
965
|
+
PoolId id = key.toId();
|
|
966
|
+
(, int24 tick,,) = POOL_MANAGER.getSlot0(id);
|
|
967
|
+
uint128 liquidity = POOL_MANAGER.getLiquidity(id);
|
|
968
|
+
|
|
969
|
+
if (liquidity == 0) revert JBRouterTerminal_NoLiquidity();
|
|
970
|
+
|
|
971
|
+
uint256 slippageTolerance = _getSlippageTolerance({
|
|
972
|
+
amountIn: amount,
|
|
973
|
+
liquidity: liquidity,
|
|
974
|
+
tokenOut: normalizedTokenOut,
|
|
975
|
+
tokenIn: normalizedTokenIn,
|
|
976
|
+
arithmeticMeanTick: tick,
|
|
977
|
+
poolFeeBps: uint256(key.fee) / 100
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
if (slippageTolerance >= SLIPPAGE_DENOMINATOR) return 0;
|
|
981
|
+
|
|
982
|
+
if (amount > type(uint128).max) revert JBRouterTerminal_AmountOverflow(amount);
|
|
983
|
+
minAmountOut = OracleLibrary.getQuoteAtTick({
|
|
984
|
+
tick: tick,
|
|
985
|
+
baseAmount: uint128(amount),
|
|
986
|
+
baseToken: normalizedTokenIn,
|
|
987
|
+
quoteToken: normalizedTokenOut
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
minAmountOut -= (minAmountOut * slippageTolerance) / SLIPPAGE_DENOMINATOR;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/// @notice Search Uniswap V3 and V4 for the best pool between two tokens.
|
|
994
|
+
/// @dev Returns the pool with the highest liquidity across both protocols.
|
|
995
|
+
/// @param normalizedTokenIn The input token (wrapped if native).
|
|
996
|
+
/// @param normalizedTokenOut The output token (wrapped if native).
|
|
997
|
+
/// @return bestPool The pool with the highest liquidity.
|
|
998
|
+
function _discoverPool(
|
|
999
|
+
address normalizedTokenIn,
|
|
1000
|
+
address normalizedTokenOut
|
|
1001
|
+
)
|
|
1002
|
+
internal
|
|
1003
|
+
view
|
|
1004
|
+
returns (PoolInfo memory bestPool)
|
|
1005
|
+
{
|
|
1006
|
+
uint128 bestLiquidity;
|
|
1007
|
+
|
|
1008
|
+
// Search V3.
|
|
1009
|
+
for (uint256 i; i < 4; i++) {
|
|
1010
|
+
address poolAddr = FACTORY.getPool(normalizedTokenIn, normalizedTokenOut, FEE_TIERS[i]);
|
|
1011
|
+
|
|
1012
|
+
if (poolAddr == address(0)) continue;
|
|
1013
|
+
|
|
1014
|
+
uint128 poolLiquidity = IUniswapV3Pool(poolAddr).liquidity();
|
|
1015
|
+
|
|
1016
|
+
if (poolLiquidity > bestLiquidity) {
|
|
1017
|
+
bestLiquidity = poolLiquidity;
|
|
1018
|
+
bestPool = PoolInfo({isV4: false, v3Pool: IUniswapV3Pool(poolAddr), v4Key: _emptyPoolKey()});
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Search V4.
|
|
1023
|
+
bestPool = _discoverV4Pool(normalizedTokenIn, normalizedTokenOut, bestLiquidity, bestPool);
|
|
1024
|
+
|
|
1025
|
+
if (!bestPool.isV4 && address(bestPool.v3Pool) == address(0)) {
|
|
1026
|
+
revert JBRouterTerminal_NoPoolFound(normalizedTokenIn, normalizedTokenOut);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/// @notice Search V4 vanilla pools and update bestPool if a V4 pool has higher liquidity.
|
|
1031
|
+
function _discoverV4Pool(
|
|
1032
|
+
address normalizedTokenIn,
|
|
1033
|
+
address normalizedTokenOut,
|
|
1034
|
+
uint128 currentBestLiquidity,
|
|
1035
|
+
PoolInfo memory bestPool
|
|
1036
|
+
)
|
|
1037
|
+
internal
|
|
1038
|
+
view
|
|
1039
|
+
returns (PoolInfo memory)
|
|
1040
|
+
{
|
|
1041
|
+
if (address(POOL_MANAGER) == address(0)) return bestPool;
|
|
1042
|
+
|
|
1043
|
+
// Convert WETH -> address(0) for V4 native ETH representation.
|
|
1044
|
+
address v4In = normalizedTokenIn == address(WETH) ? address(0) : normalizedTokenIn;
|
|
1045
|
+
address v4Out = normalizedTokenOut == address(WETH) ? address(0) : normalizedTokenOut;
|
|
1046
|
+
|
|
1047
|
+
// Sort currencies (currency0 < currency1).
|
|
1048
|
+
(address sorted0, address sorted1) = v4In < v4Out ? (v4In, v4Out) : (v4Out, v4In);
|
|
1049
|
+
|
|
1050
|
+
for (uint256 i; i < 4; i++) {
|
|
1051
|
+
PoolKey memory key = PoolKey({
|
|
1052
|
+
currency0: Currency.wrap(sorted0),
|
|
1053
|
+
currency1: Currency.wrap(sorted1),
|
|
1054
|
+
fee: V4_FEES[i],
|
|
1055
|
+
tickSpacing: V4_TICK_SPACINGS[i],
|
|
1056
|
+
hooks: IHooks(address(0))
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
(uint160 sqrtPriceX96,,,) = POOL_MANAGER.getSlot0(key.toId());
|
|
1060
|
+
if (sqrtPriceX96 == 0) continue;
|
|
1061
|
+
|
|
1062
|
+
uint128 poolLiquidity = POOL_MANAGER.getLiquidity(key.toId());
|
|
1063
|
+
if (poolLiquidity > currentBestLiquidity) {
|
|
1064
|
+
currentBestLiquidity = poolLiquidity;
|
|
1065
|
+
bestPool = PoolInfo({isV4: true, v3Pool: IUniswapV3Pool(address(0)), v4Key: key});
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return bestPool;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/// @notice Get the slippage tolerance for a given swap using the continuous sigmoid formula.
|
|
1073
|
+
/// @param amountIn The amount of tokens being swapped.
|
|
1074
|
+
/// @param liquidity The pool's in-range liquidity.
|
|
1075
|
+
/// @param tokenOut The output token.
|
|
1076
|
+
/// @param tokenIn The input token.
|
|
1077
|
+
/// @param arithmeticMeanTick The TWAP arithmetic mean tick (or spot tick for V4).
|
|
1078
|
+
/// @param poolFeeBps The pool fee in basis points.
|
|
1079
|
+
/// @return The slippage tolerance in basis points of SLIPPAGE_DENOMINATOR.
|
|
1080
|
+
function _getSlippageTolerance(
|
|
1081
|
+
uint256 amountIn,
|
|
1082
|
+
uint128 liquidity,
|
|
1083
|
+
address tokenOut,
|
|
1084
|
+
address tokenIn,
|
|
1085
|
+
int24 arithmeticMeanTick,
|
|
1086
|
+
uint256 poolFeeBps
|
|
1087
|
+
)
|
|
1088
|
+
internal
|
|
1089
|
+
pure
|
|
1090
|
+
returns (uint256)
|
|
1091
|
+
{
|
|
1092
|
+
(address token0,) = tokenOut < tokenIn ? (tokenOut, tokenIn) : (tokenIn, tokenOut);
|
|
1093
|
+
bool zeroForOne = tokenIn == token0;
|
|
1094
|
+
|
|
1095
|
+
uint160 sqrtP = TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
|
|
1096
|
+
if (sqrtP == 0) return SLIPPAGE_DENOMINATOR;
|
|
1097
|
+
|
|
1098
|
+
uint256 impact = JBSwapLib.calculateImpact(amountIn, liquidity, sqrtP, zeroForOne);
|
|
1099
|
+
return JBSwapLib.getSlippageTolerance(impact, poolFeeBps);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/// @notice Recursively cash out JB project tokens until reaching a token the destination accepts or a base token.
|
|
1103
|
+
/// @param destProjectId The ID of the destination project.
|
|
1104
|
+
/// @param token The current token being processed.
|
|
1105
|
+
/// @param amount The amount of the current token.
|
|
1106
|
+
/// @param sourceProjectIdOverride When non-zero, use this as the source project ID instead of looking up via
|
|
1107
|
+
/// `TOKENS.projectIdOf()`. Reset to 0 after first use.
|
|
1108
|
+
/// @return destTerminal The terminal that accepts the final token (address(0) if no direct acceptance found).
|
|
1109
|
+
/// @return finalToken The token after all cashouts.
|
|
1110
|
+
/// @return finalAmount The amount of the final token.
|
|
1111
|
+
function _cashOutLoop(
|
|
1112
|
+
uint256 destProjectId,
|
|
1113
|
+
address token,
|
|
1114
|
+
uint256 amount,
|
|
1115
|
+
uint256 sourceProjectIdOverride
|
|
1116
|
+
)
|
|
1117
|
+
internal
|
|
1118
|
+
returns (IJBTerminal destTerminal, address finalToken, uint256 finalAmount)
|
|
1119
|
+
{
|
|
1120
|
+
while (true) {
|
|
1121
|
+
// Skip the destination check on the first iteration if we have a credit override.
|
|
1122
|
+
if (sourceProjectIdOverride == 0) {
|
|
1123
|
+
destTerminal = DIRECTORY.primaryTerminalOf({projectId: destProjectId, token: token});
|
|
1124
|
+
if (address(destTerminal) != address(0)) {
|
|
1125
|
+
return (destTerminal, token, amount);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Use the override if provided, otherwise look up the project ID from the token.
|
|
1130
|
+
uint256 sourceProjectId =
|
|
1131
|
+
sourceProjectIdOverride != 0 ? sourceProjectIdOverride : TOKENS.projectIdOf(IJBToken(token));
|
|
1132
|
+
|
|
1133
|
+
// If it's not a JB project token, return as-is (caller handles the swap).
|
|
1134
|
+
if (sourceProjectId == 0) {
|
|
1135
|
+
return (IJBTerminal(address(0)), token, amount);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Find which terminal to cash out from and which token to reclaim.
|
|
1139
|
+
(address tokenToReclaim, IJBCashOutTerminal cashOutTerminal) =
|
|
1140
|
+
_findCashOutPath({sourceProjectId: sourceProjectId, destProjectId: destProjectId});
|
|
1141
|
+
|
|
1142
|
+
// Cash out the source project's tokens.
|
|
1143
|
+
amount = cashOutTerminal.cashOutTokensOf({
|
|
1144
|
+
holder: address(this),
|
|
1145
|
+
projectId: sourceProjectId,
|
|
1146
|
+
cashOutCount: amount,
|
|
1147
|
+
tokenToReclaim: tokenToReclaim,
|
|
1148
|
+
minTokensReclaimed: 0,
|
|
1149
|
+
beneficiary: payable(address(this)),
|
|
1150
|
+
metadata: bytes("")
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
// Update for next iteration.
|
|
1154
|
+
token = tokenToReclaim;
|
|
1155
|
+
sourceProjectIdOverride = 0;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/// @notice Find which terminal to cash out from and which token to reclaim.
|
|
1160
|
+
/// @dev Prioritizes: 1) tokens the destination directly accepts, 2) JB project tokens (recursable),
|
|
1161
|
+
/// 3) any base token (the router will swap it).
|
|
1162
|
+
/// @param sourceProjectId The ID of the project whose tokens are being cashed out.
|
|
1163
|
+
/// @param destProjectId The ID of the destination project.
|
|
1164
|
+
/// @return tokenToReclaim The token to reclaim from the cash out.
|
|
1165
|
+
/// @return cashOutTerminal The terminal to cash out from.
|
|
1166
|
+
function _findCashOutPath(
|
|
1167
|
+
uint256 sourceProjectId,
|
|
1168
|
+
uint256 destProjectId
|
|
1169
|
+
)
|
|
1170
|
+
internal
|
|
1171
|
+
view
|
|
1172
|
+
returns (address tokenToReclaim, IJBCashOutTerminal cashOutTerminal)
|
|
1173
|
+
{
|
|
1174
|
+
address fallbackToken;
|
|
1175
|
+
IJBCashOutTerminal fallbackTerminal;
|
|
1176
|
+
address baseFallbackToken;
|
|
1177
|
+
IJBCashOutTerminal baseFallbackTerminal;
|
|
1178
|
+
|
|
1179
|
+
IJBTerminal[] memory terminals = DIRECTORY.terminalsOf(sourceProjectId);
|
|
1180
|
+
|
|
1181
|
+
for (uint256 i; i < terminals.length; i++) {
|
|
1182
|
+
// Check if this terminal supports the IJBCashOutTerminal interface.
|
|
1183
|
+
{
|
|
1184
|
+
try IERC165(address(terminals[i])).supportsInterface(type(IJBCashOutTerminal).interfaceId) returns (
|
|
1185
|
+
bool supported
|
|
1186
|
+
) {
|
|
1187
|
+
if (!supported) continue;
|
|
1188
|
+
} catch {
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
IJBCashOutTerminal terminal = IJBCashOutTerminal(address(terminals[i]));
|
|
1194
|
+
JBAccountingContext[] memory contexts = terminals[i].accountingContextsOf(sourceProjectId);
|
|
1195
|
+
|
|
1196
|
+
for (uint256 j; j < contexts.length; j++) {
|
|
1197
|
+
address contextToken = contexts[j].token;
|
|
1198
|
+
|
|
1199
|
+
// Priority 1: Does the destination project directly accept this token?
|
|
1200
|
+
{
|
|
1201
|
+
IJBTerminal destTerminal =
|
|
1202
|
+
DIRECTORY.primaryTerminalOf({projectId: destProjectId, token: contextToken});
|
|
1203
|
+
if (address(destTerminal) != address(0)) {
|
|
1204
|
+
return (contextToken, terminal);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Priority 2: Is this a JB project token (so we can recurse)?
|
|
1209
|
+
if (address(fallbackTerminal) == address(0) && contextToken != JBConstants.NATIVE_TOKEN) {
|
|
1210
|
+
if (TOKENS.projectIdOf(IJBToken(contextToken)) != 0) {
|
|
1211
|
+
fallbackToken = contextToken;
|
|
1212
|
+
fallbackTerminal = terminal;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Priority 3: Any base token (the router will swap it).
|
|
1217
|
+
if (address(baseFallbackTerminal) == address(0)) {
|
|
1218
|
+
baseFallbackToken = contextToken;
|
|
1219
|
+
baseFallbackTerminal = terminal;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
if (address(fallbackTerminal) != address(0)) return (fallbackToken, fallbackTerminal);
|
|
1225
|
+
if (address(baseFallbackTerminal) != address(0)) return (baseFallbackToken, baseFallbackTerminal);
|
|
1226
|
+
|
|
1227
|
+
revert JBRouterTerminal_NoCashOutPath(sourceProjectId, destProjectId);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/// @notice Accepts a token being paid in.
|
|
1231
|
+
/// @param token The address of the token being paid in.
|
|
1232
|
+
/// @param amount The amount of tokens being paid in.
|
|
1233
|
+
/// @param metadata The metadata in which `permit2` and credit context is provided.
|
|
1234
|
+
/// @return The amount of tokens that have been accepted.
|
|
1235
|
+
function _acceptFundsFor(address token, uint256 amount, bytes calldata metadata) internal returns (uint256) {
|
|
1236
|
+
// Check for credit cash-out metadata.
|
|
1237
|
+
{
|
|
1238
|
+
(bool creditExists, bytes memory creditData) = JBMetadataResolver.getDataFor({
|
|
1239
|
+
id: JBMetadataResolver.getId("cashOutSource"),
|
|
1240
|
+
metadata: metadata
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
if (creditExists) {
|
|
1244
|
+
(uint256 sourceProjectId, uint256 creditAmount) = abi.decode(creditData, (uint256, uint256));
|
|
1245
|
+
|
|
1246
|
+
// Pull credits from the payer.
|
|
1247
|
+
TOKENS.transferCreditsFrom({
|
|
1248
|
+
holder: _msgSender(),
|
|
1249
|
+
projectId: sourceProjectId,
|
|
1250
|
+
recipient: address(this),
|
|
1251
|
+
count: creditAmount
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
return creditAmount;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
// If native tokens are being paid in, return the `msg.value`.
|
|
1259
|
+
if (token == JBConstants.NATIVE_TOKEN) return msg.value;
|
|
1260
|
+
|
|
1261
|
+
// Otherwise, the `msg.value` should be 0.
|
|
1262
|
+
if (msg.value != 0) revert JBRouterTerminal_NoMsgValueAllowed(msg.value);
|
|
1263
|
+
|
|
1264
|
+
// Unpack the `JBSingleAllowance` to use given by the frontend.
|
|
1265
|
+
(bool exists, bytes memory parsedMetadata) =
|
|
1266
|
+
JBMetadataResolver.getDataFor({id: JBMetadataResolver.getId("permit2"), metadata: metadata});
|
|
1267
|
+
|
|
1268
|
+
// If the metadata contained permit data, use it to set the allowance.
|
|
1269
|
+
if (exists) {
|
|
1270
|
+
// Keep a reference to the allowance context parsed from the metadata.
|
|
1271
|
+
(JBSingleAllowance memory allowance) = abi.decode(parsedMetadata, (JBSingleAllowance));
|
|
1272
|
+
|
|
1273
|
+
// Make sure the permit allowance is enough for this payment. If not, revert early.
|
|
1274
|
+
if (amount > allowance.amount) {
|
|
1275
|
+
revert JBRouterTerminal_PermitAllowanceNotEnough(amount, allowance.amount);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Keep a reference to the permit rules.
|
|
1279
|
+
IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({
|
|
1280
|
+
details: IAllowanceTransfer.PermitDetails({
|
|
1281
|
+
token: token, amount: allowance.amount, expiration: allowance.expiration, nonce: allowance.nonce
|
|
1282
|
+
}),
|
|
1283
|
+
spender: address(this),
|
|
1284
|
+
sigDeadline: allowance.sigDeadline
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
try PERMIT2.permit({owner: _msgSender(), permitSingle: permitSingle, signature: allowance.signature}) {}
|
|
1288
|
+
catch (bytes memory reason) {
|
|
1289
|
+
emit Permit2AllowanceFailed(token, _msgSender(), reason);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Transfer the tokens from the `_msgSender()` to this terminal.
|
|
1294
|
+
_transferFrom({from: _msgSender(), to: payable(address(this)), token: token, amount: amount});
|
|
1295
|
+
|
|
1296
|
+
// Return the amount transferred.
|
|
1297
|
+
return amount;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/// @notice Logic to be triggered before transferring tokens from this terminal.
|
|
1301
|
+
/// @param to The address to transfer tokens to.
|
|
1302
|
+
/// @param token The token being transferred.
|
|
1303
|
+
/// @param amount The amount of tokens to transfer.
|
|
1304
|
+
/// @return payValue The amount that will be paid as a `msg.value`.
|
|
1305
|
+
function _beforeTransferFor(address to, address token, uint256 amount) internal returns (uint256) {
|
|
1306
|
+
// If the token is the native token, return the amount as msg.value.
|
|
1307
|
+
if (token == JBConstants.NATIVE_TOKEN) return amount;
|
|
1308
|
+
|
|
1309
|
+
// Otherwise, set the appropriate allowance for the recipient.
|
|
1310
|
+
IERC20(token).safeIncreaseAllowance(to, amount);
|
|
1311
|
+
|
|
1312
|
+
return 0;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/// @notice Transfers tokens.
|
|
1316
|
+
/// @param from The address to transfer tokens from.
|
|
1317
|
+
/// @param to The address to transfer tokens to.
|
|
1318
|
+
/// @param token The address of the token being transferred.
|
|
1319
|
+
/// @param amount The amount of tokens to transfer.
|
|
1320
|
+
function _transferFrom(address from, address payable to, address token, uint256 amount) internal {
|
|
1321
|
+
if (from == address(this)) {
|
|
1322
|
+
// If the token is native token, assume the `sendValue` standard.
|
|
1323
|
+
if (token == JBConstants.NATIVE_TOKEN) return Address.sendValue(to, amount);
|
|
1324
|
+
|
|
1325
|
+
// If the transfer is from this terminal, use `safeTransfer`.
|
|
1326
|
+
return IERC20(token).safeTransfer(to, amount);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// If there's sufficient approval, transfer normally.
|
|
1330
|
+
if (IERC20(token).allowance({owner: address(from), spender: address(this)}) >= amount) {
|
|
1331
|
+
return IERC20(token).safeTransferFrom(from, to, amount);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// Otherwise, attempt to use the `permit2` method.
|
|
1335
|
+
PERMIT2.transferFrom(from, to, uint160(amount), token);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
/// @notice Returns an empty PoolKey for use in V3-only PoolInfo structs.
|
|
1339
|
+
function _emptyPoolKey() internal pure returns (PoolKey memory) {
|
|
1340
|
+
return PoolKey({
|
|
1341
|
+
currency0: Currency.wrap(address(0)),
|
|
1342
|
+
currency1: Currency.wrap(address(0)),
|
|
1343
|
+
fee: 0,
|
|
1344
|
+
tickSpacing: 0,
|
|
1345
|
+
hooks: IHooks(address(0))
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
//*********************************************************************//
|
|
1350
|
+
// ---------------------------- receive ----------------------------- //
|
|
1351
|
+
//*********************************************************************//
|
|
1352
|
+
|
|
1353
|
+
/// @notice Receive native tokens from cash out reclaims, WETH unwraps, and V4 PoolManager takes.
|
|
1354
|
+
receive() external payable {}
|
|
1355
|
+
}
|