@bananapus/suckers-v6 1.0.0 → 1.0.2
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 +2 -1
- package/package.json +1 -1
- package/references/entrypoints.md +6 -2
- package/references/operations.md +3 -1
- package/references/runtime.md +2 -2
- package/src/JBSucker.sol +42 -51
- package/src/JBSuckerRegistry.sol +319 -170
- package/src/interfaces/IJBSucker.sol +8 -5
- package/src/interfaces/IJBSuckerRegistry.sol +70 -0
- package/src/libraries/JBSuckerLib.sol +16 -15
- package/src/structs/JBChainAccounting.sol +13 -14
- package/src/structs/JBPeerChainContext.sol +4 -4
- package/src/structs/JBSourceContext.sol +7 -10
package/src/JBSuckerRegistry.sol
CHANGED
|
@@ -6,6 +6,7 @@ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
|
6
6
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
7
7
|
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
8
8
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
9
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
9
10
|
import {JBFixedPointNumber} from "@bananapus/core-v6/src/libraries/JBFixedPointNumber.sol";
|
|
10
11
|
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
11
12
|
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
|
|
@@ -14,13 +15,13 @@ import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
|
14
15
|
import {EnumerableMap} from "@openzeppelin/contracts/utils/structs/EnumerableMap.sol";
|
|
15
16
|
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
16
17
|
|
|
17
|
-
import {JBChainAccounting} from "./structs/JBChainAccounting.sol";
|
|
18
|
-
import {JBPeerChainContext} from "./structs/JBPeerChainContext.sol";
|
|
19
|
-
import {JBPeerChainValue} from "./structs/JBPeerChainValue.sol";
|
|
20
18
|
import {JBSuckerState} from "./enums/JBSuckerState.sol";
|
|
21
19
|
import {IJBSucker} from "./interfaces/IJBSucker.sol";
|
|
22
20
|
import {IJBSuckerDeployer} from "./interfaces/IJBSuckerDeployer.sol";
|
|
23
21
|
import {IJBSuckerRegistry} from "./interfaces/IJBSuckerRegistry.sol";
|
|
22
|
+
import {JBChainAccounting} from "./structs/JBChainAccounting.sol";
|
|
23
|
+
import {JBPeerChainContext} from "./structs/JBPeerChainContext.sol";
|
|
24
|
+
import {JBPeerChainValue} from "./structs/JBPeerChainValue.sol";
|
|
24
25
|
import {JBSuckerDeployerConfig} from "./structs/JBSuckerDeployerConfig.sol";
|
|
25
26
|
import {JBSuckersPair} from "./structs/JBSuckersPair.sol";
|
|
26
27
|
import {PeerAccountScratch} from "./structs/PeerAccountScratch.sol";
|
|
@@ -50,6 +51,14 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
50
51
|
/// @notice Thrown when a sucker is being removed from active listings but is not deprecated.
|
|
51
52
|
error JBSuckerRegistry_SuckerIsNotDeprecated(address sucker, JBSuckerState suckerState);
|
|
52
53
|
|
|
54
|
+
/// @notice Thrown when token mapping arrays have different lengths.
|
|
55
|
+
error JBSuckerRegistry_TokenMappingLengthMismatch(
|
|
56
|
+
uint256 localTokenCount, uint256 remoteChainIdCount, uint256 remoteTokenCount
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
/// @notice Thrown when a project chooses an owner-gated token mapping that has not been approved.
|
|
60
|
+
error JBSuckerRegistry_TokenMappingNotAllowed(address localToken, uint256 remoteChainId, bytes32 remoteToken);
|
|
61
|
+
|
|
53
62
|
/// @notice Thrown when a sucker reports a zero peer chain ID and cannot identify a real peer chain.
|
|
54
63
|
error JBSuckerRegistry_ZeroPeerChainId(address sucker);
|
|
55
64
|
|
|
@@ -79,10 +88,10 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
79
88
|
// --------------- public immutable stored properties ---------------- //
|
|
80
89
|
//*********************************************************************//
|
|
81
90
|
|
|
82
|
-
/// @notice The Juicebox directory
|
|
91
|
+
/// @notice The Juicebox directory for project terminals and controllers.
|
|
83
92
|
IJBDirectory public immutable override DIRECTORY;
|
|
84
93
|
|
|
85
|
-
/// @notice The prices contract
|
|
94
|
+
/// @notice The prices contract for valuing remote per-context surplus and balance into a requested currency,
|
|
86
95
|
/// exactly as the terminal store values local surplus.
|
|
87
96
|
IJBPrices public immutable PRICES;
|
|
88
97
|
|
|
@@ -97,6 +106,15 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
97
106
|
/// @custom:param deployer The address of the deployer to check.
|
|
98
107
|
mapping(address deployer => bool) public override suckerDeployerIsAllowed;
|
|
99
108
|
|
|
109
|
+
/// @notice Tracks whether projects may choose an owner-gated token mapping.
|
|
110
|
+
/// @dev Disable mappings and non-native same-address mappings pass through the mapping checks directly.
|
|
111
|
+
/// @custom:param localToken The local token address.
|
|
112
|
+
/// @custom:param remoteChainId The ID of the remote chain.
|
|
113
|
+
/// @custom:param remoteToken The remote token address encoded as bytes32.
|
|
114
|
+
mapping(address localToken => mapping(uint256 remoteChainId => mapping(bytes32 remoteToken => bool)))
|
|
115
|
+
public
|
|
116
|
+
override tokenMappingIsAllowed;
|
|
117
|
+
|
|
100
118
|
/// @notice The ETH fee (in wei) paid into the fee project via terminal.pay() on each toRemote() call.
|
|
101
119
|
uint256 public override toRemoteFee;
|
|
102
120
|
|
|
@@ -114,7 +132,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
114
132
|
|
|
115
133
|
/// @param directory The juicebox directory.
|
|
116
134
|
/// @param permissions A contract storing permissions.
|
|
117
|
-
/// @param prices The prices contract
|
|
135
|
+
/// @param prices The prices contract for valuing remote per-context surplus/balance into a requested currency.
|
|
118
136
|
/// @param initialOwner The initial owner of this contract.
|
|
119
137
|
constructor(
|
|
120
138
|
IJBDirectory directory,
|
|
@@ -216,108 +234,8 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
216
234
|
}
|
|
217
235
|
}
|
|
218
236
|
|
|
219
|
-
/// @notice Values one peer chain's raw balance held by one sucker into a currency, with peer chain ID and
|
|
220
|
-
/// freshness.
|
|
221
|
-
/// @dev Exposed as an external self-call boundary so `totalRemoteBalanceOf` can `try` it and drop a single
|
|
222
|
-
/// (sucker, chain) whose price feed is missing without losing that sucker's other chains. A context whose currency
|
|
223
|
-
/// already matches `currency` folds in at par (no feed read); a missing cross-currency feed reverts, and the
|
|
224
|
-
/// aggregator catches it and skips just this (sucker, chain).
|
|
225
|
-
/// @param sucker The sucker to read.
|
|
226
|
-
/// @param chainId The peer chain to read.
|
|
227
|
-
/// @param projectId The project whose price feeds to use.
|
|
228
|
-
/// @param currency The currency to value into.
|
|
229
|
-
/// @param decimals The decimal precision for the returned value.
|
|
230
|
-
/// @return A `JBPeerChainValue` with the valued balance, the peer chain ID, and its snapshot freshness key.
|
|
231
|
-
function remoteBalanceOf(
|
|
232
|
-
address sucker,
|
|
233
|
-
uint256 chainId,
|
|
234
|
-
uint256 projectId,
|
|
235
|
-
uint256 currency,
|
|
236
|
-
uint256 decimals
|
|
237
|
-
)
|
|
238
|
-
external
|
|
239
|
-
view
|
|
240
|
-
returns (JBPeerChainValue memory)
|
|
241
|
-
{
|
|
242
|
-
// Read this sucker's raw contexts for the chain: one per distinct local currency, plus the freshness key.
|
|
243
|
-
(JBPeerChainContext[] memory contexts, uint256 snapshot) = IJBSucker(sucker).peerChainContextsOf(chainId);
|
|
244
|
-
|
|
245
|
-
// Value each context's balance out of the currency and decimals it was recorded in, into the requested
|
|
246
|
-
// `currency` and `decimals`, and sum across every context. A context already denominated in `currency` folds
|
|
247
|
-
// in at par; a cross-currency context is converted through the project's price feed.
|
|
248
|
-
uint256 value;
|
|
249
|
-
uint256 numContexts = contexts.length;
|
|
250
|
-
for (uint256 i; i < numContexts;) {
|
|
251
|
-
value += _valued({
|
|
252
|
-
amount: contexts[i].balance,
|
|
253
|
-
fromCurrency: contexts[i].currency,
|
|
254
|
-
fromDecimals: contexts[i].decimals,
|
|
255
|
-
toCurrency: currency,
|
|
256
|
-
toDecimals: decimals,
|
|
257
|
-
projectId: projectId
|
|
258
|
-
});
|
|
259
|
-
unchecked {
|
|
260
|
-
++i;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Carry the peer chain ID and snapshot freshness alongside the summed value so the aggregator can deduplicate
|
|
265
|
-
// peers and keep only the freshest snapshot per chain.
|
|
266
|
-
return JBPeerChainValue({value: value, peerChainId: chainId, snapshotTimestamp: snapshot});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/// @notice Values one peer chain's raw surplus held by one sucker into a currency, with peer chain ID and
|
|
270
|
-
/// freshness.
|
|
271
|
-
/// @dev Exposed as an external self-call boundary so `totalRemoteSurplusOf` can `try` it and drop a single
|
|
272
|
-
/// (sucker, chain) whose price feed is missing without losing that sucker's other chains. A context whose currency
|
|
273
|
-
/// already matches `currency` folds in at par (no feed read); a missing cross-currency feed reverts, and the
|
|
274
|
-
/// aggregator catches it and skips just this (sucker, chain).
|
|
275
|
-
/// @param sucker The sucker to read.
|
|
276
|
-
/// @param chainId The peer chain to read.
|
|
277
|
-
/// @param projectId The project whose price feeds to use.
|
|
278
|
-
/// @param currency The currency to value into.
|
|
279
|
-
/// @param decimals The decimal precision for the returned value.
|
|
280
|
-
/// @return A `JBPeerChainValue` with the valued surplus, the peer chain ID, and its snapshot freshness key.
|
|
281
|
-
function remoteSurplusOf(
|
|
282
|
-
address sucker,
|
|
283
|
-
uint256 chainId,
|
|
284
|
-
uint256 projectId,
|
|
285
|
-
uint256 currency,
|
|
286
|
-
uint256 decimals
|
|
287
|
-
)
|
|
288
|
-
external
|
|
289
|
-
view
|
|
290
|
-
returns (JBPeerChainValue memory)
|
|
291
|
-
{
|
|
292
|
-
// Read this sucker's raw contexts for the chain: one per distinct local currency, plus the freshness key.
|
|
293
|
-
(JBPeerChainContext[] memory contexts, uint256 snapshot) = IJBSucker(sucker).peerChainContextsOf(chainId);
|
|
294
|
-
|
|
295
|
-
// Value each context's surplus out of the currency and decimals it was recorded in, into the requested
|
|
296
|
-
// `currency` and `decimals`, and sum across every context. A context already denominated in `currency` folds
|
|
297
|
-
// in at par; a cross-currency context is converted through the project's price feed.
|
|
298
|
-
uint256 value;
|
|
299
|
-
uint256 numContexts = contexts.length;
|
|
300
|
-
for (uint256 i; i < numContexts;) {
|
|
301
|
-
value += _valued({
|
|
302
|
-
amount: contexts[i].surplus,
|
|
303
|
-
fromCurrency: contexts[i].currency,
|
|
304
|
-
fromDecimals: contexts[i].decimals,
|
|
305
|
-
toCurrency: currency,
|
|
306
|
-
toDecimals: decimals,
|
|
307
|
-
projectId: projectId
|
|
308
|
-
});
|
|
309
|
-
unchecked {
|
|
310
|
-
++i;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Carry the peer chain ID and snapshot freshness alongside the summed value so the aggregator can deduplicate
|
|
315
|
-
// peers and keep only the freshest snapshot per chain.
|
|
316
|
-
return JBPeerChainValue({value: value, peerChainId: chainId, snapshotTimestamp: snapshot});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
237
|
/// @notice The cumulative total supply across all remote peer chains for a project.
|
|
320
|
-
/// @dev Each sucker
|
|
238
|
+
/// @dev Each sucker holds an accounting record per source chain it has heard about (its direct peer plus chains
|
|
321
239
|
/// gossiped through it), so this aggregates over every (sucker, chain) pair and dedups per chain. Includes
|
|
322
240
|
/// deprecated suckers only when no active sucker answers for the same peer chain, to prevent undercounting during
|
|
323
241
|
/// migration windows without letting stale deprecated records dominate live routes. Silently skips suckers and
|
|
@@ -367,6 +285,24 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
367
285
|
}
|
|
368
286
|
}
|
|
369
287
|
|
|
288
|
+
/// @notice Reverts unless a local-to-remote token mapping can be chosen by a project.
|
|
289
|
+
/// @dev Disable mappings never require owner approval. Non-native same-address mappings pass through directly.
|
|
290
|
+
/// Native-to-native and differing-address mappings must be explicitly allowed.
|
|
291
|
+
/// @param localToken The local token address.
|
|
292
|
+
/// @param remoteChainId The ID of the remote chain.
|
|
293
|
+
/// @param remoteToken The remote token address encoded as bytes32.
|
|
294
|
+
function requireTokenMappingAllowed(
|
|
295
|
+
address localToken,
|
|
296
|
+
uint256 remoteChainId,
|
|
297
|
+
bytes32 remoteToken
|
|
298
|
+
)
|
|
299
|
+
external
|
|
300
|
+
view
|
|
301
|
+
override
|
|
302
|
+
{
|
|
303
|
+
_requireTokenMappingAllowed({localToken: localToken, remoteChainId: remoteChainId, remoteToken: remoteToken});
|
|
304
|
+
}
|
|
305
|
+
|
|
370
306
|
/// @notice All active (non-deprecated) suckers for a project, with their remote peer address and chain ID.
|
|
371
307
|
/// @param projectId The ID of the project to get the suckers of.
|
|
372
308
|
/// @return pairs The pairs of suckers and their metadata.
|
|
@@ -489,9 +425,9 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
489
425
|
//*********************************************************************//
|
|
490
426
|
|
|
491
427
|
/// @notice Values every known peer chain held by one sucker and folds each into the per-chain dedup scratch.
|
|
492
|
-
/// @dev Each (sucker, chain) is valued
|
|
493
|
-
///
|
|
494
|
-
///
|
|
428
|
+
/// @dev Each (sucker, chain) is valued independently so a reverted sucker read or missing cross-currency price
|
|
429
|
+
/// feed drops only that one chain. Reads the sucker's chains itself, and is extracted from the aggregate view to
|
|
430
|
+
/// keep both stacks shallow.
|
|
495
431
|
/// @param scratch The per-chain dedup scratch to fold values into.
|
|
496
432
|
/// @param sucker The sucker whose chains to value.
|
|
497
433
|
/// @param isActive Whether the sucker is active (vs deprecated).
|
|
@@ -506,9 +442,8 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
506
442
|
view
|
|
507
443
|
{
|
|
508
444
|
uint256[] memory chainIds;
|
|
509
|
-
// Aggregate over
|
|
510
|
-
//
|
|
511
|
-
// reflects every chain the project knows, not only its direct bridges.
|
|
445
|
+
// Aggregate over direct peer chains and gossiped virtual chains so the result reflects every chain the project
|
|
446
|
+
// knows, not only its direct bridges.
|
|
512
447
|
try IJBSucker(sucker).peerChainIds(true) returns (uint256[] memory ids) {
|
|
513
448
|
chainIds = ids;
|
|
514
449
|
} catch {
|
|
@@ -517,37 +452,20 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
517
452
|
|
|
518
453
|
uint256 numChains = chainIds.length;
|
|
519
454
|
for (uint256 c; c < numChains;) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
scratch.chainCount = _recordPeerChainValue({
|
|
533
|
-
scratch: scratch, read: value, sucker: sucker, isActive: isActive
|
|
534
|
-
});
|
|
535
|
-
} catch {}
|
|
536
|
-
} else {
|
|
537
|
-
try this.remoteBalanceOf({
|
|
538
|
-
sucker: sucker,
|
|
539
|
-
chainId: chainIds[c],
|
|
540
|
-
projectId: params.projectId,
|
|
541
|
-
currency: params.currency,
|
|
542
|
-
decimals: params.decimals
|
|
543
|
-
}) returns (
|
|
544
|
-
JBPeerChainValue memory value
|
|
545
|
-
) {
|
|
546
|
-
scratch.chainCount = _recordPeerChainValue({
|
|
547
|
-
scratch: scratch, read: value, sucker: sucker, isActive: isActive
|
|
548
|
-
});
|
|
549
|
-
} catch {}
|
|
455
|
+
(bool ok, JBPeerChainValue memory value) = _remoteValueOf({
|
|
456
|
+
sucker: sucker,
|
|
457
|
+
chainId: chainIds[c],
|
|
458
|
+
projectId: params.projectId,
|
|
459
|
+
currency: params.currency,
|
|
460
|
+
decimals: params.decimals,
|
|
461
|
+
surplus: params.surplus
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
if (ok) {
|
|
465
|
+
scratch.chainCount =
|
|
466
|
+
_recordPeerChainValue({scratch: scratch, read: value, sucker: sucker, isActive: isActive});
|
|
550
467
|
}
|
|
468
|
+
|
|
551
469
|
unchecked {
|
|
552
470
|
++c;
|
|
553
471
|
}
|
|
@@ -610,7 +528,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
610
528
|
return ERC2771Context._contextSuffixLength();
|
|
611
529
|
}
|
|
612
530
|
|
|
613
|
-
/// @notice Reads one sucker's
|
|
531
|
+
/// @notice Reads one sucker's locally-keyed records and folds each into the per-chain gather scratch.
|
|
614
532
|
/// @dev Extracted from `peerChainAccountsOf` to keep its stack shallow. A sucker that reverts contributes nothing.
|
|
615
533
|
/// The destination chain, the local chain, and chain 0 are excluded.
|
|
616
534
|
/// @param scratch The per-chain gather scratch to fold records into.
|
|
@@ -693,7 +611,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
693
611
|
}
|
|
694
612
|
}
|
|
695
613
|
|
|
696
|
-
/// @notice Allocates scratch arrays
|
|
614
|
+
/// @notice Allocates scratch arrays for collapsing many suckers into one aggregate value per peer chain.
|
|
697
615
|
/// @dev `len` is the number of suckers being scanned, which is the maximum possible number of distinct peer
|
|
698
616
|
/// chains. `chainCount` starts at zero and is incremented as new peer chains are discovered.
|
|
699
617
|
/// @param len The maximum number of peer-chain entries the aggregation can need.
|
|
@@ -706,8 +624,8 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
706
624
|
scratch.hasActiveValue = new bool[](len);
|
|
707
625
|
}
|
|
708
626
|
|
|
709
|
-
/// @notice Records one source chain's
|
|
710
|
-
/// @dev Mirrors `_recordPeerValue`'s selection rule for
|
|
627
|
+
/// @notice Records one source chain's accounting record into a per-chain gather scratch, keeping the freshest.
|
|
628
|
+
/// @dev Mirrors `_recordPeerValue`'s selection rule for gathered records: an active sucker's record supersedes a
|
|
711
629
|
/// deprecated one's for the same chain; among same-state records the strictly-fresher timestamp wins; equal
|
|
712
630
|
/// freshness keeps the first writer, since records from one origin chain at one freshness key are identical. Used
|
|
713
631
|
/// to gather records for re-gossiping.
|
|
@@ -751,9 +669,8 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
751
669
|
|
|
752
670
|
/// @notice Records a combined peer-chain read (value, peer chain ID, snapshot freshness key) from one sucker.
|
|
753
671
|
/// @dev A wrapper over `_recordPeerValue` that unpacks the single-call `JBPeerChainValue` read and enforces the
|
|
754
|
-
/// same non-zero peer-chain requirement the registry applies everywhere else.
|
|
755
|
-
///
|
|
756
|
-
/// sucker reporting a zero peer chain ID fails the whole aggregate view.
|
|
672
|
+
/// same non-zero peer-chain requirement the registry applies everywhere else. A zero peer chain ID fails the whole
|
|
673
|
+
/// aggregate view because it makes the source chain unidentifiable.
|
|
757
674
|
/// @param scratch The per-chain aggregate values and freshness keys recorded so far.
|
|
758
675
|
/// @param read The combined value, peer chain ID, and snapshot freshness key returned by the sucker.
|
|
759
676
|
/// @param sucker The sucker the read came from, used only for the zero-peer-chain error.
|
|
@@ -847,18 +764,115 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
847
764
|
}
|
|
848
765
|
}
|
|
849
766
|
|
|
850
|
-
/// @notice Values
|
|
851
|
-
/// @dev
|
|
852
|
-
///
|
|
853
|
-
///
|
|
767
|
+
/// @notice Values one peer chain's raw balance or surplus held by one sucker into a currency.
|
|
768
|
+
/// @dev Returns `false` if the sucker read reverts or any cross-currency price read fails, allowing aggregate
|
|
769
|
+
/// views to skip only the affected (sucker, chain) record.
|
|
770
|
+
/// @param sucker The sucker to read.
|
|
771
|
+
/// @param chainId The peer chain to read.
|
|
772
|
+
/// @param projectId The project whose price feeds to use.
|
|
773
|
+
/// @param currency The currency to value into.
|
|
774
|
+
/// @param decimals The decimal precision for the returned value.
|
|
775
|
+
/// @param surplus Whether to value surplus (true) or balance (false).
|
|
776
|
+
/// @return ok Whether the peer-chain value was read and valued successfully.
|
|
777
|
+
/// @return read The peer-chain value, peer chain ID, and snapshot freshness key.
|
|
778
|
+
function _remoteValueOf(
|
|
779
|
+
address sucker,
|
|
780
|
+
uint256 chainId,
|
|
781
|
+
uint256 projectId,
|
|
782
|
+
uint256 currency,
|
|
783
|
+
uint256 decimals,
|
|
784
|
+
bool surplus
|
|
785
|
+
)
|
|
786
|
+
internal
|
|
787
|
+
view
|
|
788
|
+
returns (bool ok, JBPeerChainValue memory read)
|
|
789
|
+
{
|
|
790
|
+
JBPeerChainContext[] memory contexts;
|
|
791
|
+
uint256 snapshot;
|
|
792
|
+
|
|
793
|
+
// The sucker read is isolated so one reverting sucker/chain does not make the aggregate view unusable.
|
|
794
|
+
try IJBSucker(sucker).peerChainContextsOf(chainId) returns (
|
|
795
|
+
JBPeerChainContext[] memory readContexts, uint256 readSnapshot
|
|
796
|
+
) {
|
|
797
|
+
contexts = readContexts;
|
|
798
|
+
snapshot = readSnapshot;
|
|
799
|
+
} catch {
|
|
800
|
+
return (false, read);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
uint256 value;
|
|
804
|
+
uint256 numContexts = contexts.length;
|
|
805
|
+
for (uint256 i; i < numContexts;) {
|
|
806
|
+
// The same context set backs both aggregate views; select the requested side before pricing.
|
|
807
|
+
uint256 amount = surplus ? contexts[i].surplus : contexts[i].balance;
|
|
808
|
+
|
|
809
|
+
// If one context cannot be valued, the caller skips the whole (sucker, chain) record instead of returning a
|
|
810
|
+
// partially priced total.
|
|
811
|
+
(bool priced, uint256 valued) = _tryValued({
|
|
812
|
+
amount: amount,
|
|
813
|
+
fromCurrency: contexts[i].currency,
|
|
814
|
+
fromDecimals: contexts[i].decimals,
|
|
815
|
+
toCurrency: currency,
|
|
816
|
+
toDecimals: decimals,
|
|
817
|
+
projectId: projectId
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
if (!priced) return (false, read);
|
|
821
|
+
|
|
822
|
+
// Contexts have already been folded by currency/decimals on the sucker, so valued outputs share the
|
|
823
|
+
// requested units and can be added directly.
|
|
824
|
+
value += valued;
|
|
825
|
+
|
|
826
|
+
unchecked {
|
|
827
|
+
++i;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
read = JBPeerChainValue({value: value, peerChainId: chainId, snapshotTimestamp: snapshot});
|
|
832
|
+
ok = true;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/// @notice Reverts unless a local-to-remote token mapping is allowed.
|
|
836
|
+
/// @param localToken The local token address.
|
|
837
|
+
/// @param remoteChainId The ID of the remote chain.
|
|
838
|
+
/// @param remoteToken The remote token address encoded as bytes32.
|
|
839
|
+
function _requireTokenMappingAllowed(address localToken, uint256 remoteChainId, bytes32 remoteToken) internal view {
|
|
840
|
+
if (_tokenMappingPassesWithoutApproval({localToken: localToken, remoteToken: remoteToken})) return;
|
|
841
|
+
|
|
842
|
+
// All economically asserted mappings must be allowlisted by the registry owner before project owners choose
|
|
843
|
+
// them.
|
|
844
|
+
if (!tokenMappingIsAllowed[localToken][remoteChainId][remoteToken]) {
|
|
845
|
+
revert JBSuckerRegistry_TokenMappingNotAllowed({
|
|
846
|
+
localToken: localToken, remoteChainId: remoteChainId, remoteToken: remoteToken
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/// @notice Whether a local-to-remote token mapping can be chosen without owner approval.
|
|
852
|
+
/// @param localToken The local token address.
|
|
853
|
+
/// @param remoteToken The remote token address encoded as bytes32.
|
|
854
|
+
/// @return Whether the mapping passes without owner approval.
|
|
855
|
+
function _tokenMappingPassesWithoutApproval(address localToken, bytes32 remoteToken) internal pure returns (bool) {
|
|
856
|
+
// Disabling a mapping is a project-local safety action, not a claim about remote token equivalence.
|
|
857
|
+
if (remoteToken == bytes32(0)) return true;
|
|
858
|
+
|
|
859
|
+
// Non-native same-address mappings are unambiguous across EVM chains. Native tokens use the same sentinel
|
|
860
|
+
// even when the underlying assets differ, so native-to-native still needs registry owner approval.
|
|
861
|
+
return localToken != JBConstants.NATIVE_TOKEN && remoteToken == bytes32(uint256(uint160(localToken)));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/// @notice Tries to value an amount held in one currency/decimals into another.
|
|
865
|
+
/// @dev Returns `false` when the needed cross-currency price read reverts or returns zero. Same-currency and
|
|
866
|
+
/// zero-amount values never consult the price feed.
|
|
854
867
|
/// @param amount The raw amount in `fromCurrency`/`fromDecimals`.
|
|
855
868
|
/// @param fromCurrency The currency the amount is held in.
|
|
856
869
|
/// @param fromDecimals The decimals the amount is held in.
|
|
857
870
|
/// @param toCurrency The currency to value into.
|
|
858
871
|
/// @param toDecimals The decimals to value into.
|
|
859
872
|
/// @param projectId The project whose price feeds to use.
|
|
860
|
-
/// @return
|
|
861
|
-
|
|
873
|
+
/// @return ok Whether the amount was valued successfully.
|
|
874
|
+
/// @return converted The amount valued into `toCurrency`/`toDecimals`.
|
|
875
|
+
function _tryValued(
|
|
862
876
|
uint256 amount,
|
|
863
877
|
uint256 fromCurrency,
|
|
864
878
|
uint256 fromDecimals,
|
|
@@ -868,23 +882,34 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
868
882
|
)
|
|
869
883
|
internal
|
|
870
884
|
view
|
|
871
|
-
returns (uint256)
|
|
885
|
+
returns (bool ok, uint256 converted)
|
|
872
886
|
{
|
|
873
|
-
//
|
|
874
|
-
|
|
887
|
+
// Convert the raw context amount to the caller's requested decimals before any currency comparison, matching
|
|
888
|
+
// terminal-store valuation semantics.
|
|
889
|
+
converted = fromDecimals == toDecimals
|
|
875
890
|
? amount
|
|
876
891
|
: JBFixedPointNumber.adjustDecimals({value: amount, decimals: fromDecimals, targetDecimals: toDecimals});
|
|
877
892
|
|
|
878
|
-
//
|
|
879
|
-
//
|
|
880
|
-
if (
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
893
|
+
// Zero and same-currency values need no price feed. This is what lets same-asset remote accounting work even
|
|
894
|
+
// when a project has not configured a price feed.
|
|
895
|
+
if (converted == 0 || fromCurrency == toCurrency) return (true, converted);
|
|
896
|
+
|
|
897
|
+
// Cross-currency valuation is best-effort. A reverting or zero price makes the caller drop this (sucker, chain)
|
|
898
|
+
// record rather than mix priced and unpriced contexts.
|
|
899
|
+
try PRICES.pricePerUnitOf({
|
|
900
|
+
projectId: projectId, pricingCurrency: fromCurrency, unitCurrency: toCurrency, decimals: _PRICE_FIDELITY
|
|
901
|
+
}) returns (
|
|
902
|
+
uint256 price
|
|
903
|
+
) {
|
|
904
|
+
if (price == 0) return (false, 0);
|
|
905
|
+
|
|
906
|
+
// `pricePerUnitOf` returns the `fromCurrency` price of one `toCurrency`, so division expresses the source
|
|
907
|
+
// amount in `toCurrency`.
|
|
908
|
+
converted = mulDiv({x: converted, y: 10 ** _PRICE_FIDELITY, denominator: price});
|
|
909
|
+
ok = true;
|
|
910
|
+
} catch {
|
|
911
|
+
return (false, 0);
|
|
912
|
+
}
|
|
888
913
|
}
|
|
889
914
|
|
|
890
915
|
//*********************************************************************//
|
|
@@ -920,12 +945,74 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
920
945
|
}
|
|
921
946
|
}
|
|
922
947
|
|
|
948
|
+
/// @notice Adds a local-to-remote token mapping to the allowlist.
|
|
949
|
+
/// @dev Can only be called by this contract's owner (initially project ID 1, or JuiceboxDAO).
|
|
950
|
+
/// @param localToken The local token address.
|
|
951
|
+
/// @param remoteChainId The ID of the remote chain.
|
|
952
|
+
/// @param remoteToken The remote token address encoded as bytes32.
|
|
953
|
+
function allowTokenMapping(
|
|
954
|
+
address localToken,
|
|
955
|
+
uint256 remoteChainId,
|
|
956
|
+
bytes32 remoteToken
|
|
957
|
+
)
|
|
958
|
+
public
|
|
959
|
+
override
|
|
960
|
+
onlyOwner
|
|
961
|
+
{
|
|
962
|
+
tokenMappingIsAllowed[localToken][remoteChainId][remoteToken] = true;
|
|
963
|
+
emit TokenMappingAllowed({
|
|
964
|
+
localToken: localToken, remoteChainId: remoteChainId, remoteToken: remoteToken, caller: _msgSender()
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/// @notice Adds multiple local-to-remote token mappings to the allowlist.
|
|
969
|
+
/// @dev Can only be called by this contract's owner (initially project ID 1, or JuiceboxDAO).
|
|
970
|
+
/// @param localTokens The local token addresses.
|
|
971
|
+
/// @param remoteChainIds The remote chain IDs.
|
|
972
|
+
/// @param remoteTokens The remote token addresses encoded as bytes32.
|
|
973
|
+
function allowTokenMappings(
|
|
974
|
+
address[] calldata localTokens,
|
|
975
|
+
uint256[] calldata remoteChainIds,
|
|
976
|
+
bytes32[] calldata remoteTokens
|
|
977
|
+
)
|
|
978
|
+
public
|
|
979
|
+
override
|
|
980
|
+
onlyOwner
|
|
981
|
+
{
|
|
982
|
+
uint256 localTokenCount = localTokens.length;
|
|
983
|
+
if (localTokenCount != remoteChainIds.length || localTokenCount != remoteTokens.length) {
|
|
984
|
+
revert JBSuckerRegistry_TokenMappingLengthMismatch({
|
|
985
|
+
localTokenCount: localTokenCount,
|
|
986
|
+
remoteChainIdCount: remoteChainIds.length,
|
|
987
|
+
remoteTokenCount: remoteTokens.length
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Cache _msgSender() to avoid redundant calls in the loop.
|
|
992
|
+
address sender = _msgSender();
|
|
993
|
+
|
|
994
|
+
// Iterate over each parallel pair exactly once so a single admin transaction configures a full route set.
|
|
995
|
+
for (uint256 i; i < localTokenCount;) {
|
|
996
|
+
address localToken = localTokens[i];
|
|
997
|
+
uint256 remoteChainId = remoteChainIds[i];
|
|
998
|
+
bytes32 remoteToken = remoteTokens[i];
|
|
999
|
+
|
|
1000
|
+
tokenMappingIsAllowed[localToken][remoteChainId][remoteToken] = true;
|
|
1001
|
+
emit TokenMappingAllowed({
|
|
1002
|
+
localToken: localToken, remoteChainId: remoteChainId, remoteToken: remoteToken, caller: sender
|
|
1003
|
+
});
|
|
1004
|
+
unchecked {
|
|
1005
|
+
++i;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
923
1010
|
/// @notice Deploy one or more cross-chain suckers for a project in a single transaction. Each sucker is created via
|
|
924
1011
|
/// its deployer, registered in this registry, and immediately configured with its token mappings. Multiple suckers
|
|
925
1012
|
/// targeting the same peer chain are allowed for bridge resilience. The caller must have `DEPLOY_SUCKERS`
|
|
926
1013
|
/// permission, which also authorizes the initial token mappings in each deployment configuration.
|
|
927
1014
|
/// @param projectId The ID of the project to deploy suckers for.
|
|
928
|
-
/// @param salt The salt
|
|
1015
|
+
/// @param salt The deployment salt. For the suckers to be peers, this must be the same value on
|
|
929
1016
|
/// each chain where suckers are deployed.
|
|
930
1017
|
/// @param configurations The sucker deployer configs to use to deploy the suckers.
|
|
931
1018
|
/// @return suckers The addresses of the deployed suckers.
|
|
@@ -1030,6 +1117,68 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
1030
1117
|
emit SuckerDeployerRemoved({deployer: deployer, caller: _msgSender()});
|
|
1031
1118
|
}
|
|
1032
1119
|
|
|
1120
|
+
/// @notice Removes a local-to-remote token mapping from the allowlist.
|
|
1121
|
+
/// @dev Can only be called by this contract's owner (initially project ID 1, or JuiceboxDAO).
|
|
1122
|
+
/// @param localToken The local token address.
|
|
1123
|
+
/// @param remoteChainId The ID of the remote chain.
|
|
1124
|
+
/// @param remoteToken The remote token address encoded as bytes32.
|
|
1125
|
+
function removeTokenMapping(
|
|
1126
|
+
address localToken,
|
|
1127
|
+
uint256 remoteChainId,
|
|
1128
|
+
bytes32 remoteToken
|
|
1129
|
+
)
|
|
1130
|
+
public
|
|
1131
|
+
override
|
|
1132
|
+
onlyOwner
|
|
1133
|
+
{
|
|
1134
|
+
tokenMappingIsAllowed[localToken][remoteChainId][remoteToken] = false;
|
|
1135
|
+
emit TokenMappingRemoved({
|
|
1136
|
+
localToken: localToken, remoteChainId: remoteChainId, remoteToken: remoteToken, caller: _msgSender()
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/// @notice Removes multiple local-to-remote token mappings from the allowlist.
|
|
1141
|
+
/// @dev Can only be called by this contract's owner (initially project ID 1, or JuiceboxDAO).
|
|
1142
|
+
/// @param localTokens The local token addresses.
|
|
1143
|
+
/// @param remoteChainIds The remote chain IDs.
|
|
1144
|
+
/// @param remoteTokens The remote token addresses encoded as bytes32.
|
|
1145
|
+
function removeTokenMappings(
|
|
1146
|
+
address[] calldata localTokens,
|
|
1147
|
+
uint256[] calldata remoteChainIds,
|
|
1148
|
+
bytes32[] calldata remoteTokens
|
|
1149
|
+
)
|
|
1150
|
+
public
|
|
1151
|
+
override
|
|
1152
|
+
onlyOwner
|
|
1153
|
+
{
|
|
1154
|
+
uint256 localTokenCount = localTokens.length;
|
|
1155
|
+
if (localTokenCount != remoteChainIds.length || localTokenCount != remoteTokens.length) {
|
|
1156
|
+
revert JBSuckerRegistry_TokenMappingLengthMismatch({
|
|
1157
|
+
localTokenCount: localTokenCount,
|
|
1158
|
+
remoteChainIdCount: remoteChainIds.length,
|
|
1159
|
+
remoteTokenCount: remoteTokens.length
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Cache _msgSender() to avoid redundant calls in the loop.
|
|
1164
|
+
address sender = _msgSender();
|
|
1165
|
+
|
|
1166
|
+
// Iterate over each parallel pair exactly once so a single admin transaction can remove a route set.
|
|
1167
|
+
for (uint256 i; i < localTokenCount;) {
|
|
1168
|
+
address localToken = localTokens[i];
|
|
1169
|
+
uint256 remoteChainId = remoteChainIds[i];
|
|
1170
|
+
bytes32 remoteToken = remoteTokens[i];
|
|
1171
|
+
|
|
1172
|
+
tokenMappingIsAllowed[localToken][remoteChainId][remoteToken] = false;
|
|
1173
|
+
emit TokenMappingRemoved({
|
|
1174
|
+
localToken: localToken, remoteChainId: remoteChainId, remoteToken: remoteToken, caller: sender
|
|
1175
|
+
});
|
|
1176
|
+
unchecked {
|
|
1177
|
+
++i;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1033
1182
|
/// @notice Set the ETH fee (in wei) paid into the fee project on each toRemote() call.
|
|
1034
1183
|
/// @dev Only callable by the contract owner. Fee cannot exceed MAX_TO_REMOTE_FEE.
|
|
1035
1184
|
/// @param fee The new fee amount in wei.
|