generated from Bananapus/juice-contract-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
JBSwapTerminal.sol
726 lines (619 loc) · 35.7 KB
/
JBSwapTerminal.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import {IERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IPermit2, IAllowanceTransfer} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
import {OracleLibrary} from "@uniswap/v3-periphery/contracts/libraries/OracleLibrary.sol";
import {TickMath} from "@uniswap/v3-core/contracts/libraries/TickMath.sol";
import {IUniswapV3SwapCallback} from "@uniswap/v3-core/contracts/interfaces/callback/IUniswapV3SwapCallback.sol";
import {IJBTerminal} from "@bananapus/core/src/interfaces/IJBTerminal.sol";
import {IJBPermitTerminal} from "@bananapus/core/src/interfaces/IJBPermitTerminal.sol";
import {IJBDirectory} from "@bananapus/core/src/interfaces/IJBDirectory.sol";
import {IJBPermissions} from "@bananapus/core/src/interfaces/IJBPermissions.sol";
import {IJBProjects} from "@bananapus/core/src/interfaces/IJBProjects.sol";
import {IJBTerminalStore} from "@bananapus/core/src/interfaces/IJBTerminalStore.sol";
import {JBMetadataResolver} from "@bananapus/core/src/libraries/JBMetadataResolver.sol";
import {JBSingleAllowanceContext} from "@bananapus/core/src/structs/JBSingleAllowanceContext.sol";
import {JBPermissioned} from "@bananapus/core/src/abstract/JBPermissioned.sol";
import {JBAccountingContext} from "@bananapus/core/src/structs/JBAccountingContext.sol";
import {JBConstants} from "@bananapus/core/src/libraries/JBConstants.sol";
import {JBPermissionIds} from "@bananapus/permission-ids/src/JBPermissionIds.sol";
import {IWETH9} from "./interfaces/IWETH9.sol";
/// @notice The `JBSwapTerminal` accepts payments in any token. When the `JBSwapTerminal` is paid, it uses a Uniswap
/// pool to exchange the tokens it received for tokens that another one of its project's terminals can accept. Then, it
/// pays that terminal with the tokens it got from the pool, forwarding the specified beneficiary to receive any tokens
/// or NFTs minted by that payment, as well as payment metadata and other arguments.
/// @dev To prevent excessive slippage, the user/client can specify a minimum quote and a pool to use in their payment's
/// metadata using the `JBMetadataResolver` format. If they don't, a quote is calculated for them based on the TWAP
/// oracle for the project's default pool for that token (set by the project's owner).
/// @custom:metadata-id-used quoteForSwap and permit2
/// @custom:benediction DEVS BENEDICAT ET PROTEGAT CONTRACTVS MEAM
contract JBSwapTerminal is JBPermissioned, Ownable, IJBTerminal, IJBPermitTerminal, IUniswapV3SwapCallback {
// A library that adds default safety checks to ERC20 functionality.
using SafeERC20 for IERC20;
//*********************************************************************//
// ------------------------------ structs ---------------------------- //
//*********************************************************************//
/// @notice A struct representing the parameters of a swap.
/// @dev This struct is only used in memory (no packing).
struct SwapConfig {
uint256 projectId;
IUniswapV3Pool pool;
address tokenIn;
bool inIsNativeToken; // `tokenIn` is wETH if true.
uint256 amountIn;
uint256 minAmountOut;
bool zeroForOne;
}
/// @notice A struct representing the configuration of a pool.
/// @dev This struct is only used in storage (packed).
/// @member pool The Uniswap V3 pool to use for the swap.
/// @member tokenOutIsToken0 True if tokenIn==token0 of the pool
struct PoolConfig {
IUniswapV3Pool pool;
bool zeroForOne;
}
//*********************************************************************//
// --------------------------- custom errors ------------------------- //
//*********************************************************************//
error PERMIT_ALLOWANCE_NOT_ENOUGH();
error NO_DEFAULT_POOL_DEFINED();
error NO_MSG_VALUE_ALLOWED();
error TOKEN_NOT_ACCEPTED();
error UNSUPPORTED();
error MAX_SLIPPAGE(uint256, uint256);
//*********************************************************************//
// --------------------- internal stored properties ------------------ //
//*********************************************************************//
/// @notice The twap params for each project's pools.
/// @custom:param projectId The ID of the project to get the TWAP parameters for.
/// @custom:param pool The pool to get the TWAP parameters for.
mapping(uint256 projectId => mapping(IUniswapV3Pool pool => uint256 params)) internal _twapParamsOf;
/// @notice A mapping which stores the default pool to use for a given project ID and token.
/// @dev Default pools are set by the project owner with `addDefaultPool(...)`, the project 0 acts as a wildcard
/// @dev Default pools are used when a payer doesn't specify a pool in their payment's metadata.
/// @custom:param projectId The ID of the project to get the pool for.
/// @custom:param tokenIn The address of the token to get the pool for.
mapping(uint256 projectId => mapping(address tokenIn => PoolConfig)) internal _poolFor;
/// @notice A mapping which stores accounting contexts to use for a given project ID and token.
/// @dev Accounting contexts are set up for a project ID and token when the project's owner uses
/// `addDefaultPool(...)` for that token.
/// @custom:param projectId The ID of the project to get the accounting context for.
/// @custom:param token The address of the token to get the accounting context for.
mapping(uint256 projectId => mapping(address token => JBAccountingContext)) internal _accountingContextFor;
/// @notice A mapping which stores the tokens that have an accounting context for a given project ID.
/// @dev This is used to retrieve all the accounting contexts for a project ID.
/// @custom:param projectId The ID of the project to get the tokens with a context for.
mapping(uint256 projectId => address[]) internal _tokensWithAContext;
//*********************************************************************//
// ------------------------- public constants ------------------------ //
//*********************************************************************//
/// @notice The denominator used when calculating TWAP slippage tolerance values.
uint160 public constant SLIPPAGE_DENOMINATOR = 10_000;
/// @notice The ID to store default values in.
uint256 public constant DEFAULT_PROJECT_ID = 0;
//*********************************************************************//
// ---------------- public immutable stored properties --------------- //
//*********************************************************************//
/// @notice Mints ERC-721s that represent project ownership and transfers.
IJBProjects public immutable PROJECTS;
/// @notice The directory of terminals and controllers for `PROJECTS`.
IJBDirectory public immutable DIRECTORY;
/// @notice The permit2 utility.
IPermit2 public immutable PERMIT2;
/// @notice The ERC-20 wrapper for the native token.
/// @dev "wETH" is used as a generic term throughout, but any native token wrapper can be used.
IWETH9 public immutable WETH;
/// @notice The token which flows out of this terminal (JBConstants.NATIVE_TOKEN for the chain native token)
address public immutable TOKEN_OUT;
//*********************************************************************//
// --------------- internal immutable stored properties -------------- //
//*********************************************************************//
/// @notice A flag indicating if the token out is the chain native token (eth on mainnet for instance)
/// @dev If so, the token out should be unwrapped before being sent to the next terminal
bool internal immutable OUT_IS_NATIVE_TOKEN;
//*********************************************************************//
// ------------------------- external views -------------------------- //
//*********************************************************************//
/// @notice Returns the default pool for a given project and token or, if a project has no default pool for the
/// token, the overal default pool for the token
/// @param projectId The ID of the project to retrieve the default pool for.
/// @param tokenIn The address of the token to retrieve the default pool for.
/// @return pool The default pool for the token, or the overall default pool for the token if the
function getPoolFor(
uint256 projectId,
address tokenIn
)
external
view
returns (IUniswapV3Pool pool, bool zeroForOne)
{
PoolConfig storage poolConfig = _poolFor[projectId][tokenIn];
(pool, zeroForOne) = (poolConfig.pool, poolConfig.zeroForOne);
if (address(pool) == address(0)) {
poolConfig = _poolFor[DEFAULT_PROJECT_ID][tokenIn];
(pool, zeroForOne) = (poolConfig.pool, poolConfig.zeroForOne);
}
}
/// @notice Get the accounting context for the specified project ID and token.
/// @dev Accounting contexts are set up in `addDefaultPool(...)`.
/// @param projectId The ID of the project to get the accounting context for.
/// @param token The address of the token to get the accounting context for.
/// @return A `JBAccountingContext` containing the accounting context for the project ID and token.
function accountingContextForTokenOf(
uint256 projectId,
address token
)
external
view
override
returns (JBAccountingContext memory)
{
return _accountingContextFor[projectId][token];
}
/// @notice Return all the accounting contexts for a specified project ID.
/// @dev This includes both project-specific and generic accounting contexts, with the project-specific contexts
/// taking precedence.
/// @param projectId The ID of the project to get the accounting contexts for.
/// @return An array of `JBAccountingContext` containing the accounting contexts for the project ID.
function accountingContextsOf(uint256 projectId) external view override returns (JBAccountingContext[] memory) {
address[] memory projectTokenContexts = _tokensWithAContext[projectId];
address[] memory genericTokenContexts = _tokensWithAContext[DEFAULT_PROJECT_ID];
JBAccountingContext[] memory contexts =
new JBAccountingContext[](projectTokenContexts.length + genericTokenContexts.length);
uint256 actualLength = projectTokenContexts.length;
// include all the project specific contexts
for (uint256 i = 0; i < projectTokenContexts.length; i++) {
contexts[i] = _accountingContextFor[projectId][projectTokenContexts[i]];
}
// add the generic contexts, iff they are not defined for the project (ie do not include duplicates)
for (uint256 i = 0; i < genericTokenContexts.length; i++) {
bool skip;
for (uint256 j = 0; j < projectTokenContexts.length; j++) {
if (projectTokenContexts[j] == genericTokenContexts[i]) {
skip = true;
break;
}
}
if (!skip) {
contexts[actualLength] = _accountingContextFor[DEFAULT_PROJECT_ID][genericTokenContexts[i]];
actualLength++;
}
}
// Downsize the array to the actual length, if needed
if (actualLength < contexts.length) {
assembly {
mstore(contexts, actualLength)
}
}
return contexts;
}
/// @notice Empty implementation to satisfy the interface. This terminal has no surplus.
function currentSurplusOf(uint256 projectId, uint256 decimals, uint256 currency) external view returns (uint256) {}
//*********************************************************************//
// -------------------------- public views --------------------------- //
//*********************************************************************//
/// @notice Returns the default twap parameters for a given pool project.
/// @param projectId The ID of the project to retrieve TWAP parameters for.
/// @return secondsAgo The period of time in the past to calculate the TWAP from.
/// @return slippageTolerance The maximum allowed slippage tolerance when calculating the TWAP, as a fraction out of
/// `SLIPPAGE_DENOMINATOR`.
function twapParamsOf(
uint256 projectId,
IUniswapV3Pool pool
)
public
view
returns (uint32 secondsAgo, uint160 slippageTolerance)
{
uint256 twapParams = _twapParamsOf[projectId][pool];
if (twapParams == 0) {
twapParams = _twapParamsOf[DEFAULT_PROJECT_ID][pool];
}
return (uint32(twapParams), uint160(twapParams >> 32));
}
/// @notice Indicates if this contract adheres to the specified interface.
/// @dev See {IERC165-supportsInterface}.
/// @param interfaceId The ID of the interface to check for adherance to.
/// @return A flag indicating if the provided interface ID is supported.
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPermitTerminal).interfaceId
|| interfaceId == type(IERC165).interfaceId;
}
//*********************************************************************//
// -------------------------- constructor ---------------------------- //
//*********************************************************************//
constructor(
IJBProjects projects,
IJBPermissions permissions,
IJBDirectory directory,
IPermit2 permit2,
address owner,
IWETH9 weth,
address tokenOut
)
JBPermissioned(permissions)
Ownable(owner)
{
PROJECTS = projects;
DIRECTORY = directory;
PERMIT2 = permit2;
WETH = weth;
TOKEN_OUT = tokenOut;
OUT_IS_NATIVE_TOKEN = tokenOut == JBConstants.NATIVE_TOKEN;
}
//*********************************************************************//
// ---------------------- external transactions ---------------------- //
//*********************************************************************//
/// @notice Pay a project by swapping the incoming tokens for tokens that one of the project's other terminals
/// accepts, passing along the funds received from the swap and the specified parameters.
/// @param projectId The ID of the project being paid.
/// @param token The address of the token being paid in.
/// @param amount The amount of tokens being paid in, as a fixed point number with the same amount of decimals as
/// the `token`. If `token` is the native token, `amount` is ignored and `msg.value` is used in its place.
/// @param beneficiary The beneficiary address to pass along to the other terminal. If the other terminal mints
/// tokens, for example, they will be minted for this address.
/// @param minReturnedTokens The minimum number of project tokens expected in return, as a fixed point number with
/// the same number of decimals as the other terminal. This value will be passed along to the other terminal.
/// @param memo A memo to pass along to the emitted event.
/// @param metadata Bytes in `JBMetadataResolver`'s format which can contain a quote from the user/client. The quote
/// should contain a minimum amount of tokens to receive from the swap and the pool to use. This metadata is also
/// passed to the other terminal's emitted event, as well as its data hook and pay hook if applicable.
/// @return The number of tokens received from the swap, as a fixed point number with the same amount of decimals as
/// that token.
function pay(
uint256 projectId,
address token,
uint256 amount,
address beneficiary,
uint256 minReturnedTokens,
string calldata memo,
bytes calldata metadata
)
external
payable
virtual
override
returns (uint256)
{
// Get a reference to the project's primary terminal for `token`.
IJBTerminal terminal = DIRECTORY.primaryTerminalOf(projectId, TOKEN_OUT);
// Revert if the project does not have a primary terminal for `token`.
if (address(terminal) == address(0)) revert TOKEN_NOT_ACCEPTED();
uint256 receivedFromSwap = _handleTokenTransfersAndSwap(projectId, token, amount, address(terminal), metadata);
// Pay the primary terminal, passing along the beneficiary and other arguments.
terminal.pay{value: OUT_IS_NATIVE_TOKEN ? receivedFromSwap : 0}({
projectId: projectId,
token: TOKEN_OUT,
amount: receivedFromSwap,
beneficiary: beneficiary,
minReturnedTokens: minReturnedTokens,
memo: memo,
metadata: metadata
});
return receivedFromSwap;
}
/// @notice Accepts funds for a given project, swaps them if necessary, and adds them to the project's balance in
/// the specified terminal.
/// @dev This function handles the token in transfer, potentially swaps the tokens to the desired output token, and
/// then adds the swapped tokens to the project's balance in the specified terminal.
/// @param projectId The ID of the project for which funds are being accepted and added to its balance.
/// @param token The address of the token being paid in.
/// @param amount The amount of tokens being paid in.
/// @param shouldReturnHeldFees A boolean to indicate whether held fees should be returned.
/// @param memo A memo to pass along to the emitted event.
/// @param metadata Bytes in `JBMetadataResolver`'s format which can contain additional data for the swap and adding
/// to balance.
function addToBalanceOf(
uint256 projectId,
address token,
uint256 amount,
bool shouldReturnHeldFees,
string calldata memo,
bytes calldata metadata
)
external
payable
override
{
// Get a reference to the project's primary terminal for `token`.
IJBTerminal terminal = DIRECTORY.primaryTerminalOf(projectId, TOKEN_OUT);
// Revert if the project does not have a primary terminal for `token`.
if (address(terminal) == address(0)) revert TOKEN_NOT_ACCEPTED();
uint256 receivedFromSwap = _handleTokenTransfersAndSwap(projectId, token, amount, address(terminal), metadata);
// Pay the primary terminal, passing along the beneficiary and other arguments.
terminal.addToBalanceOf{value: OUT_IS_NATIVE_TOKEN ? receivedFromSwap : 0}({
projectId: projectId,
token: TOKEN_OUT,
amount: receivedFromSwap,
shouldReturnHeldFees: shouldReturnHeldFees,
memo: memo,
metadata: metadata
});
}
/// @notice The Uniswap v3 pool callback where the token transfer is expected to happen.
/// @dev This function has no access control, care should be taken to ensure that:
/// - terminal balance is always be 0 between tx (this callback can only be used to sweep accidental leftovers)
/// - callback cannot pull user funds via a preexisting allowance
/// @param amount0Delta The amount of token 0 being used for the swap.
/// @param amount1Delta The amount of token 1 being used for the swap.
/// @param data Data passed in by the swap operation.
function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external override {
// Unpack the data from the original swap config (forwarded through `_swap(...)`).
(address tokenIn, bool shouldWrap) = abi.decode(data, (address, bool));
// Keep a reference to the amount of tokens that should be sent to fulfill the swap (the positive delta).
uint256 amountToSendToPool = amount0Delta < 0 ? uint256(amount1Delta) : uint256(amount0Delta);
// Wrap native tokens if needed.
if (shouldWrap) WETH.deposit{value: amountToSendToPool}();
// Transfer the tokens to the pool.
// This terminal should NEVER keep a token balance.
IERC20(tokenIn).transfer(msg.sender, amountToSendToPool);
}
/// @notice Fallback to prevent native tokens being sent to this terminal.
/// @dev Native tokens should only be sent to this terminal when being unwrapped from a swap.
receive() external payable {
if (msg.sender != address(WETH)) revert NO_MSG_VALUE_ALLOWED();
}
/// @notice Set a project's default pool and accounting context for the specified token. Only the project's owner,
/// an address with `MODIFY_DEFAULT_POOL` permission from the owner or the terminal owner can call this function.
/// @param projectId The ID of the project to set the default pool for. The project 0 acts as a catch-all, where
/// non-set pools are defaulted to.
/// @param token The address of the token to set the default pool for.
/// @param pool The Uniswap V3 pool to set as the default for the specified token.
function addDefaultPool(uint256 projectId, address token, IUniswapV3Pool pool) external {
// Only the project owner can set the default pool for a token, only the project owner can set the
// pool for its project.
if (!(projectId == DEFAULT_PROJECT_ID && msg.sender == owner())) {
_requirePermissionFrom(PROJECTS.ownerOf(projectId), projectId, JBPermissionIds.ADD_SWAP_TERMINAL_POOL);
}
// Update the project's default pool for the token.
_poolFor[projectId][token] = PoolConfig({pool: pool, zeroForOne: token < formattedTokenOut()});
// Update the project's accounting context for the token.
_accountingContextFor[projectId][token] = JBAccountingContext({
token: token,
decimals: IERC20Metadata(token).decimals(),
currency: uint32(uint160(token))
});
_tokensWithAContext[projectId].push(token);
}
/// @notice Empty implementation to satisfy the interface. Accounting contexts are set in `addDefaultPool(...)`.
function addAccountingContextsFor(uint256 projectId, address[] calldata tokens) external {}
/// @notice Set the specified project's rules for calculating a quote based on the TWAP. Only the project's owner or
/// an address with `MODIFY_TWAP_PARAMS` permission from the owner or the terminal owner can call this function.
/// @param projectId The ID of the project to set the TWAP-based quote rules for.
/// @param secondsAgo The period of time over which the TWAP is calculated, in seconds.
/// @param slippageTolerance The maximum spread allowed between the amount received and the TWAP (as a fraction out
/// of `SLIPPAGE_DENOMINATOR`).
function addTwapParamsFor(
uint256 projectId,
IUniswapV3Pool pool,
uint32 secondsAgo,
uint160 slippageTolerance
)
external
{
// Only the project owner can set the default twap params for a pool, only the project owner can set the
// params for its project.
if (!(projectId == DEFAULT_PROJECT_ID && msg.sender == owner())) {
_requirePermissionFrom(
PROJECTS.ownerOf(projectId), projectId, JBPermissionIds.ADD_SWAP_TERMINAL_TWAP_PARAMS
);
}
// Set the TWAP params for the project.
_twapParamsOf[projectId][pool] = uint256(secondsAgo | uint256(slippageTolerance) << 32);
}
/// @notice Empty implementation to satisfy the interface.
function migrateBalanceOf(uint256 projectId, address token, IJBTerminal to) external returns (uint256 balance) {}
//*********************************************************************//
// ---------------------- internal transactions ---------------------- //
//*********************************************************************//
/// @notice Handles token transfers and swaps for a given project.
/// @dev This function is responsible for transferring tokens from the sender to this terminal and performing a
/// swap.
/// @param projectId The ID of the project for which tokens are being transferred and possibly swapped.
/// @param token The address of the token coming to this terminal.
/// @param nextTerminal The address of the next terminal to which the swapped tokens will be sent.
/// @param metadata Additional data to be used in the swap.
/// @return amountToSend The amount of tokens to send after the swap, to the next terminal
function _handleTokenTransfersAndSwap(
uint256 projectId,
address token,
uint256 amount,
address nextTerminal,
bytes calldata metadata
)
internal
returns (uint256 amountToSend)
{
SwapConfig memory swapConfig;
swapConfig.projectId = projectId;
if (token == JBConstants.NATIVE_TOKEN) {
// If the token being paid in is the native token, use `msg.value`.
swapConfig.tokenIn = address(WETH);
swapConfig.inIsNativeToken = true;
swapConfig.amountIn = msg.value;
} else {
// Otherwise, use `amount`.
swapConfig.tokenIn = token;
swapConfig.amountIn = amount;
}
(swapConfig.minAmountOut, swapConfig.pool, swapConfig.zeroForOne) =
_pickPoolAndQuote(metadata, projectId, swapConfig.tokenIn);
// Accept funds for the swap.
swapConfig.amountIn = _acceptFundsFor(swapConfig, metadata);
// Keep a reference to the formatted token out.
address tokenOut = formattedTokenOut();
// Swap. The callback will ensure that we're within the intended slippage tolerance.
// If the token in is the same as the token out, don't swap, just call the next terminal
if ((swapConfig.inIsNativeToken && OUT_IS_NATIVE_TOKEN) || (swapConfig.tokenIn == tokenOut)) {
amountToSend = swapConfig.amountIn;
} else {
amountToSend = _swap(swapConfig);
}
// Trigger the `beforeTransferFor` hook.
_beforeTransferFor(nextTerminal, tokenOut, amountToSend);
}
function _pickPoolAndQuote(
bytes calldata metadata,
uint256 projectId,
address token
)
internal
view
returns (uint256 minAmountOut, IUniswapV3Pool pool, bool zeroForOne)
{
{
// Check for a quote passed in by the user/client.
(bool exists, bytes memory quote) =
JBMetadataResolver.getDataFor(JBMetadataResolver.getId("quoteForSwap"), metadata);
if (exists) {
// If there is a quote, use it for the swap config.
(minAmountOut, pool, zeroForOne) = abi.decode(quote, (uint256, IUniswapV3Pool, bool));
} else {
// If there is no quote, check for this project's default pool for the token and get a quote based on
// its TWAP.
PoolConfig storage poolConfig = _poolFor[projectId][token];
(pool, zeroForOne) = (poolConfig.pool, poolConfig.zeroForOne);
// If this project doesn't have a default pool specified for this token, try using a generic one.
if (address(pool) == address(0)) {
poolConfig = _poolFor[DEFAULT_PROJECT_ID][token];
(pool, zeroForOne) = (poolConfig.pool, poolConfig.zeroForOne);
// If there's no default pool neither, revert.
if (address(pool) == address(0)) revert NO_DEFAULT_POOL_DEFINED();
}
// Get a quote based on the pool's TWAP, including a default slippage maximum.
minAmountOut = _getTwapFrom(pool, projectId, zeroForOne);
}
}
}
/// @notice Get a quote based on the TWAP.
/// @dev The TWAP is calculated over `secondsAgo` seconds, and the quote cannot unfavourably deviate from the TWAP
/// by more than `slippageTolerance` (as a fraction out of `SLIPPAGE_DENOMINATOR`).
function _getTwapFrom(IUniswapV3Pool pool, uint256 projectId, bool zeroForOne) internal view returns (uint160) {
// Unpack the project's TWAP params and get a reference to the period and slippage.
(uint32 secondsAgo, uint160 slippageTolerance) = twapParamsOf(projectId, pool);
// Keep a reference to the TWAP tick.
(int24 arithmeticMeanTick,) = OracleLibrary.consult(address(pool), secondsAgo);
// Get a quote based on that TWAP tick.
uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(arithmeticMeanTick);
// Return the lowest acceptable price for the swap based on the TWAP and slippage tolerance.
return zeroForOne
? sqrtPriceX96 - (sqrtPriceX96 * slippageTolerance) / SLIPPAGE_DENOMINATOR
: sqrtPriceX96 + (sqrtPriceX96 * slippageTolerance) / SLIPPAGE_DENOMINATOR;
}
/// @notice Accepts a token being paid in.
/// @param swapConfig The swap config which tokens are being accepted for.
/// @param metadata The metadata in which `permit2` context is provided.
/// @return amount The amount of tokens that have been accepted.
function _acceptFundsFor(SwapConfig memory swapConfig, bytes calldata metadata) internal returns (uint256) {
// Get a reference to address of the token being paid in.
address token = swapConfig.tokenIn;
// If native tokens are being paid in, return the `msg.value`.
if (swapConfig.inIsNativeToken) return msg.value;
// Otherwise, the `msg.value` should be 0.
if (msg.value != 0) revert NO_MSG_VALUE_ALLOWED();
// Unpack the `JBSingleAllowanceContext` to use given by the frontend.
(bool exists, bytes memory rawAllowance) =
JBMetadataResolver.getDataFor(JBMetadataResolver.getId("permit2"), metadata);
// If the metadata contained permit data, use it to set the allowance.
if (exists) {
// Keep a reference to the allowance context parsed from the metadata.
(JBSingleAllowanceContext memory allowance) = abi.decode(rawAllowance, (JBSingleAllowanceContext));
// Make sure the permit allowance is enough for this payment. If not, revert early.
if (allowance.amount < swapConfig.amountIn) {
revert PERMIT_ALLOWANCE_NOT_ENOUGH();
}
// Set the `permit2` allowance for the user.
_permitAllowance(allowance, token);
}
// Transfer the tokens from the `msg.sender` to this terminal.
_transferFor(msg.sender, payable(address(this)), token, swapConfig.amountIn);
// The amount actually received.
return IERC20(token).balanceOf(address(this));
}
/// @notice Swaps tokens based on the provided swap configuration.
/// @param swapConfig The configuration for the swap, including the tokens and amounts involved.
/// @return amountReceived The amount of tokens received from the swap.
function _swap(SwapConfig memory swapConfig) internal returns (uint256 amountReceived) {
// Keep references to the input and output tokens.
address tokenIn = swapConfig.tokenIn;
// Determine the direction of the swap based on the token addresses.
bool zeroForOne = tokenIn < formattedTokenOut();
// Perform the swap in the specified pool, passing in parameters from the swap configuration.
(int256 amount0, int256 amount1) = swapConfig.pool.swap({
recipient: address(this), // Send output tokens to this terminal.
zeroForOne: zeroForOne, // The direction of the swap.
amountSpecified: int256(swapConfig.amountIn), // The amount of input tokens to swap.
sqrtPriceLimitX96: zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1, // The price
// limit for the swap.
data: abi.encode(tokenIn, swapConfig.inIsNativeToken) // Additional data which will be forwarded to the
// callback.
});
// Calculate the amount of tokens received from the swap.
amountReceived = uint256(-(zeroForOne ? amount1 : amount0));
// Ensure the amount received is not less than the minimum amount specified in the swap configuration.
if (amountReceived < swapConfig.minAmountOut) revert MAX_SLIPPAGE(amountReceived, swapConfig.minAmountOut);
// If the output token is a native token, unwrap it from its wrapped form.
if (OUT_IS_NATIVE_TOKEN) WETH.withdraw(amountReceived);
}
/// @notice Transfers tokens.
/// @param from The address to transfer tokens from.
/// @param to The address to transfer tokens to.
/// @param token The address of the token being transfered.
/// @param amount The amount of tokens to transfer, as a fixed point number with the same number of decimals as the
/// token.
function _transferFor(address from, address payable to, address token, uint256 amount) internal virtual {
if (from == address(this)) {
// If the token is native token, assume the `sendValue` standard.
if (OUT_IS_NATIVE_TOKEN) return Address.sendValue(to, amount);
// If the transfer is from this terminal, use `safeTransfer`.
return IERC20(token).safeTransfer(to, amount);
}
// If there's sufficient approval, transfer normally.
if (IERC20(token).allowance(address(from), address(this)) >= amount) {
return IERC20(token).safeTransferFrom(from, to, amount);
}
// Otherwise, attempt to use the `permit2` method.
PERMIT2.transferFrom(from, to, uint160(amount), token);
}
/// @notice Logic to be triggered before transferring tokens from this terminal.
/// @param to The address to transfer tokens to.
/// @param token The token being transfered.
/// @param amount The amount of tokens to transfer, as a fixed point number with the same number of decimals as the
/// token.
function _beforeTransferFor(address to, address token, uint256 amount) internal virtual {
// If the token is the native token, return early.
if (token == JBConstants.NATIVE_TOKEN) return;
// Otherwise, set the appropriate allowance for the recipient.
IERC20(token).safeIncreaseAllowance(to, amount);
}
/// @notice Sets the `permit2` allowance for a token.
/// @param allowance The allowance to set using `permit2`.
/// @param token The token to set the allowance for.
function _permitAllowance(JBSingleAllowanceContext memory allowance, address token) internal {
PERMIT2.permit({
owner: msg.sender,
permitSingle: IAllowanceTransfer.PermitSingle({
details: IAllowanceTransfer.PermitDetails({
token: token,
amount: allowance.amount,
expiration: allowance.expiration,
nonce: allowance.nonce
}),
spender: address(this),
sigDeadline: allowance.sigDeadline
}),
signature: allowance.signature
});
}
/// @notice Returns the token that flows out of this terminal, wrapped as an ERC-20 if needed.
/// @dev If the token out is the chain native token (ETH on mainnet), wrapped ETH is returned
/// @return The token that flows out of this terminal.
function formattedTokenOut() internal view returns (address) {
return OUT_IS_NATIVE_TOKEN ? address(WETH) : TOKEN_OUT;
}
}