@bananapus/suckers-v6 0.0.77 → 0.0.79
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 +9 -4
- package/foundry.toml +2 -0
- package/package.json +1 -1
- package/references/entrypoints.md +31 -4
- package/references/operations.md +1 -1
- package/references/runtime.md +5 -3
- package/src/JBArbitrumSucker.sol +92 -36
- package/src/JBBaseSucker.sol +1 -1
- package/src/JBCCIPSucker.sol +114 -58
- package/src/JBOptimismSucker.sol +30 -2
- package/src/JBSucker.sol +445 -299
- package/src/JBSuckerRegistry.sol +375 -138
- package/src/interfaces/IJBSucker.sol +58 -24
- package/src/interfaces/IJBSuckerRegistry.sol +23 -8
- package/src/libraries/JBSuckerLib.sol +237 -16
- package/src/structs/JBAccountingSnapshot.sol +17 -0
- package/src/structs/JBChainAccounting.sol +32 -0
- package/src/structs/JBMessageRoot.sol +12 -16
- package/src/structs/PeerAccountScratch.sol +18 -0
- package/src/structs/RemoteValueParams.sol +15 -0
package/README.md
CHANGED
|
@@ -22,7 +22,8 @@ The codebase includes multiple bridge variants, but the canonical deployment and
|
|
|
22
22
|
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
|
-
- anyone can relay the current root to the peer chain with `toRemote
|
|
25
|
+
- anyone can relay the current root to the peer chain with `toRemote`, which also gossips the project's per-source-chain accounting bundle
|
|
26
|
+
- anyone can refresh or retry the accounting gossip bundle 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 a per-source-chain accounting store fresh enough for cross-chain estimates, gossiping every chain's record across the sucker mesh
|
|
48
50
|
|
|
49
51
|
That means every bridge path has two trust surfaces:
|
|
50
52
|
|
|
@@ -64,7 +66,9 @@ That means every bridge path has two trust surfaces:
|
|
|
64
66
|
- do not reason about suckers as if they were generic ERC-20 bridges
|
|
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
|
-
- 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
|
|
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 — and this merge applies per source chain, since each chain's record is stored and folded on its own
|
|
70
|
+
- accounting propagates as a gossip bundle: a sucker sends its own chain's record plus every peer-chain record the project knows (gathered through the registry), so one sync round from a hub propagates every chain's data to every spoke without a direct sucker between each pair
|
|
71
|
+
- `syncAccountingData` pays no registry `toRemoteFee`, but bridge-specific transport costs still apply and duplicate bundles can still consume bridge/indexer resources
|
|
68
72
|
- emergency and deprecation paths are part of normal operational safety
|
|
69
73
|
|
|
70
74
|
## Where state lives
|
|
@@ -80,6 +84,7 @@ That means every bridge path has two trust surfaces:
|
|
|
80
84
|
3. `test/ForkClaimMainnet.t.sol`
|
|
81
85
|
4. `test/regression/PeerSnapshotDesync.t.sol`
|
|
82
86
|
5. `test/regression/ToRemoteFeeIrrecoverable.t.sol`
|
|
87
|
+
6. `test/regression/CCIPUntypedMessageRejected.t.sol`
|
|
83
88
|
|
|
84
89
|
## Install
|
|
85
90
|
|
package/foundry.toml
CHANGED
|
@@ -3,6 +3,8 @@ solc = '0.8.28'
|
|
|
3
3
|
bytecode_hash = "none"
|
|
4
4
|
evm_version = 'cancun'
|
|
5
5
|
optimizer_runs = 200
|
|
6
|
+
# The IR pipeline keeps the per-chain accounting suckers (Arbitrum, CCIP) under the EIP-170 runtime size limit.
|
|
7
|
+
via_ir = true
|
|
6
8
|
libs = ["node_modules", "lib"]
|
|
7
9
|
fs_permissions = [{ access = "read-write", path = "./"}]
|
|
8
10
|
# Archived, unused contracts (e.g. JBSwapCCIPSucker) and their tests live under `archive/` folders and are
|
package/package.json
CHANGED
|
@@ -49,6 +49,26 @@ 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
|
+
A cross-chain accounting gossip bundle: the sending chain's own record plus every peer-chain record it currently holds.
|
|
55
|
+
|
|
56
|
+
| Field | Type | Meaning |
|
|
57
|
+
|-------|------|---------|
|
|
58
|
+
| `version` | `uint8` | Message format version. Must match `MESSAGE_VERSION`. |
|
|
59
|
+
| `accounts` | `JBChainAccounting[]` | One accounting record per source chain known to the sender (its own chain plus forwarded peers), each carrying its origin chain id and freshness key. The receiver stores the freshest record per source chain. |
|
|
60
|
+
|
|
61
|
+
The same `JBChainAccounting[] accounts` bundle is also carried by the root message `JBMessageRoot` (alongside `token`, `amount`, and `remoteRoot`), so a `toRemote` send propagates accounting too.
|
|
62
|
+
|
|
63
|
+
### `JBChainAccounting` (one source chain's record in a bundle)
|
|
64
|
+
|
|
65
|
+
| Field | Type | Meaning |
|
|
66
|
+
|-------|------|---------|
|
|
67
|
+
| `chainId` | `uint256` | The source chain this record describes. A receiver ignores a record for its own chain or chain 0. |
|
|
68
|
+
| `totalSupply` | `uint256` | Source-chain project-token supply, including reserved tokens. |
|
|
69
|
+
| `contexts` | `JBSourceContext[]` | Raw source-chain surplus and balance contexts, un-valued (in the source chain's own token addresses and decimals). |
|
|
70
|
+
| `timestamp` | `uint256` | Monotonic source-chain freshness key, gated independently per source chain. |
|
|
71
|
+
|
|
52
72
|
## Key functions
|
|
53
73
|
|
|
54
74
|
### JBSucker — bridge flow (`IJBSucker`)
|
|
@@ -57,6 +77,8 @@ A sucker bridges a Juicebox project's token economy between two chains. Cashed-o
|
|
|
57
77
|
|----------|--------------|
|
|
58
78
|
| `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
79
|
| `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`. |
|
|
80
|
+
| `syncAccountingData() payable` | Send the cross-chain accounting gossip bundle — this chain's own record plus every peer-chain record it holds (gathered across the project's suckers via the registry, minus the destination) — without sending an outbox root or paying the registry `toRemoteFee`. Can be retried with unchanged data; `payable` only funds bridge transport. |
|
|
81
|
+
| `fromRemoteAccounting(JBAccountingSnapshot calldata snapshot)` | Authenticated receive path for an accounting-only gossip bundle. Stores the freshest record per source chain, without touching any token-local inbox root. |
|
|
60
82
|
| `claim(JBClaim calldata claimData)` | Claim bridged project tokens for the leaf's beneficiary by proving inclusion against the inbox root. |
|
|
61
83
|
| `claim(JBClaim[] calldata claims)` | Claim multiple leaves in one call. Each leaf is routed through an external `this.claim` sub-call, so one failing leaf emits `ClaimFailed` and is reverted in isolation while the rest of the batch proceeds; the failed leaf stays claimable later. |
|
|
62
84
|
| `mapToken(JBTokenMapping calldata map) payable` | Map a single local token to a remote token for bridging. Mappings are immutable once the outbox tree has entries (can only be disabled, not remapped). Requires `MAP_SUCKER_TOKEN` permission (initial mappings are applied at deploy under `DEPLOY_SUCKERS`). |
|
|
@@ -86,8 +108,12 @@ A sucker bridges a Juicebox project's token economy between two chains. Cashed-o
|
|
|
86
108
|
| `outboxOf(address token)` | The outbox merkle tree (`JBOutboxTree`) for a token. |
|
|
87
109
|
| `amountToAddToBalanceOf(address token)` | Tokens received from bridging that are waiting to be added to the project's terminal balance. |
|
|
88
110
|
| `executedLeafHashOf(address token, uint256 index)` | The committed leaf hash at `(token, index)`, or `bytes32(0)` if unexecuted. Beneficiary contracts re-derive this to authenticate a settlement that a front-runner's direct `claim` already executed. |
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
111
|
+
| `peerChainIds(bool includeVirtual)` | The peer chains this sucker reports accounting for: its directly-connected peer, plus — when `includeVirtual` is true — every chain learned about through gossip. The registry aggregates the `includeVirtual: true` set. |
|
|
112
|
+
| `peerChainAccountsOf()` | The raw, un-valued `JBChainAccounting[]` record this sucker holds for every known peer chain. The registry reads this to gather a project's cross-chain knowledge and re-gossip it. |
|
|
113
|
+
| `peerChainContextsOf(uint256 chainId)` | Per-context surplus and balance for one peer chain, resolved to local currencies and folded at read time. Un-valued; returned with the chain's freshness key. |
|
|
114
|
+
| `peerChainTotalSupplyOf(uint256 chainId)` | The last-known total token supply on one peer chain (the registry sums these to compute effective cross-chain supply). |
|
|
115
|
+
| `peerChainTotalSupplyValue(uint256 chainId)` | One peer chain's total supply bundled with its chain id and freshness key (`JBPeerChainValue`). |
|
|
116
|
+
| `snapshotTimestampOf(uint256 chainId)` | The freshness key of the latest accepted record for one peer chain. |
|
|
91
117
|
| `retainedToRemoteFeeOf(address account)` | ETH owed to `account` from a failed `toRemote` fee payment. |
|
|
92
118
|
| `retainedTransportPaymentRefundOf(address account)` | ETH owed to `account` from a failed transport-payment refund. |
|
|
93
119
|
|
|
@@ -105,7 +131,8 @@ A sucker bridges a Juicebox project's token economy between two chains. Cashed-o
|
|
|
105
131
|
| `suckerPairsOf(uint256 projectId)` | The local/remote sucker pairs (`JBSuckersPair[]`) for a project. |
|
|
106
132
|
| `isSuckerOf(uint256 projectId, address addr)` | Whether `addr` is a registry-deployed sucker for the project. |
|
|
107
133
|
| `suckerDeployerIsAllowed(address deployer)` | Whether a deployer is on the allowlist. |
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
134
|
+
| `peerChainAccountsOf(uint256 projectId, uint256 exceptChainId)` | The freshest accounting record per source chain across all of a project's suckers, deduped and minus `exceptChainId` — the peer set a sucker forwards when re-gossiping. |
|
|
135
|
+
| `remoteTotalSupplyOf(uint256 projectId)` | Combined peer-chain total supply across all remote chains (aggregates every (sucker, chain) pair, deduped per source chain by freshest record). |
|
|
136
|
+
| `totalRemoteSurplusOf(uint256 projectId, uint256 currency, uint256 decimals)` | Combined peer-chain surplus valued into `currency`. Matching-currency contexts taken at par; a missing cross-currency feed skips that (sucker, chain) (conservative). |
|
|
110
137
|
| `totalRemoteBalanceOf(uint256 projectId, uint256 currency, uint256 decimals)` | Combined peer-chain balance valued into `currency`, same valuation rules as above. |
|
|
111
138
|
| `toRemoteFee()` / `MAX_TO_REMOTE_FEE()` | The current `toRemote` fee and its hardcoded ceiling. |
|
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,37 +91,34 @@ 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.
|
|
115
|
+
/// @param gasLimit The destination L2 gas to provision, scaled to the message's gossip bundle.
|
|
119
116
|
function _createRetryableTicket(
|
|
120
117
|
uint256 callTransportCost,
|
|
121
118
|
uint256 nativeValue,
|
|
122
119
|
uint256 maxSubmissionCost,
|
|
123
120
|
uint256 maxFeePerGas,
|
|
121
|
+
uint256 gasLimit,
|
|
124
122
|
bytes memory data
|
|
125
123
|
)
|
|
126
124
|
internal
|
|
@@ -132,19 +130,44 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
132
130
|
maxSubmissionCost: maxSubmissionCost,
|
|
133
131
|
excessFeeRefundAddress: _msgSender(),
|
|
134
132
|
callValueRefundAddress: peerAddress,
|
|
135
|
-
gasLimit:
|
|
133
|
+
gasLimit: gasLimit,
|
|
136
134
|
maxFeePerGas: maxFeePerGas,
|
|
137
135
|
data: data
|
|
138
136
|
});
|
|
139
137
|
}
|
|
140
138
|
|
|
141
|
-
/// @notice
|
|
142
|
-
/// @param
|
|
143
|
-
/// @param
|
|
144
|
-
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
139
|
+
/// @notice Uses the L1/L2 message bridge to send accounting data over the bridge to the peer.
|
|
140
|
+
/// @param transportPayment The amount of `msg.value` that is going to get paid for sending this message.
|
|
141
|
+
/// @param snapshot The accounting snapshot to send to the remote peer.
|
|
142
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
143
|
+
function _sendAccountingSnapshotOverAMB(
|
|
144
|
+
uint256 transportPayment,
|
|
145
|
+
JBAccountingSnapshot memory snapshot
|
|
146
|
+
)
|
|
147
|
+
internal
|
|
148
|
+
override
|
|
149
|
+
{
|
|
150
|
+
// Build the calldata that will be sent to the peer. This calls `JBSucker.fromRemoteAccounting` remotely.
|
|
151
|
+
bytes memory data = abi.encodeCall(JBSucker.fromRemoteAccounting, (snapshot));
|
|
152
|
+
JBRemoteToken memory remoteToken;
|
|
153
|
+
|
|
154
|
+
// Depending on which layer we are on, send the call to the other layer.
|
|
155
|
+
if (LAYER == JBLayer.L1) {
|
|
156
|
+
// L1→L2 requires transport payment for retryable tickets.
|
|
157
|
+
if (transportPayment == 0) revert JBSucker_ExpectedMsgValue({msgValue: transportPayment});
|
|
158
|
+
_toL2({
|
|
159
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
160
|
+
transportPayment: transportPayment,
|
|
161
|
+
amount: 0,
|
|
162
|
+
data: data,
|
|
163
|
+
remoteToken: remoteToken,
|
|
164
|
+
gasLimit: _messagingGasLimit({accounts: snapshot.accounts})
|
|
165
|
+
});
|
|
166
|
+
} else {
|
|
167
|
+
// L2→L1 via ArbSys is free — reject any transport payment.
|
|
168
|
+
if (transportPayment != 0) revert JBSucker_UnexpectedMsgValue(transportPayment);
|
|
169
|
+
_toL1({token: JBConstants.NATIVE_TOKEN, amount: 0, data: data, remoteToken: remoteToken});
|
|
170
|
+
}
|
|
148
171
|
}
|
|
149
172
|
|
|
150
173
|
/// @notice Uses the L1/L2 gateway to send the root and assets over the bridge to the peer.
|
|
@@ -171,7 +194,12 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
171
194
|
// L1→L2 requires transport payment for retryable tickets.
|
|
172
195
|
if (transportPayment == 0) revert JBSucker_ExpectedMsgValue({msgValue: transportPayment});
|
|
173
196
|
_toL2({
|
|
174
|
-
token: token,
|
|
197
|
+
token: token,
|
|
198
|
+
transportPayment: transportPayment,
|
|
199
|
+
amount: amount,
|
|
200
|
+
data: data,
|
|
201
|
+
remoteToken: remoteToken,
|
|
202
|
+
gasLimit: _messagingGasLimit({accounts: message.accounts})
|
|
175
203
|
});
|
|
176
204
|
} else {
|
|
177
205
|
// L2→L1 via ArbSys is free — reject any transport payment.
|
|
@@ -202,7 +230,9 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
202
230
|
// If the token is an ERC-20, bridge it to the peer.
|
|
203
231
|
// If the amount is `0` then we do not need to bridge any ERC20.
|
|
204
232
|
if (token != JBConstants.NATIVE_TOKEN && amount != 0) {
|
|
205
|
-
address gateway = _approveGateway({
|
|
233
|
+
address gateway = _approveGateway({
|
|
234
|
+
approvalToken: token, gatewayLookupToken: _toAddress(remoteToken.addr), amount: amount
|
|
235
|
+
});
|
|
206
236
|
|
|
207
237
|
// Convert bytes32 types to address at the Arbitrum bridge API boundary.
|
|
208
238
|
IArbL2GatewayRouter(address(GATEWAYROUTER))
|
|
@@ -233,12 +263,14 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
233
263
|
/// @param token The token to bridge.
|
|
234
264
|
/// @param amount The amount of tokens to bridge.
|
|
235
265
|
/// @param data The calldata to send to the remote chain. This calls `JBSucker.fromRemote` on the remote peer.
|
|
266
|
+
/// @param gasLimit The destination L2 gas to provision, scaled to the message's gossip bundle.
|
|
236
267
|
function _toL2(
|
|
237
268
|
address token,
|
|
238
269
|
uint256 transportPayment,
|
|
239
270
|
uint256 amount,
|
|
240
271
|
bytes memory data,
|
|
241
|
-
JBRemoteToken memory remoteToken
|
|
272
|
+
JBRemoteToken memory remoteToken,
|
|
273
|
+
uint256 gasLimit
|
|
242
274
|
)
|
|
243
275
|
internal
|
|
244
276
|
{
|
|
@@ -251,8 +283,9 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
251
283
|
maxSubmissionCost =
|
|
252
284
|
ARBINBOX.calculateRetryableSubmissionFee({dataLength: data.length, baseFee: maxFeePerGas});
|
|
253
285
|
|
|
254
|
-
// Tracks the cost for the call to the remote peer.
|
|
255
|
-
|
|
286
|
+
// Tracks the cost for the call to the remote peer. Scaled to the bundle so a larger mesh's accounting
|
|
287
|
+
// store does not run out of gas on the destination.
|
|
288
|
+
callTransportCost = maxSubmissionCost + (gasLimit * maxFeePerGas);
|
|
256
289
|
}
|
|
257
290
|
|
|
258
291
|
// If the token is an ERC-20, bridge it to the peer.
|
|
@@ -291,7 +324,7 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
291
324
|
}
|
|
292
325
|
|
|
293
326
|
// Approve the tokens to be bridged.
|
|
294
|
-
_approveGateway({token: token, amount: amount});
|
|
327
|
+
_approveGateway({approvalToken: token, gatewayLookupToken: token, amount: amount});
|
|
295
328
|
|
|
296
329
|
// Perform the ERC-20 bridge transfer. Convert bytes32 peer to address at the Arbitrum bridge API boundary.
|
|
297
330
|
IArbL1GatewayRouter(address(GATEWAYROUTER)).outboundTransferCustomRefund{value: tokenTransportCost}({
|
|
@@ -329,7 +362,30 @@ contract JBArbitrumSucker is JBSucker, IJBArbitrumSucker {
|
|
|
329
362
|
nativeValue: nativeValue,
|
|
330
363
|
maxSubmissionCost: maxSubmissionCost,
|
|
331
364
|
maxFeePerGas: maxFeePerGas,
|
|
365
|
+
gasLimit: gasLimit,
|
|
332
366
|
data: data
|
|
333
367
|
});
|
|
334
368
|
}
|
|
369
|
+
|
|
370
|
+
//*********************************************************************//
|
|
371
|
+
// ------------------------ internal views --------------------------- //
|
|
372
|
+
//*********************************************************************//
|
|
373
|
+
|
|
374
|
+
/// @notice Checks if the `sender` (`_msgSender()`) is a valid representative of the remote peer.
|
|
375
|
+
/// @param sender The message's sender.
|
|
376
|
+
/// @return valid A flag if the sender is a valid representative of the remote peer.
|
|
377
|
+
function _isRemotePeer(address sender) internal view override returns (bool) {
|
|
378
|
+
// Convert the bytes32 peer to an address for comparison with EVM bridge contracts.
|
|
379
|
+
address peerAddress = _peerAddress();
|
|
380
|
+
|
|
381
|
+
// If we are the L1 peer,
|
|
382
|
+
if (LAYER == JBLayer.L1) {
|
|
383
|
+
IBridge bridge = ARBINBOX.bridge();
|
|
384
|
+
// Check that the sender is the bridge and that the outbox has our peer as the sender.
|
|
385
|
+
return sender == address(bridge) && peerAddress == IOutbox(bridge.activeOutbox()).l2ToL1Sender();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// If we are the L2 peer, check using the `AddressAliasHelper`.
|
|
389
|
+
return sender == AddressAliasHelper.applyL1ToL2Alias(peerAddress);
|
|
390
|
+
}
|
|
335
391
|
}
|
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,8 @@ 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";
|
|
28
|
+
import {JBChainAccounting} from "./structs/JBChainAccounting.sol";
|
|
27
29
|
import {JBMessageRoot} from "./structs/JBMessageRoot.sol";
|
|
28
30
|
import {JBRemoteToken} from "./structs/JBRemoteToken.sol";
|
|
29
31
|
import {JBTokenMapping} from "./structs/JBTokenMapping.sol";
|
|
@@ -70,6 +72,9 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
70
72
|
// ----------------------- internal constants ------------------------ //
|
|
71
73
|
//*********************************************************************//
|
|
72
74
|
|
|
75
|
+
/// @notice Message type prefix for accounting-only messages (fromRemoteAccounting).
|
|
76
|
+
uint8 internal constant _CCIP_MSG_TYPE_ACCOUNTING = 1;
|
|
77
|
+
|
|
73
78
|
/// @notice Message type prefix for root messages (fromRemote).
|
|
74
79
|
uint8 internal constant _CCIP_MSG_TYPE_ROOT = 0;
|
|
75
80
|
|
|
@@ -121,39 +126,6 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
121
126
|
if (address(CCIP_ROUTER) == address(0)) revert JBCCIPSucker_InvalidRouter(address(CCIP_ROUTER));
|
|
122
127
|
}
|
|
123
128
|
|
|
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
129
|
//*********************************************************************//
|
|
158
130
|
// --------------------- external transactions ----------------------- //
|
|
159
131
|
//*********************************************************************//
|
|
@@ -180,7 +152,7 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
180
152
|
}
|
|
181
153
|
|
|
182
154
|
// Discriminate message type: abi.encode(uint8 type, bytes payload).
|
|
183
|
-
(uint8 messageType, bytes memory payload) =
|
|
155
|
+
(uint8 messageType, bytes memory payload) = abi.decode(any2EvmMessage.data, (uint8, bytes));
|
|
184
156
|
|
|
185
157
|
// Handle root messages (merkle tree updates with bridged assets).
|
|
186
158
|
if (messageType == _CCIP_MSG_TYPE_ROOT) {
|
|
@@ -231,15 +203,119 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
231
203
|
|
|
232
204
|
// Forward the root message to this contract's fromRemote handler.
|
|
233
205
|
this.fromRemote(root);
|
|
206
|
+
} else if (messageType == _CCIP_MSG_TYPE_ACCOUNTING) {
|
|
207
|
+
// Accounting-only messages must not carry tokens; transported value belongs exclusively to root messages.
|
|
208
|
+
uint256 deliveryCount = any2EvmMessage.destTokenAmounts.length;
|
|
209
|
+
if (deliveryCount != 0) {
|
|
210
|
+
revert JBCCIPSucker_UnexpectedDeliveredTokens(deliveryCount);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
JBAccountingSnapshot memory snapshot = abi.decode(payload, (JBAccountingSnapshot));
|
|
214
|
+
|
|
215
|
+
// Forward the accounting message to this contract's authenticated accounting handler.
|
|
216
|
+
this.fromRemoteAccounting(snapshot);
|
|
234
217
|
} else {
|
|
235
218
|
revert JBCCIPSucker_UnknownMessageType({messageType: messageType});
|
|
236
219
|
}
|
|
237
220
|
}
|
|
238
221
|
|
|
222
|
+
//*********************************************************************//
|
|
223
|
+
// ------------------------- public views ---------------------------- //
|
|
224
|
+
//*********************************************************************//
|
|
225
|
+
|
|
226
|
+
/// @notice Returns the address of the current CCIP router.
|
|
227
|
+
/// @return router The CCIP router address.
|
|
228
|
+
function getRouter() public view returns (address router) {
|
|
229
|
+
return address(CCIP_ROUTER);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// @notice Returns the chain on which the peer is located.
|
|
233
|
+
/// @return chainId The chain ID of the peer.
|
|
234
|
+
function peerChainId() public view virtual override returns (uint256 chainId) {
|
|
235
|
+
return REMOTE_CHAIN_ID;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// @notice Checks whether this contract supports a given interface.
|
|
239
|
+
/// @param interfaceId The interface ID to check.
|
|
240
|
+
/// @return supported Whether the interface is supported.
|
|
241
|
+
/// @dev Should indicate whether the contract implements IAny2EVMMessageReceiver.
|
|
242
|
+
/// This allows CCIP to check if ccipReceive is available before calling it.
|
|
243
|
+
/// If this returns false or reverts, only tokens are transferred to the receiver.
|
|
244
|
+
/// If this returns true, tokens are transferred and ccipReceive is called atomically.
|
|
245
|
+
/// Additionally, if the receiver address does not have code associated with
|
|
246
|
+
/// it at the time of execution (EXTCODESIZE returns 0), only tokens will be transferred.
|
|
247
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool supported) {
|
|
248
|
+
return interfaceId == type(IAny2EVMMessageReceiver).interfaceId || super.supportsInterface(interfaceId);
|
|
249
|
+
}
|
|
250
|
+
|
|
239
251
|
//*********************************************************************//
|
|
240
252
|
// --------------------- internal transactions ----------------------- //
|
|
241
253
|
//*********************************************************************//
|
|
242
254
|
|
|
255
|
+
/// @notice Uses CCIP to send accounting data over the bridge to the peer.
|
|
256
|
+
/// @dev Supports the same native/LINK fee modes as root messages, but never transports token amounts.
|
|
257
|
+
/// @param transportPayment The amount of `msg.value` that is going to get paid for sending this message.
|
|
258
|
+
/// @param snapshot The accounting snapshot to send to the remote peer.
|
|
259
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
260
|
+
function _sendAccountingSnapshotOverAMB(
|
|
261
|
+
uint256 transportPayment,
|
|
262
|
+
JBAccountingSnapshot memory snapshot
|
|
263
|
+
)
|
|
264
|
+
internal
|
|
265
|
+
virtual
|
|
266
|
+
override
|
|
267
|
+
{
|
|
268
|
+
_sendCcipMessage({
|
|
269
|
+
transportPayment: transportPayment,
|
|
270
|
+
gasLimit: _messagingGasLimit({accounts: snapshot.accounts}),
|
|
271
|
+
encodedPayload: abi.encode(_CCIP_MSG_TYPE_ACCOUNTING, abi.encode(snapshot)),
|
|
272
|
+
tokenAmounts: new Client.EVMTokenAmount[](0)
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// @notice Sends a CCIP message and records failed native-fee refunds as caller credit.
|
|
277
|
+
/// @param transportPayment The amount of `msg.value` available to pay native CCIP fees.
|
|
278
|
+
/// @param gasLimit The destination gas limit to ask CCIP to provide.
|
|
279
|
+
/// @param encodedPayload The typed CCIP payload to send to the peer sucker.
|
|
280
|
+
/// @param tokenAmounts The token amounts to bridge with the message.
|
|
281
|
+
function _sendCcipMessage(
|
|
282
|
+
uint256 transportPayment,
|
|
283
|
+
uint256 gasLimit,
|
|
284
|
+
bytes memory encodedPayload,
|
|
285
|
+
Client.EVMTokenAmount[] memory tokenAmounts
|
|
286
|
+
)
|
|
287
|
+
internal
|
|
288
|
+
{
|
|
289
|
+
// Cache the caller so refund accounting and LINK fee pulls are charged to the same account.
|
|
290
|
+
address sender = _msgSender();
|
|
291
|
+
|
|
292
|
+
// Determine fee payment mode: native ETH or LINK token.
|
|
293
|
+
// When transportPayment == 0, we pay in LINK pulled from the caller via transferFrom.
|
|
294
|
+
// This enables chains with no meaningful native token (e.g. Tempo) while keeping
|
|
295
|
+
// toRemote permissionless — the caller provides LINK inline with their bridge intent.
|
|
296
|
+
address feeToken = transportPayment == 0 ? CCIPHelper.linkOfChain(block.chainid) : address(0);
|
|
297
|
+
|
|
298
|
+
// Build and send the CCIP message with the provided typed payload.
|
|
299
|
+
(bool refundFailed, uint256 refundAmount) = JBCCIPLib.sendCCIPMessage({
|
|
300
|
+
ccipRouter: CCIP_ROUTER,
|
|
301
|
+
remoteChainSelector: REMOTE_CHAIN_SELECTOR,
|
|
302
|
+
peerAddress: _peerAddress(),
|
|
303
|
+
transportPayment: transportPayment,
|
|
304
|
+
feeToken: feeToken,
|
|
305
|
+
feeTokenPayer: feeToken != address(0) ? sender : address(0),
|
|
306
|
+
gasLimit: gasLimit,
|
|
307
|
+
encodedPayload: encodedPayload,
|
|
308
|
+
tokenAmounts: tokenAmounts,
|
|
309
|
+
refundRecipient: sender
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Retain failed refunds as caller credit instead of leaving them project-addable or stranded.
|
|
313
|
+
if (refundFailed) {
|
|
314
|
+
_retainTransportPaymentRefund({account: sender, amount: refundAmount});
|
|
315
|
+
emit TransportPaymentRefundFailed({recipient: sender, amount: refundAmount, caller: sender});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
243
319
|
/// @notice Uses CCIP to send the root and assets over the bridge to the peer.
|
|
244
320
|
/// @dev Delegates CCIP message construction and sending to JBCCIPLib (via DELEGATECALL) to reduce bytecode.
|
|
245
321
|
/// @dev Supports two fee modes:
|
|
@@ -264,8 +340,8 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
264
340
|
virtual
|
|
265
341
|
override
|
|
266
342
|
{
|
|
267
|
-
//
|
|
268
|
-
uint256 gasLimit =
|
|
343
|
+
// Budget for the root receiver plus the accounting contexts carried in the root message.
|
|
344
|
+
uint256 gasLimit = _messagingGasLimit({accounts: suckerMessage.accounts});
|
|
269
345
|
Client.EVMTokenAmount[] memory tokenAmounts;
|
|
270
346
|
|
|
271
347
|
if (amount != 0) {
|
|
@@ -279,32 +355,12 @@ contract JBCCIPSucker is JBSucker, IAny2EVMMessageReceiver {
|
|
|
279
355
|
tokenAmounts = new Client.EVMTokenAmount[](0);
|
|
280
356
|
}
|
|
281
357
|
|
|
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(),
|
|
358
|
+
_sendCcipMessage({
|
|
293
359
|
transportPayment: transportPayment,
|
|
294
|
-
feeToken: feeToken,
|
|
295
|
-
feeTokenPayer: feeToken != address(0) ? _msgSender() : address(0),
|
|
296
360
|
gasLimit: gasLimit,
|
|
297
361
|
encodedPayload: abi.encode(_CCIP_MSG_TYPE_ROOT, abi.encode(suckerMessage)),
|
|
298
|
-
tokenAmounts: tokenAmounts
|
|
299
|
-
refundRecipient: _msgSender()
|
|
362
|
+
tokenAmounts: tokenAmounts
|
|
300
363
|
});
|
|
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
364
|
}
|
|
309
365
|
|
|
310
366
|
//*********************************************************************//
|