@bananapus/suckers-v6 0.0.47 → 0.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/script/Deploy.s.sol +125 -139
- package/script/helpers/SuckerDeploymentLib.sol +10 -12
- package/src/JBArbitrumSucker.sol +5 -2
- package/src/JBCCIPSucker.sol +3 -3
- package/src/JBCeloSucker.sol +2 -0
- package/src/JBOptimismSucker.sol +2 -0
- package/src/JBSucker.sol +42 -8
- package/src/JBSuckerRegistry.sol +79 -60
- package/src/JBSwapCCIPSucker.sol +38 -101
- package/src/deployers/JBSwapCCIPSuckerDeployer.sol +17 -17
- package/src/interfaces/IJBSucker.sol +11 -6
- package/src/interfaces/IJBSuckerRegistry.sol +6 -3
- package/src/interfaces/IL1ArbitrumGateway.sol +10 -10
- package/src/libraries/CCIPHelper.sol +64 -64
- package/src/libraries/JBCCIPLib.sol +18 -0
- package/src/libraries/JBRelayBeneficiary.sol +1 -1
- package/src/libraries/JBSuckerLib.sol +157 -161
- package/src/libraries/JBSwapPoolLib.sol +268 -268
- package/src/structs/PeerValueScratch.sol +18 -0
- package/src/utils/MerkleLib.sol +108 -108
|
@@ -23,11 +23,10 @@ library SuckerDeploymentLib {
|
|
|
23
23
|
Vm internal constant vm = Vm(VM_ADDRESS);
|
|
24
24
|
|
|
25
25
|
function getDeployment(string memory path) internal returns (SuckerDeployment memory deployment) {
|
|
26
|
-
//
|
|
26
|
+
// Match the current chain ID to the Sphinx network name used in deployment artifacts.
|
|
27
27
|
uint256 chainId = block.chainid;
|
|
28
28
|
|
|
29
|
-
//
|
|
30
|
-
// TODO: get constants without deploy.
|
|
29
|
+
// `SphinxConstants` exposes Sphinx's supported chain ID to network name mapping.
|
|
31
30
|
SphinxConstants sphinxConstants = new SphinxConstants();
|
|
32
31
|
NetworkInfo[] memory networks = sphinxConstants.getNetworkInfoArray();
|
|
33
32
|
|
|
@@ -55,14 +54,13 @@ library SuckerDeploymentLib {
|
|
|
55
54
|
})
|
|
56
55
|
);
|
|
57
56
|
|
|
58
|
-
bytes32
|
|
59
|
-
bool
|
|
60
|
-
|
|
61
|
-
bool
|
|
62
|
-
bool
|
|
63
|
-
bool _isArb = _network == keccak256("arbitrum") || _network == keccak256("arbitrum_sepolia");
|
|
57
|
+
bytes32 networkHash = keccak256(abi.encodePacked(networkName));
|
|
58
|
+
bool isMainnet = networkHash == keccak256("ethereum") || networkHash == keccak256("ethereum_sepolia");
|
|
59
|
+
bool isOp = networkHash == keccak256("optimism") || networkHash == keccak256("optimism_sepolia");
|
|
60
|
+
bool isBase = networkHash == keccak256("base") || networkHash == keccak256("base_sepolia");
|
|
61
|
+
bool isArb = networkHash == keccak256("arbitrum") || networkHash == keccak256("arbitrum_sepolia");
|
|
64
62
|
|
|
65
|
-
if (
|
|
63
|
+
if (isMainnet || isOp) {
|
|
66
64
|
deployment.optimismDeployer = IJBSuckerDeployer(
|
|
67
65
|
_getDeploymentAddress({
|
|
68
66
|
path: path,
|
|
@@ -73,7 +71,7 @@ library SuckerDeploymentLib {
|
|
|
73
71
|
);
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
if (
|
|
74
|
+
if (isMainnet || isBase) {
|
|
77
75
|
deployment.baseDeployer = IJBSuckerDeployer(
|
|
78
76
|
_getDeploymentAddress({
|
|
79
77
|
path: path,
|
|
@@ -84,7 +82,7 @@ library SuckerDeploymentLib {
|
|
|
84
82
|
);
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
if (
|
|
85
|
+
if (isMainnet || isArb) {
|
|
88
86
|
deployment.arbitrumDeployer = IJBSuckerDeployer(
|
|
89
87
|
_getDeploymentAddress({
|
|
90
88
|
path: path,
|
package/src/JBArbitrumSucker.sol
CHANGED
|
@@ -255,16 +255,17 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
255
255
|
// If the token is an ERC-20, bridge it to the peer.
|
|
256
256
|
// If the amount is `0` then we do not need to bridge any ERC20.
|
|
257
257
|
if (token != JBConstants.NATIVE_TOKEN && amount != 0) {
|
|
258
|
+
address gateway;
|
|
258
259
|
uint256 tokenTransportCost;
|
|
259
260
|
uint256 maxSubmissionCostERC20;
|
|
260
261
|
{
|
|
261
262
|
// Get the exact calldata length the gateway will create for the retryable ticket.
|
|
262
263
|
// The Arbitrum Inbox validates maxSubmissionCost against this actual payload, not the user data.
|
|
263
|
-
|
|
264
|
+
gateway = GATEWAYROUTER.getGateway(token);
|
|
264
265
|
uint256 outboundCalldataLength =
|
|
265
266
|
IL1ArbitrumGateway(gateway)
|
|
266
267
|
.getOutboundCalldata({
|
|
267
|
-
|
|
268
|
+
token: token, from: address(this), to: _peerAddress(), amount: amount, data: bytes("")
|
|
268
269
|
}).length;
|
|
269
270
|
maxSubmissionCostERC20 = ARBINBOX.calculateRetryableSubmissionFee({
|
|
270
271
|
dataLength: outboundCalldataLength, baseFee: maxFeePerGas
|
|
@@ -299,6 +300,8 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
299
300
|
gasPriceBid: maxFeePerGas,
|
|
300
301
|
data: bytes(abi.encode(maxSubmissionCostERC20, bytes("")))
|
|
301
302
|
});
|
|
303
|
+
|
|
304
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: gateway, value: 0});
|
|
302
305
|
} else {
|
|
303
306
|
// Ensure we bridge enough for gas costs on L2 side
|
|
304
307
|
if (transportPayment < callTransportCost) {
|
package/src/JBCCIPSucker.sol
CHANGED
|
@@ -202,7 +202,7 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
202
202
|
/// @param token The token to bridge the outbox tree for.
|
|
203
203
|
/// @param amount The amount of tokens to bridge.
|
|
204
204
|
/// @param remoteToken Information about the remote token to bridge to.
|
|
205
|
-
/// @param
|
|
205
|
+
/// @param suckerMessage The message root to send to the remote peer.
|
|
206
206
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
207
207
|
function _sendRootOverAMB(
|
|
208
208
|
uint256 transportPayment,
|
|
@@ -210,7 +210,7 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
210
210
|
address token,
|
|
211
211
|
uint256 amount,
|
|
212
212
|
JBRemoteToken memory remoteToken,
|
|
213
|
-
JBMessageRoot memory
|
|
213
|
+
JBMessageRoot memory suckerMessage
|
|
214
214
|
)
|
|
215
215
|
internal
|
|
216
216
|
virtual
|
|
@@ -246,7 +246,7 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
246
246
|
feeToken: feeToken,
|
|
247
247
|
feeTokenPayer: feeToken != address(0) ? _msgSender() : address(0),
|
|
248
248
|
gasLimit: gasLimit,
|
|
249
|
-
encodedPayload: abi.encode(_CCIP_MSG_TYPE_ROOT, abi.encode(
|
|
249
|
+
encodedPayload: abi.encode(_CCIP_MSG_TYPE_ROOT, abi.encode(suckerMessage)),
|
|
250
250
|
tokenAmounts: tokenAmounts,
|
|
251
251
|
refundRecipient: _msgSender()
|
|
252
252
|
});
|
package/src/JBCeloSucker.sol
CHANGED
|
@@ -164,6 +164,8 @@ contract JBCeloSucker is JBOptimismSucker {
|
|
|
164
164
|
minGasLimit: remoteToken.minGas,
|
|
165
165
|
extraData: bytes("")
|
|
166
166
|
});
|
|
167
|
+
|
|
168
|
+
SafeERC20.forceApprove({token: IERC20(bridgeToken), spender: address(OPBRIDGE), value: 0});
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
// Send the messenger message with nativeValue = 0.
|
package/src/JBOptimismSucker.sol
CHANGED
|
@@ -126,6 +126,8 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
|
|
|
126
126
|
minGasLimit: remoteToken.minGas,
|
|
127
127
|
extraData: bytes("")
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
SafeERC20.forceApprove({token: IERC20(token), spender: address(OPBRIDGE), value: 0});
|
|
129
131
|
} else {
|
|
130
132
|
// Otherwise, the token is the native token, and the amount will be sent as `msg.value`.
|
|
131
133
|
nativeValue = amount;
|
package/src/JBSucker.sol
CHANGED
|
@@ -83,6 +83,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
83
83
|
error JBSucker_NoRetainedToRemoteFee(address account);
|
|
84
84
|
error JBSucker_NoRetainedTransportPaymentRefund(address account);
|
|
85
85
|
error JBSucker_RefundFailed(address beneficiary, uint256 amount);
|
|
86
|
+
error JBSucker_RemoteTokenAlreadyMapped(bytes32 remoteToken, address localToken);
|
|
86
87
|
error JBSucker_TokenAlreadyMapped(address localToken, bytes32 mappedTo);
|
|
87
88
|
error JBSucker_TokenHasInvalidEmergencyHatchState(address token);
|
|
88
89
|
error JBSucker_TokenNotMapped(address token);
|
|
@@ -175,6 +176,14 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
175
176
|
/// @custom:param token The local terminal token to get the inbox for.
|
|
176
177
|
mapping(address token => JBInboxTreeRoot root) internal _inboxOf;
|
|
177
178
|
|
|
179
|
+
/// @notice The local token that has reserved each remote token address in this sucker.
|
|
180
|
+
/// @dev Inbound roots are keyed by `root.token` on the destination chain. Within a single sucker, allowing two
|
|
181
|
+
/// local tokens to send roots to the same remote token would give them independent source nonces but one shared
|
|
182
|
+
/// destination inbox, causing stale rejections or root overwrites. Each sucker keeps its own reservation map, so
|
|
183
|
+
/// separate bridge lanes for the same asset pair can coexist.
|
|
184
|
+
/// @custom:param remoteToken The remote terminal token address encoded as bytes32.
|
|
185
|
+
mapping(bytes32 remoteToken => address localToken) internal _localTokenForRemoteToken;
|
|
186
|
+
|
|
178
187
|
/// @notice The outbox merkle tree for a given token.
|
|
179
188
|
/// @custom:param token The local terminal token to get the outbox for.
|
|
180
189
|
mapping(address token => JBOutboxTree) internal _outboxOf;
|
|
@@ -856,23 +865,23 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
856
865
|
}
|
|
857
866
|
|
|
858
867
|
/// @notice Initializes the sucker with the project ID.
|
|
859
|
-
/// @param
|
|
860
|
-
function initialize(uint256
|
|
861
|
-
_initialize({
|
|
868
|
+
/// @param initialProjectId The ID of the project (on the local chain) that this sucker is associated with.
|
|
869
|
+
function initialize(uint256 initialProjectId) public initializer {
|
|
870
|
+
_initialize({initialProjectId: initialProjectId, remotePeer: bytes32(0)});
|
|
862
871
|
}
|
|
863
872
|
|
|
864
873
|
/// @notice Initializes the sucker with the project ID and an explicit peer address.
|
|
865
874
|
/// @param localProjectId The ID of the project (on the local chain) that this sucker is associated with.
|
|
866
875
|
/// @param remotePeer The remote peer address. Leave zero to use the default deterministic same-address peer.
|
|
867
876
|
function initialize(uint256 localProjectId, bytes32 remotePeer) public initializer {
|
|
868
|
-
_initialize({
|
|
877
|
+
_initialize({initialProjectId: localProjectId, remotePeer: remotePeer});
|
|
869
878
|
}
|
|
870
879
|
|
|
871
880
|
/// @notice Initializes the sucker's project and optional peer address.
|
|
872
|
-
/// @param
|
|
881
|
+
/// @param initialProjectId The ID of the project (on the local chain) that this sucker is associated with.
|
|
873
882
|
/// @param remotePeer The remote peer address. Leave zero to use the default deterministic same-address peer.
|
|
874
|
-
function _initialize(uint256
|
|
875
|
-
_localProjectId =
|
|
883
|
+
function _initialize(uint256 initialProjectId, bytes32 remotePeer) internal {
|
|
884
|
+
_localProjectId = initialProjectId;
|
|
876
885
|
_peer = remotePeer;
|
|
877
886
|
deployer = _msgSender();
|
|
878
887
|
}
|
|
@@ -1031,6 +1040,10 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1031
1040
|
/// after outbox activity, the same local funds could be claimed against two different remote tokens. A
|
|
1032
1041
|
/// misconfigured mapping therefore requires deploying a new sucker. Re-enabling a previously disabled mapping
|
|
1033
1042
|
/// (back to the same remote token) is supported.
|
|
1043
|
+
/// @dev Remote tokens are also unique per local token within this sucker. The source side keeps separate
|
|
1044
|
+
/// outboxes/nonces per local token, but the destination side stores roots under the remote token address. Sharing
|
|
1045
|
+
/// one remote token across multiple local tokens in the same sucker would merge those inboxes on the destination
|
|
1046
|
+
/// chain. Separate suckers can still map the same local/remote token pair, letting users choose a bridge lane.
|
|
1034
1047
|
/// @param map The local and remote terminal token addresses to map, and minimum amount/gas limits for bridging
|
|
1035
1048
|
/// them.
|
|
1036
1049
|
/// @param transportPaymentValue The amount of `msg.value` to send for the token mapping.
|
|
@@ -1068,6 +1081,16 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1068
1081
|
revert JBSucker_TokenAlreadyMapped({localToken: token, mappedTo: currentMapping.addr});
|
|
1069
1082
|
}
|
|
1070
1083
|
|
|
1084
|
+
// A remote token can back only one local token's outbox in this sucker. Otherwise two independent source
|
|
1085
|
+
// nonces would race into the same destination inbox key (`root.token`), making one token's root stale or
|
|
1086
|
+
// overwriting the other. Other suckers have separate inbox/outbox storage and are unaffected.
|
|
1087
|
+
if (map.remoteToken != bytes32(0)) {
|
|
1088
|
+
address mappedLocalToken = _localTokenForRemoteToken[map.remoteToken];
|
|
1089
|
+
if (mappedLocalToken != address(0) && mappedLocalToken != token) {
|
|
1090
|
+
revert JBSucker_RemoteTokenAlreadyMapped({remoteToken: map.remoteToken, localToken: mappedLocalToken});
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1071
1094
|
// No inbox guard needed here. Token remapping only affects the outbound (sending) path —
|
|
1072
1095
|
// it changes where tokens get bridged TO. Existing inbox claims are resolved against the inbox merkle
|
|
1073
1096
|
// tree keyed by the local token address. Changing the remote token doesn't invalidate those claims
|
|
@@ -1085,6 +1108,17 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1085
1108
|
_sendRoot({transportPayment: transportPaymentValue, token: token, remoteToken: currentMapping});
|
|
1086
1109
|
}
|
|
1087
1110
|
|
|
1111
|
+
// Update the reverse reservation if an unused local token is being remapped to a new remote token.
|
|
1112
|
+
if (
|
|
1113
|
+
map.remoteToken != bytes32(0) && currentMapping.addr != bytes32(0) && currentMapping.addr != map.remoteToken
|
|
1114
|
+
&& _localTokenForRemoteToken[currentMapping.addr] == token
|
|
1115
|
+
) {
|
|
1116
|
+
delete _localTokenForRemoteToken[currentMapping.addr];
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
bytes32 remoteToken = map.remoteToken == bytes32(0) ? currentMapping.addr : map.remoteToken;
|
|
1120
|
+
if (remoteToken != bytes32(0)) _localTokenForRemoteToken[remoteToken] = token;
|
|
1121
|
+
|
|
1088
1122
|
// Update the token mapping.
|
|
1089
1123
|
_remoteTokenFor[token] = JBRemoteToken({
|
|
1090
1124
|
enabled: map.remoteToken != bytes32(0),
|
|
@@ -1092,7 +1126,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1092
1126
|
minGas: map.minGas,
|
|
1093
1127
|
// This is done so that a token can be disabled and then enabled again
|
|
1094
1128
|
// while ensuring the remoteToken never changes (unless it hasn't been used yet)
|
|
1095
|
-
addr:
|
|
1129
|
+
addr: remoteToken
|
|
1096
1130
|
});
|
|
1097
1131
|
}
|
|
1098
1132
|
|
package/src/JBSuckerRegistry.sol
CHANGED
|
@@ -18,6 +18,7 @@ import {IJBSuckerDeployer} from "./interfaces/IJBSuckerDeployer.sol";
|
|
|
18
18
|
import {IJBSuckerRegistry} from "./interfaces/IJBSuckerRegistry.sol";
|
|
19
19
|
import {JBSuckerDeployerConfig} from "./structs/JBSuckerDeployerConfig.sol";
|
|
20
20
|
import {JBSuckersPair} from "./structs/JBSuckersPair.sol";
|
|
21
|
+
import {PeerValueScratch} from "./structs/PeerValueScratch.sol";
|
|
21
22
|
|
|
22
23
|
/// @notice The canonical registry that deploys, tracks, and governs cross-chain suckers for Juicebox projects. It
|
|
23
24
|
/// maintains an allowlist of approved deployer contracts, allows multiple active suckers per peer chain for bridge
|
|
@@ -210,26 +211,21 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
210
211
|
|
|
211
212
|
// Per-chain dedup arrays. The number of suckers per project is small (typically 1-5),
|
|
212
213
|
// so a linear scan is cheaper than a mapping.
|
|
213
|
-
|
|
214
|
-
uint256[] memory maxBalances = new uint256[](len);
|
|
215
|
-
bool[] memory hasActiveValue = new bool[](len);
|
|
216
|
-
uint256 chainCount;
|
|
214
|
+
PeerValueScratch memory scratch = _peerValueScratch(len);
|
|
217
215
|
|
|
218
216
|
for (uint256 i; i < len;) {
|
|
219
217
|
(, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
|
|
220
218
|
// Include both active and deprecated suckers in aggregate economic views.
|
|
221
219
|
if (val == _SUCKER_EXISTS || val == _SUCKER_DEPRECATED) {
|
|
222
|
-
try IJBSucker(allSuckers[i]).peerChainBalanceOf(decimals, currency) returns (
|
|
220
|
+
try IJBSucker(allSuckers[i]).peerChainBalanceOf({decimals: decimals, currency: currency}) returns (
|
|
223
221
|
JBDenominatedAmount memory amt
|
|
224
222
|
) {
|
|
225
223
|
uint256 chainId = IJBSucker(allSuckers[i]).peerChainId();
|
|
226
|
-
chainCount = _recordPeerValue({
|
|
227
|
-
|
|
228
|
-
values: maxBalances,
|
|
229
|
-
hasActiveValue: hasActiveValue,
|
|
230
|
-
chainCount: chainCount,
|
|
224
|
+
scratch.chainCount = _recordPeerValue({
|
|
225
|
+
scratch: scratch,
|
|
231
226
|
chainId: chainId,
|
|
232
227
|
value: amt.value,
|
|
228
|
+
snapshotTimestamp: _snapshotTimestampOf(allSuckers[i]),
|
|
233
229
|
isActive: val == _SUCKER_EXISTS
|
|
234
230
|
});
|
|
235
231
|
} catch {}
|
|
@@ -239,9 +235,9 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
239
235
|
}
|
|
240
236
|
}
|
|
241
237
|
|
|
242
|
-
// Sum the per-chain
|
|
243
|
-
for (uint256 k; k < chainCount;) {
|
|
244
|
-
balance +=
|
|
238
|
+
// Sum the per-chain selected values.
|
|
239
|
+
for (uint256 k; k < scratch.chainCount;) {
|
|
240
|
+
balance += scratch.values[k];
|
|
245
241
|
unchecked {
|
|
246
242
|
++k;
|
|
247
243
|
}
|
|
@@ -271,26 +267,21 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
271
267
|
|
|
272
268
|
// Per-chain dedup arrays. The number of suckers per project is small (typically 1-5),
|
|
273
269
|
// so a linear scan is cheaper than a mapping.
|
|
274
|
-
|
|
275
|
-
uint256[] memory maxSurplus = new uint256[](len);
|
|
276
|
-
bool[] memory hasActiveValue = new bool[](len);
|
|
277
|
-
uint256 chainCount;
|
|
270
|
+
PeerValueScratch memory scratch = _peerValueScratch(len);
|
|
278
271
|
|
|
279
272
|
for (uint256 i; i < len;) {
|
|
280
273
|
(, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
|
|
281
274
|
// Include both active and deprecated suckers in aggregate economic views.
|
|
282
275
|
if (val == _SUCKER_EXISTS || val == _SUCKER_DEPRECATED) {
|
|
283
|
-
try IJBSucker(allSuckers[i]).peerChainSurplusOf(decimals, currency) returns (
|
|
276
|
+
try IJBSucker(allSuckers[i]).peerChainSurplusOf({decimals: decimals, currency: currency}) returns (
|
|
284
277
|
JBDenominatedAmount memory amt
|
|
285
278
|
) {
|
|
286
279
|
uint256 chainId = IJBSucker(allSuckers[i]).peerChainId();
|
|
287
|
-
chainCount = _recordPeerValue({
|
|
288
|
-
|
|
289
|
-
values: maxSurplus,
|
|
290
|
-
hasActiveValue: hasActiveValue,
|
|
291
|
-
chainCount: chainCount,
|
|
280
|
+
scratch.chainCount = _recordPeerValue({
|
|
281
|
+
scratch: scratch,
|
|
292
282
|
chainId: chainId,
|
|
293
283
|
value: amt.value,
|
|
284
|
+
snapshotTimestamp: _snapshotTimestampOf(allSuckers[i]),
|
|
294
285
|
isActive: val == _SUCKER_EXISTS
|
|
295
286
|
});
|
|
296
287
|
} catch {}
|
|
@@ -300,9 +291,9 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
300
291
|
}
|
|
301
292
|
}
|
|
302
293
|
|
|
303
|
-
// Sum the per-chain
|
|
304
|
-
for (uint256 k; k < chainCount;) {
|
|
305
|
-
surplus +=
|
|
294
|
+
// Sum the per-chain selected values.
|
|
295
|
+
for (uint256 k; k < scratch.chainCount;) {
|
|
296
|
+
surplus += scratch.values[k];
|
|
306
297
|
unchecked {
|
|
307
298
|
++k;
|
|
308
299
|
}
|
|
@@ -321,10 +312,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
321
312
|
|
|
322
313
|
// Per-chain dedup arrays. The number of suckers per project is small (typically 1-5),
|
|
323
314
|
// so a linear scan is cheaper than a mapping.
|
|
324
|
-
|
|
325
|
-
uint256[] memory maxSupply = new uint256[](len);
|
|
326
|
-
bool[] memory hasActiveValue = new bool[](len);
|
|
327
|
-
uint256 chainCount;
|
|
315
|
+
PeerValueScratch memory scratch = _peerValueScratch(len);
|
|
328
316
|
|
|
329
317
|
for (uint256 i; i < len;) {
|
|
330
318
|
(, uint256 val) = _suckersOf[projectId].tryGet(allSuckers[i]);
|
|
@@ -332,13 +320,11 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
332
320
|
if (val == _SUCKER_EXISTS || val == _SUCKER_DEPRECATED) {
|
|
333
321
|
try IJBSucker(allSuckers[i]).peerChainTotalSupply() returns (uint256 supply) {
|
|
334
322
|
uint256 chainId = IJBSucker(allSuckers[i]).peerChainId();
|
|
335
|
-
chainCount = _recordPeerValue({
|
|
336
|
-
|
|
337
|
-
values: maxSupply,
|
|
338
|
-
hasActiveValue: hasActiveValue,
|
|
339
|
-
chainCount: chainCount,
|
|
323
|
+
scratch.chainCount = _recordPeerValue({
|
|
324
|
+
scratch: scratch,
|
|
340
325
|
chainId: chainId,
|
|
341
326
|
value: supply,
|
|
327
|
+
snapshotTimestamp: _snapshotTimestampOf(allSuckers[i]),
|
|
342
328
|
isActive: val == _SUCKER_EXISTS
|
|
343
329
|
});
|
|
344
330
|
} catch {}
|
|
@@ -348,9 +334,9 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
348
334
|
}
|
|
349
335
|
}
|
|
350
336
|
|
|
351
|
-
// Sum the per-chain
|
|
352
|
-
for (uint256 k; k < chainCount;) {
|
|
353
|
-
totalSupply +=
|
|
337
|
+
// Sum the per-chain selected values.
|
|
338
|
+
for (uint256 k; k < scratch.chainCount;) {
|
|
339
|
+
totalSupply += scratch.values[k];
|
|
354
340
|
unchecked {
|
|
355
341
|
++k;
|
|
356
342
|
}
|
|
@@ -378,58 +364,91 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
378
364
|
return ERC2771Context._msgSender();
|
|
379
365
|
}
|
|
380
366
|
|
|
367
|
+
/// @notice Allocates scratch arrays used to collapse many suckers into one aggregate value per peer chain.
|
|
368
|
+
/// @dev `len` is the number of suckers being scanned, which is the maximum possible number of distinct peer
|
|
369
|
+
/// chains. `chainCount` starts at zero and is incremented as new peer chains are discovered.
|
|
370
|
+
/// @param len The maximum number of peer-chain entries the aggregation can need.
|
|
371
|
+
/// @return scratch Empty scratch space sized for the current aggregation pass.
|
|
372
|
+
function _peerValueScratch(uint256 len) internal pure returns (PeerValueScratch memory scratch) {
|
|
373
|
+
// Allocate each parallel array up front so `_recordPeerValue` can update by index without resizing memory.
|
|
374
|
+
scratch.chainIds = new uint256[](len);
|
|
375
|
+
scratch.values = new uint256[](len);
|
|
376
|
+
scratch.snapshotTimestamps = new uint256[](len);
|
|
377
|
+
scratch.hasActiveValue = new bool[](len);
|
|
378
|
+
}
|
|
379
|
+
|
|
381
380
|
/// @notice Records a project-scoped peer-chain aggregate value.
|
|
382
381
|
/// @dev Callers pass scratch arrays sized from `_suckersOf[projectId].keys()`, so entries are already scoped to
|
|
383
382
|
/// the project being aggregated. For each peer chain, active suckers replace deprecated suckers; deprecated
|
|
384
383
|
/// values are only used as a migration fallback when no active sucker has reported for that chain.
|
|
385
|
-
/// @param
|
|
386
|
-
/// @param values The aggregate values recorded so far.
|
|
387
|
-
/// @param hasActiveValue Whether the recorded value for each index came from an active sucker.
|
|
388
|
-
/// @param chainCount The number of populated chain entries.
|
|
384
|
+
/// @param scratch The per-chain aggregate values and freshness keys recorded so far.
|
|
389
385
|
/// @param chainId The peer-chain id to record.
|
|
390
386
|
/// @param value The value to record.
|
|
387
|
+
/// @param snapshotTimestamp The snapshot freshness key to record.
|
|
391
388
|
/// @param isActive Whether the value came from an active sucker.
|
|
392
389
|
/// @return The updated number of populated chain entries.
|
|
393
390
|
function _recordPeerValue(
|
|
394
|
-
|
|
395
|
-
uint256[] memory values,
|
|
396
|
-
bool[] memory hasActiveValue,
|
|
397
|
-
uint256 chainCount,
|
|
391
|
+
PeerValueScratch memory scratch,
|
|
398
392
|
uint256 chainId,
|
|
399
393
|
uint256 value,
|
|
394
|
+
uint256 snapshotTimestamp,
|
|
400
395
|
bool isActive
|
|
401
396
|
)
|
|
402
397
|
internal
|
|
403
398
|
pure
|
|
404
399
|
returns (uint256)
|
|
405
400
|
{
|
|
406
|
-
for (uint256 j; j < chainCount;) {
|
|
407
|
-
if (chainIds[j] == chainId) {
|
|
401
|
+
for (uint256 j; j < scratch.chainCount;) {
|
|
402
|
+
if (scratch.chainIds[j] == chainId) {
|
|
408
403
|
// Each sucker caches the entire remote chain's state (not a per-sucker share), so multiple
|
|
409
|
-
// suckers targeting the same chain report redundant snapshots.
|
|
410
|
-
//
|
|
404
|
+
// suckers targeting the same chain report redundant snapshots. Prefer the freshest source-chain
|
|
405
|
+
// snapshot; use MAX only as a same-freshness tie-breaker or deprecated fallback.
|
|
411
406
|
if (isActive) {
|
|
412
|
-
if (
|
|
413
|
-
|
|
414
|
-
|
|
407
|
+
if (
|
|
408
|
+
!scratch.hasActiveValue[j] || snapshotTimestamp > scratch.snapshotTimestamps[j]
|
|
409
|
+
|| (snapshotTimestamp == scratch.snapshotTimestamps[j] && value > scratch.values[j])
|
|
410
|
+
) {
|
|
411
|
+
scratch.values[j] = value;
|
|
412
|
+
scratch.snapshotTimestamps[j] = snapshotTimestamp;
|
|
413
|
+
}
|
|
414
|
+
scratch.hasActiveValue[j] = true;
|
|
415
|
+
} else if (
|
|
416
|
+
!scratch.hasActiveValue[j]
|
|
417
|
+
&& (snapshotTimestamp > scratch.snapshotTimestamps[j]
|
|
418
|
+
|| (snapshotTimestamp == scratch.snapshotTimestamps[j] && value > scratch.values[j]))
|
|
419
|
+
) {
|
|
415
420
|
// Deprecated suckers only fill the gap until an active value for this chain has been observed.
|
|
416
|
-
values[j] = value;
|
|
421
|
+
scratch.values[j] = value;
|
|
422
|
+
scratch.snapshotTimestamps[j] = snapshotTimestamp;
|
|
417
423
|
}
|
|
418
|
-
return chainCount;
|
|
424
|
+
return scratch.chainCount;
|
|
419
425
|
}
|
|
420
426
|
unchecked {
|
|
421
427
|
++j;
|
|
422
428
|
}
|
|
423
429
|
}
|
|
424
430
|
|
|
425
|
-
chainIds[chainCount] = chainId;
|
|
426
|
-
values[chainCount] = value;
|
|
427
|
-
|
|
431
|
+
scratch.chainIds[scratch.chainCount] = chainId;
|
|
432
|
+
scratch.values[scratch.chainCount] = value;
|
|
433
|
+
scratch.snapshotTimestamps[scratch.chainCount] = snapshotTimestamp;
|
|
434
|
+
scratch.hasActiveValue[scratch.chainCount] = isActive;
|
|
428
435
|
unchecked {
|
|
429
|
-
return chainCount + 1;
|
|
436
|
+
return scratch.chainCount + 1;
|
|
430
437
|
}
|
|
431
438
|
}
|
|
432
439
|
|
|
440
|
+
/// @notice Reads a sucker's snapshot timestamp, returning zero if the sucker does not expose it.
|
|
441
|
+
/// @dev Older or malformed suckers should not brick aggregate registry views. A zero timestamp makes their value
|
|
442
|
+
/// lose to any successful fresh read for the same peer chain.
|
|
443
|
+
/// @param sucker The sucker to query.
|
|
444
|
+
/// @return timestamp The reported snapshot timestamp, or zero if the call fails.
|
|
445
|
+
function _snapshotTimestampOf(address sucker) internal view returns (uint256 timestamp) {
|
|
446
|
+
// Keep aggregate views available even if one registered sucker has a stale ABI or reverts unexpectedly.
|
|
447
|
+
try IJBSucker(sucker).snapshotTimestamp() returns (uint256 result) {
|
|
448
|
+
timestamp = result;
|
|
449
|
+
} catch {}
|
|
450
|
+
}
|
|
451
|
+
|
|
433
452
|
//*********************************************************************//
|
|
434
453
|
// ---------------------- public transactions ----------------------- //
|
|
435
454
|
//*********************************************************************//
|
|
@@ -564,7 +583,7 @@ contract JBSuckerRegistry is ERC2771Context, Ownable, JBPermissioned, IJBSuckerR
|
|
|
564
583
|
}
|
|
565
584
|
|
|
566
585
|
// Mark the sucker as deprecated (retains mint permission, excluded from active listings).
|
|
567
|
-
_suckersOf[projectId].set(address(sucker), _SUCKER_DEPRECATED);
|
|
586
|
+
_suckersOf[projectId].set({key: address(sucker), value: _SUCKER_DEPRECATED});
|
|
568
587
|
emit SuckerDeprecated({projectId: projectId, sucker: address(sucker), caller: _msgSender()});
|
|
569
588
|
}
|
|
570
589
|
|