@bananapus/router-terminal-v6 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/foundry.toml +3 -0
- package/package.json +1 -1
- package/src/JBRouterTerminal.sol +29 -5
- package/test/RouterTerminal.t.sol +193 -0
- package/test/RouterTerminalFork.t.sol +509 -0
package/foundry.toml
CHANGED
package/package.json
CHANGED
package/src/JBRouterTerminal.sol
CHANGED
|
@@ -483,7 +483,8 @@ contract JBRouterTerminal is
|
|
|
483
483
|
destProjectId: destProjectId,
|
|
484
484
|
token: tokenIn,
|
|
485
485
|
amount: amount,
|
|
486
|
-
sourceProjectIdOverride: sourceProjectIdOverride
|
|
486
|
+
sourceProjectIdOverride: sourceProjectIdOverride,
|
|
487
|
+
metadata: metadata
|
|
487
488
|
});
|
|
488
489
|
|
|
489
490
|
// If the cashout loop found a terminal that accepts the reclaimed token, we're done.
|
|
@@ -711,6 +712,12 @@ contract JBRouterTerminal is
|
|
|
711
712
|
callbackData: abi.encode(projectId, tokenIn, tokenOut)
|
|
712
713
|
});
|
|
713
714
|
|
|
715
|
+
// For native token inputs, wrap any raw ETH remaining from partial fills so the leftover check catches it.
|
|
716
|
+
// In partial fills, the swap callback only wraps the amount the pool consumed, leaving excess as raw ETH.
|
|
717
|
+
if (tokenIn == JBConstants.NATIVE_TOKEN && address(this).balance != 0) {
|
|
718
|
+
WETH.deposit{value: address(this).balance}();
|
|
719
|
+
}
|
|
720
|
+
|
|
714
721
|
// Unwrap if output is native token.
|
|
715
722
|
if (tokenOut == JBConstants.NATIVE_TOKEN) WETH.withdraw(amountOut);
|
|
716
723
|
|
|
@@ -804,8 +811,9 @@ contract JBRouterTerminal is
|
|
|
804
811
|
|
|
805
812
|
uint160 sqrtPriceLimitX96 = zeroForOne ? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1;
|
|
806
813
|
|
|
814
|
+
// V4 sign convention: negative = exact input, positive = exact output.
|
|
807
815
|
bytes memory result =
|
|
808
|
-
POOL_MANAGER.unlock(abi.encode(key, zeroForOne, int256(amount), sqrtPriceLimitX96, minAmountOut));
|
|
816
|
+
POOL_MANAGER.unlock(abi.encode(key, zeroForOne, -int256(amount), sqrtPriceLimitX96, minAmountOut));
|
|
809
817
|
|
|
810
818
|
amountOut = abi.decode(result, (uint256));
|
|
811
819
|
}
|
|
@@ -1094,6 +1102,7 @@ contract JBRouterTerminal is
|
|
|
1094
1102
|
/// @param amount The amount of the current token.
|
|
1095
1103
|
/// @param sourceProjectIdOverride When non-zero, use this as the source project ID instead of looking up via
|
|
1096
1104
|
/// `TOKENS.projectIdOf()`. Reset to 0 after first use.
|
|
1105
|
+
/// @param metadata Bytes in `JBMetadataResolver`'s format (may contain cashOutMinReclaimed).
|
|
1097
1106
|
/// @return destTerminal The terminal that accepts the final token (address(0) if no direct acceptance found).
|
|
1098
1107
|
/// @return finalToken The token after all cashouts.
|
|
1099
1108
|
/// @return finalAmount The amount of the final token.
|
|
@@ -1101,11 +1110,20 @@ contract JBRouterTerminal is
|
|
|
1101
1110
|
uint256 destProjectId,
|
|
1102
1111
|
address token,
|
|
1103
1112
|
uint256 amount,
|
|
1104
|
-
uint256 sourceProjectIdOverride
|
|
1113
|
+
uint256 sourceProjectIdOverride,
|
|
1114
|
+
bytes calldata metadata
|
|
1105
1115
|
)
|
|
1106
1116
|
internal
|
|
1107
1117
|
returns (IJBTerminal destTerminal, address finalToken, uint256 finalAmount)
|
|
1108
1118
|
{
|
|
1119
|
+
// Check for a user-provided minimum cashout reclaim amount (slippage protection).
|
|
1120
|
+
uint256 minTokensReclaimed;
|
|
1121
|
+
{
|
|
1122
|
+
(bool exists, bytes memory minData) =
|
|
1123
|
+
JBMetadataResolver.getDataFor({id: JBMetadataResolver.getId("cashOutMinReclaimed"), metadata: metadata});
|
|
1124
|
+
if (exists) minTokensReclaimed = abi.decode(minData, (uint256));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1109
1127
|
while (true) {
|
|
1110
1128
|
// Skip the destination check on the first iteration if we have a credit override.
|
|
1111
1129
|
if (sourceProjectIdOverride == 0) {
|
|
@@ -1137,11 +1155,14 @@ contract JBRouterTerminal is
|
|
|
1137
1155
|
projectId: sourceProjectId,
|
|
1138
1156
|
cashOutCount: amount,
|
|
1139
1157
|
tokenToReclaim: tokenToReclaim,
|
|
1140
|
-
minTokensReclaimed:
|
|
1158
|
+
minTokensReclaimed: minTokensReclaimed,
|
|
1141
1159
|
beneficiary: payable(address(this)),
|
|
1142
1160
|
metadata: bytes("")
|
|
1143
1161
|
});
|
|
1144
1162
|
|
|
1163
|
+
// Only apply the minimum to the first cashout step.
|
|
1164
|
+
minTokensReclaimed = 0;
|
|
1165
|
+
|
|
1145
1166
|
// Update for next iteration.
|
|
1146
1167
|
token = tokenToReclaim;
|
|
1147
1168
|
sourceProjectIdOverride = 0;
|
|
@@ -1236,9 +1257,12 @@ contract JBRouterTerminal is
|
|
|
1236
1257
|
JBMetadataResolver.getDataFor({id: JBMetadataResolver.getId("cashOutSource"), metadata: metadata});
|
|
1237
1258
|
|
|
1238
1259
|
if (creditExists) {
|
|
1260
|
+
// Credit cashouts don't use msg.value — revert if ETH was sent to prevent it being trapped.
|
|
1261
|
+
if (msg.value != 0) revert JBRouterTerminal_NoMsgValueAllowed(msg.value);
|
|
1262
|
+
|
|
1239
1263
|
(uint256 sourceProjectId, uint256 creditAmount) = abi.decode(creditData, (uint256, uint256));
|
|
1240
1264
|
|
|
1241
|
-
// Pull credits from the payer.
|
|
1265
|
+
// Pull credits from the payer (requires payer to have granted TRANSFER_CREDITS to this contract).
|
|
1242
1266
|
TOKENS.transferCreditsFrom({
|
|
1243
1267
|
holder: _msgSender(), projectId: sourceProjectId, recipient: address(this), count: creditAmount
|
|
1244
1268
|
});
|
|
@@ -823,4 +823,197 @@ contract RouterTerminalTest is Test {
|
|
|
823
823
|
_mockV4PoolNotExists(sorted0, sorted1, 10_000, int24(200));
|
|
824
824
|
_mockV4PoolNotExists(sorted0, sorted1, 100, int24(1));
|
|
825
825
|
}
|
|
826
|
+
|
|
827
|
+
//*********************************************************************//
|
|
828
|
+
// ---------- Bug fix regression tests: ETH + credit revert ---------- //
|
|
829
|
+
//*********************************************************************//
|
|
830
|
+
|
|
831
|
+
/// @notice Sending msg.value alongside cashOutSource credit metadata should revert (fix #2).
|
|
832
|
+
function test_pay_revertsWhenETHSentWithCreditMetadata() public {
|
|
833
|
+
uint256 projectId = 1;
|
|
834
|
+
address payer = makeAddr("payer");
|
|
835
|
+
|
|
836
|
+
// Build metadata with cashOutSource — must use router address for getId.
|
|
837
|
+
bytes4 metadataId = JBMetadataResolver.getId("cashOutSource", address(routerTerminal));
|
|
838
|
+
bytes memory metadata = JBMetadataResolver.addToMetadata("", metadataId, abi.encode(uint256(2), uint256(1e18)));
|
|
839
|
+
|
|
840
|
+
vm.deal(payer, 1 ether);
|
|
841
|
+
vm.prank(payer);
|
|
842
|
+
vm.expectRevert(abi.encodeWithSelector(IJBRouterTerminal.JBRouterTerminal_NoMsgValueAllowed.selector, 1 ether));
|
|
843
|
+
routerTerminal.pay{value: 1 ether}(projectId, JBConstants.NATIVE_TOKEN, 1 ether, payer, 0, "", metadata);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/// @notice addToBalanceOf should also revert when ETH sent with credit metadata.
|
|
847
|
+
function test_addToBalanceOf_revertsWhenETHSentWithCreditMetadata() public {
|
|
848
|
+
address payer = makeAddr("payer");
|
|
849
|
+
|
|
850
|
+
// Build metadata with cashOutSource — must use router address for getId.
|
|
851
|
+
bytes4 metadataId = JBMetadataResolver.getId("cashOutSource", address(routerTerminal));
|
|
852
|
+
bytes memory metadata = JBMetadataResolver.addToMetadata("", metadataId, abi.encode(uint256(2), uint256(1e18)));
|
|
853
|
+
|
|
854
|
+
vm.deal(payer, 1 ether);
|
|
855
|
+
vm.prank(payer);
|
|
856
|
+
vm.expectRevert(abi.encodeWithSelector(IJBRouterTerminal.JBRouterTerminal_NoMsgValueAllowed.selector, 1 ether));
|
|
857
|
+
routerTerminal.addToBalanceOf{value: 1 ether}(1, JBConstants.NATIVE_TOKEN, 1 ether, false, "", metadata);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
//*********************************************************************//
|
|
861
|
+
// ----------- Bug fix regression tests: V4 sign convention ---------- //
|
|
862
|
+
//*********************************************************************//
|
|
863
|
+
|
|
864
|
+
/// @notice The V4 unlock callback should receive a negative amountSpecified for exact-input (fix #1).
|
|
865
|
+
function test_unlockCallback_negativeAmountSpecified() public {
|
|
866
|
+
address tokenA = makeAddr("tokenA");
|
|
867
|
+
address tokenB = makeAddr("tokenB");
|
|
868
|
+
(address sorted0, address sorted1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
|
|
869
|
+
|
|
870
|
+
PoolKey memory key = PoolKey({
|
|
871
|
+
currency0: Currency.wrap(sorted0),
|
|
872
|
+
currency1: Currency.wrap(sorted1),
|
|
873
|
+
fee: 3000,
|
|
874
|
+
tickSpacing: int24(60),
|
|
875
|
+
hooks: IHooks(address(0))
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
uint256 amount = 1e18;
|
|
879
|
+
int256 amountSpecified = -int256(amount); // exact-input: NEGATIVE
|
|
880
|
+
uint160 sqrtPriceLimitX96 = 4_295_128_740; // MIN_SQRT_RATIO + 1
|
|
881
|
+
uint256 minAmountOut = 0;
|
|
882
|
+
|
|
883
|
+
// Encode the data as the contract would encode it (with the sign fix).
|
|
884
|
+
bytes memory callbackData = abi.encode(key, true, amountSpecified, sqrtPriceLimitX96, minAmountOut);
|
|
885
|
+
|
|
886
|
+
// Mock the PoolManager.swap call — it should receive a negative amountSpecified.
|
|
887
|
+
// We construct the expected SwapParams to verify the sign.
|
|
888
|
+
vm.mockCall(
|
|
889
|
+
address(mockPoolManager),
|
|
890
|
+
abi.encodeWithSelector(IPoolManager.swap.selector),
|
|
891
|
+
// Return a BalanceDelta where token0 goes in (-1e18) and token1 comes out (+5e17).
|
|
892
|
+
abi.encode(int256(-1e18) << 128 | int256(uint256(5e17)))
|
|
893
|
+
);
|
|
894
|
+
|
|
895
|
+
// Mock settle and take.
|
|
896
|
+
vm.mockCall(address(mockPoolManager), abi.encodeWithSignature("settle()"), abi.encode(uint256(1e18)));
|
|
897
|
+
vm.mockCall(address(mockPoolManager), abi.encodeWithSignature("settle{value}()"), abi.encode(uint256(1e18)));
|
|
898
|
+
vm.mockCall(address(mockPoolManager), abi.encodeWithSignature("sync(address)"), abi.encode());
|
|
899
|
+
vm.mockCall(address(mockPoolManager), abi.encodeWithSignature("take(address,address,uint256)"), abi.encode());
|
|
900
|
+
vm.mockCall(sorted0, abi.encodeCall(IERC20.transfer, (address(mockPoolManager), 1e18)), abi.encode(true));
|
|
901
|
+
|
|
902
|
+
// Call from the PoolManager (authorized).
|
|
903
|
+
vm.prank(address(mockPoolManager));
|
|
904
|
+
|
|
905
|
+
// The callback should decode a NEGATIVE amountSpecified and process the swap.
|
|
906
|
+
// If the old bug existed (positive), the swap behavior would be different.
|
|
907
|
+
bytes memory result = routerTerminal.unlockCallback(callbackData);
|
|
908
|
+
|
|
909
|
+
// Verify amountOut is decoded correctly.
|
|
910
|
+
uint256 amountOut = abi.decode(result, (uint256));
|
|
911
|
+
assertEq(amountOut, 5e17);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
//*********************************************************************//
|
|
915
|
+
// --------- Bug fix regression tests: cashout slippage -------------- //
|
|
916
|
+
//*********************************************************************//
|
|
917
|
+
|
|
918
|
+
/// @notice cashOutMinReclaimed metadata should be forwarded to the cashout terminal (fix #4).
|
|
919
|
+
function test_pay_cashOutMinReclaimedMetadata() public {
|
|
920
|
+
uint256 destProjectId = 1;
|
|
921
|
+
address payer = makeAddr("payer");
|
|
922
|
+
address jbToken = makeAddr("jbToken");
|
|
923
|
+
vm.etch(jbToken, hex"00");
|
|
924
|
+
uint256 sourceProjectId = 2;
|
|
925
|
+
uint256 amount = 100e18;
|
|
926
|
+
uint256 minReclaimed = 50e18;
|
|
927
|
+
address mockTerminal = makeAddr("destTerminal");
|
|
928
|
+
vm.etch(mockTerminal, hex"00");
|
|
929
|
+
address mockCashOutTerminal = makeAddr("cashOutTerminal");
|
|
930
|
+
vm.etch(mockCashOutTerminal, hex"00");
|
|
931
|
+
|
|
932
|
+
// Build metadata with cashOutMinReclaimed — must use router address for getId.
|
|
933
|
+
bytes4 metadataId = JBMetadataResolver.getId("cashOutMinReclaimed", address(routerTerminal));
|
|
934
|
+
bytes memory metadata = JBMetadataResolver.addToMetadata("", metadataId, abi.encode(minReclaimed));
|
|
935
|
+
|
|
936
|
+
// jbToken is a JB project token for sourceProjectId.
|
|
937
|
+
vm.mockCall(
|
|
938
|
+
address(mockTokens), abi.encodeCall(IJBTokens.projectIdOf, (IJBToken(jbToken))), abi.encode(sourceProjectId)
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
// Dest project accepts NATIVE_TOKEN.
|
|
942
|
+
vm.mockCall(
|
|
943
|
+
address(mockDirectory),
|
|
944
|
+
abi.encodeCall(IJBDirectory.primaryTerminalOf, (destProjectId, JBConstants.NATIVE_TOKEN)),
|
|
945
|
+
abi.encode(mockTerminal)
|
|
946
|
+
);
|
|
947
|
+
|
|
948
|
+
// Dest project doesn't accept jbToken directly.
|
|
949
|
+
vm.mockCall(
|
|
950
|
+
address(mockDirectory),
|
|
951
|
+
abi.encodeCall(IJBDirectory.primaryTerminalOf, (destProjectId, jbToken)),
|
|
952
|
+
abi.encode(address(0))
|
|
953
|
+
);
|
|
954
|
+
|
|
955
|
+
// Source project's terminals (for _findCashOutPath).
|
|
956
|
+
IJBTerminal[] memory sourceTerminals = new IJBTerminal[](1);
|
|
957
|
+
sourceTerminals[0] = IJBTerminal(mockCashOutTerminal);
|
|
958
|
+
vm.mockCall(
|
|
959
|
+
address(mockDirectory),
|
|
960
|
+
abi.encodeCall(IJBDirectory.terminalsOf, (sourceProjectId)),
|
|
961
|
+
abi.encode(sourceTerminals)
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
// Mock supportsInterface for IJBCashOutTerminal.
|
|
965
|
+
vm.mockCall(
|
|
966
|
+
mockCashOutTerminal,
|
|
967
|
+
abi.encodeCall(IERC165.supportsInterface, (type(IJBCashOutTerminal).interfaceId)),
|
|
968
|
+
abi.encode(true)
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
// Accounting context: source project terminal accepts NATIVE_TOKEN.
|
|
972
|
+
JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
|
|
973
|
+
contexts[0] = JBAccountingContext({
|
|
974
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
975
|
+
});
|
|
976
|
+
vm.mockCall(
|
|
977
|
+
mockCashOutTerminal,
|
|
978
|
+
abi.encodeCall(IJBTerminal.accountingContextsOf, (sourceProjectId)),
|
|
979
|
+
abi.encode(contexts)
|
|
980
|
+
);
|
|
981
|
+
|
|
982
|
+
// Mock cashOutTokensOf — use broad selector matching since vm.mockCall matches by prefix.
|
|
983
|
+
// The key assertion: the call should use minReclaimed (not 0).
|
|
984
|
+
vm.mockCall(
|
|
985
|
+
mockCashOutTerminal,
|
|
986
|
+
abi.encodeWithSelector(IJBCashOutTerminal.cashOutTokensOf.selector),
|
|
987
|
+
abi.encode(uint256(60e18))
|
|
988
|
+
);
|
|
989
|
+
|
|
990
|
+
// Expect the specific cashOutTokensOf call with minReclaimed = minReclaimed.
|
|
991
|
+
vm.expectCall(
|
|
992
|
+
mockCashOutTerminal,
|
|
993
|
+
abi.encodeCall(
|
|
994
|
+
IJBCashOutTerminal.cashOutTokensOf,
|
|
995
|
+
(
|
|
996
|
+
address(routerTerminal),
|
|
997
|
+
sourceProjectId,
|
|
998
|
+
amount,
|
|
999
|
+
JBConstants.NATIVE_TOKEN,
|
|
1000
|
+
minReclaimed,
|
|
1001
|
+
payable(address(routerTerminal)),
|
|
1002
|
+
bytes("")
|
|
1003
|
+
)
|
|
1004
|
+
)
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
// Mock token transfer from payer.
|
|
1008
|
+
vm.mockCall(jbToken, abi.encodeCall(IERC20.allowance, (payer, address(routerTerminal))), abi.encode(amount));
|
|
1009
|
+
vm.mockCall(
|
|
1010
|
+
jbToken, abi.encodeCall(IERC20.transferFrom, (payer, address(routerTerminal), amount)), abi.encode(true)
|
|
1011
|
+
);
|
|
1012
|
+
|
|
1013
|
+
// Mock dest terminal pay.
|
|
1014
|
+
vm.mockCall(mockTerminal, abi.encodeWithSelector(IJBTerminal.pay.selector), abi.encode(uint256(10)));
|
|
1015
|
+
|
|
1016
|
+
vm.prank(payer);
|
|
1017
|
+
routerTerminal.pay(destProjectId, jbToken, amount, payer, 0, "", metadata);
|
|
1018
|
+
}
|
|
826
1019
|
}
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
// JB core (via TestBaseWorkflow pattern — deploy fresh within fork).
|
|
7
|
+
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
8
|
+
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
9
|
+
import {JBDirectory} from "@bananapus/core-v6/src/JBDirectory.sol";
|
|
10
|
+
import {JBRulesets} from "@bananapus/core-v6/src/JBRulesets.sol";
|
|
11
|
+
import {JBTokens} from "@bananapus/core-v6/src/JBTokens.sol";
|
|
12
|
+
import {JBERC20} from "@bananapus/core-v6/src/JBERC20.sol";
|
|
13
|
+
import {JBSplits} from "@bananapus/core-v6/src/JBSplits.sol";
|
|
14
|
+
import {JBPrices} from "@bananapus/core-v6/src/JBPrices.sol";
|
|
15
|
+
import {JBController} from "@bananapus/core-v6/src/JBController.sol";
|
|
16
|
+
import {JBFundAccessLimits} from "@bananapus/core-v6/src/JBFundAccessLimits.sol";
|
|
17
|
+
import {JBFeelessAddresses} from "@bananapus/core-v6/src/JBFeelessAddresses.sol";
|
|
18
|
+
import {JBTerminalStore} from "@bananapus/core-v6/src/JBTerminalStore.sol";
|
|
19
|
+
import {JBMultiTerminal} from "@bananapus/core-v6/src/JBMultiTerminal.sol";
|
|
20
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
21
|
+
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
22
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
23
|
+
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
24
|
+
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
25
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
26
|
+
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
27
|
+
import {JBFundAccessLimitGroup} from "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
28
|
+
import {JBCurrencyAmount} from "@bananapus/core-v6/src/structs/JBCurrencyAmount.sol";
|
|
29
|
+
import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
30
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
31
|
+
|
|
32
|
+
// Uniswap.
|
|
33
|
+
import {IUniswapV3Factory} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Factory.sol";
|
|
34
|
+
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
|
35
|
+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
36
|
+
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
37
|
+
|
|
38
|
+
// OpenZeppelin.
|
|
39
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
40
|
+
|
|
41
|
+
// Router terminal.
|
|
42
|
+
import {JBRouterTerminal} from "../src/JBRouterTerminal.sol";
|
|
43
|
+
import {IWETH9} from "../src/interfaces/IWETH9.sol";
|
|
44
|
+
|
|
45
|
+
/// @notice Fork tests for JBRouterTerminal against real Uniswap V3 pools on Ethereum mainnet.
|
|
46
|
+
/// @dev Uses a pinned block for determinism. JB core is deployed fresh within the fork so we control project state.
|
|
47
|
+
contract RouterTerminalForkTest is Test {
|
|
48
|
+
// ───────────────────────── Mainnet addresses
|
|
49
|
+
// ──────────────────────────
|
|
50
|
+
|
|
51
|
+
// Post-V4-deployment block (V4 PoolManager deployed ~21,690,000) with good TWAP history.
|
|
52
|
+
uint256 constant BLOCK_NUMBER = 21_700_000;
|
|
53
|
+
|
|
54
|
+
IWETH9 constant WETH = IWETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
|
|
55
|
+
IERC20 constant USDC = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
|
|
56
|
+
IERC20 constant DAI = IERC20(0x6B175474E89094C44Da98b954EedeAC495271d0F);
|
|
57
|
+
IUniswapV3Factory constant V3_FACTORY = IUniswapV3Factory(0x1F98431c8aD98523631AE4a59f267346ea31F984);
|
|
58
|
+
IPermit2 constant PERMIT2 = IPermit2(0x000000000022D473030F116dDEE9F6B43aC78BA3);
|
|
59
|
+
IPoolManager constant V4_POOL_MANAGER = IPoolManager(0x000000000004444c5dc75cB358380D2e3dE08A90);
|
|
60
|
+
|
|
61
|
+
// Well-known V3 pools (we don't create them — they exist on mainnet).
|
|
62
|
+
// WETH/USDC 0.05%: 0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640
|
|
63
|
+
// WETH/DAI 0.3%: 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8
|
|
64
|
+
|
|
65
|
+
// ───────────────────────── JB core (deployed fresh)
|
|
66
|
+
// ────────────────────
|
|
67
|
+
|
|
68
|
+
address multisig = address(0xBEEF);
|
|
69
|
+
address payer = makeAddr("payer");
|
|
70
|
+
address beneficiary = makeAddr("beneficiary");
|
|
71
|
+
address trustedForwarder = address(0);
|
|
72
|
+
|
|
73
|
+
JBPermissions jbPermissions;
|
|
74
|
+
JBProjects jbProjects;
|
|
75
|
+
JBDirectory jbDirectory;
|
|
76
|
+
JBRulesets jbRulesets;
|
|
77
|
+
JBTokens jbTokens;
|
|
78
|
+
JBSplits jbSplits;
|
|
79
|
+
JBPrices jbPrices;
|
|
80
|
+
JBFundAccessLimits jbFundAccessLimits;
|
|
81
|
+
JBFeelessAddresses jbFeelessAddresses;
|
|
82
|
+
JBController jbController;
|
|
83
|
+
JBTerminalStore jbTerminalStore;
|
|
84
|
+
JBMultiTerminal jbMultiTerminal;
|
|
85
|
+
JBRouterTerminal routerTerminal;
|
|
86
|
+
|
|
87
|
+
// Project IDs.
|
|
88
|
+
uint256 feeProjectId; // Project 1 (fee recipient).
|
|
89
|
+
uint256 ethProjectId; // Accepts NATIVE_TOKEN (ETH).
|
|
90
|
+
uint256 usdcProjectId; // Accepts USDC.
|
|
91
|
+
uint256 daiProjectId; // Accepts DAI.
|
|
92
|
+
|
|
93
|
+
// ───────────────────────── Setup
|
|
94
|
+
// ──────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
function setUp() public {
|
|
97
|
+
// Skip fork tests in CI when no RPC URL is configured.
|
|
98
|
+
string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
|
|
99
|
+
if (bytes(rpcUrl).length == 0) {
|
|
100
|
+
vm.skip(true);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
vm.createSelectFork(rpcUrl, BLOCK_NUMBER);
|
|
104
|
+
|
|
105
|
+
// Deploy all JB core contracts fresh within the fork.
|
|
106
|
+
_deployJBCore();
|
|
107
|
+
|
|
108
|
+
// Deploy the router terminal with real Uniswap + real Permit2, but fresh JB core.
|
|
109
|
+
routerTerminal = new JBRouterTerminal({
|
|
110
|
+
directory: jbDirectory,
|
|
111
|
+
permissions: jbPermissions,
|
|
112
|
+
projects: jbProjects,
|
|
113
|
+
tokens: jbTokens,
|
|
114
|
+
permit2: PERMIT2,
|
|
115
|
+
owner: multisig,
|
|
116
|
+
weth: WETH,
|
|
117
|
+
factory: V3_FACTORY,
|
|
118
|
+
poolManager: V4_POOL_MANAGER,
|
|
119
|
+
trustedForwarder: trustedForwarder
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Create test projects.
|
|
123
|
+
feeProjectId = _launchProject({acceptedToken: JBConstants.NATIVE_TOKEN, decimals: 18});
|
|
124
|
+
ethProjectId = _launchProject({acceptedToken: JBConstants.NATIVE_TOKEN, decimals: 18});
|
|
125
|
+
usdcProjectId = _launchProject({acceptedToken: address(USDC), decimals: 6});
|
|
126
|
+
daiProjectId = _launchProject({acceptedToken: address(DAI), decimals: 18});
|
|
127
|
+
|
|
128
|
+
// Labels for trace readability.
|
|
129
|
+
vm.label(address(WETH), "WETH");
|
|
130
|
+
vm.label(address(USDC), "USDC");
|
|
131
|
+
vm.label(address(DAI), "DAI");
|
|
132
|
+
vm.label(address(routerTerminal), "RouterTerminal");
|
|
133
|
+
vm.label(address(jbMultiTerminal), "JBMultiTerminal");
|
|
134
|
+
vm.label(payer, "payer");
|
|
135
|
+
vm.label(beneficiary, "beneficiary");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
139
|
+
// V3 SWAP: ETH → USDC (pay project that accepts USDC with ETH)
|
|
140
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
141
|
+
|
|
142
|
+
function test_fork_payETH_projectAcceptsUSDC_small() public {
|
|
143
|
+
uint256 amountIn = 0.01 ether;
|
|
144
|
+
_payETHAndAssert(usdcProjectId, amountIn, address(USDC));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function test_fork_payETH_projectAcceptsUSDC_medium() public {
|
|
148
|
+
uint256 amountIn = 1 ether;
|
|
149
|
+
_payETHAndAssert(usdcProjectId, amountIn, address(USDC));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function test_fork_payETH_projectAcceptsUSDC_large() public {
|
|
153
|
+
uint256 amountIn = 100 ether;
|
|
154
|
+
_payETHAndAssert(usdcProjectId, amountIn, address(USDC));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function test_fork_payETH_projectAcceptsUSDC_veryLarge() public {
|
|
158
|
+
uint256 amountIn = 1000 ether;
|
|
159
|
+
_payETHAndAssert(usdcProjectId, amountIn, address(USDC));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
163
|
+
// V3 SWAP: USDC → ETH (pay project that accepts ETH with USDC)
|
|
164
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
165
|
+
|
|
166
|
+
function test_fork_payUSDC_projectAcceptsETH_small() public {
|
|
167
|
+
uint256 amountIn = 10e6; // 10 USDC
|
|
168
|
+
_payERC20AndAssert(usdcProjectId, ethProjectId, address(USDC), amountIn, JBConstants.NATIVE_TOKEN);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function test_fork_payUSDC_projectAcceptsETH_large() public {
|
|
172
|
+
uint256 amountIn = 1_000_000e6; // 1M USDC
|
|
173
|
+
_payERC20AndAssert(usdcProjectId, ethProjectId, address(USDC), amountIn, JBConstants.NATIVE_TOKEN);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
177
|
+
// DIRECT FORWARD (no swap needed)
|
|
178
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
179
|
+
|
|
180
|
+
function test_fork_payETH_projectAcceptsETH() public {
|
|
181
|
+
uint256 amountIn = 1 ether;
|
|
182
|
+
|
|
183
|
+
vm.deal(payer, amountIn);
|
|
184
|
+
uint256 payerBalBefore = payer.balance;
|
|
185
|
+
|
|
186
|
+
vm.prank(payer);
|
|
187
|
+
uint256 tokenCount = routerTerminal.pay{value: amountIn}({
|
|
188
|
+
projectId: ethProjectId,
|
|
189
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
190
|
+
amount: amountIn,
|
|
191
|
+
beneficiary: beneficiary,
|
|
192
|
+
minReturnedTokens: 0,
|
|
193
|
+
memo: "direct ETH forward",
|
|
194
|
+
metadata: ""
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Project tokens minted.
|
|
198
|
+
assertGt(tokenCount, 0, "no tokens minted for direct ETH forward");
|
|
199
|
+
|
|
200
|
+
// Payer spent exactly amountIn.
|
|
201
|
+
assertEq(payerBalBefore - payer.balance, amountIn, "payer balance mismatch");
|
|
202
|
+
|
|
203
|
+
// Terminal received the ETH.
|
|
204
|
+
uint256 terminalBal =
|
|
205
|
+
jbTerminalStore.balanceOf(address(jbMultiTerminal), ethProjectId, JBConstants.NATIVE_TOKEN);
|
|
206
|
+
assertGt(terminalBal, 0, "terminal has no ETH balance");
|
|
207
|
+
|
|
208
|
+
// Router has no leftover.
|
|
209
|
+
assertEq(address(routerTerminal).balance, 0, "router has leftover ETH");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function test_fork_payUSDC_projectAcceptsUSDC() public {
|
|
213
|
+
uint256 amountIn = 1000e6; // 1000 USDC
|
|
214
|
+
|
|
215
|
+
deal(address(USDC), payer, amountIn);
|
|
216
|
+
|
|
217
|
+
vm.startPrank(payer);
|
|
218
|
+
USDC.approve(address(routerTerminal), amountIn);
|
|
219
|
+
uint256 tokenCount = routerTerminal.pay({
|
|
220
|
+
projectId: usdcProjectId,
|
|
221
|
+
token: address(USDC),
|
|
222
|
+
amount: amountIn,
|
|
223
|
+
beneficiary: beneficiary,
|
|
224
|
+
minReturnedTokens: 0,
|
|
225
|
+
memo: "direct USDC forward",
|
|
226
|
+
metadata: ""
|
|
227
|
+
});
|
|
228
|
+
vm.stopPrank();
|
|
229
|
+
|
|
230
|
+
assertGt(tokenCount, 0, "no tokens minted for direct USDC forward");
|
|
231
|
+
|
|
232
|
+
// Terminal received the USDC.
|
|
233
|
+
uint256 terminalBal = jbTerminalStore.balanceOf(address(jbMultiTerminal), usdcProjectId, address(USDC));
|
|
234
|
+
assertGt(terminalBal, 0, "terminal has no USDC balance");
|
|
235
|
+
|
|
236
|
+
// Router has no leftover.
|
|
237
|
+
assertEq(USDC.balanceOf(address(routerTerminal)), 0, "router has leftover USDC");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
241
|
+
// V3 SWAP: ETH → DAI
|
|
242
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
243
|
+
|
|
244
|
+
function test_fork_payETH_projectAcceptsDAI() public {
|
|
245
|
+
uint256 amountIn = 1 ether;
|
|
246
|
+
_payETHAndAssert(daiProjectId, amountIn, address(DAI));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
250
|
+
// addToBalanceOf
|
|
251
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
252
|
+
|
|
253
|
+
function test_fork_addToBalance_ETHtoUSDC() public {
|
|
254
|
+
uint256 amountIn = 1 ether;
|
|
255
|
+
|
|
256
|
+
vm.deal(payer, amountIn);
|
|
257
|
+
uint256 terminalBalBefore = jbTerminalStore.balanceOf(address(jbMultiTerminal), usdcProjectId, address(USDC));
|
|
258
|
+
|
|
259
|
+
vm.prank(payer);
|
|
260
|
+
routerTerminal.addToBalanceOf{value: amountIn}({
|
|
261
|
+
projectId: usdcProjectId,
|
|
262
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
263
|
+
amount: amountIn,
|
|
264
|
+
shouldReturnHeldFees: false,
|
|
265
|
+
memo: "add to balance ETH->USDC",
|
|
266
|
+
metadata: ""
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
uint256 terminalBalAfter = jbTerminalStore.balanceOf(address(jbMultiTerminal), usdcProjectId, address(USDC));
|
|
270
|
+
|
|
271
|
+
assertGt(terminalBalAfter, terminalBalBefore, "terminal balance did not increase");
|
|
272
|
+
assertEq(address(routerTerminal).balance, 0, "router has leftover ETH");
|
|
273
|
+
assertEq(USDC.balanceOf(address(routerTerminal)), 0, "router has leftover USDC");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
277
|
+
// User-provided quote (metadata override)
|
|
278
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
279
|
+
|
|
280
|
+
function test_fork_payETH_withQuoteMetadata() public {
|
|
281
|
+
uint256 amountIn = 1 ether;
|
|
282
|
+
|
|
283
|
+
// Set a generous quote (1 USDC minimum — well below market, should succeed).
|
|
284
|
+
bytes4 quoteId = JBMetadataResolver.getId("quoteForSwap", address(routerTerminal));
|
|
285
|
+
bytes memory metadata = JBMetadataResolver.addToMetadata("", quoteId, abi.encode(uint256(1e6)));
|
|
286
|
+
|
|
287
|
+
vm.deal(payer, amountIn);
|
|
288
|
+
|
|
289
|
+
vm.prank(payer);
|
|
290
|
+
uint256 tokenCount = routerTerminal.pay{value: amountIn}({
|
|
291
|
+
projectId: usdcProjectId,
|
|
292
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
293
|
+
amount: amountIn,
|
|
294
|
+
beneficiary: beneficiary,
|
|
295
|
+
minReturnedTokens: 0,
|
|
296
|
+
memo: "ETH->USDC with quote metadata",
|
|
297
|
+
metadata: metadata
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
assertGt(tokenCount, 0, "no tokens minted with quote metadata");
|
|
301
|
+
|
|
302
|
+
// Terminal received USDC.
|
|
303
|
+
uint256 terminalBal = jbTerminalStore.balanceOf(address(jbMultiTerminal), usdcProjectId, address(USDC));
|
|
304
|
+
assertGt(terminalBal, 0, "terminal has no USDC balance after quote metadata pay");
|
|
305
|
+
|
|
306
|
+
// Router clean.
|
|
307
|
+
assertEq(address(routerTerminal).balance, 0, "router has leftover ETH");
|
|
308
|
+
assertEq(USDC.balanceOf(address(routerTerminal)), 0, "router has leftover USDC");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
312
|
+
// Slippage / revert scenarios
|
|
313
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
314
|
+
|
|
315
|
+
function test_fork_payETH_tightQuote_reverts() public {
|
|
316
|
+
uint256 amountIn = 1 ether;
|
|
317
|
+
|
|
318
|
+
// Set a quote 3x above market (~3,300 USDC/ETH → require 10,000 USDC).
|
|
319
|
+
// This is tight enough to trigger SlippageExceeded but not so extreme that
|
|
320
|
+
// sqrtPriceLimitFromAmounts falls back to "no limit".
|
|
321
|
+
bytes4 quoteId = JBMetadataResolver.getId("quoteForSwap", address(routerTerminal));
|
|
322
|
+
bytes memory metadata = JBMetadataResolver.addToMetadata("", quoteId, abi.encode(uint256(10_000e6)));
|
|
323
|
+
|
|
324
|
+
vm.deal(payer, amountIn);
|
|
325
|
+
|
|
326
|
+
vm.prank(payer);
|
|
327
|
+
vm.expectRevert();
|
|
328
|
+
routerTerminal.pay{value: amountIn}({
|
|
329
|
+
projectId: usdcProjectId,
|
|
330
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
331
|
+
amount: amountIn,
|
|
332
|
+
beneficiary: beneficiary,
|
|
333
|
+
minReturnedTokens: 0,
|
|
334
|
+
memo: "should revert - tight quote",
|
|
335
|
+
metadata: metadata
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
340
|
+
// Internal helpers
|
|
341
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
342
|
+
|
|
343
|
+
/// @dev Pay ETH via the router terminal to a project that accepts `expectedTokenOut`. Asserts swap happened.
|
|
344
|
+
function _payETHAndAssert(uint256 projectId, uint256 amountIn, address expectedTokenOut) internal {
|
|
345
|
+
vm.deal(payer, amountIn);
|
|
346
|
+
uint256 payerBalBefore = payer.balance;
|
|
347
|
+
|
|
348
|
+
vm.prank(payer);
|
|
349
|
+
uint256 tokenCount = routerTerminal.pay{value: amountIn}({
|
|
350
|
+
projectId: projectId,
|
|
351
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
352
|
+
amount: amountIn,
|
|
353
|
+
beneficiary: beneficiary,
|
|
354
|
+
minReturnedTokens: 0,
|
|
355
|
+
memo: "fork test ETH swap",
|
|
356
|
+
metadata: ""
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// 1. Swap produced output — tokens minted.
|
|
360
|
+
assertGt(tokenCount, 0, "no tokens minted");
|
|
361
|
+
|
|
362
|
+
// 2. Destination terminal received the output tokens (project balance increased).
|
|
363
|
+
uint256 terminalBal = jbTerminalStore.balanceOf(address(jbMultiTerminal), projectId, expectedTokenOut);
|
|
364
|
+
assertGt(terminalBal, 0, "terminal has no balance in expected token");
|
|
365
|
+
|
|
366
|
+
// 3. No leftover tokens stuck in the router.
|
|
367
|
+
assertEq(address(routerTerminal).balance, 0, "router has leftover ETH");
|
|
368
|
+
assertEq(IERC20(address(WETH)).balanceOf(address(routerTerminal)), 0, "router has leftover WETH");
|
|
369
|
+
assertEq(IERC20(expectedTokenOut).balanceOf(address(routerTerminal)), 0, "router has leftover output token");
|
|
370
|
+
|
|
371
|
+
// 4. Payer's balance decreased by exactly the input amount.
|
|
372
|
+
assertEq(payerBalBefore - payer.balance, amountIn, "payer balance mismatch");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/// @dev Pay ERC-20 via the router terminal to a project that accepts a different token.
|
|
376
|
+
function _payERC20AndAssert(
|
|
377
|
+
uint256, /* sourceProjectIdForDeal — unused */
|
|
378
|
+
uint256 destProjectId,
|
|
379
|
+
address tokenIn,
|
|
380
|
+
uint256 amountIn,
|
|
381
|
+
address expectedTokenOut
|
|
382
|
+
)
|
|
383
|
+
internal
|
|
384
|
+
{
|
|
385
|
+
deal(tokenIn, payer, amountIn);
|
|
386
|
+
|
|
387
|
+
vm.startPrank(payer);
|
|
388
|
+
IERC20(tokenIn).approve(address(routerTerminal), amountIn);
|
|
389
|
+
uint256 tokenCount = routerTerminal.pay({
|
|
390
|
+
projectId: destProjectId,
|
|
391
|
+
token: tokenIn,
|
|
392
|
+
amount: amountIn,
|
|
393
|
+
beneficiary: beneficiary,
|
|
394
|
+
minReturnedTokens: 0,
|
|
395
|
+
memo: "fork test ERC20 swap",
|
|
396
|
+
metadata: ""
|
|
397
|
+
});
|
|
398
|
+
vm.stopPrank();
|
|
399
|
+
|
|
400
|
+
// Tokens minted.
|
|
401
|
+
assertGt(tokenCount, 0, "no tokens minted for ERC20 swap");
|
|
402
|
+
|
|
403
|
+
// Terminal received expected output token.
|
|
404
|
+
uint256 terminalBal = jbTerminalStore.balanceOf(address(jbMultiTerminal), destProjectId, expectedTokenOut);
|
|
405
|
+
assertGt(terminalBal, 0, "terminal has no balance in expected token");
|
|
406
|
+
|
|
407
|
+
// Router clean.
|
|
408
|
+
assertEq(IERC20(tokenIn).balanceOf(address(routerTerminal)), 0, "router has leftover tokenIn");
|
|
409
|
+
if (expectedTokenOut != JBConstants.NATIVE_TOKEN) {
|
|
410
|
+
assertEq(IERC20(expectedTokenOut).balanceOf(address(routerTerminal)), 0, "router has leftover tokenOut");
|
|
411
|
+
}
|
|
412
|
+
assertEq(address(routerTerminal).balance, 0, "router has leftover ETH");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ───────────────────────── JB Core Deployment
|
|
416
|
+
// ─────────────────────────
|
|
417
|
+
|
|
418
|
+
function _deployJBCore() internal {
|
|
419
|
+
jbPermissions = new JBPermissions(trustedForwarder);
|
|
420
|
+
jbProjects = new JBProjects(multisig, address(0), trustedForwarder);
|
|
421
|
+
jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
|
|
422
|
+
JBERC20 jbErc20 = new JBERC20();
|
|
423
|
+
jbTokens = new JBTokens(jbDirectory, jbErc20);
|
|
424
|
+
jbRulesets = new JBRulesets(jbDirectory);
|
|
425
|
+
jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, trustedForwarder);
|
|
426
|
+
jbSplits = new JBSplits(jbDirectory);
|
|
427
|
+
jbFundAccessLimits = new JBFundAccessLimits(jbDirectory);
|
|
428
|
+
jbFeelessAddresses = new JBFeelessAddresses(multisig);
|
|
429
|
+
|
|
430
|
+
jbController = new JBController(
|
|
431
|
+
jbDirectory,
|
|
432
|
+
jbFundAccessLimits,
|
|
433
|
+
jbPermissions,
|
|
434
|
+
jbPrices,
|
|
435
|
+
jbProjects,
|
|
436
|
+
jbRulesets,
|
|
437
|
+
jbSplits,
|
|
438
|
+
jbTokens,
|
|
439
|
+
address(0), // omnichainRulesetOperator
|
|
440
|
+
trustedForwarder
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
vm.prank(multisig);
|
|
444
|
+
jbDirectory.setIsAllowedToSetFirstController(address(jbController), true);
|
|
445
|
+
|
|
446
|
+
jbTerminalStore = new JBTerminalStore(jbDirectory, jbPrices, jbRulesets);
|
|
447
|
+
|
|
448
|
+
jbMultiTerminal = new JBMultiTerminal(
|
|
449
|
+
jbFeelessAddresses,
|
|
450
|
+
jbPermissions,
|
|
451
|
+
jbProjects,
|
|
452
|
+
jbSplits,
|
|
453
|
+
jbTerminalStore,
|
|
454
|
+
jbTokens,
|
|
455
|
+
PERMIT2,
|
|
456
|
+
trustedForwarder
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/// @dev Launch a JB project that accepts `acceptedToken` via the multi terminal.
|
|
461
|
+
function _launchProject(address acceptedToken, uint8 decimals) internal returns (uint256 projectId) {
|
|
462
|
+
JBRulesetMetadata memory metadata = JBRulesetMetadata({
|
|
463
|
+
reservedPercent: 0,
|
|
464
|
+
cashOutTaxRate: 0,
|
|
465
|
+
baseCurrency: uint32(uint160(acceptedToken)),
|
|
466
|
+
pausePay: false,
|
|
467
|
+
pauseCreditTransfers: false,
|
|
468
|
+
allowOwnerMinting: false,
|
|
469
|
+
allowSetCustomToken: false,
|
|
470
|
+
allowTerminalMigration: false,
|
|
471
|
+
allowSetTerminals: false,
|
|
472
|
+
allowSetController: false,
|
|
473
|
+
allowAddAccountingContext: true,
|
|
474
|
+
allowAddPriceFeed: false,
|
|
475
|
+
ownerMustSendPayouts: false,
|
|
476
|
+
holdFees: false,
|
|
477
|
+
useTotalSurplusForCashOuts: false,
|
|
478
|
+
useDataHookForPay: false,
|
|
479
|
+
useDataHookForCashOut: false,
|
|
480
|
+
dataHook: address(0),
|
|
481
|
+
metadata: 0
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
|
|
485
|
+
rulesetConfigs[0].mustStartAtOrAfter = 0;
|
|
486
|
+
rulesetConfigs[0].duration = 0;
|
|
487
|
+
rulesetConfigs[0].weight = 1_000_000e18; // 1M tokens per unit of currency
|
|
488
|
+
rulesetConfigs[0].weightCutPercent = 0;
|
|
489
|
+
rulesetConfigs[0].approvalHook = IJBRulesetApprovalHook(address(0));
|
|
490
|
+
rulesetConfigs[0].metadata = metadata;
|
|
491
|
+
rulesetConfigs[0].splitGroups = new JBSplitGroup[](0);
|
|
492
|
+
rulesetConfigs[0].fundAccessLimitGroups = new JBFundAccessLimitGroup[](0);
|
|
493
|
+
|
|
494
|
+
JBAccountingContext[] memory tokensToAccept = new JBAccountingContext[](1);
|
|
495
|
+
tokensToAccept[0] =
|
|
496
|
+
JBAccountingContext({token: acceptedToken, decimals: decimals, currency: uint32(uint160(acceptedToken))});
|
|
497
|
+
|
|
498
|
+
JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
|
|
499
|
+
terminalConfigs[0] = JBTerminalConfig({terminal: jbMultiTerminal, accountingContextsToAccept: tokensToAccept});
|
|
500
|
+
|
|
501
|
+
projectId = jbController.launchProjectFor({
|
|
502
|
+
owner: multisig,
|
|
503
|
+
projectUri: "test-project",
|
|
504
|
+
rulesetConfigurations: rulesetConfigs,
|
|
505
|
+
terminalConfigurations: terminalConfigs,
|
|
506
|
+
memo: ""
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|