@bananapus/suckers-v6 0.0.76 → 0.0.78
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 +6 -2
- package/package.json +3 -3
- package/references/entrypoints.md +11 -0
- package/references/operations.md +1 -1
- package/references/runtime.md +5 -3
- package/src/JBArbitrumSucker.sol +75 -31
- package/src/JBBaseSucker.sol +1 -1
- package/src/JBCCIPSucker.sol +123 -58
- package/src/JBOptimismSucker.sol +26 -1
- package/src/JBSucker.sol +289 -205
- package/src/interfaces/IJBSucker.sol +14 -0
- package/src/libraries/JBSuckerLib.sol +32 -0
- package/src/structs/JBAccountingSnapshot.sol +19 -0
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ Suckers bridge a project by tracking claims in append-only Merkle trees:
|
|
|
23
23
|
|
|
24
24
|
- users call `prepare` to burn tokens and create a bridge claim in the local outbox tree
|
|
25
25
|
- anyone can relay the current root to the peer chain with `toRemote`
|
|
26
|
+
- anyone can refresh or retry peer accounting with `syncAccountingData`
|
|
26
27
|
- claimants prove inclusion against the peer inbox tree to mint on the destination chain
|
|
27
28
|
|
|
28
29
|
The base implementation is extended for multiple bridge families so the same project model can work across different networks.
|
|
@@ -35,16 +36,17 @@ The main idea is not "bridge the token contract." The main idea is "bridge a Jui
|
|
|
35
36
|
|
|
36
37
|
| Contract | Role |
|
|
37
38
|
| --- | --- |
|
|
38
|
-
| `JBSucker` | Base bridge logic for prepare, relay, claim, token mapping, and lifecycle controls. |
|
|
39
|
+
| `JBSucker` | Base bridge logic for prepare, relay, accounting sync, claim, token mapping, and lifecycle controls. |
|
|
39
40
|
| `JBSuckerRegistry` | Registry for per-project sucker deployments, deployer allowlists, and shared bridge fee settings. |
|
|
40
41
|
| Chain-specific suckers | Transport-specific implementations for OP Stack, Arbitrum, CCIP, and related environments. |
|
|
41
42
|
|
|
42
43
|
## Mental model
|
|
43
44
|
|
|
44
|
-
Each sucker pair has
|
|
45
|
+
Each sucker pair has three jobs:
|
|
45
46
|
|
|
46
47
|
1. destroy or lock the local economic position into a claimable message
|
|
47
48
|
2. recreate the remote position from a bridged Merkle root plus transported value
|
|
49
|
+
3. keep the peer's aggregate accounting views fresh enough for cross-chain estimates
|
|
48
50
|
|
|
49
51
|
That means every bridge path has two trust surfaces:
|
|
50
52
|
|
|
@@ -65,6 +67,7 @@ That means every bridge path has two trust surfaces:
|
|
|
65
67
|
- root ordering and message delivery semantics matter as much as proof format
|
|
66
68
|
- token mapping is part of the economic invariant
|
|
67
69
|
- 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
|
|
70
|
+
- `syncAccountingData` pays no registry `toRemoteFee`, but bridge-specific transport costs still apply and duplicate snapshots can still consume bridge/indexer resources
|
|
68
71
|
- emergency and deprecation paths are part of normal operational safety
|
|
69
72
|
|
|
70
73
|
## Where state lives
|
|
@@ -80,6 +83,7 @@ That means every bridge path has two trust surfaces:
|
|
|
80
83
|
3. `test/ForkClaimMainnet.t.sol`
|
|
81
84
|
4. `test/regression/PeerSnapshotDesync.t.sol`
|
|
82
85
|
5. `test/regression/ToRemoteFeeIrrecoverable.t.sol`
|
|
86
|
+
6. `test/regression/CCIPUntypedMessageRejected.t.sol`
|
|
83
87
|
|
|
84
88
|
## Install
|
|
85
89
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/suckers-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.78",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@arbitrum/nitro-contracts": "3.2.0",
|
|
30
|
-
"@bananapus/core-v6": "^0.0.
|
|
31
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
30
|
+
"@bananapus/core-v6": "^0.0.87",
|
|
31
|
+
"@bananapus/permission-ids-v6": "^0.0.32",
|
|
32
32
|
"@chainlink/contracts-ccip": "1.6.4",
|
|
33
33
|
"@openzeppelin/contracts": "5.6.1",
|
|
34
34
|
"@prb/math": "4.1.2",
|
|
@@ -49,6 +49,15 @@ A sucker bridges a Juicebox project's token economy between two chains. Cashed-o
|
|
|
49
49
|
| `terminalTokenAmount` | `uint256` | The amount of terminal tokens to claim. |
|
|
50
50
|
| `metadata` | `bytes32` | Opaque, caller-defined payload covered by the leaf hash. `bytes32(0)` when no extra context. |
|
|
51
51
|
|
|
52
|
+
### `JBAccountingSnapshot` (argument to `fromRemoteAccounting`)
|
|
53
|
+
|
|
54
|
+
| Field | Type | Meaning |
|
|
55
|
+
|-------|------|---------|
|
|
56
|
+
| `version` | `uint8` | Message format version. Must match `MESSAGE_VERSION`. |
|
|
57
|
+
| `sourceTotalSupply` | `uint256` | Source-chain project-token supply, including reserved tokens. |
|
|
58
|
+
| `sourceContexts` | `JBSourceContext[]` | Raw source-chain surplus and balance contexts, un-valued. |
|
|
59
|
+
| `sourceTimestamp` | `uint256` | Monotonic freshness key used to reject stale accounting updates. |
|
|
60
|
+
|
|
52
61
|
## Key functions
|
|
53
62
|
|
|
54
63
|
### JBSucker — bridge flow (`IJBSucker`)
|
|
@@ -57,6 +66,8 @@ A sucker bridges a Juicebox project's token economy between two chains. Cashed-o
|
|
|
57
66
|
|----------|--------------|
|
|
58
67
|
| `prepare(uint256 projectTokenCount, bytes32 beneficiary, uint256 minTokensReclaimed, address token, bytes32 metadata)` | Cash out `projectTokenCount` project tokens into `token` and insert a leaf for `beneficiary` into the outbox tree for bridging. `minTokensReclaimed` bounds slippage; `metadata` is an opaque attribution payload carried in the leaf hash (`bytes32(0)` for a plain bridge). |
|
|
59
68
|
| `toRemote(address token) payable` | Send the current outbox tree root and bridged assets for `token` to the remote peer through the chain-specific transport. `payable` to fund the transport message and the registry's `toRemoteFee`. |
|
|
69
|
+
| `syncAccountingData() payable` | Send the latest total supply, surplus, and balance snapshot without sending an outbox root or paying the registry `toRemoteFee`. Can be retried with unchanged accounting data; `payable` only funds bridge transport. |
|
|
70
|
+
| `fromRemoteAccounting(JBAccountingSnapshot calldata snapshot)` | Authenticated receive path for accounting-only snapshots. Updates peer accounting if the snapshot is fresher, without touching any token-local inbox root. |
|
|
60
71
|
| `claim(JBClaim calldata claimData)` | Claim bridged project tokens for the leaf's beneficiary by proving inclusion against the inbox root. |
|
|
61
72
|
| `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. |
|
|
62
73
|
| `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`). |
|
package/references/operations.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- If you edit token mapping logic, re-check the registry and deployer assumptions that feed it.
|
|
13
13
|
- If you edit token mapping semantics, verify that remapping is still impossible once outbox activity has made economic equivalence depend on permanence.
|
|
14
14
|
- If you edit deprecation or emergency paths, verify the intended operator workflow still works end to end.
|
|
15
|
-
- If you edit snapshot or claim-boundary logic, verify `numberOfClaimsSent`, peer snapshots, and emergency exit behavior together.
|
|
15
|
+
- If you edit snapshot or claim-boundary logic, verify `numberOfClaimsSent`, peer snapshots, `syncAccountingData`, and emergency exit behavior together.
|
|
16
16
|
- If you touch bridge-specific code, confirm whether the real bug is transport-side or shared accounting-side.
|
|
17
17
|
|
|
18
18
|
## Common failure modes
|
package/references/runtime.md
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
1. Local state is prepared into a claimable Merkle leaf.
|
|
13
13
|
2. A root is relayed to the peer chain through the bridge-specific transport.
|
|
14
|
-
3. The remote side records the root in its inbox state.
|
|
15
|
-
4.
|
|
14
|
+
3. The remote side records the root in its inbox state and stores the latest peer accounting snapshot.
|
|
15
|
+
4. Accounting-only snapshots can also be relayed or retried with `syncAccountingData` without sending a new root.
|
|
16
|
+
5. Claimants prove inclusion and recreate their position on the destination chain.
|
|
16
17
|
|
|
17
18
|
## High-risk areas
|
|
18
19
|
|
|
@@ -21,10 +22,11 @@
|
|
|
21
22
|
- Emergency and deprecation paths: these are operational safety surfaces that must remain reliable.
|
|
22
23
|
- Shared accounting vs transport logic: many incidents stem from confusing these layers.
|
|
23
24
|
- Peer snapshots and `numberOfClaimsSent`: these guard against double-spend at the cost of conservative locking when timing goes wrong.
|
|
25
|
+
- Accounting-only messages: these must never mutate token-local inbox roots or claimable value.
|
|
24
26
|
|
|
25
27
|
## Tests to trust first
|
|
26
28
|
|
|
27
29
|
- [`test/ForkMainnet.t.sol`](../test/ForkMainnet.t.sol), [`test/ForkArbitrum.t.sol`](../test/ForkArbitrum.t.sol), [`test/ForkCelo.t.sol`](../test/ForkCelo.t.sol), and [`test/ForkOPStack.t.sol`](../test/ForkOPStack.t.sol) for real transport assumptions.
|
|
28
30
|
- [`test/ForkSwap.t.sol`](../test/ForkSwap.t.sol), [`test/ForkClaimMainnet.t.sol`](../test/ForkClaimMainnet.t.sol), and [`test/SuckerRegressions.t.sol`](../test/SuckerRegressions.t.sol) for pinned cross-chain edge cases.
|
|
29
31
|
- [`test/unit/invariants.t.sol`](../test/unit/invariants.t.sol), [`test/unit/peer_chain_state.t.sol`](../test/unit/peer_chain_state.t.sol), and [`test/unit/registry.t.sol`](../test/unit/registry.t.sol) for shared-accounting invariants.
|
|
30
|
-
- [`test/SuckerAttacks.t.sol`](../test/SuckerAttacks.t.sol), [`test/SuckerDeepAttacks.t.sol`](../test/SuckerDeepAttacks.t.sol), [`test/regression/PeerSnapshotDesync.t.sol`](../test/regression/PeerSnapshotDesync.t.sol), and [`test/regression/PeerDeterminism.t.sol`](../test/regression/PeerDeterminism.t.sol) when the bug could involve base logic, registry behavior, or a specific bridge implementation.
|
|
32
|
+
- [`test/SuckerAttacks.t.sol`](../test/SuckerAttacks.t.sol), [`test/SuckerDeepAttacks.t.sol`](../test/SuckerDeepAttacks.t.sol), [`test/regression/PeerSnapshotDesync.t.sol`](../test/regression/PeerSnapshotDesync.t.sol), [`test/regression/CCIPUntypedMessageRejected.t.sol`](../test/regression/CCIPUntypedMessageRejected.t.sol), and [`test/regression/PeerDeterminism.t.sol`](../test/regression/PeerDeterminism.t.sol) when the bug could involve base logic, registry behavior, or a specific bridge implementation.
|
package/src/JBArbitrumSucker.sol
CHANGED
|
@@ -23,6 +23,7 @@ import {IArbL2GatewayRouter} from "./interfaces/IArbL2GatewayRouter.sol";
|
|
|
23
23
|
import {IL1ArbitrumGateway} from "./interfaces/IL1ArbitrumGateway.sol";
|
|
24
24
|
import {IJBArbitrumSucker} from "./interfaces/IJBArbitrumSucker.sol";
|
|
25
25
|
import {ARBChains} from "./libraries/ARBChains.sol";
|
|
26
|
+
import {JBAccountingSnapshot} from "./structs/JBAccountingSnapshot.sol";
|
|
26
27
|
import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
|
|
27
28
|
import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
|
|
28
29
|
|
|
@@ -75,7 +76,7 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
//*********************************************************************//
|
|
78
|
-
//
|
|
79
|
+
// ------------------------- public views ---------------------------- //
|
|
79
80
|
//*********************************************************************//
|
|
80
81
|
|
|
81
82
|
/// @notice Returns the chain on which the peer is located.
|
|
@@ -90,31 +91,26 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
//*********************************************************************//
|
|
93
|
-
//
|
|
94
|
+
// --------------------- internal transactions ----------------------- //
|
|
94
95
|
//*********************************************************************//
|
|
95
96
|
|
|
96
|
-
/// @notice
|
|
97
|
-
/// @param
|
|
98
|
-
/// @
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return sender == AddressAliasHelper.applyL1ToL2Alias(peerAddress);
|
|
97
|
+
/// @notice Approves the Arbitrum gateway to spend `amount` of `approvalToken`.
|
|
98
|
+
/// @param approvalToken The ERC-20 token to approve.
|
|
99
|
+
/// @param gatewayLookupToken The token key used by the gateway router to resolve the spender.
|
|
100
|
+
/// @param amount The amount to approve.
|
|
101
|
+
/// @return gateway The gateway that was approved.
|
|
102
|
+
function _approveGateway(
|
|
103
|
+
address approvalToken,
|
|
104
|
+
address gatewayLookupToken,
|
|
105
|
+
uint256 amount
|
|
106
|
+
)
|
|
107
|
+
internal
|
|
108
|
+
returns (address gateway)
|
|
109
|
+
{
|
|
110
|
+
gateway = GATEWAYROUTER.getGateway(gatewayLookupToken);
|
|
111
|
+
SafeERC20.forceApprove({token: IERC20(approvalToken), spender: gateway, value: amount});
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
//*********************************************************************//
|
|
115
|
-
// --------------------- internal transactions ----------------------- //
|
|
116
|
-
//*********************************************************************//
|
|
117
|
-
|
|
118
114
|
/// @notice Helper to create the retryable ticket, avoiding stack-too-deep.
|
|
119
115
|
function _createRetryableTicket(
|
|
120
116
|
uint256 callTransportCost,
|
|
@@ -138,13 +134,37 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
138
134
|
});
|
|
139
135
|
}
|
|
140
136
|
|
|
141
|
-
/// @notice
|
|
142
|
-
/// @param
|
|
143
|
-
/// @param
|
|
144
|
-
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
137
|
+
/// @notice Uses the L1/L2 message bridge to send accounting data over the bridge to the peer.
|
|
138
|
+
/// @param transportPayment The amount of `msg.value` that is going to get paid for sending this message.
|
|
139
|
+
/// @param snapshot The accounting snapshot to send to the remote peer.
|
|
140
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
141
|
+
function _sendAccountingSnapshotOverAMB(
|
|
142
|
+
uint256 transportPayment,
|
|
143
|
+
JBAccountingSnapshot memory snapshot
|
|
144
|
+
)
|
|
145
|
+
internal
|
|
146
|
+
override
|
|
147
|
+
{
|
|
148
|
+
// Build the calldata that will be sent to the peer. This calls `JBSucker.fromRemoteAccounting` remotely.
|
|
149
|
+
bytes memory data = abi.encodeCall(JBSucker.fromRemoteAccounting, (snapshot));
|
|
150
|
+
JBRemoteToken memory remoteToken;
|
|
151
|
+
|
|
152
|
+
// Depending on which layer we are on, send the call to the other layer.
|
|
153
|
+
if (LAYER == JBLayer.L1) {
|
|
154
|
+
// L1→L2 requires transport payment for retryable tickets.
|
|
155
|
+
if (transportPayment == 0) revert JBSucker_ExpectedMsgValue({msgValue: transportPayment});
|
|
156
|
+
_toL2({
|
|
157
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
158
|
+
transportPayment: transportPayment,
|
|
159
|
+
amount: 0,
|
|
160
|
+
data: data,
|
|
161
|
+
remoteToken: remoteToken
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
// L2→L1 via ArbSys is free — reject any transport payment.
|
|
165
|
+
if (transportPayment != 0) revert JBSucker_UnexpectedMsgValue(transportPayment);
|
|
166
|
+
_toL1({token: JBConstants.NATIVE_TOKEN, amount: 0, data: data, remoteToken: remoteToken});
|
|
167
|
+
}
|
|
148
168
|
}
|
|
149
169
|
|
|
150
170
|
/// @notice Uses the L1/L2 gateway to send the root and assets over the bridge to the peer.
|
|
@@ -202,7 +222,9 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
202
222
|
// If the token is an ERC-20, bridge it to the peer.
|
|
203
223
|
// If the amount is `0` then we do not need to bridge any ERC20.
|
|
204
224
|
if (token != JBConstants.NATIVE_TOKEN && amount != 0) {
|
|
205
|
-
address gateway = _approveGateway({
|
|
225
|
+
address gateway = _approveGateway({
|
|
226
|
+
approvalToken: token, gatewayLookupToken: _toAddress(remoteToken.addr), amount: amount
|
|
227
|
+
});
|
|
206
228
|
|
|
207
229
|
// Convert bytes32 types to address at the Arbitrum bridge API boundary.
|
|
208
230
|
IArbL2GatewayRouter(address(GATEWAYROUTER))
|
|
@@ -291,7 +313,7 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
291
313
|
}
|
|
292
314
|
|
|
293
315
|
// Approve the tokens to be bridged.
|
|
294
|
-
_approveGateway({token: token, amount: amount});
|
|
316
|
+
_approveGateway({approvalToken: token, gatewayLookupToken: token, amount: amount});
|
|
295
317
|
|
|
296
318
|
// Perform the ERC-20 bridge transfer. Convert bytes32 peer to address at the Arbitrum bridge API boundary.
|
|
297
319
|
IArbL1GatewayRouter(address(GATEWAYROUTER)).outboundTransferCustomRefund{value: tokenTransportCost}({
|
|
@@ -332,4 +354,26 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
332
354
|
data: data
|
|
333
355
|
});
|
|
334
356
|
}
|
|
357
|
+
|
|
358
|
+
//*********************************************************************//
|
|
359
|
+
// ------------------------ internal views --------------------------- //
|
|
360
|
+
//*********************************************************************//
|
|
361
|
+
|
|
362
|
+
/// @notice Checks if the `sender` (`_msgSender()`) is a valid representative of the remote peer.
|
|
363
|
+
/// @param sender The message's sender.
|
|
364
|
+
/// @return valid A flag if the sender is a valid representative of the remote peer.
|
|
365
|
+
function _isRemotePeer(address sender) internal view override returns (bool) {
|
|
366
|
+
// Convert the bytes32 peer to an address for comparison with EVM bridge contracts.
|
|
367
|
+
address peerAddress = _peerAddress();
|
|
368
|
+
|
|
369
|
+
// If we are the L1 peer,
|
|
370
|
+
if (LAYER == JBLayer.L1) {
|
|
371
|
+
IBridge bridge = ARBINBOX.bridge();
|
|
372
|
+
// Check that the sender is the bridge and that the outbox has our peer as the sender.
|
|
373
|
+
return sender == address(bridge) && peerAddress == IOutbox(bridge.activeOutbox()).l2ToL1Sender();
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// If we are the L2 peer, check using the `AddressAliasHelper`.
|
|
377
|
+
return sender == AddressAliasHelper.applyL1ToL2Alias(peerAddress);
|
|
378
|
+
}
|
|
335
379
|
}
|
package/src/JBBaseSucker.sol
CHANGED
|
@@ -32,7 +32,7 @@ contract JBBaseSucker is JBOptimismSucker {
|
|
|
32
32
|
{}
|
|
33
33
|
|
|
34
34
|
//*********************************************************************//
|
|
35
|
-
//
|
|
35
|
+
// ------------------------- public views ---------------------------- //
|
|
36
36
|
//*********************************************************************//
|
|
37
37
|
|
|
38
38
|
/// @notice Returns the chain on which the peer is located.
|
package/src/JBCCIPSucker.sol
CHANGED
|
@@ -24,6 +24,7 @@ import {CCIPHelper} from "./libraries/CCIPHelper.sol";
|
|
|
24
24
|
import {JBCCIPLib} from "./libraries/JBCCIPLib.sol";
|
|
25
25
|
|
|
26
26
|
// Local: structs (alphabetized)
|
|
27
|
+
import {JBAccountingSnapshot} from "./structs/JBAccountingSnapshot.sol";
|
|
27
28
|
import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
|
|
28
29
|
import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
|
|
29
30
|
import {JBTokenMapping} from "./structs/JBTokenMapping.sol";
|
|
@@ -70,9 +71,15 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
70
71
|
// ----------------------- internal constants ------------------------ //
|
|
71
72
|
//*********************************************************************//
|
|
72
73
|
|
|
74
|
+
/// @notice Message type prefix for accounting-only messages (fromRemoteAccounting).
|
|
75
|
+
uint8 internal constant _CCIP_MSG_TYPE_ACCOUNTING = 1;
|
|
76
|
+
|
|
73
77
|
/// @notice Message type prefix for root messages (fromRemote).
|
|
74
78
|
uint8 internal constant _CCIP_MSG_TYPE_ROOT = 0;
|
|
75
79
|
|
|
80
|
+
/// @notice Extra destination gas budgeted for each source accounting context carried in a CCIP message.
|
|
81
|
+
uint256 internal constant _CCIP_SOURCE_CONTEXT_GAS_LIMIT = 75_000;
|
|
82
|
+
|
|
76
83
|
//*********************************************************************//
|
|
77
84
|
// --------------- public immutable stored properties ---------------- //
|
|
78
85
|
//*********************************************************************//
|
|
@@ -121,39 +128,6 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
121
128
|
if (address(CCIP_ROUTER) == address(0)) revert JBCCIPSucker_InvalidRouter(address(CCIP_ROUTER));
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
//*********************************************************************//
|
|
125
|
-
// ------------------------ external views --------------------------- //
|
|
126
|
-
//*********************************************************************//
|
|
127
|
-
|
|
128
|
-
/// @notice Returns the chain on which the peer is located.
|
|
129
|
-
/// @return chainId The chain ID of the peer.
|
|
130
|
-
function peerChainId() public view virtual override returns (uint256 chainId) {
|
|
131
|
-
return REMOTE_CHAIN_ID;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
//*********************************************************************//
|
|
135
|
-
// ------------------------- public views ---------------------------- //
|
|
136
|
-
//*********************************************************************//
|
|
137
|
-
|
|
138
|
-
/// @notice Returns the address of the current CCIP router.
|
|
139
|
-
/// @return router The CCIP router address.
|
|
140
|
-
function getRouter() public view returns (address router) {
|
|
141
|
-
return address(CCIP_ROUTER);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/// @notice Checks whether this contract supports a given interface.
|
|
145
|
-
/// @param interfaceId The interface ID to check.
|
|
146
|
-
/// @return supported Whether the interface is supported.
|
|
147
|
-
/// @dev Should indicate whether the contract implements IAny2EVMMessageReceiver.
|
|
148
|
-
/// This allows CCIP to check if ccipReceive is available before calling it.
|
|
149
|
-
/// If this returns false or reverts, only tokens are transferred to the receiver.
|
|
150
|
-
/// If this returns true, tokens are transferred and ccipReceive is called atomically.
|
|
151
|
-
/// Additionally, if the receiver address does not have code associated with
|
|
152
|
-
/// it at the time of execution (EXTCODESIZE returns 0), only tokens will be transferred.
|
|
153
|
-
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool supported) {
|
|
154
|
-
return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || super.supportsInterface(interfaceId);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
131
|
//*********************************************************************//
|
|
158
132
|
// --------------------- external transactions ----------------------- //
|
|
159
133
|
//*********************************************************************//
|
|
@@ -180,7 +154,7 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
180
154
|
}
|
|
181
155
|
|
|
182
156
|
// Discriminate message type: abi.encode(uint8 type, bytes payload).
|
|
183
|
-
(uint8 messageType, bytes memory payload) =
|
|
157
|
+
(uint8 messageType, bytes memory payload) = abi.decode(any2EvmMessage.data, (uint8, bytes));
|
|
184
158
|
|
|
185
159
|
// Handle root messages (merkle tree updates with bridged assets).
|
|
186
160
|
if (messageType == _CCIP_MSG_TYPE_ROOT) {
|
|
@@ -231,15 +205,119 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
231
205
|
|
|
232
206
|
// Forward the root message to this contract's fromRemote handler.
|
|
233
207
|
this.fromRemote(root);
|
|
208
|
+
} else if (messageType == _CCIP_MSG_TYPE_ACCOUNTING) {
|
|
209
|
+
// Accounting-only messages must not carry tokens; transported value belongs exclusively to root messages.
|
|
210
|
+
uint256 deliveryCount = any2EvmMessage.destTokenAmounts.length;
|
|
211
|
+
if (deliveryCount != 0) {
|
|
212
|
+
revert JBCCIPSucker_UnexpectedDeliveredTokens(deliveryCount);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
JBAccountingSnapshot memory snapshot = abi.decode(payload, (JBAccountingSnapshot));
|
|
216
|
+
|
|
217
|
+
// Forward the accounting message to this contract's authenticated accounting handler.
|
|
218
|
+
this.fromRemoteAccounting(snapshot);
|
|
234
219
|
} else {
|
|
235
220
|
revert JBCCIPSucker_UnknownMessageType({messageType: messageType});
|
|
236
221
|
}
|
|
237
222
|
}
|
|
238
223
|
|
|
224
|
+
//*********************************************************************//
|
|
225
|
+
// ------------------------- public views ---------------------------- //
|
|
226
|
+
//*********************************************************************//
|
|
227
|
+
|
|
228
|
+
/// @notice Returns the address of the current CCIP router.
|
|
229
|
+
/// @return router The CCIP router address.
|
|
230
|
+
function getRouter() public view returns (address router) {
|
|
231
|
+
return address(CCIP_ROUTER);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/// @notice Returns the chain on which the peer is located.
|
|
235
|
+
/// @return chainId The chain ID of the peer.
|
|
236
|
+
function peerChainId() public view virtual override returns (uint256 chainId) {
|
|
237
|
+
return REMOTE_CHAIN_ID;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// @notice Checks whether this contract supports a given interface.
|
|
241
|
+
/// @param interfaceId The interface ID to check.
|
|
242
|
+
/// @return supported Whether the interface is supported.
|
|
243
|
+
/// @dev Should indicate whether the contract implements IAny2EVMMessageReceiver.
|
|
244
|
+
/// This allows CCIP to check if ccipReceive is available before calling it.
|
|
245
|
+
/// If this returns false or reverts, only tokens are transferred to the receiver.
|
|
246
|
+
/// If this returns true, tokens are transferred and ccipReceive is called atomically.
|
|
247
|
+
/// Additionally, if the receiver address does not have code associated with
|
|
248
|
+
/// it at the time of execution (EXTCODESIZE returns 0), only tokens will be transferred.
|
|
249
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool supported) {
|
|
250
|
+
return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || super.supportsInterface(interfaceId);
|
|
251
|
+
}
|
|
252
|
+
|
|
239
253
|
//*********************************************************************//
|
|
240
254
|
// --------------------- internal transactions ----------------------- //
|
|
241
255
|
//*********************************************************************//
|
|
242
256
|
|
|
257
|
+
/// @notice Uses CCIP to send accounting data over the bridge to the peer.
|
|
258
|
+
/// @dev Supports the same native/LINK fee modes as root messages, but never transports token amounts.
|
|
259
|
+
/// @param transportPayment The amount of `msg.value` that is going to get paid for sending this message.
|
|
260
|
+
/// @param snapshot The accounting snapshot to send to the remote peer.
|
|
261
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
262
|
+
function _sendAccountingSnapshotOverAMB(
|
|
263
|
+
uint256 transportPayment,
|
|
264
|
+
JBAccountingSnapshot memory snapshot
|
|
265
|
+
)
|
|
266
|
+
internal
|
|
267
|
+
virtual
|
|
268
|
+
override
|
|
269
|
+
{
|
|
270
|
+
_sendCcipMessage({
|
|
271
|
+
transportPayment: transportPayment,
|
|
272
|
+
gasLimit: _ccipGasLimitFor({sourceContextCount: snapshot.sourceContexts.length}),
|
|
273
|
+
encodedPayload: abi.encode(_CCIP_MSG_TYPE_ACCOUNTING, abi.encode(snapshot)),
|
|
274
|
+
tokenAmounts: new Client.EVMTokenAmount[](0)
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/// @notice Sends a CCIP message and records failed native-fee refunds as caller credit.
|
|
279
|
+
/// @param transportPayment The amount of `msg.value` available to pay native CCIP fees.
|
|
280
|
+
/// @param gasLimit The destination gas limit to ask CCIP to provide.
|
|
281
|
+
/// @param encodedPayload The typed CCIP payload to send to the peer sucker.
|
|
282
|
+
/// @param tokenAmounts The token amounts to bridge with the message.
|
|
283
|
+
function _sendCcipMessage(
|
|
284
|
+
uint256 transportPayment,
|
|
285
|
+
uint256 gasLimit,
|
|
286
|
+
bytes memory encodedPayload,
|
|
287
|
+
Client.EVMTokenAmount[] memory tokenAmounts
|
|
288
|
+
)
|
|
289
|
+
internal
|
|
290
|
+
{
|
|
291
|
+
// Cache the caller so refund accounting and LINK fee pulls are charged to the same account.
|
|
292
|
+
address sender = _msgSender();
|
|
293
|
+
|
|
294
|
+
// Determine fee payment mode: native ETH or LINK token.
|
|
295
|
+
// When transportPayment == 0, we pay in LINK pulled from the caller via transferFrom.
|
|
296
|
+
// This enables chains with no meaningful native token (e.g. Tempo) while keeping
|
|
297
|
+
// toRemote permissionless — the caller provides LINK inline with their bridge intent.
|
|
298
|
+
address feeToken = transportPayment == 0 ? CCIPHelper.linkOfChain(block.chainid) : address(0);
|
|
299
|
+
|
|
300
|
+
// Build and send the CCIP message with the provided typed payload.
|
|
301
|
+
(bool refundFailed, uint256 refundAmount) = JBCCIPLib.sendCCIPMessage({
|
|
302
|
+
ccipRouter: CCIP_ROUTER,
|
|
303
|
+
remoteChainSelector: REMOTE_CHAIN_SELECTOR,
|
|
304
|
+
peerAddress: _peerAddress(),
|
|
305
|
+
transportPayment: transportPayment,
|
|
306
|
+
feeToken: feeToken,
|
|
307
|
+
feeTokenPayer: feeToken != address(0) ? sender : address(0),
|
|
308
|
+
gasLimit: gasLimit,
|
|
309
|
+
encodedPayload: encodedPayload,
|
|
310
|
+
tokenAmounts: tokenAmounts,
|
|
311
|
+
refundRecipient: sender
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Retain failed refunds as caller credit instead of leaving them project-addable or stranded.
|
|
315
|
+
if (refundFailed) {
|
|
316
|
+
_retainTransportPaymentRefund({account: sender, amount: refundAmount});
|
|
317
|
+
emit TransportPaymentRefundFailed({recipient: sender, amount: refundAmount, caller: sender});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
243
321
|
/// @notice Uses CCIP to send the root and assets over the bridge to the peer.
|
|
244
322
|
/// @dev Delegates CCIP message construction and sending to JBCCIPLib (via DELEGATECALL) to reduce bytecode.
|
|
245
323
|
/// @dev Supports two fee modes:
|
|
@@ -264,8 +342,8 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
264
342
|
virtual
|
|
265
343
|
override
|
|
266
344
|
{
|
|
267
|
-
//
|
|
268
|
-
uint256 gasLimit =
|
|
345
|
+
// Budget for the root receiver plus the accounting contexts carried in the root message.
|
|
346
|
+
uint256 gasLimit = _ccipGasLimitFor({sourceContextCount: suckerMessage.sourceContexts.length});
|
|
269
347
|
Client.EVMTokenAmount[] memory tokenAmounts;
|
|
270
348
|
|
|
271
349
|
if (amount != 0) {
|
|
@@ -279,38 +357,25 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
279
357
|
tokenAmounts = new Client.EVMTokenAmount[](0);
|
|
280
358
|
}
|
|
281
359
|
|
|
282
|
-
|
|
283
|
-
// When transportPayment == 0, we pay in LINK pulled from the caller via transferFrom.
|
|
284
|
-
// This enables chains with no meaningful native token (e.g. Tempo) while keeping
|
|
285
|
-
// toRemote permissionless — the caller provides LINK inline with their bridge intent.
|
|
286
|
-
address feeToken = transportPayment == 0 ? CCIPHelper.linkOfChain(block.chainid) : address(0);
|
|
287
|
-
|
|
288
|
-
// Build and send the CCIP message with the root payload.
|
|
289
|
-
(bool refundFailed, uint256 refundAmount) = JBCCIPLib.sendCCIPMessage({
|
|
290
|
-
ccipRouter: CCIP_ROUTER,
|
|
291
|
-
remoteChainSelector: REMOTE_CHAIN_SELECTOR,
|
|
292
|
-
peerAddress: _peerAddress(),
|
|
360
|
+
_sendCcipMessage({
|
|
293
361
|
transportPayment: transportPayment,
|
|
294
|
-
feeToken: feeToken,
|
|
295
|
-
feeTokenPayer: feeToken != address(0) ? _msgSender() : address(0),
|
|
296
362
|
gasLimit: gasLimit,
|
|
297
363
|
encodedPayload: abi.encode(_CCIP_MSG_TYPE_ROOT, abi.encode(suckerMessage)),
|
|
298
|
-
tokenAmounts: tokenAmounts
|
|
299
|
-
refundRecipient: _msgSender()
|
|
364
|
+
tokenAmounts: tokenAmounts
|
|
300
365
|
});
|
|
301
|
-
|
|
302
|
-
// Retain failed refunds as caller credit instead of leaving them project-addable or stranded.
|
|
303
|
-
if (refundFailed) {
|
|
304
|
-
// Refund accounting is isolated per caller; reentry cannot increase the retained credit.
|
|
305
|
-
_retainTransportPaymentRefund({account: _msgSender(), amount: refundAmount});
|
|
306
|
-
emit TransportPaymentRefundFailed({recipient: _msgSender(), amount: refundAmount, caller: _msgSender()});
|
|
307
|
-
}
|
|
308
366
|
}
|
|
309
367
|
|
|
310
368
|
//*********************************************************************//
|
|
311
369
|
// ------------------------ internal views --------------------------- //
|
|
312
370
|
//*********************************************************************//
|
|
313
371
|
|
|
372
|
+
/// @notice The CCIP destination gas limit for a message carrying `sourceContextCount` accounting contexts.
|
|
373
|
+
/// @param sourceContextCount The number of source accounting contexts in the message.
|
|
374
|
+
/// @return gasLimit The destination gas limit to ask CCIP to provide.
|
|
375
|
+
function _ccipGasLimitFor(uint256 sourceContextCount) internal pure returns (uint256 gasLimit) {
|
|
376
|
+
return MESSENGER_BASE_GAS_LIMIT + (sourceContextCount * _CCIP_SOURCE_CONTEXT_GAS_LIMIT);
|
|
377
|
+
}
|
|
378
|
+
|
|
314
379
|
/// @notice Checks whether the given sender is a remote peer. Unused in this context.
|
|
315
380
|
/// @param sender The address to check.
|
|
316
381
|
/// @return _valid Whether the sender is a remote peer.
|
package/src/JBOptimismSucker.sol
CHANGED
|
@@ -14,6 +14,7 @@ import {IJBSuckerRegistry} from "./interfaces/IJBSuckerRegistry.sol";
|
|
|
14
14
|
import {IJBOptimismSucker} from "./interfaces/IJBOptimismSucker.sol";
|
|
15
15
|
import {IOPMessenger} from "./interfaces/IOPMessenger.sol";
|
|
16
16
|
import {IOPStandardBridge} from "./interfaces/IOPStandardBridge.sol";
|
|
17
|
+
import {JBAccountingSnapshot} from "./structs/JBAccountingSnapshot.sol";
|
|
17
18
|
import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
|
|
18
19
|
import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
|
|
19
20
|
|
|
@@ -56,7 +57,7 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
//*********************************************************************//
|
|
59
|
-
//
|
|
60
|
+
// ------------------------- public views ---------------------------- //
|
|
60
61
|
//*********************************************************************//
|
|
61
62
|
|
|
62
63
|
/// @notice Returns the chain on which the peer is located.
|
|
@@ -81,6 +82,30 @@ contract JBOptimismSucker is JBSucker, IJBOptimismSucker {
|
|
|
81
82
|
return sender == address(OPMESSENGER) && _toBytes32(OPMESSENGER.xDomainMessageSender()) == peer();
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
/// @notice Uses the OP messenger to send accounting data over the bridge to the peer.
|
|
86
|
+
/// @param transportPayment The amount of `msg.value` that is going to get paid for sending this message.
|
|
87
|
+
/// @param snapshot The accounting snapshot to send to the remote peer.
|
|
88
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
89
|
+
function _sendAccountingSnapshotOverAMB(
|
|
90
|
+
uint256 transportPayment,
|
|
91
|
+
JBAccountingSnapshot memory snapshot
|
|
92
|
+
)
|
|
93
|
+
internal
|
|
94
|
+
virtual
|
|
95
|
+
override
|
|
96
|
+
{
|
|
97
|
+
// The OP messenger does not expect native transport payment for accounting-only messages.
|
|
98
|
+
if (transportPayment != 0) {
|
|
99
|
+
revert JBSucker_UnexpectedMsgValue({value: transportPayment});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
OPMESSENGER.sendMessage({
|
|
103
|
+
target: _toAddress(peer()),
|
|
104
|
+
message: abi.encodeCall(JBSucker.fromRemoteAccounting, (snapshot)),
|
|
105
|
+
gasLimit: MESSENGER_BASE_GAS_LIMIT
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
84
109
|
/// @notice Use the `OPMESSENGER` to send the outbox tree for the `token` and the corresponding funds to the peer
|
|
85
110
|
/// over the `OPBRIDGE`.
|
|
86
111
|
/// @param transportPayment the amount of `msg.value` that is going to get paid for sending this message.
|