@bananapus/suckers-v6 0.0.35 → 0.0.37
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/README.md +3 -3
- package/package.json +2 -2
- package/references/operations.md +2 -2
- package/references/runtime.md +1 -1
- package/src/JBArbitrumSucker.sol +5 -17
- package/src/JBCCIPSucker.sol +7 -10
- package/src/JBCeloSucker.sol +20 -26
- package/src/JBOptimismSucker.sol +1 -4
- package/src/JBSucker.sol +55 -63
- package/src/JBSuckerRegistry.sol +13 -59
- package/src/JBSwapCCIPSucker.sol +62 -31
- package/src/deployers/JBArbitrumSuckerDeployer.sol +5 -3
- package/src/deployers/JBCCIPSuckerDeployer.sol +5 -3
- package/src/deployers/JBCeloSuckerDeployer.sol +7 -5
- package/src/deployers/JBOptimismSuckerDeployer.sol +5 -3
- package/src/deployers/JBSuckerDeployer.sol +14 -10
- package/src/deployers/JBSwapCCIPSuckerDeployer.sol +7 -9
- package/src/interfaces/IJBCeloSuckerDeployer.sol +1 -1
- package/src/interfaces/IJBSwapCCIPSuckerDeployer.sol +2 -1
- package/src/libraries/ARBAddresses.sol +6 -3
- package/src/libraries/ARBChains.sol +4 -1
- package/src/libraries/CCIPHelper.sol +68 -11
- package/src/libraries/JBCCIPLib.sol +8 -17
- package/src/libraries/JBSuckerLib.sol +2 -13
- package/src/libraries/JBSwapPoolLib.sol +57 -49
- package/src/utils/MerkleLib.sol +3 -5
package/src/JBSucker.sol
CHANGED
|
@@ -67,9 +67,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
67
67
|
|
|
68
68
|
error JBSucker_AmountExceedsUint128(uint256 amount);
|
|
69
69
|
error JBSucker_BelowMinGas(uint256 minGas, uint256 minGasLimit);
|
|
70
|
-
error JBSucker_Deprecated();
|
|
70
|
+
error JBSucker_Deprecated(JBSuckerState state);
|
|
71
71
|
error JBSucker_DeprecationTimestampTooSoon(uint256 givenTime, uint256 minimumTime);
|
|
72
|
-
error JBSucker_ExpectedMsgValue();
|
|
72
|
+
error JBSucker_ExpectedMsgValue(uint256 msgValue);
|
|
73
73
|
error JBSucker_IndexOutOfRange(uint256 index);
|
|
74
74
|
error JBSucker_InsufficientBalance(uint256 amount, uint256 balance);
|
|
75
75
|
error JBSucker_InsufficientMsgValue(uint256 received, uint256 expected);
|
|
@@ -79,16 +79,16 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
79
79
|
error JBSucker_LeafAlreadyExecuted(address token, uint256 index);
|
|
80
80
|
error JBSucker_NoTerminalForToken(uint256 projectId, address token);
|
|
81
81
|
error JBSucker_NotPeer(bytes32 caller);
|
|
82
|
-
error JBSucker_NothingToSend();
|
|
82
|
+
error JBSucker_NothingToSend(address token, uint256 outboxBalance, uint256 treeCount, uint256 numberOfClaimsSent);
|
|
83
83
|
error JBSucker_NoRetainedToRemoteFee(address account);
|
|
84
84
|
error JBSucker_NoRetainedTransportPaymentRefund(address account);
|
|
85
|
-
error JBSucker_RefundFailed();
|
|
85
|
+
error JBSucker_RefundFailed(address beneficiary, uint256 amount);
|
|
86
86
|
error JBSucker_TokenAlreadyMapped(address localToken, bytes32 mappedTo);
|
|
87
87
|
error JBSucker_TokenHasInvalidEmergencyHatchState(address token);
|
|
88
88
|
error JBSucker_TokenNotMapped(address token);
|
|
89
89
|
error JBSucker_UnexpectedMsgValue(uint256 value);
|
|
90
|
-
error JBSucker_ZeroBeneficiary();
|
|
91
|
-
error JBSucker_ZeroERC20Token();
|
|
90
|
+
error JBSucker_ZeroBeneficiary(bytes32 beneficiary);
|
|
91
|
+
error JBSucker_ZeroERC20Token(uint256 projectId);
|
|
92
92
|
|
|
93
93
|
//*********************************************************************//
|
|
94
94
|
// ------------------------- public constants ------------------------ //
|
|
@@ -255,8 +255,8 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
255
255
|
/// @notice Accepts incoming native token (ETH) transfers.
|
|
256
256
|
/// @dev This receive function is intentionally unrestricted. It must accept ETH from multiple sources:
|
|
257
257
|
/// - Bridge contracts (e.g., Optimism's StandardBridge, Arbitrum's gateway) delivering bridged native tokens.
|
|
258
|
-
/// -
|
|
259
|
-
/// - Terminals returning native tokens during `cashOutTokensOf` (backing asset pulls).
|
|
258
|
+
/// - Wrapped native token contracts during unwrapping (e.g., CCIP sucker unwraps via `withdraw()` which sends
|
|
259
|
+
/// native tokens here). - Terminals returning native tokens during `cashOutTokensOf` (backing asset pulls).
|
|
260
260
|
/// @dev Restricting this to known senders would risk breaking bridge integrations, as bridge contracts may change
|
|
261
261
|
/// addresses or use proxy patterns. The sucker's accounting (`_outboxOf[token].balance` and
|
|
262
262
|
/// `amountToAddToBalanceOf`) already tracks expected native token amounts, so excess ETH sent here does not
|
|
@@ -318,7 +318,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
318
318
|
/// @param tokens The terminal tokens to enable the emergency hatch for.
|
|
319
319
|
function enableEmergencyHatchFor(address[] calldata tokens) external override {
|
|
320
320
|
// The caller must be the project owner or have the `QUEUE_RULESETS` permission from them.
|
|
321
|
-
// slither-disable-next-line calls-loop
|
|
322
321
|
uint256 _projectId = projectId();
|
|
323
322
|
|
|
324
323
|
_requirePermissionFrom({
|
|
@@ -396,7 +395,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
396
395
|
// Using _msgSender() would allow a trusted forwarder to spoof the bridge messenger address via the
|
|
397
396
|
// ERC-2771 calldata suffix.
|
|
398
397
|
if (!_isRemotePeer(msg.sender)) {
|
|
399
|
-
revert JBSucker_NotPeer(_toBytes32(msg.sender));
|
|
398
|
+
revert JBSucker_NotPeer({caller: _toBytes32(msg.sender)});
|
|
400
399
|
}
|
|
401
400
|
|
|
402
401
|
// Validate the message version to reject incompatible messages.
|
|
@@ -479,7 +478,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
479
478
|
|
|
480
479
|
// Perform each token mapping.
|
|
481
480
|
for (uint256 i; i < maps.length;) {
|
|
482
|
-
// slither-disable-next-line msg-value-loop
|
|
483
481
|
_mapToken({map: maps[i], transportPaymentValue: numberToDisable > 0 ? msg.value / numberToDisable : 0});
|
|
484
482
|
unchecked {
|
|
485
483
|
++i;
|
|
@@ -489,15 +487,13 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
489
487
|
// If no tokens were disabled, the full `msg.value` is unused — refund it.
|
|
490
488
|
if (numberToDisable == 0) {
|
|
491
489
|
if (msg.value > 0) {
|
|
492
|
-
(
|
|
493
|
-
if (!_ok) revert JBSucker_RefundFailed();
|
|
490
|
+
_sendNativeTo({beneficiary: payable(_msgSender()), amount: msg.value});
|
|
494
491
|
}
|
|
495
492
|
} else {
|
|
496
493
|
// Refund any remainder from integer division so dust wei isn't stuck in the contract.
|
|
497
494
|
uint256 remainder = msg.value % numberToDisable;
|
|
498
495
|
if (remainder > 0) {
|
|
499
496
|
// Best-effort refund — don't revert if caller can't accept ETH.
|
|
500
|
-
// slither-disable-next-line low-level-calls,unchecked-lowlevel
|
|
501
497
|
(bool _ok,) = _msgSender().call{value: remainder}("");
|
|
502
498
|
_ok; // Silence unused-variable warning; failure is intentionally ignored.
|
|
503
499
|
}
|
|
@@ -534,32 +530,27 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
534
530
|
{
|
|
535
531
|
// Make sure the beneficiary is not the zero address, as this would revert when minting on the remote chain.
|
|
536
532
|
if (beneficiary == bytes32(0)) {
|
|
537
|
-
revert JBSucker_ZeroBeneficiary();
|
|
533
|
+
revert JBSucker_ZeroBeneficiary({beneficiary: beneficiary});
|
|
538
534
|
}
|
|
539
535
|
|
|
540
536
|
// Get the project's token.
|
|
541
537
|
IERC20 projectToken = IERC20(address(TOKENS.tokenOf(projectId())));
|
|
542
538
|
if (address(projectToken) == address(0)) {
|
|
543
|
-
revert JBSucker_ZeroERC20Token();
|
|
539
|
+
revert JBSucker_ZeroERC20Token({projectId: projectId()});
|
|
544
540
|
}
|
|
545
541
|
|
|
546
542
|
// Make sure that the token is mapped to a remote token.
|
|
547
543
|
if (!_remoteTokenFor[token].enabled) {
|
|
548
|
-
revert JBSucker_TokenNotMapped(token);
|
|
544
|
+
revert JBSucker_TokenNotMapped({token: token});
|
|
549
545
|
}
|
|
550
546
|
|
|
551
547
|
// Make sure that the sucker still allows sending new messaged.
|
|
552
|
-
|
|
553
|
-
if (deprecationState == JBSuckerState.DEPRECATED || deprecationState == JBSuckerState.SENDING_DISABLED) {
|
|
554
|
-
revert JBSucker_Deprecated();
|
|
555
|
-
}
|
|
548
|
+
_requireSendingEnabled();
|
|
556
549
|
|
|
557
550
|
// Transfer the tokens to this contract.
|
|
558
|
-
// slither-disable-next-line reentrancy-events,reentrancy-benign
|
|
559
551
|
projectToken.safeTransferFrom({from: _msgSender(), to: address(this), value: projectTokenCount});
|
|
560
552
|
|
|
561
553
|
// Cash out the tokens.
|
|
562
|
-
// slither-disable-next-line reentrancy-events,reentrancy-benign
|
|
563
554
|
uint256 terminalTokenAmount = _pullBackingAssets({
|
|
564
555
|
projectToken: projectToken, count: projectTokenCount, token: token, minTokensReclaimed: minTokensReclaimed
|
|
565
556
|
});
|
|
@@ -580,12 +571,8 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
580
571
|
function setDeprecation(uint40 timestamp) external override {
|
|
581
572
|
// As long as the sucker has not started letting users withdrawal, its deprecation time can be
|
|
582
573
|
// extended/shortened.
|
|
583
|
-
|
|
584
|
-
if (deprecationState == JBSuckerState.DEPRECATED || deprecationState == JBSuckerState.SENDING_DISABLED) {
|
|
585
|
-
revert JBSucker_Deprecated();
|
|
586
|
-
}
|
|
574
|
+
_requireSendingEnabled();
|
|
587
575
|
|
|
588
|
-
// slither-disable-next-line calls-loop
|
|
589
576
|
uint256 _projectId = projectId();
|
|
590
577
|
|
|
591
578
|
// The caller must be the project owner or have the `SET_SUCKER_DEPRECATION` permission from them.
|
|
@@ -626,13 +613,18 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
626
613
|
|
|
627
614
|
// Ensure that the token does not have an emergency hatch enabled.
|
|
628
615
|
if (remoteToken.emergencyHatch) {
|
|
629
|
-
revert JBSucker_TokenHasInvalidEmergencyHatchState(token);
|
|
616
|
+
revert JBSucker_TokenHasInvalidEmergencyHatchState({token: token});
|
|
630
617
|
}
|
|
631
618
|
|
|
632
619
|
// Revert if nothing has changed since the last toRemote() call.
|
|
633
620
|
JBOutboxTree storage outbox = _outboxOf[token];
|
|
634
621
|
if (outbox.balance == 0 && outbox.tree.count == outbox.numberOfClaimsSent) {
|
|
635
|
-
revert JBSucker_NothingToSend(
|
|
622
|
+
revert JBSucker_NothingToSend({
|
|
623
|
+
token: token,
|
|
624
|
+
outboxBalance: outbox.balance,
|
|
625
|
+
treeCount: outbox.tree.count,
|
|
626
|
+
numberOfClaimsSent: outbox.numberOfClaimsSent
|
|
627
|
+
});
|
|
636
628
|
}
|
|
637
629
|
|
|
638
630
|
// Read the fee from the registry.
|
|
@@ -650,7 +642,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
650
642
|
IJBTerminal feeTerminal = _primaryTerminalOf({forProjectId: FEE_PROJECT_ID, token: JBConstants.NATIVE_TOKEN});
|
|
651
643
|
bool feePaid;
|
|
652
644
|
if (address(feeTerminal) != address(0) && _toRemoteFee != 0) {
|
|
653
|
-
// slither-disable-next-line unused-return,reentrancy-events,reentrancy-benign,arbitrary-send-eth
|
|
654
645
|
try feeTerminal.pay{value: _toRemoteFee}({
|
|
655
646
|
projectId: FEE_PROJECT_ID,
|
|
656
647
|
token: JBConstants.NATIVE_TOKEN,
|
|
@@ -827,7 +818,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
827
818
|
/// @notice Claim retained failed-fee ETH.
|
|
828
819
|
/// @param beneficiary The address that should receive the retained ETH.
|
|
829
820
|
function claimRetainedToRemoteFee(address payable beneficiary) external override {
|
|
830
|
-
if (beneficiary == address(0)) revert JBSucker_ZeroBeneficiary();
|
|
821
|
+
if (beneficiary == address(0)) revert JBSucker_ZeroBeneficiary({beneficiary: bytes32(0)});
|
|
831
822
|
|
|
832
823
|
address account = _msgSender();
|
|
833
824
|
uint256 amount = retainedToRemoteFeeOf[account];
|
|
@@ -836,12 +827,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
836
827
|
retainedToRemoteFeeOf[account] = 0;
|
|
837
828
|
retainedToRemoteFeeBalance -= amount;
|
|
838
829
|
|
|
839
|
-
|
|
840
|
-
(bool success,) = beneficiary.call{value: amount}("");
|
|
841
|
-
if (!success) revert JBSucker_RefundFailed();
|
|
830
|
+
_sendNativeTo({beneficiary: beneficiary, amount: amount});
|
|
842
831
|
|
|
843
832
|
// State was cleared before sending ETH; the event is emitted after the transfer so failed sends do not log.
|
|
844
|
-
// slither-disable-next-line reentrancy-events
|
|
845
833
|
emit RetainedToRemoteFeeClaimed({
|
|
846
834
|
account: account, beneficiary: beneficiary, amount: amount, caller: _msgSender()
|
|
847
835
|
});
|
|
@@ -850,7 +838,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
850
838
|
/// @notice Claim retained failed transport-payment refund ETH.
|
|
851
839
|
/// @param beneficiary The address that should receive the retained ETH.
|
|
852
840
|
function claimRetainedTransportPaymentRefund(address payable beneficiary) external override {
|
|
853
|
-
if (beneficiary == address(0)) revert JBSucker_ZeroBeneficiary();
|
|
841
|
+
if (beneficiary == address(0)) revert JBSucker_ZeroBeneficiary({beneficiary: bytes32(0)});
|
|
854
842
|
|
|
855
843
|
address account = _msgSender();
|
|
856
844
|
uint256 amount = retainedTransportPaymentRefundOf[account];
|
|
@@ -859,12 +847,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
859
847
|
retainedTransportPaymentRefundOf[account] = 0;
|
|
860
848
|
retainedTransportPaymentRefundBalance -= amount;
|
|
861
849
|
|
|
862
|
-
|
|
863
|
-
(bool success,) = beneficiary.call{value: amount}("");
|
|
864
|
-
if (!success) revert JBSucker_RefundFailed();
|
|
850
|
+
_sendNativeTo({beneficiary: beneficiary, amount: amount});
|
|
865
851
|
|
|
866
852
|
// State was cleared before sending ETH; the event is emitted after the transfer so failed sends do not log.
|
|
867
|
-
// slither-disable-next-line reentrancy-events
|
|
868
853
|
emit RetainedTransportPaymentRefundClaimed({
|
|
869
854
|
account: account, beneficiary: beneficiary, amount: amount, caller: _msgSender()
|
|
870
855
|
});
|
|
@@ -887,7 +872,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
887
872
|
/// @param _projectId The ID of the project (on the local chain) that this sucker is associated with.
|
|
888
873
|
/// @param remotePeer The remote peer address. Leave zero to use the default deterministic same-address peer.
|
|
889
874
|
function _initialize(uint256 _projectId, bytes32 remotePeer) internal {
|
|
890
|
-
// slither-disable-next-line missing-zero-check
|
|
891
875
|
_localProjectId = _projectId;
|
|
892
876
|
_peer = remotePeer;
|
|
893
877
|
deployer = _msgSender();
|
|
@@ -920,7 +904,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
920
904
|
IJBTerminal terminal = _primaryTerminalOf({forProjectId: cachedProjectId, token: token});
|
|
921
905
|
|
|
922
906
|
// Revert if no terminal is configured for this token.
|
|
923
|
-
// slither-disable-next-line incorrect-equality
|
|
924
907
|
if (address(terminal) == address(0)) {
|
|
925
908
|
revert JBSucker_NoTerminalForToken({projectId: cachedProjectId, token: token});
|
|
926
909
|
}
|
|
@@ -928,14 +911,12 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
928
911
|
// Perform the `addToBalance` for ERC-20 tokens.
|
|
929
912
|
if (token != JBConstants.NATIVE_TOKEN) {
|
|
930
913
|
// Record the balance before the transfer for the sanity check.
|
|
931
|
-
// slither-disable-next-line calls-loop
|
|
932
914
|
uint256 balanceBefore = IERC20(token).balanceOf(address(this));
|
|
933
915
|
|
|
934
916
|
// Approve the terminal to spend the ERC-20 tokens.
|
|
935
917
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
|
|
936
918
|
|
|
937
919
|
// Add the tokens to the project's balance.
|
|
938
|
-
// slither-disable-next-line calls-loop
|
|
939
920
|
terminal.addToBalanceOf({
|
|
940
921
|
projectId: cachedProjectId,
|
|
941
922
|
token: token,
|
|
@@ -946,11 +927,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
946
927
|
});
|
|
947
928
|
|
|
948
929
|
// Sanity check: make sure we transferred the full amount.
|
|
949
|
-
// slither-disable-next-line calls-loop,incorrect-equality
|
|
950
930
|
assert(IERC20(token).balanceOf(address(this)) == balanceBefore - amount);
|
|
951
931
|
} else {
|
|
952
932
|
// If the token is the native token, send ETH with the call.
|
|
953
|
-
// slither-disable-next-line arbitrary-send-eth,calls-loop
|
|
954
933
|
terminal.addToBalanceOf{value: amount}({
|
|
955
934
|
projectId: cachedProjectId,
|
|
956
935
|
token: token,
|
|
@@ -988,7 +967,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
988
967
|
// before enabling suckers.
|
|
989
968
|
//
|
|
990
969
|
// Mint the project tokens for the beneficiary via the project's controller.
|
|
991
|
-
// slither-disable-next-line calls-loop,unused-return
|
|
992
970
|
IJBController(address(DIRECTORY.controllerOf(cachedProjectId)))
|
|
993
971
|
.mintTokensOf({
|
|
994
972
|
projectId: cachedProjectId,
|
|
@@ -1062,7 +1040,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1062
1040
|
|
|
1063
1041
|
// Once the emergency hatch for a token is enabled it can't be disabled.
|
|
1064
1042
|
if (currentMapping.emergencyHatch) {
|
|
1065
|
-
revert JBSucker_TokenHasInvalidEmergencyHatchState(token);
|
|
1043
|
+
revert JBSucker_TokenHasInvalidEmergencyHatchState({token: token});
|
|
1066
1044
|
}
|
|
1067
1045
|
|
|
1068
1046
|
// Validate the token mapping according to the rules of the sucker.
|
|
@@ -1072,7 +1050,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1072
1050
|
uint256 _projectId = projectId();
|
|
1073
1051
|
|
|
1074
1052
|
// The registry can map during authorized deployment. Otherwise, require the project's mapping permission.
|
|
1075
|
-
// slither-disable-next-line calls-loop
|
|
1076
1053
|
_requirePermissionAllowingOverrideFrom({
|
|
1077
1054
|
account: PROJECTS.ownerOf(_projectId),
|
|
1078
1055
|
projectId: _projectId,
|
|
@@ -1171,7 +1148,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1171
1148
|
});
|
|
1172
1149
|
|
|
1173
1150
|
// Sanity check to make sure we received the expected amount.
|
|
1174
|
-
// slither-disable-next-line incorrect-equality
|
|
1175
1151
|
assert(reclaimedAmount == _balanceOf({token: token, addr: address(this)}) - balanceBefore);
|
|
1176
1152
|
}
|
|
1177
1153
|
|
|
@@ -1187,12 +1163,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1187
1163
|
if (remoteToken.addr == bytes32(0)) revert JBSucker_TokenNotMapped(token);
|
|
1188
1164
|
|
|
1189
1165
|
// Make sure that the sucker still allows sending new messaged.
|
|
1190
|
-
|
|
1191
|
-
JBSuckerState deprecationState = state();
|
|
1192
|
-
if (deprecationState == JBSuckerState.DEPRECATED || deprecationState == JBSuckerState.SENDING_DISABLED) {
|
|
1193
|
-
revert JBSucker_Deprecated();
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1166
|
+
_requireSendingEnabled();
|
|
1196
1167
|
|
|
1197
1168
|
// Drain the outbox: read balance/nonce/root, clear balance, advance nonce and numberOfClaimsSent.
|
|
1198
1169
|
uint256 amount;
|
|
@@ -1241,6 +1212,14 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1241
1212
|
});
|
|
1242
1213
|
}
|
|
1243
1214
|
|
|
1215
|
+
/// @notice Send native tokens, reverting if the recipient rejects them.
|
|
1216
|
+
/// @param beneficiary The recipient.
|
|
1217
|
+
/// @param amount The amount to send.
|
|
1218
|
+
function _sendNativeTo(address payable beneficiary, uint256 amount) internal {
|
|
1219
|
+
(bool success,) = beneficiary.call{value: amount}("");
|
|
1220
|
+
if (!success) revert JBSucker_RefundFailed({beneficiary: beneficiary, amount: amount});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1244
1223
|
/// @notice Performs the logic to send a message to the peer over the AMB.
|
|
1245
1224
|
/// @dev This is chain/sucker/bridge specific logic.
|
|
1246
1225
|
/// @param transportPayment The amount of `msg.value` that is going to get paid for sending this message.
|
|
@@ -1386,7 +1365,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1386
1365
|
deprecationState != JBSuckerState.DEPRECATED && deprecationState != JBSuckerState.SENDING_DISABLED
|
|
1387
1366
|
&& !_remoteTokenFor[terminalToken].emergencyHatch
|
|
1388
1367
|
) {
|
|
1389
|
-
revert JBSucker_TokenHasInvalidEmergencyHatchState(terminalToken);
|
|
1368
|
+
revert JBSucker_TokenHasInvalidEmergencyHatchState({token: terminalToken});
|
|
1390
1369
|
}
|
|
1391
1370
|
|
|
1392
1371
|
// Check that this claim is within the bounds of who can claim.
|
|
@@ -1440,7 +1419,6 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1440
1419
|
return addr.balance;
|
|
1441
1420
|
}
|
|
1442
1421
|
|
|
1443
|
-
// slither-disable-next-line calls-loop
|
|
1444
1422
|
return IERC20(token).balanceOf(addr);
|
|
1445
1423
|
}
|
|
1446
1424
|
|
|
@@ -1561,10 +1539,17 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1561
1539
|
/// @return The primary terminal.
|
|
1562
1540
|
function _primaryTerminalOf(uint256 forProjectId, address token) internal view returns (IJBTerminal) {
|
|
1563
1541
|
// Claim processing may call this through a bounded claim list; each lookup must use the live directory state.
|
|
1564
|
-
// slither-disable-next-line calls-loop
|
|
1565
1542
|
return DIRECTORY.primaryTerminalOf({projectId: forProjectId, token: token});
|
|
1566
1543
|
}
|
|
1567
1544
|
|
|
1545
|
+
/// @notice Revert if new outbound sends are disabled or deprecated.
|
|
1546
|
+
function _requireSendingEnabled() internal view {
|
|
1547
|
+
JBSuckerState deprecationState = state();
|
|
1548
|
+
if (deprecationState == JBSuckerState.DEPRECATED || deprecationState == JBSuckerState.SENDING_DISABLED) {
|
|
1549
|
+
revert JBSucker_Deprecated({state: deprecationState});
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1568
1553
|
/// @notice Convert a bytes32 remote address to a local EVM address.
|
|
1569
1554
|
/// @param remote The bytes32 representation of the address.
|
|
1570
1555
|
/// @return The EVM address (lower 20 bytes).
|
|
@@ -1587,7 +1572,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1587
1572
|
// If the token being mapped is the native token, the `remoteToken` must also be the native token.
|
|
1588
1573
|
// The native token can also be mapped to the 0 address, which is used to disable native token bridging.
|
|
1589
1574
|
if (isNative && map.remoteToken != _toBytes32(JBConstants.NATIVE_TOKEN) && map.remoteToken != bytes32(0)) {
|
|
1590
|
-
revert JBSucker_InvalidNativeRemoteAddress(map.remoteToken);
|
|
1575
|
+
revert JBSucker_InvalidNativeRemoteAddress({remoteToken: map.remoteToken});
|
|
1591
1576
|
}
|
|
1592
1577
|
|
|
1593
1578
|
// Enforce a reasonable minimum gas limit for bridging. A minimum which is too low could lead to the loss of
|
|
@@ -1641,7 +1626,14 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1641
1626
|
sourceTimestamp: sourceTimestamp
|
|
1642
1627
|
});
|
|
1643
1628
|
|
|
1644
|
-
// Send the root over the AMB
|
|
1645
|
-
_sendRootOverAMB(
|
|
1629
|
+
// Send the root over the AMB. This overloaded interface call is intentionally positional.
|
|
1630
|
+
_sendRootOverAMB({
|
|
1631
|
+
transportPayment: transportPayment,
|
|
1632
|
+
index: index,
|
|
1633
|
+
token: token,
|
|
1634
|
+
amount: amount,
|
|
1635
|
+
remoteToken: remoteToken,
|
|
1636
|
+
message: message
|
|
1637
|
+
});
|
|
1646
1638
|
}
|
|
1647
1639
|
}
|
package/src/JBSuckerRegistry.sol
CHANGED
|
@@ -20,9 +20,9 @@ import {JBSuckerDeployerConfig} from "./structs/JBSuckerDeployerConfig.sol";
|
|
|
20
20
|
import {JBSuckersPair} from "./structs/JBSuckersPair.sol";
|
|
21
21
|
|
|
22
22
|
/// @notice The canonical registry that deploys, tracks, and governs cross-chain suckers for Juicebox projects. It
|
|
23
|
-
/// maintains an allowlist of approved deployer contracts,
|
|
24
|
-
/// manages the global `toRemoteFee` (paid into the protocol fee project on each bridge send), and provides
|
|
25
|
-
/// views of remote-chain balances, surplus, and token supply across all of a project's suckers.
|
|
23
|
+
/// maintains an allowlist of approved deployer contracts, allows multiple active suckers per peer chain for bridge
|
|
24
|
+
/// resilience, manages the global `toRemoteFee` (paid into the protocol fee project on each bridge send), and provides
|
|
25
|
+
/// aggregate views of remote-chain balances, surplus, and token supply across all of a project's suckers.
|
|
26
26
|
contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerRegistry {
|
|
27
27
|
using EnumerableMap for EnumerableMap.AddressToUintMap;
|
|
28
28
|
|
|
@@ -30,7 +30,6 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
30
30
|
// --------------------------- custom errors ------------------------- //
|
|
31
31
|
//*********************************************************************//
|
|
32
32
|
|
|
33
|
-
error JBSuckerRegistry_DuplicatePeerChain(uint256 projectId, uint256 peerChainId);
|
|
34
33
|
error JBSuckerRegistry_FeeExceedsMax(uint256 fee, uint256 max);
|
|
35
34
|
error JBSuckerRegistry_InvalidDeployer(IJBSuckerDeployer deployer);
|
|
36
35
|
error JBSuckerRegistry_SuckerDoesNotBelongToProject(uint256 projectId, address sucker);
|
|
@@ -128,7 +127,6 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
128
127
|
// Count active suckers.
|
|
129
128
|
uint256 activeCount;
|
|
130
129
|
for (uint256 i; i < allSuckers.length;) {
|
|
131
|
-
// slither-disable-next-line unused-return
|
|
132
130
|
(, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
|
|
133
131
|
if (val == _SUCKER_EXISTS) activeCount++;
|
|
134
132
|
unchecked {
|
|
@@ -140,11 +138,9 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
140
138
|
pairs = new JBSuckersPair[](activeCount);
|
|
141
139
|
uint256 j;
|
|
142
140
|
for (uint256 i; i < allSuckers.length;) {
|
|
143
|
-
// slither-disable-next-line unused-return
|
|
144
141
|
(, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
|
|
145
142
|
if (val == _SUCKER_EXISTS) {
|
|
146
143
|
IJBSucker sucker = IJBSucker(allSuckers[i]);
|
|
147
|
-
// slither-disable-next-line calls-loop
|
|
148
144
|
pairs[j] =
|
|
149
145
|
JBSuckersPair({local: address(sucker), remote: sucker.peer(), remoteChainId: sucker.peerChainId()});
|
|
150
146
|
unchecked {
|
|
@@ -167,7 +163,6 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
167
163
|
// Count active suckers.
|
|
168
164
|
uint256 activeCount;
|
|
169
165
|
for (uint256 i; i < allSuckers.length;) {
|
|
170
|
-
// slither-disable-next-line unused-return
|
|
171
166
|
(, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
|
|
172
167
|
if (val == _SUCKER_EXISTS) activeCount++;
|
|
173
168
|
unchecked {
|
|
@@ -179,7 +174,6 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
179
174
|
suckers = new address[](activeCount);
|
|
180
175
|
uint256 j;
|
|
181
176
|
for (uint256 i; i < allSuckers.length;) {
|
|
182
|
-
// slither-disable-next-line unused-return
|
|
183
177
|
(, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
|
|
184
178
|
if (val == _SUCKER_EXISTS) {
|
|
185
179
|
suckers[j] = allSuckers[i];
|
|
@@ -222,15 +216,12 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
222
216
|
uint256 chainCount;
|
|
223
217
|
|
|
224
218
|
for (uint256 i; i < len;) {
|
|
225
|
-
// slither-disable-next-line unused-return
|
|
226
219
|
(, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
|
|
227
220
|
// Include both active and deprecated suckers in aggregate economic views.
|
|
228
221
|
if (val == _SUCKER_EXISTS || val == _SUCKER_DEPRECATED) {
|
|
229
|
-
// slither-disable-next-line calls-loop
|
|
230
222
|
try IJBSucker(allSuckers[i]).peerChainBalanceOf(decimals, currency) returns (
|
|
231
223
|
JBDenominatedAmount memory amt
|
|
232
224
|
) {
|
|
233
|
-
// slither-disable-next-line calls-loop
|
|
234
225
|
uint256 chainId = IJBSucker(allSuckers[i]).peerChainId();
|
|
235
226
|
chainCount = _recordPeerValue({
|
|
236
227
|
chainIds: chainIds,
|
|
@@ -286,15 +277,12 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
286
277
|
uint256 chainCount;
|
|
287
278
|
|
|
288
279
|
for (uint256 i; i < len;) {
|
|
289
|
-
// slither-disable-next-line unused-return
|
|
290
280
|
(, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
|
|
291
281
|
// Include both active and deprecated suckers in aggregate economic views.
|
|
292
282
|
if (val == _SUCKER_EXISTS || val == _SUCKER_DEPRECATED) {
|
|
293
|
-
// slither-disable-next-line calls-loop
|
|
294
283
|
try IJBSucker(allSuckers[i]).peerChainSurplusOf(decimals, currency) returns (
|
|
295
284
|
JBDenominatedAmount memory amt
|
|
296
285
|
) {
|
|
297
|
-
// slither-disable-next-line calls-loop
|
|
298
286
|
uint256 chainId = IJBSucker(allSuckers[i]).peerChainId();
|
|
299
287
|
chainCount = _recordPeerValue({
|
|
300
288
|
chainIds: chainIds,
|
|
@@ -339,13 +327,10 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
339
327
|
uint256 chainCount;
|
|
340
328
|
|
|
341
329
|
for (uint256 i; i < len;) {
|
|
342
|
-
// slither-disable-next-line unused-return
|
|
343
330
|
(, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
|
|
344
331
|
// Include both active and deprecated suckers in aggregate economic views.
|
|
345
332
|
if (val == _SUCKER_EXISTS || val == _SUCKER_DEPRECATED) {
|
|
346
|
-
// slither-disable-next-line calls-loop
|
|
347
333
|
try IJBSucker(allSuckers[i]).peerChainTotalSupply() returns (uint256 supply) {
|
|
348
|
-
// slither-disable-next-line calls-loop
|
|
349
334
|
uint256 chainId = IJBSucker(allSuckers[i]).peerChainId();
|
|
350
335
|
chainCount = _recordPeerValue({
|
|
351
336
|
chainIds: chainIds,
|
|
@@ -420,8 +405,9 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
420
405
|
{
|
|
421
406
|
for (uint256 j; j < chainCount;) {
|
|
422
407
|
if (chainIds[j] == chainId) {
|
|
423
|
-
//
|
|
424
|
-
//
|
|
408
|
+
// Each sucker caches the entire remote chain's state (not a per-sucker share), so multiple
|
|
409
|
+
// suckers targeting the same chain report redundant snapshots. MAX picks the freshest value
|
|
410
|
+
// without double-counting — SUM would inflate bonding curve denominators.
|
|
425
411
|
if (isActive) {
|
|
426
412
|
if (!hasActiveValue[j] || value > values[j]) values[j] = value;
|
|
427
413
|
hasActiveValue[j] = true;
|
|
@@ -444,30 +430,6 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
444
430
|
}
|
|
445
431
|
}
|
|
446
432
|
|
|
447
|
-
/// @notice Reverts if any active sucker for the given project already targets the same peer chain as the new
|
|
448
|
-
/// sucker.
|
|
449
|
-
/// @param projectId The ID of the project to check.
|
|
450
|
-
/// @param newSucker The newly created sucker to validate.
|
|
451
|
-
function _revertIfDuplicatePeerChain(uint256 projectId, IJBSucker newSucker) internal view {
|
|
452
|
-
// The new sucker is registry-deployed and trusted for this validation.
|
|
453
|
-
// slither-disable-next-line calls-loop
|
|
454
|
-
uint256 newPeerChainId = newSucker.peerChainId();
|
|
455
|
-
address[] memory existing = _suckersOf[projectId].keys();
|
|
456
|
-
for (uint256 i; i < existing.length;) {
|
|
457
|
-
// slither-disable-next-line unused-return
|
|
458
|
-
(, uint256 val) = _suckersOf[projectId].tryGet(existing[i]);
|
|
459
|
-
if (val == _SUCKER_EXISTS) {
|
|
460
|
-
// slither-disable-next-line calls-loop
|
|
461
|
-
if (IJBSucker(existing[i]).peerChainId() == newPeerChainId) {
|
|
462
|
-
revert JBSuckerRegistry_DuplicatePeerChain(projectId, newPeerChainId);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
unchecked {
|
|
466
|
-
++i;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
433
|
//*********************************************************************//
|
|
472
434
|
// ---------------------- public transactions ----------------------- //
|
|
473
435
|
//*********************************************************************//
|
|
@@ -502,9 +464,9 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
502
464
|
}
|
|
503
465
|
|
|
504
466
|
/// @notice Deploy one or more cross-chain suckers for a project in a single transaction. Each sucker is created via
|
|
505
|
-
/// its deployer, registered in this registry,
|
|
506
|
-
///
|
|
507
|
-
/// `MAP_SUCKER_TOKEN` permission for the project.
|
|
467
|
+
/// its deployer, registered in this registry, and immediately configured with its token mappings. Multiple suckers
|
|
468
|
+
/// targeting the same peer chain are allowed for bridge resilience. The caller must have `DEPLOY_SUCKERS`
|
|
469
|
+
/// permission, and this registry must hold `MAP_SUCKER_TOKEN` permission for the project.
|
|
508
470
|
/// @param projectId The ID of the project to deploy suckers for.
|
|
509
471
|
/// @param salt The salt used to deploy the contract. For the suckers to be peers, this must be the same value on
|
|
510
472
|
/// each chain where suckers are deployed.
|
|
@@ -542,25 +504,18 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
542
504
|
|
|
543
505
|
// Make sure the deployer is allowed.
|
|
544
506
|
if (!suckerDeployerIsAllowed[address(configuration.deployer)]) {
|
|
545
|
-
revert JBSuckerRegistry_InvalidDeployer(configuration.deployer);
|
|
507
|
+
revert JBSuckerRegistry_InvalidDeployer({deployer: configuration.deployer});
|
|
546
508
|
}
|
|
547
509
|
|
|
548
510
|
// Create the sucker.
|
|
549
|
-
// slither-disable-next-line reentrancy-event,calls-loop
|
|
550
511
|
IJBSucker sucker = configuration.deployer
|
|
551
512
|
.createForSender({localProjectId: projectId, salt: salt, peer: configuration.peer});
|
|
552
513
|
suckers[i] = address(sucker);
|
|
553
514
|
|
|
554
|
-
// Make sure no active sucker already targets the same peer chain.
|
|
555
|
-
// slither-disable-next-line calls-loop
|
|
556
|
-
_revertIfDuplicatePeerChain({projectId: projectId, newSucker: sucker});
|
|
557
|
-
|
|
558
515
|
// Store the sucker as being deployed for this project.
|
|
559
|
-
// slither-disable-next-line unused-return
|
|
560
516
|
_suckersOf[projectId].set({key: address(sucker), value: _SUCKER_EXISTS});
|
|
561
517
|
|
|
562
518
|
// Map the tokens for the sucker.
|
|
563
|
-
// slither-disable-next-line reentrancy-events,calls-loop
|
|
564
519
|
sucker.mapTokens(configuration.mappings);
|
|
565
520
|
emit SuckerDeployedFor({
|
|
566
521
|
projectId: projectId, sucker: address(sucker), configuration: configuration, caller: sender
|
|
@@ -580,17 +535,16 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
580
535
|
// Sanity check, make sure that the sucker does actually belong to the project.
|
|
581
536
|
(bool belongsToProject, uint256 val) = _suckersOf[projectId].tryGet(sucker);
|
|
582
537
|
if (!belongsToProject || val != _SUCKER_EXISTS) {
|
|
583
|
-
revert JBSuckerRegistry_SuckerDoesNotBelongToProject(projectId, address(sucker));
|
|
538
|
+
revert JBSuckerRegistry_SuckerDoesNotBelongToProject({projectId: projectId, sucker: address(sucker)});
|
|
584
539
|
}
|
|
585
540
|
|
|
586
541
|
// Check if the sucker is deprecated.
|
|
587
542
|
JBSuckerState state = IJBSucker(sucker).state();
|
|
588
543
|
if (state != JBSuckerState.DEPRECATED) {
|
|
589
|
-
revert JBSuckerRegistry_SuckerIsNotDeprecated(address(sucker), state);
|
|
544
|
+
revert JBSuckerRegistry_SuckerIsNotDeprecated({sucker: address(sucker), suckerState: state});
|
|
590
545
|
}
|
|
591
546
|
|
|
592
547
|
// Mark the sucker as deprecated (retains mint permission, excluded from active listings).
|
|
593
|
-
// slither-disable-next-line unused-return
|
|
594
548
|
_suckersOf[projectId].set(address(sucker), _SUCKER_DEPRECATED);
|
|
595
549
|
emit SuckerDeprecated({projectId: projectId, sucker: address(sucker), caller: _msgSender()});
|
|
596
550
|
}
|
|
@@ -602,7 +556,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
602
556
|
if (fee > MAX_TO_REMOTE_FEE) revert JBSuckerRegistry_FeeExceedsMax(fee, MAX_TO_REMOTE_FEE);
|
|
603
557
|
uint256 oldFee = toRemoteFee;
|
|
604
558
|
toRemoteFee = fee;
|
|
605
|
-
emit ToRemoteFeeChanged(oldFee, fee, _msgSender());
|
|
559
|
+
emit ToRemoteFeeChanged({oldFee: oldFee, newFee: fee, caller: _msgSender()});
|
|
606
560
|
}
|
|
607
561
|
|
|
608
562
|
/// @notice Removes a sucker deployer from the allowlist.
|