@bananapus/suckers-v6 0.0.57 → 0.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/JBBaseSucker.sol +1 -1
- package/src/JBCeloSucker.sol +2 -2
- package/src/JBOptimismSucker.sol +1 -1
- package/src/JBSucker.sol +60 -68
- package/src/JBSuckerRegistry.sol +28 -5
- package/src/JBSwapCCIPSucker.sol +68 -78
- package/src/interfaces/IJBSucker.sol +12 -0
- package/src/interfaces/IJBSuckerRegistry.sol +8 -0
- package/src/structs/JBConversionRate.sol +12 -0
- package/src/structs/JBPendingSwap.sol +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/suckers-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.59",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@arbitrum/nitro-contracts": "3.2.0",
|
|
29
|
-
"@bananapus/core-v6": "^0.0.
|
|
29
|
+
"@bananapus/core-v6": "^0.0.66",
|
|
30
30
|
"@bananapus/permission-ids-v6": "^0.0.27",
|
|
31
31
|
"@chainlink/contracts-ccip": "1.6.4",
|
|
32
32
|
"@chainlink/local": "0.2.7",
|
package/src/JBBaseSucker.sol
CHANGED
|
@@ -16,7 +16,7 @@ contract JBBaseSucker is JBOptimismSucker {
|
|
|
16
16
|
// ---------------------------- constructor -------------------------- //
|
|
17
17
|
//*********************************************************************//
|
|
18
18
|
|
|
19
|
-
/// @param deployer A contract that deploys
|
|
19
|
+
/// @param deployer A contract that deploys clones of this contract.
|
|
20
20
|
/// @param directory A contract storing directories of terminals and controllers for each project.
|
|
21
21
|
/// @param permissions A contract storing permissions.
|
|
22
22
|
/// @param prices The price oracle used to convert peer-chain balances and surplus.
|
package/src/JBCeloSucker.sol
CHANGED
|
@@ -35,7 +35,7 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
35
35
|
// ---------------------------- constructor -------------------------- //
|
|
36
36
|
//*********************************************************************//
|
|
37
37
|
|
|
38
|
-
/// @param deployer A contract that deploys
|
|
38
|
+
/// @param deployer A contract that deploys clones of this contract.
|
|
39
39
|
/// @param directory A contract storing directories of terminals and controllers for each project.
|
|
40
40
|
/// @param permissions A contract storing permissions.
|
|
41
41
|
/// @param prices The price oracle used to convert peer-chain balances and surplus.
|
|
@@ -135,7 +135,7 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
135
135
|
{
|
|
136
136
|
index; // Silence unused parameter warning (not needed for Celo bridge).
|
|
137
137
|
|
|
138
|
-
// Revert if there's a `msg.value`. The
|
|
138
|
+
// Revert if there's a `msg.value`. The Celo bridge does not expect to be paid.
|
|
139
139
|
if (transportPayment != 0) {
|
|
140
140
|
revert JBSucker_UnexpectedMsgValue({value: transportPayment});
|
|
141
141
|
}
|
package/src/JBOptimismSucker.sol
CHANGED
|
@@ -36,7 +36,7 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
|
|
|
36
36
|
// ---------------------------- constructor -------------------------- //
|
|
37
37
|
//*********************************************************************//
|
|
38
38
|
|
|
39
|
-
/// @param deployer A contract that deploys
|
|
39
|
+
/// @param deployer A contract that deploys clones of this contract.
|
|
40
40
|
/// @param directory A contract storing directories of terminals and controllers for each project.
|
|
41
41
|
/// @param permissions A contract storing permissions.
|
|
42
42
|
/// @param prices The price oracle used to convert peer-chain balances and surplus.
|
package/src/JBSucker.sol
CHANGED
|
@@ -95,12 +95,10 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
95
95
|
// ------------------------- public constants ------------------------ //
|
|
96
96
|
//*********************************************************************//
|
|
97
97
|
|
|
98
|
-
/// @notice A reasonable minimum gas limit for a basic cross-chain call
|
|
99
|
-
/// the `fromRemote` (successfully/safely) on the remote chain.
|
|
98
|
+
/// @notice A reasonable minimum gas limit for a basic cross-chain call to `fromRemote` on the remote chain.
|
|
100
99
|
uint32 public constant override MESSENGER_BASE_GAS_LIMIT = 300_000;
|
|
101
100
|
|
|
102
|
-
/// @notice A reasonable minimum gas limit
|
|
103
|
-
/// (successfully/safely) perform a transfer on the remote chain.
|
|
101
|
+
/// @notice A reasonable minimum gas limit for performing an ERC-20 transfer on the remote chain.
|
|
104
102
|
uint32 public constant override MESSENGER_ERC20_MIN_GAS_LIMIT = 200_000;
|
|
105
103
|
|
|
106
104
|
/// @notice The message format version. Used to reject incompatible messages from remote chains.
|
|
@@ -142,6 +140,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
142
140
|
/// @notice The address of this contract's deployer.
|
|
143
141
|
address public override deployer;
|
|
144
142
|
|
|
143
|
+
/// @notice The keccak256 hash of the leaf data committed at execution time, keyed by `(terminalToken,
|
|
144
|
+
/// leafIndex)`. Beneficiary contracts (e.g. `JBReferralSplitHook`) use this to authenticate post-hoc
|
|
145
|
+
/// settlement when their `claim()` call was front-run by a direct external caller — they re-derive the
|
|
146
|
+
/// hash from the claim data they hold and compare. Returns `bytes32(0)` for unexecuted indices —
|
|
147
|
+
/// `_buildTreeHash` is pre-image-resistant so zero unambiguously means "not executed".
|
|
148
|
+
/// @custom:param token The token whose inbox tree contains the leaf.
|
|
149
|
+
/// @custom:param index The leaf's index in the inbox tree.
|
|
150
|
+
mapping(address token => mapping(uint256 index => bytes32)) public override executedLeafHashOf;
|
|
151
|
+
|
|
145
152
|
/// @notice The last known total token supply on the peer chain, updated each time a bridge message is received.
|
|
146
153
|
/// @dev Used by data hooks to compute `effectiveTotalSupply = localSupply + sum(peerChainTotalSupply)` across all
|
|
147
154
|
/// suckers, preventing cash out tax bypass on chains where a holder dominates the local supply.
|
|
@@ -601,7 +608,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
601
608
|
account: _ownerOf(_projectId), projectId: _projectId, permissionId: JBPermissionIds.SET_SUCKER_DEPRECATION
|
|
602
609
|
});
|
|
603
610
|
|
|
604
|
-
// This is the earliest time
|
|
611
|
+
// This is the earliest time the sucker can be considered deprecated.
|
|
605
612
|
// There is a mandatory delay to allow for remaining messages to be received.
|
|
606
613
|
// This should be called on both sides of the suckers, preferably with a matching timestamp.
|
|
607
614
|
uint256 nextEarliestDeprecationTime = block.timestamp + _maxMessagingDelay();
|
|
@@ -804,7 +811,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
804
811
|
return JBSuckerState.ENABLED;
|
|
805
812
|
}
|
|
806
813
|
|
|
807
|
-
// The sucker
|
|
814
|
+
// The sucker is close to deprecation; this state only warns users.
|
|
808
815
|
// Deprecation state is intentionally time-based.
|
|
809
816
|
// forge-lint: disable-next-line(block-timestamp)
|
|
810
817
|
if (block.timestamp < _deprecatedAfter - _maxMessagingDelay()) {
|
|
@@ -927,40 +934,35 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
927
934
|
revert JBSucker_NoTerminalForToken({projectId: cachedProjectId, token: token});
|
|
928
935
|
}
|
|
929
936
|
|
|
930
|
-
//
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
937
|
+
// Native and ERC-20 differ only in (a) value attachment to the call, and (b) ERC-20 requires an
|
|
938
|
+
// allowance grant + post-transfer balance assertion to catch fee-on-transfer / non-conforming tokens.
|
|
939
|
+
// The terminal call itself is identical for both, so it lives outside the branch.
|
|
940
|
+
uint256 nativeValue;
|
|
941
|
+
uint256 balanceBefore;
|
|
942
|
+
bool isErc20 = token != JBConstants.NATIVE_TOKEN;
|
|
943
|
+
if (isErc20) {
|
|
944
|
+
balanceBefore = IERC20(token).balanceOf(address(this));
|
|
936
945
|
SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
|
|
946
|
+
} else {
|
|
947
|
+
nativeValue = amount;
|
|
948
|
+
}
|
|
937
949
|
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
});
|
|
950
|
+
terminal.addToBalanceOf{value: nativeValue}({
|
|
951
|
+
projectId: cachedProjectId,
|
|
952
|
+
token: token,
|
|
953
|
+
amount: amount,
|
|
954
|
+
shouldReturnHeldFees: false,
|
|
955
|
+
memo: "",
|
|
956
|
+
metadata: ""
|
|
957
|
+
});
|
|
947
958
|
|
|
948
|
-
|
|
959
|
+
if (isErc20) {
|
|
960
|
+
// Sanity check: catches fee-on-transfer / non-conforming ERC-20s that move less than `amount`.
|
|
949
961
|
assert(IERC20(token).balanceOf(address(this)) == balanceBefore - amount);
|
|
950
|
-
} else {
|
|
951
|
-
// If the token is the native token, send ETH with the call.
|
|
952
|
-
terminal.addToBalanceOf{value: amount}({
|
|
953
|
-
projectId: cachedProjectId,
|
|
954
|
-
token: token,
|
|
955
|
-
amount: amount,
|
|
956
|
-
shouldReturnHeldFees: false,
|
|
957
|
-
memo: "",
|
|
958
|
-
metadata: ""
|
|
959
|
-
});
|
|
960
962
|
}
|
|
961
963
|
}
|
|
962
964
|
|
|
963
|
-
/// @notice
|
|
965
|
+
/// @notice Actions to perform after a user has successfully proven their claim.
|
|
964
966
|
/// @param terminalToken The terminal token to suck.
|
|
965
967
|
/// @param terminalTokenAmount The amount of terminal tokens.
|
|
966
968
|
/// @param projectTokenAmount The amount of project tokens.
|
|
@@ -1204,10 +1206,8 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1204
1206
|
}
|
|
1205
1207
|
|
|
1206
1208
|
/// @notice Send the outbox root for the specified token to the remote peer.
|
|
1207
|
-
/// @dev
|
|
1208
|
-
///
|
|
1209
|
-
/// @param transportPayment the amount of `msg.value` that is going to get paid for sending this message. (usually
|
|
1210
|
-
/// derived from `msg.value`)
|
|
1209
|
+
/// @dev Some bridges require a nonzero `transportPayment`; zero-cost bridges must reject nonzero values.
|
|
1210
|
+
/// @param transportPayment The amount of `msg.value` paid to the transport for this message.
|
|
1211
1211
|
/// @param token The terminal token to bridge the merkle tree of.
|
|
1212
1212
|
/// @param remoteToken The remote token which the `token` is mapped to.
|
|
1213
1213
|
function _sendRoot(uint256 transportPayment, address token, JBRemoteToken memory remoteToken) internal virtual {
|
|
@@ -1324,16 +1324,21 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1324
1324
|
// Register the leaf as executed to prevent double-spending.
|
|
1325
1325
|
_executedFor[terminalToken].set(index);
|
|
1326
1326
|
|
|
1327
|
-
//
|
|
1328
|
-
//
|
|
1329
|
-
_validateBranchRoot
|
|
1330
|
-
|
|
1327
|
+
// Compute the leaf hash once. It's used twice: stored in `executedLeafHashOf` (so beneficiary contracts
|
|
1328
|
+
// can authenticate post-hoc settlement when their `claim()` was front-run) and passed to
|
|
1329
|
+
// `_validateBranchRoot` for merkle verification. The bare executed bitmap proves "some leaf at index I
|
|
1330
|
+
// was executed" but not "which leaf"; storing the hash binds the index to the actual leaf content.
|
|
1331
|
+
bytes32 leafHash = _buildTreeHash({
|
|
1331
1332
|
projectTokenCount: projectTokenCount,
|
|
1332
1333
|
terminalTokenAmount: terminalTokenAmount,
|
|
1333
1334
|
beneficiary: beneficiary,
|
|
1334
|
-
metadata: metadata
|
|
1335
|
-
|
|
1336
|
-
|
|
1335
|
+
metadata: metadata
|
|
1336
|
+
});
|
|
1337
|
+
executedLeafHashOf[terminalToken][index] = leafHash;
|
|
1338
|
+
|
|
1339
|
+
// Calculate the root and compare it to the current inbox root.
|
|
1340
|
+
_validateBranchRoot({
|
|
1341
|
+
expectedRoot: _inboxOf[terminalToken].root, leafHash: leafHash, index: index, leaves: leaves
|
|
1337
1342
|
});
|
|
1338
1343
|
}
|
|
1339
1344
|
|
|
@@ -1341,17 +1346,12 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1341
1346
|
/// @dev This is a virtual function to allow a tests to override the behavior, it should never be overwritten
|
|
1342
1347
|
/// otherwise.
|
|
1343
1348
|
/// @param expectedRoot The expected merkle root to validate against.
|
|
1344
|
-
/// @param
|
|
1345
|
-
/// @param terminalTokenAmount The amount of terminal tokens in the leaf.
|
|
1346
|
-
/// @param beneficiary The beneficiary address in the leaf (bytes32 for cross-VM compatibility).
|
|
1349
|
+
/// @param leafHash The precomputed leaf hash (`_buildTreeHash` output) for the leaf being validated.
|
|
1347
1350
|
/// @param index The index of the leaf in the merkle tree.
|
|
1348
1351
|
/// @param leaves The merkle branch proving the leaf's inclusion.
|
|
1349
1352
|
function _validateBranchRoot(
|
|
1350
1353
|
bytes32 expectedRoot,
|
|
1351
|
-
|
|
1352
|
-
uint256 terminalTokenAmount,
|
|
1353
|
-
bytes32 beneficiary,
|
|
1354
|
-
bytes32 metadata,
|
|
1354
|
+
bytes32 leafHash,
|
|
1355
1355
|
uint256 index,
|
|
1356
1356
|
bytes32[_TREE_DEPTH] calldata leaves
|
|
1357
1357
|
)
|
|
@@ -1360,18 +1360,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1360
1360
|
{
|
|
1361
1361
|
// Calculate the root based on the leaf, the branch, and the index.
|
|
1362
1362
|
// Delegates to JBSuckerLib (via DELEGATECALL) to keep MerkleLib.branchRoot bytecode out of each sucker.
|
|
1363
|
-
bytes32 root = JBSuckerLib.computeBranchRoot({
|
|
1364
|
-
item: _buildTreeHash({
|
|
1365
|
-
projectTokenCount: projectTokenCount,
|
|
1366
|
-
terminalTokenAmount: terminalTokenAmount,
|
|
1367
|
-
beneficiary: beneficiary,
|
|
1368
|
-
metadata: metadata
|
|
1369
|
-
}),
|
|
1370
|
-
branch: leaves,
|
|
1371
|
-
index: index
|
|
1372
|
-
});
|
|
1363
|
+
bytes32 root = JBSuckerLib.computeBranchRoot({item: leafHash, branch: leaves, index: index});
|
|
1373
1364
|
|
|
1374
|
-
//
|
|
1365
|
+
// Revert if the computed root does not match the expected inbox root.
|
|
1375
1366
|
if (root != expectedRoot) {
|
|
1376
1367
|
revert JBSucker_InvalidProof({root: root, inboxRoot: expectedRoot});
|
|
1377
1368
|
}
|
|
@@ -1453,14 +1444,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1453
1444
|
_executedFor[emergencyExitAddress].set(index);
|
|
1454
1445
|
}
|
|
1455
1446
|
|
|
1456
|
-
// Calculate the root
|
|
1457
|
-
// Compare to the current root, Revert if they do not match.
|
|
1447
|
+
// Calculate the root and compare it to the current outbox root.
|
|
1458
1448
|
_validateBranchRoot({
|
|
1459
1449
|
expectedRoot: _computeOutboxRoot(_outboxOf[terminalToken].tree),
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1450
|
+
leafHash: _buildTreeHash({
|
|
1451
|
+
projectTokenCount: projectTokenCount,
|
|
1452
|
+
terminalTokenAmount: terminalTokenAmount,
|
|
1453
|
+
beneficiary: beneficiary,
|
|
1454
|
+
metadata: metadata
|
|
1455
|
+
}),
|
|
1464
1456
|
index: index,
|
|
1465
1457
|
leaves: leaves
|
|
1466
1458
|
});
|
package/src/JBSuckerRegistry.sol
CHANGED
|
@@ -35,6 +35,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
35
35
|
error JBSuckerRegistry_InvalidDeployer(IJBSuckerDeployer deployer);
|
|
36
36
|
error JBSuckerRegistry_SuckerDoesNotBelongToProject(uint256 projectId, address sucker);
|
|
37
37
|
error JBSuckerRegistry_SuckerIsNotDeprecated(address sucker, JBSuckerState suckerState);
|
|
38
|
+
error JBSuckerRegistry_ZeroPeerChainId(address sucker);
|
|
38
39
|
|
|
39
40
|
//*********************************************************************//
|
|
40
41
|
// ------------------------- public constants ------------------------ //
|
|
@@ -108,6 +109,18 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
108
109
|
// ------------------------- external views -------------------------- //
|
|
109
110
|
//*********************************************************************//
|
|
110
111
|
|
|
112
|
+
/// @notice All suckers for a project, INCLUDING deprecated entries that are no longer listed in `suckersOf`.
|
|
113
|
+
/// @dev Used by consumers that need to detect "has any sucker ever peered to chain X?" — e.g. to prevent
|
|
114
|
+
/// premature burn of bridgeable credit by `JBReferralSplitHook.burnUnbridgeableCreditFor`. Returns every key
|
|
115
|
+
/// from `_suckersOf[projectId]` regardless of active/deprecated state. Order matches the underlying
|
|
116
|
+
/// `EnumerableMap` iteration order (insertion order, with swap-and-pop semantics on removal — which this
|
|
117
|
+
/// registry doesn't trigger, since deprecation transitions to `_SUCKER_DEPRECATED` rather than deleting).
|
|
118
|
+
/// @param projectId The ID of the project to get the suckers of.
|
|
119
|
+
/// @return suckers The addresses of every sucker ever registered for `projectId`.
|
|
120
|
+
function allSuckersOf(uint256 projectId) external view override returns (address[] memory suckers) {
|
|
121
|
+
return _suckersOf[projectId].keys();
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
/// @notice Whether the given address is a sucker (active or deprecated) that was deployed through this registry for
|
|
112
125
|
/// the specified project. Used by controllers to authorize mint calls from suckers.
|
|
113
126
|
/// @param projectId The ID of the project to check for.
|
|
@@ -142,8 +155,9 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
142
155
|
(, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
|
|
143
156
|
if (val == _SUCKER_EXISTS) {
|
|
144
157
|
IJBSucker sucker = IJBSucker(allSuckers[i]);
|
|
145
|
-
pairs[j] =
|
|
146
|
-
|
|
158
|
+
pairs[j] = JBSuckersPair({
|
|
159
|
+
local: address(sucker), remote: sucker.peer(), remoteChainId: _peerChainIdOf(sucker)
|
|
160
|
+
});
|
|
147
161
|
unchecked {
|
|
148
162
|
++j;
|
|
149
163
|
}
|
|
@@ -220,7 +234,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
220
234
|
try IJBSucker(allSuckers[i]).peerChainBalanceOf({decimals: decimals, currency: currency}) returns (
|
|
221
235
|
JBDenominatedAmount memory amt
|
|
222
236
|
) {
|
|
223
|
-
uint256 chainId = IJBSucker(allSuckers[i])
|
|
237
|
+
uint256 chainId = _peerChainIdOf(IJBSucker(allSuckers[i]));
|
|
224
238
|
scratch.chainCount = _recordPeerValue({
|
|
225
239
|
scratch: scratch,
|
|
226
240
|
chainId: chainId,
|
|
@@ -276,7 +290,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
276
290
|
try IJBSucker(allSuckers[i]).peerChainSurplusOf({decimals: decimals, currency: currency}) returns (
|
|
277
291
|
JBDenominatedAmount memory amt
|
|
278
292
|
) {
|
|
279
|
-
uint256 chainId = IJBSucker(allSuckers[i])
|
|
293
|
+
uint256 chainId = _peerChainIdOf(IJBSucker(allSuckers[i]));
|
|
280
294
|
scratch.chainCount = _recordPeerValue({
|
|
281
295
|
scratch: scratch,
|
|
282
296
|
chainId: chainId,
|
|
@@ -319,7 +333,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
319
333
|
// Include both active and deprecated suckers in aggregate economic views.
|
|
320
334
|
if (val == _SUCKER_EXISTS || val == _SUCKER_DEPRECATED) {
|
|
321
335
|
try IJBSucker(allSuckers[i]).peerChainTotalSupply() returns (uint256 supply) {
|
|
322
|
-
uint256 chainId = IJBSucker(allSuckers[i])
|
|
336
|
+
uint256 chainId = _peerChainIdOf(IJBSucker(allSuckers[i]));
|
|
323
337
|
scratch.chainCount = _recordPeerValue({
|
|
324
338
|
scratch: scratch,
|
|
325
339
|
chainId: chainId,
|
|
@@ -377,6 +391,14 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
377
391
|
scratch.hasActiveValue = new bool[](len);
|
|
378
392
|
}
|
|
379
393
|
|
|
394
|
+
/// @notice Reads a sucker's peer chain ID, reverting if the sucker cannot identify a real peer chain.
|
|
395
|
+
/// @param sucker The sucker to query.
|
|
396
|
+
/// @return chainId The non-zero peer chain ID.
|
|
397
|
+
function _peerChainIdOf(IJBSucker sucker) internal view returns (uint256 chainId) {
|
|
398
|
+
chainId = sucker.peerChainId();
|
|
399
|
+
if (chainId == 0) revert JBSuckerRegistry_ZeroPeerChainId({sucker: address(sucker)});
|
|
400
|
+
}
|
|
401
|
+
|
|
380
402
|
/// @notice Records a project-scoped peer-chain aggregate value.
|
|
381
403
|
/// @dev Callers pass scratch arrays sized from `_suckersOf[projectId].keys()`, so entries are already scoped to
|
|
382
404
|
/// the project being aggregated. For each peer chain, active suckers replace deprecated suckers; deprecated
|
|
@@ -544,6 +566,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
544
566
|
// Create the sucker.
|
|
545
567
|
IJBSucker sucker = configuration.deployer
|
|
546
568
|
.createForSender({localProjectId: projectId, salt: salt, peer: configuration.peer});
|
|
569
|
+
_peerChainIdOf(sucker);
|
|
547
570
|
suckers[i] = address(sucker);
|
|
548
571
|
|
|
549
572
|
// Store the sucker as being deployed for this project.
|
package/src/JBSwapCCIPSucker.sol
CHANGED
|
@@ -39,7 +39,9 @@ import {JBSwapPoolLib} from "./libraries/JBSwapPoolLib.sol";
|
|
|
39
39
|
|
|
40
40
|
// Local: structs (alphabetized).
|
|
41
41
|
import {JBClaim} from "./structs/JBClaim.sol";
|
|
42
|
+
import {JBConversionRate} from "./structs/JBConversionRate.sol";
|
|
42
43
|
import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
|
|
44
|
+
import {JBPendingSwap} from "./structs/JBPendingSwap.sol";
|
|
43
45
|
import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
|
|
44
46
|
|
|
45
47
|
/// @notice A `JBCCIPSucker` extension that swaps between local and bridge tokens using the best
|
|
@@ -71,7 +73,7 @@ import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
|
|
|
71
73
|
///
|
|
72
74
|
/// **Inbound swap resilience**: If a swap reverts during `ccipReceive` (due to insufficient
|
|
73
75
|
/// liquidity, stale TWAP observations, or extreme price impact), the CCIP message still succeeds.
|
|
74
|
-
/// The unswapped bridge tokens are stored in a `
|
|
76
|
+
/// The unswapped bridge tokens are stored in a `JBPendingSwap` and the merkle root is recorded
|
|
75
77
|
/// normally. Claims for the affected batch are gated until `retrySwap` is called successfully.
|
|
76
78
|
/// Anyone can call `retrySwap` once swap conditions improve.
|
|
77
79
|
///
|
|
@@ -88,6 +90,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
88
90
|
|
|
89
91
|
error JBSwapCCIPSucker_BatchNotReceived(uint64 nonce);
|
|
90
92
|
error JBSwapCCIPSucker_CallerNotPoolManager(address caller);
|
|
93
|
+
error JBSwapCCIPSucker_DuplicateBatch(uint64 nonce);
|
|
91
94
|
error JBSwapCCIPSucker_InvalidBridgeToken(address bridgeToken, address wrappedNativeToken);
|
|
92
95
|
error JBSwapCCIPSucker_NoPendingSwap(address localToken, uint64 nonce, bool retrySwapLocked);
|
|
93
96
|
error JBSwapCCIPSucker_OnlySelf(address caller, address expected);
|
|
@@ -135,31 +138,11 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
135
138
|
// ------------------- internal stored properties -------------------- //
|
|
136
139
|
//*********************************************************************//
|
|
137
140
|
|
|
138
|
-
/// @notice Bridge tokens from a failed inbound swap, stored for later retry via `retrySwap`.
|
|
139
|
-
/// @custom:member bridgeToken The bridge token received from CCIP.
|
|
140
|
-
/// @custom:member bridgeAmount Amount of bridge tokens to swap.
|
|
141
|
-
/// @custom:member leafTotal Original leaf-denomination total (for conversion rate).
|
|
142
|
-
struct PendingSwap {
|
|
143
|
-
address bridgeToken;
|
|
144
|
-
uint256 bridgeAmount;
|
|
145
|
-
uint256 leafTotal;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
141
|
/// @notice Pending (failed) inbound swaps, keyed by local token and batch nonce.
|
|
149
142
|
/// @dev Populated when `ccipReceive` swap fails; cleared when `retrySwap` succeeds.
|
|
150
143
|
/// @custom:param localToken The local token the swap targets.
|
|
151
144
|
/// @custom:param nonce The CCIP nonce identifying the batch.
|
|
152
|
-
mapping(address localToken => mapping(uint64 nonce =>
|
|
153
|
-
|
|
154
|
-
/// @notice Immutable conversion rate for one received root batch, keyed by nonce.
|
|
155
|
-
/// @dev Each batch stores its total leaf and local amounts. Individual claims compute their
|
|
156
|
-
/// scaled amount as `claimLeafAmount * localTotal / leafTotal` — no mutable state changes.
|
|
157
|
-
/// @custom:member leafTotal Total leaf-denomination (source chain) amount for this batch.
|
|
158
|
-
/// @custom:member localTotal Total local-denomination (after swap) amount for this batch.
|
|
159
|
-
struct ConversionRate {
|
|
160
|
-
uint256 leafTotal;
|
|
161
|
-
uint256 localTotal;
|
|
162
|
-
}
|
|
145
|
+
mapping(address localToken => mapping(uint64 nonce => JBPendingSwap)) public pendingSwapOf;
|
|
163
146
|
|
|
164
147
|
/// @notice End leaf index (exclusive) for each received root batch, keyed by token and nonce.
|
|
165
148
|
/// @custom:param token The local token address.
|
|
@@ -176,7 +159,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
176
159
|
/// @notice Conversion rate for each received root batch, keyed by token and nonce.
|
|
177
160
|
/// @custom:param token The local token address.
|
|
178
161
|
/// @custom:param nonce The CCIP nonce identifying the batch.
|
|
179
|
-
mapping(address token => mapping(uint64 nonce =>
|
|
162
|
+
mapping(address token => mapping(uint64 nonce => JBConversionRate)) internal _conversionRateOf;
|
|
180
163
|
|
|
181
164
|
/// @notice Count of populated batch nonces per token. Appended exactly once per batch in
|
|
182
165
|
/// `ccipReceive`, so it equals the number of received batches independent of CCIP ordering.
|
|
@@ -336,6 +319,19 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
336
319
|
}
|
|
337
320
|
}
|
|
338
321
|
|
|
322
|
+
// Detect an already-processed batch before the swap path. The inbox nonce alone cannot be used here:
|
|
323
|
+
// CCIP can deliver nonce 2 before nonce 1, and nonce 1 still needs its self-described batch metadata.
|
|
324
|
+
if (
|
|
325
|
+
_batchEndOf[localToken][nonce] != 0 || _conversionRateOf[localToken][nonce].leafTotal != 0
|
|
326
|
+
|| pendingSwapOf[localToken][nonce].bridgeAmount != 0
|
|
327
|
+
) {
|
|
328
|
+
if (deliveredAmount != 0) {
|
|
329
|
+
revert JBSwapCCIPSucker_DuplicateBatch({nonce: nonce});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
339
335
|
// After the validation block above, `deliveredToken != address(0)` iff a delivery was present,
|
|
340
336
|
// because the invariants ensure it equals `BRIDGE_TOKEN` (a non-zero ERC-20) whenever there is one.
|
|
341
337
|
if (deliveredToken != address(0)) {
|
|
@@ -378,61 +374,55 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
378
374
|
//
|
|
379
375
|
// Detect "already seen" without extra storage: a nonce has been processed if it has
|
|
380
376
|
// either a batch range (batchEnd > 0) or a conversion rate / pending swap recorded.
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
// batches than `uint64.max`, which the inbox can never produce.
|
|
405
|
-
uint64 priorCount = _populatedNonceCount[localToken];
|
|
406
|
-
_populatedNonceByIndex[localToken][priorCount] = nonce;
|
|
407
|
-
unchecked {
|
|
408
|
-
_populatedNonceCount[localToken] = priorCount + 1;
|
|
409
|
-
}
|
|
377
|
+
// Record the batch range so _findNonceForLeafIndex can resolve leaf ownership
|
|
378
|
+
// independently of nonce ordering. Each nonce is self-describing: [start, end).
|
|
379
|
+
if (batchEnd > 0) {
|
|
380
|
+
// Record this batch's half-open leaf range `[batchStart, batchEnd)`. Self-
|
|
381
|
+
// describing per-nonce — no implicit chain across nonces — so out-of-order
|
|
382
|
+
// delivery can still resolve a leaf to its batch.
|
|
383
|
+
_batchStartOf[localToken][nonce] = batchStart;
|
|
384
|
+
_batchEndOf[localToken][nonce] = batchEnd;
|
|
385
|
+
|
|
386
|
+
// Append `nonce` to the populated-nonce list for this token. The duplicate guard
|
|
387
|
+
// above fires at most once per (token, nonce), so each populated nonce is appended
|
|
388
|
+
// exactly once — the array stays duplicate-free without extra checks.
|
|
389
|
+
//
|
|
390
|
+
// Reading `_populatedNonceCount[localToken]` first into a local lets us write
|
|
391
|
+
// the new slot and the new count in a single read-modify-write pair (one
|
|
392
|
+
// SLOAD, two SSTOREs to distinct slots). The `unchecked` increment is safe:
|
|
393
|
+
// `priorCount` is bounded by the total number of populated nonces, which is
|
|
394
|
+
// upper-bounded by the CCIP nonce space (`uint64`) — overflow requires more
|
|
395
|
+
// batches than `uint64.max`, which the inbox can never produce.
|
|
396
|
+
uint64 priorCount = _populatedNonceCount[localToken];
|
|
397
|
+
_populatedNonceByIndex[localToken][priorCount] = nonce;
|
|
398
|
+
unchecked {
|
|
399
|
+
_populatedNonceCount[localToken] = priorCount + 1;
|
|
410
400
|
}
|
|
401
|
+
}
|
|
411
402
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
403
|
+
// Store pendingSwapOf for failed swaps now that nonce is validated.
|
|
404
|
+
if (swapFailed) {
|
|
405
|
+
pendingSwapOf[localToken][nonce] =
|
|
406
|
+
JBPendingSwap({bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal});
|
|
407
|
+
}
|
|
417
408
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
409
|
+
// Zero-output swap guard: When a swap succeeds but returns zero local tokens, the
|
|
410
|
+
// batch must NOT be marked claimable. Without this guard, `_addToBalance` would see
|
|
411
|
+
// `pendingSwapOf.bridgeAmount == 0` (no pending swap stored) and allow claims to
|
|
412
|
+
// proceed — minting the full bridged project-token amount while adding zero terminal
|
|
413
|
+
// backing, breaking cross-chain solvency.
|
|
414
|
+
//
|
|
415
|
+
// Route zero-output swaps into `pendingSwapOf` so the swap can be retried via
|
|
416
|
+
// `retrySwap` once pool conditions improve. Only store the conversion rate when
|
|
417
|
+
// the swap produced a positive local amount.
|
|
418
|
+
if (leafTotal > 0 && !swapFailed) {
|
|
419
|
+
if (localAmount == 0 && deliveredAmount > 0) {
|
|
420
|
+
pendingSwapOf[localToken][nonce] = JBPendingSwap({
|
|
421
|
+
bridgeToken: deliveredToken, bridgeAmount: deliveredAmount, leafTotal: leafTotal
|
|
422
|
+
});
|
|
423
|
+
} else {
|
|
424
|
+
_conversionRateOf[localToken][nonce] =
|
|
425
|
+
JBConversionRate({leafTotal: leafTotal, localTotal: localAmount});
|
|
436
426
|
}
|
|
437
427
|
}
|
|
438
428
|
} else {
|
|
@@ -497,7 +487,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
497
487
|
}
|
|
498
488
|
_retrySwapLocked = true;
|
|
499
489
|
|
|
500
|
-
|
|
490
|
+
JBPendingSwap memory pending = pendingSwapOf[localToken][nonce];
|
|
501
491
|
if (pending.bridgeAmount == 0) {
|
|
502
492
|
revert JBSwapCCIPSucker_NoPendingSwap({
|
|
503
493
|
localToken: localToken, nonce: nonce, retrySwapLocked: _retrySwapLocked
|
|
@@ -508,7 +498,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
508
498
|
_executeSwapOrRevert({tokenIn: pending.bridgeToken, tokenOut: localToken, amount: pending.bridgeAmount});
|
|
509
499
|
|
|
510
500
|
// Update the conversion rate so claims can proceed, then clear the pending swap.
|
|
511
|
-
_conversionRateOf[localToken][nonce] =
|
|
501
|
+
_conversionRateOf[localToken][nonce] = JBConversionRate({leafTotal: pending.leafTotal, localTotal: localAmount});
|
|
512
502
|
delete pendingSwapOf[localToken][nonce];
|
|
513
503
|
|
|
514
504
|
_retrySwapLocked = false;
|
|
@@ -564,7 +554,7 @@ contract JBSwapCCIPSucker is JBCCIPSucker, IUnlockCallback, IUniswapV3SwapCallba
|
|
|
564
554
|
if (pendingSwapOf[token][nonce].bridgeAmount > 0) {
|
|
565
555
|
revert JBSwapCCIPSucker_SwapPending({nonce: nonce});
|
|
566
556
|
}
|
|
567
|
-
|
|
557
|
+
JBConversionRate storage rate = _conversionRateOf[token][nonce];
|
|
568
558
|
if (rate.leafTotal > 0) {
|
|
569
559
|
amount = amount * rate.localTotal / rate.leafTotal;
|
|
570
560
|
}
|
|
@@ -108,6 +108,18 @@ interface IJBSucker is IERC165 {
|
|
|
108
108
|
/// @return The deployer address.
|
|
109
109
|
function deployer() external view returns (address);
|
|
110
110
|
|
|
111
|
+
/// @notice The keccak256 hash of the leaf data committed at execution time, for the leaf at the given
|
|
112
|
+
/// `(terminalToken, index)`. Returns `bytes32(0)` for unexecuted indices.
|
|
113
|
+
/// @dev Beneficiary contracts (e.g. `JBReferralSplitHook`) use this to authenticate post-hoc settlement when
|
|
114
|
+
/// their `claim()` call was front-run by a direct external caller — they re-derive the hash from the claim
|
|
115
|
+
/// data they hold and compare. The hash is computed via `_buildTreeHash(projectTokenCount,
|
|
116
|
+
/// terminalTokenAmount, beneficiary, metadata)` and is pre-image-resistant, so zero unambiguously means
|
|
117
|
+
/// "not executed".
|
|
118
|
+
/// @param token The terminal token whose tree contains the leaf.
|
|
119
|
+
/// @param index The index of the leaf in the inbox tree.
|
|
120
|
+
/// @return hash The committed leaf hash (or `bytes32(0)` if unexecuted).
|
|
121
|
+
function executedLeafHashOf(address token, uint256 index) external view returns (bytes32 hash);
|
|
122
|
+
|
|
111
123
|
/// @notice The inbox merkle tree root for a given token.
|
|
112
124
|
/// @param token The local terminal token.
|
|
113
125
|
/// @return The inbox tree root.
|
|
@@ -54,6 +54,14 @@ interface IJBSuckerRegistry {
|
|
|
54
54
|
/// @return The projects contract.
|
|
55
55
|
function PROJECTS() external view returns (IJBProjects);
|
|
56
56
|
|
|
57
|
+
/// @notice All suckers for a project, INCLUDING deprecated entries that are no longer listed in `suckersOf`.
|
|
58
|
+
/// @dev Used by consumers that need to detect "has any sucker ever peered to chain X?" — e.g. to prevent
|
|
59
|
+
/// premature burn of bridgeable credit by `JBReferralSplitHook.burnUnbridgeableCreditFor`. Returns every key
|
|
60
|
+
/// from `_suckersOf[projectId]` regardless of active/deprecated state.
|
|
61
|
+
/// @param projectId The ID of the project.
|
|
62
|
+
/// @return The addresses of every sucker ever registered for `projectId`.
|
|
63
|
+
function allSuckersOf(uint256 projectId) external view returns (address[] memory);
|
|
64
|
+
|
|
57
65
|
/// @notice Returns true if the specified sucker belongs to the specified project and was deployed through this
|
|
58
66
|
/// registry.
|
|
59
67
|
/// @param projectId The ID of the project to check for.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
/// @notice Immutable conversion rate for one received root batch, keyed by nonce.
|
|
5
|
+
/// @dev Each batch stores its total leaf and local amounts. Individual claims compute their scaled amount as
|
|
6
|
+
/// `claimLeafAmount * localTotal / leafTotal` — no mutable state changes.
|
|
7
|
+
/// @custom:member leafTotal Total leaf-denomination (source chain) amount for this batch.
|
|
8
|
+
/// @custom:member localTotal Total local-denomination (after swap) amount for this batch.
|
|
9
|
+
struct JBConversionRate {
|
|
10
|
+
uint256 leafTotal;
|
|
11
|
+
uint256 localTotal;
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
/// @notice Bridge tokens from a failed inbound swap, stored for later retry via `retrySwap`.
|
|
5
|
+
/// @custom:member bridgeToken The bridge token received from CCIP.
|
|
6
|
+
/// @custom:member bridgeAmount Amount of bridge tokens to swap.
|
|
7
|
+
/// @custom:member leafTotal Original leaf-denomination total (for conversion rate).
|
|
8
|
+
struct JBPendingSwap {
|
|
9
|
+
address bridgeToken;
|
|
10
|
+
uint256 bridgeAmount;
|
|
11
|
+
uint256 leafTotal;
|
|
12
|
+
}
|