@bananapus/suckers-v6 1.0.1 → 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 +17 -12
- package/src/JBSuckerRegistry.sol +316 -167
- package/src/interfaces/IJBSuckerRegistry.sol +70 -0
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ The main idea is not "bridge the token contract." The main idea is "bridge a Jui
|
|
|
37
37
|
| Contract | Role |
|
|
38
38
|
| --- | --- |
|
|
39
39
|
| `JBSucker` | Base bridge logic for prepare, relay, accounting sync, claim, token mapping, and lifecycle controls. |
|
|
40
|
-
| `JBSuckerRegistry` | Registry for per-project sucker deployments, deployer allowlists, and shared bridge fee settings. |
|
|
40
|
+
| `JBSuckerRegistry` | Registry for per-project sucker deployments, deployer and owner-gated token-pair allowlists, and shared bridge fee settings. |
|
|
41
41
|
| Chain-specific suckers | Transport-specific implementations for OP Stack, Arbitrum, CCIP, and related environments. |
|
|
42
42
|
|
|
43
43
|
## Mental model
|
|
@@ -66,6 +66,7 @@ That means every bridge path has two trust surfaces:
|
|
|
66
66
|
- do not reason about suckers as if they were generic ERC-20 bridges
|
|
67
67
|
- root ordering and message delivery semantics matter as much as proof format
|
|
68
68
|
- token mapping is part of the economic invariant
|
|
69
|
+
- native/native mappings and different-address local/remote token mappings must be approved by the registry owner for the specific `(localToken, remoteChainId, remoteToken)` route before a project can choose them; non-native same-address mappings and disabled mappings do not need owner approval
|
|
69
70
|
- peer contexts are merged only when they share both currency and decimals; same-currency contexts with different decimals are kept separate and valued independently at read time, never summed across precisions — and this merge applies per source chain, since each chain's record is stored and folded on its own
|
|
70
71
|
- accounting propagates as a gossip bundle: a sucker sends its own chain's record plus every peer-chain record the project knows (gathered through the registry), so one sync round from a hub propagates every chain's data to every spoke without a direct sucker between each pair
|
|
71
72
|
- `syncAccountingData` pays no registry `toRemoteFee`, but bridge-specific transport costs still apply and duplicate bundles can still consume bridge/indexer resources
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@ A sucker bridges a Juicebox project's token economy between two chains. Cashed-o
|
|
|
17
17
|
| `JBOptimismSucker` | OP Stack bridge transport. |
|
|
18
18
|
| `JBCCIPSucker` | Chainlink CCIP transport. |
|
|
19
19
|
| `JBBaseSucker` | Base/OP Stack transport. |
|
|
20
|
-
| `JBSuckerRegistry` | Project-to-sucker inventory, deployer allowlist, shared `toRemote` fee, deprecation removal, cross-chain surplus/supply aggregation. |
|
|
20
|
+
| `JBSuckerRegistry` | Project-to-sucker inventory, deployer allowlist, owner-gated token-pair allowlist, shared `toRemote` fee, deprecation removal, cross-chain surplus/supply aggregation. |
|
|
21
21
|
|
|
22
22
|
`IJBSucker` is the minimal interface; `IJBSuckerExtended` adds the deprecation, emergency-hatch, and retained-fee surface.
|
|
23
23
|
|
|
@@ -81,7 +81,7 @@ The same `JBChainAccounting[] accounts` bundle is also carried by the root messa
|
|
|
81
81
|
| `fromRemoteAccounting(JBAccountingSnapshot calldata snapshot)` | Authenticated receive path for an accounting-only gossip bundle. Stores the freshest record per source chain, without touching any token-local inbox root. |
|
|
82
82
|
| `claim(JBClaim calldata claimData)` | Claim bridged project tokens for the leaf's beneficiary by proving inclusion against the inbox root. |
|
|
83
83
|
| `claim(JBClaim[] calldata claims)` | Claim multiple leaves in one call. Each leaf is routed through an external `this.claim` sub-call, so one failing leaf emits `ClaimFailed` and is reverted in isolation while the rest of the batch proceeds; the failed leaf stays claimable later. |
|
|
84
|
-
| `mapToken(JBTokenMapping calldata map) payable` | Map a single local token to a remote token for bridging. Mappings are immutable once the outbox tree has entries (can only be disabled, not remapped). Requires `MAP_SUCKER_TOKEN` permission (initial mappings are applied at deploy under `DEPLOY_SUCKERS`). |
|
|
84
|
+
| `mapToken(JBTokenMapping calldata map) payable` | Map a single local token to a remote token for bridging. Mappings are immutable once the outbox tree has entries (can only be disabled, not remapped). Requires `MAP_SUCKER_TOKEN` permission (initial mappings are applied at deploy under `DEPLOY_SUCKERS`). Native/native mappings and different-address mappings must also be approved by the registry owner for this sucker's peer chain. |
|
|
85
85
|
| `mapTokens(JBTokenMapping[] calldata maps) payable` | Map multiple local tokens to remote tokens in one call. |
|
|
86
86
|
|
|
87
87
|
### JBSucker — deprecation & emergency (`IJBSuckerExtended`)
|
|
@@ -125,6 +125,10 @@ The same `JBChainAccounting[] accounts` bundle is also carried by the root messa
|
|
|
125
125
|
| `removeDeprecatedSucker(uint256 projectId, address sucker)` | Remove a fully deprecated sucker from a project's inventory. |
|
|
126
126
|
| `allowSuckerDeployer(address deployer)` / `allowSuckerDeployers(address[] calldata deployers)` | Add deployer(s) to the allowlist. Owner-only. |
|
|
127
127
|
| `removeSuckerDeployer(address deployer)` | Remove a deployer from the allowlist. Owner-only. |
|
|
128
|
+
| `allowTokenMapping(address localToken, uint256 remoteChainId, bytes32 remoteToken)` / `allowTokenMappings(address[] calldata localTokens, uint256[] calldata remoteChainIds, bytes32[] calldata remoteTokens)` | Add route-scoped approvals for native/native or different-address local/remote token mappings. Owner-only. |
|
|
129
|
+
| `removeTokenMapping(address localToken, uint256 remoteChainId, bytes32 remoteToken)` / `removeTokenMappings(address[] calldata localTokens, uint256[] calldata remoteChainIds, bytes32[] calldata remoteTokens)` | Remove route-scoped token-mapping approvals. Owner-only. |
|
|
130
|
+
| `requireTokenMappingAllowed(address localToken, uint256 remoteChainId, bytes32 remoteToken)` | Revert unless the mapping can be chosen. Disabled mappings and non-native same-address mappings pass directly; native/native and different-address mappings require route approval. |
|
|
131
|
+
| `tokenMappingIsAllowed(address localToken, uint256 remoteChainId, bytes32 remoteToken)` | Stored owner approval for an owner-gated token route. |
|
|
128
132
|
| `setToRemoteFee(uint256 fee)` | Set the ETH fee (wei) paid into the fee project on each `toRemote` call. Reverts if `fee > MAX_TO_REMOTE_FEE`. Owner-only. |
|
|
129
133
|
| `suckersOf(uint256 projectId)` | All active suckers for a project. |
|
|
130
134
|
| `allSuckersOf(uint256 projectId)` | Every sucker ever registered for a project, including deprecated ones. |
|
package/references/operations.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Configuration surface
|
|
4
4
|
|
|
5
|
-
- [`src/JBSuckerRegistry.sol`](../src/JBSuckerRegistry.sol) is the first stop for deployer allowlists, shared fees, project inventory, and deprecation helpers.
|
|
5
|
+
- [`src/JBSuckerRegistry.sol`](../src/JBSuckerRegistry.sol) is the first stop for deployer allowlists, owner-gated token-pair allowlists, shared fees, project inventory, and deprecation helpers.
|
|
6
6
|
- Transport-specific deployers in `src/deployers/` are where chain-specific constants and bridge addresses live.
|
|
7
7
|
- [`script/Deploy.s.sol`](../script/Deploy.s.sol) is where deployment-time environment wiring belongs.
|
|
8
8
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
- If you edit base sucker accounting, verify claim flow across at least one chain-specific implementation.
|
|
12
12
|
- If you edit token mapping logic, re-check the registry and deployer assumptions that feed it.
|
|
13
|
+
- If you edit owner-gated token mapping rules, verify native/native, different-address, non-native same-address, disabled, deploy-time, and route-scoped cases.
|
|
13
14
|
- If you edit token mapping semantics, verify that remapping is still impossible once outbox activity has made economic equivalence depend on permanence.
|
|
14
15
|
- If you edit deprecation or emergency paths, verify the intended operator workflow still works end to end.
|
|
15
16
|
- If you edit snapshot or claim-boundary logic, verify `numberOfClaimsSent`, peer snapshots, `syncAccountingData`, and emergency exit behavior together.
|
|
@@ -18,6 +19,7 @@
|
|
|
18
19
|
## Common failure modes
|
|
19
20
|
|
|
20
21
|
- Cross-chain issue is blamed on transport when the root or token mapping was wrong before message delivery.
|
|
22
|
+
- A token-pair approval is assumed to be global when it is actually scoped to one `(localToken, remoteChainId, remoteToken)` route.
|
|
21
23
|
- Registry configuration drifts from what a deployer or external operator expects.
|
|
22
24
|
- Emergency hatches or deprecation paths are stale because nobody exercises them until stress conditions arrive.
|
|
23
25
|
|
package/references/runtime.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Core roles
|
|
4
4
|
|
|
5
5
|
- [`src/JBSucker.sol`](../src/JBSucker.sol) owns the shared prepare, relay, claim, token-mapping, and lifecycle logic.
|
|
6
|
-
- [`src/JBSuckerRegistry.sol`](../src/JBSuckerRegistry.sol) owns project-to-sucker inventory, deployer allowlists, and shared remote-fee settings.
|
|
6
|
+
- [`src/JBSuckerRegistry.sol`](../src/JBSuckerRegistry.sol) owns project-to-sucker inventory, deployer allowlists, owner-gated token-pair allowlists, and shared remote-fee settings.
|
|
7
7
|
- Chain-specific sucker contracts such as [`src/JBArbitrumSucker.sol`](../src/JBArbitrumSucker.sol), [`src/JBOptimismSucker.sol`](../src/JBOptimismSucker.sol), [`src/JBCCIPSucker.sol`](../src/JBCCIPSucker.sol), and [`src/JBCeloSucker.sol`](../src/JBCeloSucker.sol) own transport-specific delivery and verification.
|
|
8
8
|
- Matching deployers under `src/deployers/` own clone and transport configuration.
|
|
9
9
|
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
## High-risk areas
|
|
19
19
|
|
|
20
|
-
- Token mapping: mapping mistakes break economic equivalence, not just UX.
|
|
20
|
+
- Token mapping: mapping mistakes break economic equivalence, not just UX. Native/native and different-address mappings require route-scoped registry-owner approval before a project can choose them.
|
|
21
21
|
- Root ordering and replay protection: message sequencing is part of correctness.
|
|
22
22
|
- Emergency and deprecation paths: these are operational safety surfaces that must remain reliable.
|
|
23
23
|
- Shared accounting vs transport logic: many incidents stem from confusing these layers.
|
package/src/JBSucker.sol
CHANGED
|
@@ -1307,6 +1307,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1307
1307
|
returns (uint256 transportPaymentSpent)
|
|
1308
1308
|
{
|
|
1309
1309
|
address token = map.localToken;
|
|
1310
|
+
bytes32 remoteToken = map.remoteToken;
|
|
1310
1311
|
JBRemoteToken memory currentMapping = _remoteTokenFor[token];
|
|
1311
1312
|
|
|
1312
1313
|
// Once the emergency hatch for a token is enabled it can't be disabled.
|
|
@@ -1328,12 +1329,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1328
1329
|
alsoGrantAccessIf: _msgSender() == address(REGISTRY)
|
|
1329
1330
|
});
|
|
1330
1331
|
|
|
1332
|
+
// The registry owner gates mappings that assert economic equivalence across distinct chain assets.
|
|
1333
|
+
REGISTRY.requireTokenMappingAllowed({localToken: token, remoteChainId: peerChainId(), remoteToken: remoteToken});
|
|
1334
|
+
|
|
1331
1335
|
// Make sure that the token does not get remapped to another remote token.
|
|
1332
1336
|
// As this would cause the funds for this token to be double spendable on the other side.
|
|
1333
1337
|
// It should not be possible to cause any issues even without this check
|
|
1334
1338
|
// a bridge *should* never accept such a request. This is mostly a sanity check.
|
|
1335
1339
|
if (
|
|
1336
|
-
currentMapping.addr != bytes32(0) && currentMapping.addr !=
|
|
1340
|
+
currentMapping.addr != bytes32(0) && currentMapping.addr != remoteToken && remoteToken != bytes32(0)
|
|
1337
1341
|
&& _outboxOf[token].tree.count != 0
|
|
1338
1342
|
) {
|
|
1339
1343
|
revert JBSucker_TokenAlreadyMapped({localToken: token, mappedTo: currentMapping.addr});
|
|
@@ -1342,10 +1346,10 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1342
1346
|
// A remote token can back only one local token's outbox in this sucker. Otherwise two independent source
|
|
1343
1347
|
// nonces would race into the same destination inbox key (`root.token`), making one token's root stale or
|
|
1344
1348
|
// overwriting the other. Other suckers have separate inbox/outbox storage and are unaffected.
|
|
1345
|
-
if (
|
|
1346
|
-
address mappedLocalToken = _localTokenForRemoteToken[
|
|
1349
|
+
if (remoteToken != bytes32(0)) {
|
|
1350
|
+
address mappedLocalToken = _localTokenForRemoteToken[remoteToken];
|
|
1347
1351
|
if (mappedLocalToken != address(0) && mappedLocalToken != token) {
|
|
1348
|
-
revert JBSucker_RemoteTokenAlreadyMapped({remoteToken:
|
|
1352
|
+
revert JBSucker_RemoteTokenAlreadyMapped({remoteToken: remoteToken, localToken: mappedLocalToken});
|
|
1349
1353
|
}
|
|
1350
1354
|
}
|
|
1351
1355
|
|
|
@@ -1359,7 +1363,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1359
1363
|
// created and the mapping cannot be changed to a different remote token. If tokens are stuck in this state
|
|
1360
1364
|
// (e.g., the bridge is non-functional), the project owner can call `enableEmergencyHatchFor` to allow
|
|
1361
1365
|
// local withdrawals via `exitThroughEmergencyHatch`.
|
|
1362
|
-
if (
|
|
1366
|
+
if (remoteToken == bytes32(0) && _outboxOf[token].numberOfClaimsSent != _outboxOf[token].tree.count) {
|
|
1363
1367
|
// Disable before external call to prevent reentrancy via prepare().
|
|
1364
1368
|
// _sendRoot uses the `currentMapping` parameter, not storage, so this is safe.
|
|
1365
1369
|
_remoteTokenFor[token].enabled = false;
|
|
@@ -1369,13 +1373,13 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1369
1373
|
|
|
1370
1374
|
// Update the reverse reservation if an unused local token is being remapped to a new remote token.
|
|
1371
1375
|
if (
|
|
1372
|
-
|
|
1376
|
+
remoteToken != bytes32(0) && currentMapping.addr != bytes32(0) && currentMapping.addr != remoteToken
|
|
1373
1377
|
&& _localTokenForRemoteToken[currentMapping.addr] == token
|
|
1374
1378
|
) {
|
|
1375
1379
|
delete _localTokenForRemoteToken[currentMapping.addr];
|
|
1376
1380
|
}
|
|
1377
1381
|
|
|
1378
|
-
|
|
1382
|
+
remoteToken = remoteToken == bytes32(0) ? currentMapping.addr : remoteToken;
|
|
1379
1383
|
if (remoteToken != bytes32(0)) _localTokenForRemoteToken[remoteToken] = token;
|
|
1380
1384
|
|
|
1381
1385
|
// Update the token mapping.
|
|
@@ -2022,17 +2026,18 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
2022
2026
|
/// @notice Allow sucker implementations to add/override mapping rules to suit their specific needs.
|
|
2023
2027
|
/// @param map The token mapping to validate.
|
|
2024
2028
|
function _validateTokenMapping(JBTokenMapping calldata map) internal pure virtual {
|
|
2025
|
-
bool isNative = map.localToken == JBConstants.NATIVE_TOKEN;
|
|
2026
|
-
|
|
2027
2029
|
// If the token being mapped is the native token, the `remoteToken` must also be the native token.
|
|
2028
2030
|
// The native token can also be mapped to the 0 address, which is used to disable native token bridging.
|
|
2029
|
-
if (
|
|
2030
|
-
|
|
2031
|
+
if (map.localToken == JBConstants.NATIVE_TOKEN) {
|
|
2032
|
+
if (map.remoteToken != _toBytes32(JBConstants.NATIVE_TOKEN) && map.remoteToken != bytes32(0)) {
|
|
2033
|
+
revert JBSucker_InvalidNativeRemoteAddress({remoteToken: map.remoteToken});
|
|
2034
|
+
}
|
|
2035
|
+
return;
|
|
2031
2036
|
}
|
|
2032
2037
|
|
|
2033
2038
|
// Enforce a reasonable minimum gas limit for bridging. A minimum which is too low could lead to the loss of
|
|
2034
2039
|
// funds.
|
|
2035
|
-
if (map.minGas < MESSENGER_ERC20_MIN_GAS_LIMIT
|
|
2040
|
+
if (map.minGas < MESSENGER_ERC20_MIN_GAS_LIMIT) {
|
|
2036
2041
|
revert JBSucker_BelowMinGas({minGas: map.minGas, minGasLimit: MESSENGER_ERC20_MIN_GAS_LIMIT});
|
|
2037
2042
|
}
|
|
2038
2043
|
}
|
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
|
}
|
|
@@ -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.
|
|
@@ -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.
|
|
@@ -35,6 +35,20 @@ interface IJBSuckerRegistry {
|
|
|
35
35
|
/// @param caller The address that removed the sucker.
|
|
36
36
|
event SuckerDeprecated(uint256 projectId, address sucker, address caller);
|
|
37
37
|
|
|
38
|
+
/// @notice Emitted when a local-to-remote token mapping is added to the allowlist.
|
|
39
|
+
/// @param localToken The local token address.
|
|
40
|
+
/// @param remoteChainId The ID of the remote chain.
|
|
41
|
+
/// @param remoteToken The remote token address encoded as bytes32.
|
|
42
|
+
/// @param caller The address that allowed the token mapping.
|
|
43
|
+
event TokenMappingAllowed(address localToken, uint256 remoteChainId, bytes32 remoteToken, address caller);
|
|
44
|
+
|
|
45
|
+
/// @notice Emitted when a local-to-remote token mapping is removed from the allowlist.
|
|
46
|
+
/// @param localToken The local token address.
|
|
47
|
+
/// @param remoteChainId The ID of the remote chain.
|
|
48
|
+
/// @param remoteToken The remote token address encoded as bytes32.
|
|
49
|
+
/// @param caller The address that removed the token mapping.
|
|
50
|
+
event TokenMappingRemoved(address localToken, uint256 remoteChainId, bytes32 remoteToken, address caller);
|
|
51
|
+
|
|
38
52
|
/// @notice Emitted when the toRemoteFee is changed.
|
|
39
53
|
/// @param oldFee The previous fee.
|
|
40
54
|
/// @param newFee The new fee.
|
|
@@ -90,6 +104,14 @@ interface IJBSuckerRegistry {
|
|
|
90
104
|
/// @return totalSupply The combined peer chain total supply.
|
|
91
105
|
function remoteTotalSupplyOf(uint256 projectId) external view returns (uint256 totalSupply);
|
|
92
106
|
|
|
107
|
+
/// @notice Reverts unless a token mapping can be chosen by a project.
|
|
108
|
+
/// @dev Disable mappings never require owner approval. Non-native same-address mappings pass through directly.
|
|
109
|
+
/// Native-to-native and differing-address mappings must be explicitly allowed.
|
|
110
|
+
/// @param localToken The local token address.
|
|
111
|
+
/// @param remoteChainId The ID of the remote chain.
|
|
112
|
+
/// @param remoteToken The remote token address encoded as bytes32.
|
|
113
|
+
function requireTokenMappingAllowed(address localToken, uint256 remoteChainId, bytes32 remoteToken) external view;
|
|
114
|
+
|
|
93
115
|
/// @notice Whether the specified sucker deployer is approved by this registry.
|
|
94
116
|
/// @param deployer The address of the deployer to check.
|
|
95
117
|
/// @return Whether the deployer is allowed.
|
|
@@ -105,6 +127,20 @@ interface IJBSuckerRegistry {
|
|
|
105
127
|
/// @return The addresses of the suckers.
|
|
106
128
|
function suckersOf(uint256 projectId) external view returns (address[] memory);
|
|
107
129
|
|
|
130
|
+
/// @notice Whether a local-to-remote token mapping is approved by this registry.
|
|
131
|
+
/// @param localToken The local token address.
|
|
132
|
+
/// @param remoteChainId The ID of the remote chain.
|
|
133
|
+
/// @param remoteToken The remote token address encoded as bytes32.
|
|
134
|
+
/// @return Whether the token mapping is allowed.
|
|
135
|
+
function tokenMappingIsAllowed(
|
|
136
|
+
address localToken,
|
|
137
|
+
uint256 remoteChainId,
|
|
138
|
+
bytes32 remoteToken
|
|
139
|
+
)
|
|
140
|
+
external
|
|
141
|
+
view
|
|
142
|
+
returns (bool);
|
|
143
|
+
|
|
108
144
|
/// @notice The ETH fee (in wei) paid into the fee project on each toRemote() call.
|
|
109
145
|
/// @return The current fee.
|
|
110
146
|
function toRemoteFee() external view returns (uint256);
|
|
@@ -153,6 +189,23 @@ interface IJBSuckerRegistry {
|
|
|
153
189
|
/// @param deployers The addresses of the deployers to allow.
|
|
154
190
|
function allowSuckerDeployers(address[] calldata deployers) external;
|
|
155
191
|
|
|
192
|
+
/// @notice Add a local-to-remote token mapping to the allowlist.
|
|
193
|
+
/// @param localToken The local token address.
|
|
194
|
+
/// @param remoteChainId The ID of the remote chain.
|
|
195
|
+
/// @param remoteToken The remote token address encoded as bytes32.
|
|
196
|
+
function allowTokenMapping(address localToken, uint256 remoteChainId, bytes32 remoteToken) external;
|
|
197
|
+
|
|
198
|
+
/// @notice Add multiple local-to-remote token mappings to the allowlist.
|
|
199
|
+
/// @param localTokens The local token addresses.
|
|
200
|
+
/// @param remoteChainIds The remote chain IDs.
|
|
201
|
+
/// @param remoteTokens The remote token addresses encoded as bytes32.
|
|
202
|
+
function allowTokenMappings(
|
|
203
|
+
address[] calldata localTokens,
|
|
204
|
+
uint256[] calldata remoteChainIds,
|
|
205
|
+
bytes32[] calldata remoteTokens
|
|
206
|
+
)
|
|
207
|
+
external;
|
|
208
|
+
|
|
156
209
|
/// @notice Deploy one or more suckers for the specified project.
|
|
157
210
|
/// @dev This call also applies each configuration's token mappings on the deployed suckers. `DEPLOY_SUCKERS`
|
|
158
211
|
/// authorizes those initial mappings; use `MAP_SUCKER_TOKEN` for post-deployment mapping changes.
|
|
@@ -177,6 +230,23 @@ interface IJBSuckerRegistry {
|
|
|
177
230
|
/// @param deployer The address of the deployer to remove.
|
|
178
231
|
function removeSuckerDeployer(address deployer) external;
|
|
179
232
|
|
|
233
|
+
/// @notice Remove a local-to-remote token mapping from the allowlist.
|
|
234
|
+
/// @param localToken The local token address.
|
|
235
|
+
/// @param remoteChainId The ID of the remote chain.
|
|
236
|
+
/// @param remoteToken The remote token address encoded as bytes32.
|
|
237
|
+
function removeTokenMapping(address localToken, uint256 remoteChainId, bytes32 remoteToken) external;
|
|
238
|
+
|
|
239
|
+
/// @notice Remove multiple local-to-remote token mappings from the allowlist.
|
|
240
|
+
/// @param localTokens The local token addresses.
|
|
241
|
+
/// @param remoteChainIds The remote chain IDs.
|
|
242
|
+
/// @param remoteTokens The remote token addresses encoded as bytes32.
|
|
243
|
+
function removeTokenMappings(
|
|
244
|
+
address[] calldata localTokens,
|
|
245
|
+
uint256[] calldata remoteChainIds,
|
|
246
|
+
bytes32[] calldata remoteTokens
|
|
247
|
+
)
|
|
248
|
+
external;
|
|
249
|
+
|
|
180
250
|
/// @notice Set the ETH fee (in wei) paid on each toRemote() call. Owner only.
|
|
181
251
|
/// @param fee The new fee amount in wei.
|
|
182
252
|
function setToRemoteFee(uint256 fee) external;
|