@bananapus/suckers-v6 0.0.48 → 0.0.50
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/CHANGELOG.md +35 -0
- package/package.json +3 -3
- package/src/JBArbitrumSucker.sol +4 -1
- package/src/JBCeloSucker.sol +2 -0
- package/src/JBOptimismSucker.sol +2 -0
- package/src/JBSucker.sol +95 -20
- package/src/JBSuckerRegistry.sol +76 -57
- package/src/JBSwapCCIPSucker.sol +33 -96
- package/src/interfaces/IJBSucker.sol +19 -7
- package/src/interfaces/IJBSuckerRegistry.sol +6 -3
- package/src/libraries/CCIPHelper.sol +64 -64
- package/src/libraries/JBCCIPLib.sol +18 -0
- package/src/libraries/JBSuckerLib.sol +157 -161
- package/src/libraries/JBSwapPoolLib.sol +268 -268
- package/src/structs/JBLeaf.sol +6 -0
- package/src/structs/PeerValueScratch.sol +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,41 @@ This file describes the verified change from `nana-suckers-v5` to the current `n
|
|
|
15
15
|
- `JBCeloSucker`
|
|
16
16
|
- the deployers, structs, and interfaces under `src/`
|
|
17
17
|
|
|
18
|
+
## Unreleased — `JBLeaf.metadata` attribution field
|
|
19
|
+
|
|
20
|
+
The merkle leaf now carries a fifth field: a `bytes32 metadata` payload that travels inside the leaf hash but is
|
|
21
|
+
opaque to the sucker protocol itself.
|
|
22
|
+
|
|
23
|
+
**What changed**
|
|
24
|
+
|
|
25
|
+
- `JBLeaf` struct: new trailing `bytes32 metadata` field.
|
|
26
|
+
- `IJBSucker.prepare`: new trailing `bytes32 metadata` parameter. The metadata is included in the leaf hash, so it's
|
|
27
|
+
covered by the merkle root — receivers can trust it once the claim's merkle proof verifies.
|
|
28
|
+
- `_buildTreeHash` hashes 128 bytes (was 96): `keccak256(projectTokenCount || terminalTokenAmount || beneficiary || metadata)`.
|
|
29
|
+
- `_insertIntoTree`, `_validate`, `_validateBranchRoot`, `_validateForEmergencyExit` all thread `metadata` through.
|
|
30
|
+
- Events `InsertToOutboxTree` and `Claimed` carry the field so off-chain indexers can read it directly without
|
|
31
|
+
cracking the leaf.
|
|
32
|
+
- SVM leaf encoding widens from 96 bytes to 128 bytes; the new 32-byte suffix is the `metadata` field. The
|
|
33
|
+
`_svmBuildTreeHash` interop test mirrors the layout exactly so EVM↔SVM hash equality is preserved.
|
|
34
|
+
|
|
35
|
+
**Intended use**
|
|
36
|
+
|
|
37
|
+
The original motivator is the cross-chain referral split hook (`nana-referral-split-hook-v6`): when a referrer
|
|
38
|
+
on chain Y earns credit for fee-paying activity on chain X, the hook on X uses the fee project's sucker to
|
|
39
|
+
bridge the entitled fee-project tokens. The leaf's `metadata` carries `(originChainId, referralProjectId)` so the
|
|
40
|
+
sibling hook on chain Y can atomically claim, re-pay the fee project locally, and push to the local distributor
|
|
41
|
+
for the right referrer — all under the merkle proof's authentication, no off-chain coordination needed.
|
|
42
|
+
|
|
43
|
+
The field is generic: any future leaf consumer (NFT split hooks, buyback hooks, etc.) can use it for its own
|
|
44
|
+
attribution scheme without further sucker changes. Pass `bytes32(0)` for ordinary bridges that don't need it.
|
|
45
|
+
|
|
46
|
+
**Risk surface**
|
|
47
|
+
|
|
48
|
+
Zero new trust paths. The bridge protocol stays leaf-in-leaf-out; we just put one more 32-byte field under the
|
|
49
|
+
same root. Existing claim, emergency-exit, and root-relay flows behave identically when `metadata == bytes32(0)`.
|
|
50
|
+
The leaf-hash domain changes (96 → 128 bytes), but since nothing is deployed yet there's no on-chain
|
|
51
|
+
compatibility concern.
|
|
52
|
+
|
|
18
53
|
## 0.0.46 — Bump nana-core-v6 to 0.0.53
|
|
19
54
|
|
|
20
55
|
`@bananapus/core-v6@0.0.53` ([nana-core-v6 PR #145](https://github.com/Bananapus/nana-core-v6/pull/145)) drops the `via_ir` requirement on `JBCashOutHookSpecsLib`, which lets this package consume the cross-project cashout work (`payAfterCashOutTokensOf` / `addToBalanceAfterCashOutTokensOf`) without needing `via_ir = true` in its own foundry profile.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/suckers-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.50",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@arbitrum/nitro-contracts": "3.2.0",
|
|
33
|
-
"@bananapus/core-v6": "^0.0.
|
|
34
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
33
|
+
"@bananapus/core-v6": "^0.0.59",
|
|
34
|
+
"@bananapus/permission-ids-v6": "^0.0.26",
|
|
35
35
|
"@chainlink/contracts-ccip": "1.6.4",
|
|
36
36
|
"@chainlink/local": "0.2.7",
|
|
37
37
|
"@openzeppelin/contracts": "5.6.1",
|
package/src/JBArbitrumSucker.sol
CHANGED
|
@@ -255,12 +255,13 @@ 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({
|
|
@@ -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/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;
|
|
@@ -285,12 +294,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
285
294
|
/// project tokens for the beneficiary, and deposits the terminal tokens into the project's local balance.
|
|
286
295
|
/// @param claimData The terminal token, merkle tree leaf, and proof for the claim.
|
|
287
296
|
function claim(JBClaim calldata claimData) public virtual override {
|
|
288
|
-
// Attempt to validate the proof against the inbox tree for the terminal token.
|
|
297
|
+
// Attempt to validate the proof against the inbox tree for the terminal token. The leaf hash includes
|
|
298
|
+
// `claimData.leaf.metadata` so the proof is only valid for the exact (amount, beneficiary, metadata) tuple the
|
|
299
|
+
// origin committed to.
|
|
289
300
|
_validate({
|
|
290
301
|
projectTokenCount: claimData.leaf.projectTokenCount,
|
|
291
302
|
terminalToken: claimData.token,
|
|
292
303
|
terminalTokenAmount: claimData.leaf.terminalTokenAmount,
|
|
293
304
|
beneficiary: claimData.leaf.beneficiary,
|
|
305
|
+
metadata: claimData.leaf.metadata,
|
|
294
306
|
index: claimData.leaf.index,
|
|
295
307
|
leaves: claimData.proof
|
|
296
308
|
});
|
|
@@ -301,6 +313,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
301
313
|
projectTokenCount: claimData.leaf.projectTokenCount,
|
|
302
314
|
terminalTokenAmount: claimData.leaf.terminalTokenAmount,
|
|
303
315
|
index: claimData.leaf.index,
|
|
316
|
+
metadata: claimData.leaf.metadata,
|
|
304
317
|
caller: _msgSender()
|
|
305
318
|
});
|
|
306
319
|
|
|
@@ -321,7 +334,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
321
334
|
uint256 _projectId = projectId();
|
|
322
335
|
|
|
323
336
|
_requirePermissionFrom({
|
|
324
|
-
account:
|
|
337
|
+
account: _ownerOf(_projectId), projectId: _projectId, permissionId: JBPermissionIds.SUCKER_SAFETY
|
|
325
338
|
});
|
|
326
339
|
|
|
327
340
|
// Enable the emergency hatch for each token.
|
|
@@ -343,12 +356,14 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
343
356
|
/// @param claimData The terminal token, merkle tree leaf, and proof for the claim.
|
|
344
357
|
function exitThroughEmergencyHatch(JBClaim calldata claimData) external override {
|
|
345
358
|
// Does all the needed validation to ensure that the claim is valid *and* that claiming through the emergency
|
|
346
|
-
// hatch is allowed.
|
|
359
|
+
// hatch is allowed. The leaf hash covers `metadata` so a remote-attribution leaf is only exitable if the
|
|
360
|
+
// emergency exiter knows the exact `metadata` value the origin committed to.
|
|
347
361
|
_validateForEmergencyExit({
|
|
348
362
|
projectTokenCount: claimData.leaf.projectTokenCount,
|
|
349
363
|
terminalToken: claimData.token,
|
|
350
364
|
terminalTokenAmount: claimData.leaf.terminalTokenAmount,
|
|
351
365
|
beneficiary: claimData.leaf.beneficiary,
|
|
366
|
+
metadata: claimData.leaf.metadata,
|
|
352
367
|
index: claimData.leaf.index,
|
|
353
368
|
leaves: claimData.proof
|
|
354
369
|
});
|
|
@@ -523,7 +538,8 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
523
538
|
uint256 projectTokenCount,
|
|
524
539
|
bytes32 beneficiary,
|
|
525
540
|
uint256 minTokensReclaimed,
|
|
526
|
-
address token
|
|
541
|
+
address token,
|
|
542
|
+
bytes32 metadata
|
|
527
543
|
)
|
|
528
544
|
external
|
|
529
545
|
override
|
|
@@ -555,12 +571,15 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
555
571
|
projectToken: projectToken, count: projectTokenCount, token: token, minTokensReclaimed: minTokensReclaimed
|
|
556
572
|
});
|
|
557
573
|
|
|
558
|
-
// Insert the item into the outbox tree for the terminal `token`.
|
|
574
|
+
// Insert the item into the outbox tree for the terminal `token`. The `metadata` field travels inside the leaf
|
|
575
|
+
// hash so receivers can read attribution context from a proven claim — the sucker protocol itself never
|
|
576
|
+
// inspects it.
|
|
559
577
|
_insertIntoTree({
|
|
560
578
|
projectTokenCount: projectTokenCount,
|
|
561
579
|
token: token,
|
|
562
580
|
terminalTokenAmount: terminalTokenAmount,
|
|
563
|
-
beneficiary: beneficiary
|
|
581
|
+
beneficiary: beneficiary,
|
|
582
|
+
metadata: metadata
|
|
564
583
|
});
|
|
565
584
|
}
|
|
566
585
|
|
|
@@ -577,9 +596,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
577
596
|
|
|
578
597
|
// The caller must be the project owner or have the `SET_SUCKER_DEPRECATION` permission from them.
|
|
579
598
|
_requirePermissionFrom({
|
|
580
|
-
account:
|
|
581
|
-
projectId: _projectId,
|
|
582
|
-
permissionId: JBPermissionIds.SET_SUCKER_DEPRECATION
|
|
599
|
+
account: _ownerOf(_projectId), projectId: _projectId, permissionId: JBPermissionIds.SET_SUCKER_DEPRECATION
|
|
583
600
|
});
|
|
584
601
|
|
|
585
602
|
// This is the earliest time for when the sucker can be considered deprecated.
|
|
@@ -987,16 +1004,20 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
987
1004
|
uint256 projectTokenCount,
|
|
988
1005
|
address token,
|
|
989
1006
|
uint256 terminalTokenAmount,
|
|
990
|
-
bytes32 beneficiary
|
|
1007
|
+
bytes32 beneficiary,
|
|
1008
|
+
bytes32 metadata
|
|
991
1009
|
)
|
|
992
1010
|
internal
|
|
993
1011
|
{
|
|
994
1012
|
// Guard against amounts that would overflow uint128 on SVM (INTEROP-5).
|
|
995
1013
|
if (terminalTokenAmount > type(uint128).max) revert JBSucker_AmountExceedsUint128(terminalTokenAmount);
|
|
996
1014
|
if (projectTokenCount > type(uint128).max) revert JBSucker_AmountExceedsUint128(projectTokenCount);
|
|
997
|
-
// Build a hash based on the token amounts and the
|
|
1015
|
+
// Build a hash based on the token amounts, the beneficiary, and the attribution metadata.
|
|
998
1016
|
bytes32 hashed = _buildTreeHash({
|
|
999
|
-
projectTokenCount: projectTokenCount,
|
|
1017
|
+
projectTokenCount: projectTokenCount,
|
|
1018
|
+
terminalTokenAmount: terminalTokenAmount,
|
|
1019
|
+
beneficiary: beneficiary,
|
|
1020
|
+
metadata: metadata
|
|
1000
1021
|
});
|
|
1001
1022
|
|
|
1002
1023
|
// Get the outbox in storage.
|
|
@@ -1014,6 +1035,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1014
1035
|
root: _computeOutboxRoot(outbox.tree),
|
|
1015
1036
|
projectTokenCount: projectTokenCount,
|
|
1016
1037
|
terminalTokenAmount: terminalTokenAmount,
|
|
1038
|
+
metadata: metadata,
|
|
1017
1039
|
caller: _msgSender()
|
|
1018
1040
|
});
|
|
1019
1041
|
}
|
|
@@ -1031,6 +1053,10 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1031
1053
|
/// after outbox activity, the same local funds could be claimed against two different remote tokens. A
|
|
1032
1054
|
/// misconfigured mapping therefore requires deploying a new sucker. Re-enabling a previously disabled mapping
|
|
1033
1055
|
/// (back to the same remote token) is supported.
|
|
1056
|
+
/// @dev Remote tokens are also unique per local token within this sucker. The source side keeps separate
|
|
1057
|
+
/// outboxes/nonces per local token, but the destination side stores roots under the remote token address. Sharing
|
|
1058
|
+
/// one remote token across multiple local tokens in the same sucker would merge those inboxes on the destination
|
|
1059
|
+
/// chain. Separate suckers can still map the same local/remote token pair, letting users choose a bridge lane.
|
|
1034
1060
|
/// @param map The local and remote terminal token addresses to map, and minimum amount/gas limits for bridging
|
|
1035
1061
|
/// them.
|
|
1036
1062
|
/// @param transportPaymentValue The amount of `msg.value` to send for the token mapping.
|
|
@@ -1051,7 +1077,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1051
1077
|
|
|
1052
1078
|
// The registry can map during authorized deployment. Otherwise, require the project's mapping permission.
|
|
1053
1079
|
_requirePermissionAllowingOverrideFrom({
|
|
1054
|
-
account:
|
|
1080
|
+
account: _ownerOf(_projectId),
|
|
1055
1081
|
projectId: _projectId,
|
|
1056
1082
|
permissionId: JBPermissionIds.MAP_SUCKER_TOKEN,
|
|
1057
1083
|
alsoGrantAccessIf: _msgSender() == address(REGISTRY)
|
|
@@ -1068,6 +1094,16 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1068
1094
|
revert JBSucker_TokenAlreadyMapped({localToken: token, mappedTo: currentMapping.addr});
|
|
1069
1095
|
}
|
|
1070
1096
|
|
|
1097
|
+
// A remote token can back only one local token's outbox in this sucker. Otherwise two independent source
|
|
1098
|
+
// nonces would race into the same destination inbox key (`root.token`), making one token's root stale or
|
|
1099
|
+
// overwriting the other. Other suckers have separate inbox/outbox storage and are unaffected.
|
|
1100
|
+
if (map.remoteToken != bytes32(0)) {
|
|
1101
|
+
address mappedLocalToken = _localTokenForRemoteToken[map.remoteToken];
|
|
1102
|
+
if (mappedLocalToken != address(0) && mappedLocalToken != token) {
|
|
1103
|
+
revert JBSucker_RemoteTokenAlreadyMapped({remoteToken: map.remoteToken, localToken: mappedLocalToken});
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1071
1107
|
// No inbox guard needed here. Token remapping only affects the outbound (sending) path —
|
|
1072
1108
|
// it changes where tokens get bridged TO. Existing inbox claims are resolved against the inbox merkle
|
|
1073
1109
|
// tree keyed by the local token address. Changing the remote token doesn't invalidate those claims
|
|
@@ -1085,6 +1121,17 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1085
1121
|
_sendRoot({transportPayment: transportPaymentValue, token: token, remoteToken: currentMapping});
|
|
1086
1122
|
}
|
|
1087
1123
|
|
|
1124
|
+
// Update the reverse reservation if an unused local token is being remapped to a new remote token.
|
|
1125
|
+
if (
|
|
1126
|
+
map.remoteToken != bytes32(0) && currentMapping.addr != bytes32(0) && currentMapping.addr != map.remoteToken
|
|
1127
|
+
&& _localTokenForRemoteToken[currentMapping.addr] == token
|
|
1128
|
+
) {
|
|
1129
|
+
delete _localTokenForRemoteToken[currentMapping.addr];
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
bytes32 remoteToken = map.remoteToken == bytes32(0) ? currentMapping.addr : map.remoteToken;
|
|
1133
|
+
if (remoteToken != bytes32(0)) _localTokenForRemoteToken[remoteToken] = token;
|
|
1134
|
+
|
|
1088
1135
|
// Update the token mapping.
|
|
1089
1136
|
_remoteTokenFor[token] = JBRemoteToken({
|
|
1090
1137
|
enabled: map.remoteToken != bytes32(0),
|
|
@@ -1092,7 +1139,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1092
1139
|
minGas: map.minGas,
|
|
1093
1140
|
// This is done so that a token can be disabled and then enabled again
|
|
1094
1141
|
// while ensuring the remoteToken never changes (unless it hasn't been used yet)
|
|
1095
|
-
addr:
|
|
1142
|
+
addr: remoteToken
|
|
1096
1143
|
});
|
|
1097
1144
|
}
|
|
1098
1145
|
|
|
@@ -1136,7 +1183,9 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1136
1183
|
// Record the balance before the cash out for the sanity check.
|
|
1137
1184
|
uint256 balanceBefore = _balanceOf({token: token, addr: address(this)});
|
|
1138
1185
|
|
|
1139
|
-
// Cash out the project tokens for terminal tokens.
|
|
1186
|
+
// Cash out the project tokens for terminal tokens. Suckers are a transparent value-mover (the bridge
|
|
1187
|
+
// accounting is the entirety of their function) — they're not a fee-paying entry point for any referrer,
|
|
1188
|
+
// so `referralProjectId: 0` is correct.
|
|
1140
1189
|
reclaimedAmount = terminal.cashOutTokensOf({
|
|
1141
1190
|
holder: address(this),
|
|
1142
1191
|
projectId: cachedProjectId,
|
|
@@ -1144,7 +1193,8 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1144
1193
|
tokenToReclaim: token,
|
|
1145
1194
|
minTokensReclaimed: minTokensReclaimed,
|
|
1146
1195
|
beneficiary: payable(address(this)),
|
|
1147
|
-
metadata: bytes("")
|
|
1196
|
+
metadata: bytes(""),
|
|
1197
|
+
referralProjectId: 0
|
|
1148
1198
|
});
|
|
1149
1199
|
|
|
1150
1200
|
// Sanity check to make sure we received the expected amount.
|
|
@@ -1255,6 +1305,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1255
1305
|
address terminalToken,
|
|
1256
1306
|
uint256 terminalTokenAmount,
|
|
1257
1307
|
bytes32 beneficiary,
|
|
1308
|
+
bytes32 metadata,
|
|
1258
1309
|
uint256 index,
|
|
1259
1310
|
bytes32[_TREE_DEPTH] calldata leaves
|
|
1260
1311
|
)
|
|
@@ -1278,6 +1329,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1278
1329
|
projectTokenCount: projectTokenCount,
|
|
1279
1330
|
terminalTokenAmount: terminalTokenAmount,
|
|
1280
1331
|
beneficiary: beneficiary,
|
|
1332
|
+
metadata: metadata,
|
|
1281
1333
|
index: index,
|
|
1282
1334
|
leaves: leaves
|
|
1283
1335
|
});
|
|
@@ -1297,6 +1349,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1297
1349
|
uint256 projectTokenCount,
|
|
1298
1350
|
uint256 terminalTokenAmount,
|
|
1299
1351
|
bytes32 beneficiary,
|
|
1352
|
+
bytes32 metadata,
|
|
1300
1353
|
uint256 index,
|
|
1301
1354
|
bytes32[_TREE_DEPTH] calldata leaves
|
|
1302
1355
|
)
|
|
@@ -1307,7 +1360,10 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1307
1360
|
// Delegates to JBSuckerLib (via DELEGATECALL) to keep MerkleLib.branchRoot bytecode out of each sucker.
|
|
1308
1361
|
bytes32 root = JBSuckerLib.computeBranchRoot({
|
|
1309
1362
|
item: _buildTreeHash({
|
|
1310
|
-
projectTokenCount: projectTokenCount,
|
|
1363
|
+
projectTokenCount: projectTokenCount,
|
|
1364
|
+
terminalTokenAmount: terminalTokenAmount,
|
|
1365
|
+
beneficiary: beneficiary,
|
|
1366
|
+
metadata: metadata
|
|
1311
1367
|
}),
|
|
1312
1368
|
branch: leaves,
|
|
1313
1369
|
index: index
|
|
@@ -1351,6 +1407,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1351
1407
|
address terminalToken,
|
|
1352
1408
|
uint256 terminalTokenAmount,
|
|
1353
1409
|
bytes32 beneficiary,
|
|
1410
|
+
bytes32 metadata,
|
|
1354
1411
|
uint256 index,
|
|
1355
1412
|
bytes32[_TREE_DEPTH] calldata leaves
|
|
1356
1413
|
)
|
|
@@ -1401,6 +1458,7 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1401
1458
|
projectTokenCount: projectTokenCount,
|
|
1402
1459
|
terminalTokenAmount: terminalTokenAmount,
|
|
1403
1460
|
beneficiary: beneficiary,
|
|
1461
|
+
metadata: metadata,
|
|
1404
1462
|
index: index,
|
|
1405
1463
|
leaves: leaves
|
|
1406
1464
|
});
|
|
@@ -1448,24 +1506,27 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1448
1506
|
/// @param projectTokenCount The number of project tokens to cash out.
|
|
1449
1507
|
/// @param terminalTokenAmount The amount of terminal tokens to reclaim from the cash out.
|
|
1450
1508
|
/// @param beneficiary The beneficiary which will receive the project tokens (bytes32 for cross-VM compatibility).
|
|
1509
|
+
/// @param metadata Opaque caller-defined attribution payload travelling inside the leaf hash.
|
|
1451
1510
|
/// @return hash The keccak256 hash of the leaf data.
|
|
1452
1511
|
function _buildTreeHash(
|
|
1453
1512
|
uint256 projectTokenCount,
|
|
1454
1513
|
uint256 terminalTokenAmount,
|
|
1455
|
-
bytes32 beneficiary
|
|
1514
|
+
bytes32 beneficiary,
|
|
1515
|
+
bytes32 metadata
|
|
1456
1516
|
)
|
|
1457
1517
|
internal
|
|
1458
1518
|
pure
|
|
1459
1519
|
returns (bytes32 hash)
|
|
1460
1520
|
{
|
|
1461
|
-
// All
|
|
1521
|
+
// All four arguments are 32 bytes — hash from free memory to avoid abi.encode allocation overhead.
|
|
1462
1522
|
// forge-lint: disable-next-line(asm-keccak256)
|
|
1463
1523
|
assembly {
|
|
1464
1524
|
let ptr := mload(0x40)
|
|
1465
1525
|
mstore(ptr, projectTokenCount)
|
|
1466
1526
|
mstore(add(ptr, 0x20), terminalTokenAmount)
|
|
1467
1527
|
mstore(add(ptr, 0x40), beneficiary)
|
|
1468
|
-
|
|
1528
|
+
mstore(add(ptr, 0x60), metadata)
|
|
1529
|
+
hash := keccak256(ptr, 0x80)
|
|
1469
1530
|
}
|
|
1470
1531
|
}
|
|
1471
1532
|
|
|
@@ -1509,6 +1570,20 @@ abstract contract JBSucker is ERC2771Context, JBPermissioned, Initializable, ERC
|
|
|
1509
1570
|
return ERC2771Context._msgSender();
|
|
1510
1571
|
}
|
|
1511
1572
|
|
|
1573
|
+
/// @notice Resolve the current owner of the project this sucker belongs to.
|
|
1574
|
+
/// @dev `PROJECTS.ownerOf(...)` is the source of truth for "project owner" permission checks; we hit it from
|
|
1575
|
+
/// every permission-gated entrypoint (`enableEmergencyHatchFor`, `setDeprecation`, `_mapToken`). Routing all
|
|
1576
|
+
/// three through this internal helper emits the abi-encode + STATICCALL + return-decode sequence once in the
|
|
1577
|
+
/// child contract's bytecode instead of inlining it at each call site, which is what keeps `JBSwapCCIPSucker`
|
|
1578
|
+
/// under the EIP-170 limit after the leaf-`metadata` thread-through landed.
|
|
1579
|
+
/// @param forProjectId The project ID to look up — always the sucker's own `projectId()`, but accepted as a
|
|
1580
|
+
/// parameter so callers can pass the cached local they already computed (avoiding a redundant `projectId()`
|
|
1581
|
+
/// call against the read-only registry).
|
|
1582
|
+
/// @return owner The address currently registered as the project's ERC-721 holder.
|
|
1583
|
+
function _ownerOf(uint256 forProjectId) internal view returns (address owner) {
|
|
1584
|
+
return PROJECTS.ownerOf(forProjectId);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1512
1587
|
/// @notice Retain a failed `toRemoteFee` payment for later caller refund.
|
|
1513
1588
|
/// @param account The account that can reclaim the retained fee.
|
|
1514
1589
|
/// @param amount The retained fee amount.
|