@bananapus/suckers-v6 0.0.78 → 1.0.0
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 -5
- package/foundry.toml +2 -0
- package/package.json +3 -3
- package/references/entrypoints.md +25 -9
- package/src/JBArbitrumSucker.sol +18 -6
- package/src/JBCCIPSucker.sol +3 -12
- package/src/JBOptimismSucker.sol +5 -2
- package/src/JBSucker.sol +220 -158
- package/src/JBSuckerRegistry.sol +375 -138
- package/src/interfaces/IJBSucker.sol +44 -24
- package/src/interfaces/IJBSuckerRegistry.sol +23 -8
- package/src/libraries/JBSuckerLib.sol +215 -26
- package/src/structs/JBAccountingSnapshot.sol +9 -11
- 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
|
@@ -6,6 +6,7 @@ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
|
6
6
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
7
7
|
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
8
8
|
import {JBAccountingSnapshot} from "../structs/JBAccountingSnapshot.sol";
|
|
9
|
+
import {JBChainAccounting} from "../structs/JBChainAccounting.sol";
|
|
9
10
|
import {JBClaim} from "../structs/JBClaim.sol";
|
|
10
11
|
import {JBInboxTreeRoot} from "../structs/JBInboxTreeRoot.sol";
|
|
11
12
|
import {JBOutboxTree} from "../structs/JBOutboxTree.sol";
|
|
@@ -157,33 +158,51 @@ interface IJBSucker is IERC165 {
|
|
|
157
158
|
/// @return The peer address.
|
|
158
159
|
function peer() external view returns (bytes32);
|
|
159
160
|
|
|
160
|
-
/// @notice The
|
|
161
|
-
/// @
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
/// @
|
|
167
|
-
///
|
|
168
|
-
/// @
|
|
169
|
-
///
|
|
170
|
-
/// @
|
|
171
|
-
|
|
161
|
+
/// @notice The raw, un-valued accounting record this sucker holds for every peer chain it has heard about.
|
|
162
|
+
/// @dev The registry reads this to gather a project's full cross-chain knowledge and re-gossip it. Records are
|
|
163
|
+
/// returned exactly as received so the next receiver resolves them to its own local currencies independently.
|
|
164
|
+
/// @return accounts One raw accounting record per known peer chain.
|
|
165
|
+
function peerChainAccountsOf() external view returns (JBChainAccounting[] memory accounts);
|
|
166
|
+
|
|
167
|
+
/// @notice One peer chain's per-currency surplus and balance from its latest accepted record, plus its freshness
|
|
168
|
+
/// key.
|
|
169
|
+
/// @dev Un-valued — each context is in its own currency and decimals. The registry values each context into a
|
|
170
|
+
/// requested currency; the sucker consults no price oracle.
|
|
171
|
+
/// @param chainId The peer chain to read the contexts of.
|
|
172
|
+
/// @return contexts The per-currency surplus and balance for the chain.
|
|
173
|
+
/// @return snapshot The source freshness key of the chain's latest accepted record.
|
|
174
|
+
function peerChainContextsOf(uint256 chainId)
|
|
172
175
|
external
|
|
173
176
|
view
|
|
174
|
-
returns (JBPeerChainContext[] memory contexts, uint256
|
|
177
|
+
returns (JBPeerChainContext[] memory contexts, uint256 snapshot);
|
|
178
|
+
|
|
179
|
+
/// @notice The chain ID of the remote peer this sucker is directly paired with.
|
|
180
|
+
/// @return chainId The remote chain ID.
|
|
181
|
+
function peerChainId() external view returns (uint256 chainId);
|
|
175
182
|
|
|
176
|
-
/// @notice The
|
|
177
|
-
///
|
|
178
|
-
///
|
|
183
|
+
/// @notice The peer chains this sucker reports accounting for. With `includeVirtual` false, returns only the
|
|
184
|
+
/// directly-connected peer chain (the one it is bridged to); with `includeVirtual` true, also returns every chain
|
|
185
|
+
/// learned about through gossip relayed by that peer.
|
|
186
|
+
/// @dev The directly-connected peer is always present (with a zero value until its first record) so a
|
|
187
|
+
/// freshly-deployed active sucker immediately owns that chain's accounting during a migration window.
|
|
188
|
+
/// @param includeVirtual Whether to also include virtually-known (gossiped) peer chains.
|
|
189
|
+
/// @return chainIds The peer chain IDs.
|
|
190
|
+
function peerChainIds(bool includeVirtual) external view returns (uint256[] memory chainIds);
|
|
191
|
+
|
|
192
|
+
/// @notice The last known total token supply on a peer chain, updated each time a bridge message carries that
|
|
193
|
+
/// chain's accounting record.
|
|
194
|
+
/// @dev The registry sums the freshest value across every peer chain to drive cross-chain cash out tax, preventing
|
|
195
|
+
/// a holder who dominates one chain's local supply from bypassing the tax.
|
|
196
|
+
/// @param chainId The peer chain to read the total supply of.
|
|
179
197
|
/// @return The peer chain's total supply.
|
|
180
|
-
function
|
|
198
|
+
function peerChainTotalSupplyOf(uint256 chainId) external view returns (uint256);
|
|
181
199
|
|
|
182
|
-
/// @notice
|
|
183
|
-
/// @dev Lets
|
|
184
|
-
///
|
|
200
|
+
/// @notice One peer chain's total supply bundled with the peer chain ID and the record's freshness key.
|
|
201
|
+
/// @dev Lets the registry read the value, the peer chain it belongs to, and its freshness in one call. The `value`
|
|
202
|
+
/// matches `peerChainTotalSupplyOf(chainId)`.
|
|
203
|
+
/// @param chainId The peer chain to read the total supply value of.
|
|
185
204
|
/// @return A `JBPeerChainValue` with the total supply, peer chain ID, and snapshot freshness key.
|
|
186
|
-
function peerChainTotalSupplyValue() external view returns (JBPeerChainValue memory);
|
|
205
|
+
function peerChainTotalSupplyValue(uint256 chainId) external view returns (JBPeerChainValue memory);
|
|
187
206
|
|
|
188
207
|
/// @notice The ID of the project on the local chain that this sucker is associated with.
|
|
189
208
|
/// @return The project ID.
|
|
@@ -194,10 +213,11 @@ interface IJBSucker is IERC165 {
|
|
|
194
213
|
/// @return The remote token info.
|
|
195
214
|
function remoteTokenFor(address token) external view returns (JBRemoteToken memory);
|
|
196
215
|
|
|
197
|
-
/// @notice The freshness key of the latest accepted peer
|
|
216
|
+
/// @notice The freshness key of the latest accepted accounting record for a peer chain.
|
|
198
217
|
/// @dev Higher values are fresher. The key is source-chain monotonic, not a value magnitude.
|
|
199
|
-
/// @
|
|
200
|
-
|
|
218
|
+
/// @param chainId The peer chain to read the latest accepted freshness key of.
|
|
219
|
+
/// @return The latest accepted freshness key for the chain.
|
|
220
|
+
function snapshotTimestampOf(uint256 chainId) external view returns (uint256);
|
|
201
221
|
|
|
202
222
|
/// @notice The current deprecation state of this sucker.
|
|
203
223
|
/// @return The sucker state.
|
|
@@ -4,6 +4,7 @@ pragma solidity ^0.8.0;
|
|
|
4
4
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
5
5
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
6
6
|
|
|
7
|
+
import {JBChainAccounting} from "../structs/JBChainAccounting.sol";
|
|
7
8
|
import {JBSuckerDeployerConfig} from "../structs/JBSuckerDeployerConfig.sol";
|
|
8
9
|
import {JBSuckersPair} from "../structs/JBSuckersPair.sol";
|
|
9
10
|
|
|
@@ -68,9 +69,23 @@ interface IJBSuckerRegistry {
|
|
|
68
69
|
/// @return Whether the sucker belongs to the project.
|
|
69
70
|
function isSuckerOf(uint256 projectId, address addr) external view returns (bool);
|
|
70
71
|
|
|
72
|
+
/// @notice The freshest accounting record per source chain that a project's suckers hold, for re-gossiping.
|
|
73
|
+
/// @dev A sucker building an outbound gossip bundle calls this to gather the project's full cross-chain knowledge,
|
|
74
|
+
/// deduped per chain (freshest wins; active supersedes deprecated), excluding the destination and local chains.
|
|
75
|
+
/// @param projectId The ID of the project.
|
|
76
|
+
/// @param exceptChainId The destination chain to exclude.
|
|
77
|
+
/// @return accounts The deduped raw accounting records, one per known source chain.
|
|
78
|
+
function peerChainAccountsOf(
|
|
79
|
+
uint256 projectId,
|
|
80
|
+
uint256 exceptChainId
|
|
81
|
+
)
|
|
82
|
+
external
|
|
83
|
+
view
|
|
84
|
+
returns (JBChainAccounting[] memory accounts);
|
|
85
|
+
|
|
71
86
|
/// @notice The cumulative total supply across all remote peer chains for a project.
|
|
72
|
-
/// @dev
|
|
73
|
-
/// that revert.
|
|
87
|
+
/// @dev Aggregates over every (sucker, chain) pair and dedups per chain by freshest record. Silently skips suckers
|
|
88
|
+
/// and records that revert.
|
|
74
89
|
/// @param projectId The ID of the project.
|
|
75
90
|
/// @return totalSupply The combined peer chain total supply.
|
|
76
91
|
function remoteTotalSupplyOf(uint256 projectId) external view returns (uint256 totalSupply);
|
|
@@ -95,9 +110,9 @@ interface IJBSuckerRegistry {
|
|
|
95
110
|
function toRemoteFee() external view returns (uint256);
|
|
96
111
|
|
|
97
112
|
/// @notice The cumulative peer-chain balance across all remote peer chains for a project, valued into a currency.
|
|
98
|
-
/// @dev
|
|
99
|
-
/// `currency`. A context whose currency already matches is taken at par (no feed); a missing
|
|
100
|
-
/// reverts and that sucker is silently skipped (conservative, bias-low).
|
|
113
|
+
/// @dev Aggregates over every (sucker, chain) pair and dedups per chain by freshest record, then sums each chain's
|
|
114
|
+
/// balance valued into `currency`. A context whose currency already matches is taken at par (no feed); a missing
|
|
115
|
+
/// cross-currency feed reverts and that (sucker, chain) is silently skipped (conservative, bias-low).
|
|
101
116
|
/// @param projectId The ID of the project.
|
|
102
117
|
/// @param currency The currency to value the combined balance into.
|
|
103
118
|
/// @param decimals The decimal precision for the returned value.
|
|
@@ -112,9 +127,9 @@ interface IJBSuckerRegistry {
|
|
|
112
127
|
returns (uint256 balance);
|
|
113
128
|
|
|
114
129
|
/// @notice The cumulative peer-chain surplus across all remote peer chains for a project, valued into a currency.
|
|
115
|
-
/// @dev
|
|
116
|
-
/// `currency`. A context whose currency already matches is taken at par (no feed); a missing
|
|
117
|
-
/// reverts and that sucker is silently skipped (conservative, bias-low).
|
|
130
|
+
/// @dev Aggregates over every (sucker, chain) pair and dedups per chain by freshest record, then sums each chain's
|
|
131
|
+
/// surplus valued into `currency`. A context whose currency already matches is taken at par (no feed); a missing
|
|
132
|
+
/// cross-currency feed reverts and that (sucker, chain) is silently skipped (conservative, bias-low).
|
|
118
133
|
/// @param projectId The ID of the project.
|
|
119
134
|
/// @param currency The currency to value the combined surplus into.
|
|
120
135
|
/// @param decimals The decimal precision for the returned value.
|
|
@@ -12,10 +12,13 @@ import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadat
|
|
|
12
12
|
import {IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
|
|
13
13
|
|
|
14
14
|
import {IJBPeerChainAdjustedAccounts} from "../interfaces/IJBPeerChainAdjustedAccounts.sol";
|
|
15
|
+
import {IJBSuckerRegistry} from "../interfaces/IJBSuckerRegistry.sol";
|
|
15
16
|
import {JBPeerChainAdjustedAccountsLib} from "./JBPeerChainAdjustedAccountsLib.sol";
|
|
16
17
|
import {JBAccountingSnapshot} from "../structs/JBAccountingSnapshot.sol";
|
|
18
|
+
import {JBChainAccounting} from "../structs/JBChainAccounting.sol";
|
|
17
19
|
import {JBInboxTreeRoot} from "../structs/JBInboxTreeRoot.sol";
|
|
18
20
|
import {JBMessageRoot} from "../structs/JBMessageRoot.sol";
|
|
21
|
+
import {JBPeerChainContext} from "../structs/JBPeerChainContext.sol";
|
|
19
22
|
import {JBSourceContext} from "../structs/JBSourceContext.sol";
|
|
20
23
|
import {MerkleLib} from "../utils/MerkleLib.sol";
|
|
21
24
|
|
|
@@ -34,20 +37,25 @@ library JBSuckerLib {
|
|
|
34
37
|
uint256 internal constant _CURRENT_RULESET_OF_RETURN_BYTES = (9 + 19) * 32;
|
|
35
38
|
|
|
36
39
|
//*********************************************************************//
|
|
37
|
-
//
|
|
40
|
+
// ------------------------- external views -------------------------- //
|
|
38
41
|
//*********************************************************************//
|
|
39
42
|
|
|
40
|
-
/// @notice Build the cross-chain accounting
|
|
43
|
+
/// @notice Build the cross-chain accounting gossip bundle (the local chain's record plus known peer records).
|
|
41
44
|
/// @dev Extracted from `JBSucker.syncAccountingData` to reduce child contract bytecode. Called via DELEGATECALL.
|
|
42
|
-
///
|
|
45
|
+
/// Each record carries its source chain's surplus and balance per context in that context's own currency, without
|
|
46
|
+
/// price-feed valuation.
|
|
43
47
|
/// @param directory The JB directory to look up controllers and terminals.
|
|
48
|
+
/// @param registry The sucker registry that aggregates the project's per-chain records.
|
|
44
49
|
/// @param projectId The project ID.
|
|
50
|
+
/// @param exceptChainId The destination chain, excluded from the gathered peer records.
|
|
45
51
|
/// @param messageVersion The message format version.
|
|
46
|
-
/// @param sourceTimestamp The monotonic source freshness key for
|
|
52
|
+
/// @param sourceTimestamp The monotonic source freshness key for the local chain's record.
|
|
47
53
|
/// @return snapshot The constructed accounting snapshot.
|
|
48
54
|
function buildAccountingSnapshot(
|
|
49
55
|
IJBDirectory directory,
|
|
56
|
+
IJBSuckerRegistry registry,
|
|
50
57
|
uint256 projectId,
|
|
58
|
+
uint256 exceptChainId,
|
|
51
59
|
uint8 messageVersion,
|
|
52
60
|
uint256 sourceTimestamp
|
|
53
61
|
)
|
|
@@ -55,34 +63,39 @@ library JBSuckerLib {
|
|
|
55
63
|
view
|
|
56
64
|
returns (JBAccountingSnapshot memory snapshot)
|
|
57
65
|
{
|
|
58
|
-
// Snapshot the project's per-context surplus and balance, un-valued. No price oracle is consulted on send.
|
|
59
|
-
(uint256 localTotalSupply, JBSourceContext[] memory sourceContexts) =
|
|
60
|
-
_snapshotAccountsOf({directory: directory, projectId: projectId});
|
|
61
|
-
|
|
62
66
|
// Construct the accounting-only message without any token-local merkle root.
|
|
63
67
|
snapshot = JBAccountingSnapshot({
|
|
64
68
|
version: messageVersion,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
accounts: _buildGossipBundle({
|
|
70
|
+
directory: directory,
|
|
71
|
+
registry: registry,
|
|
72
|
+
projectId: projectId,
|
|
73
|
+
exceptChainId: exceptChainId,
|
|
74
|
+
sourceTimestamp: sourceTimestamp
|
|
75
|
+
})
|
|
68
76
|
});
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
/// @notice Build the cross-chain
|
|
79
|
+
/// @notice Build the cross-chain root message, carrying the accounting gossip bundle alongside the outbox root.
|
|
72
80
|
/// @dev Extracted from `JBSucker._buildSnapshotAndSend` to reduce child contract bytecode. Called via DELEGATECALL.
|
|
73
|
-
///
|
|
81
|
+
/// Each accounting record carries its source chain's surplus and balance per context in that context's own
|
|
82
|
+
/// currency, without price-feed valuation.
|
|
74
83
|
/// @param directory The JB directory to look up controllers and terminals.
|
|
84
|
+
/// @param registry The sucker registry that aggregates the project's per-chain records.
|
|
75
85
|
/// @param projectId The project ID.
|
|
86
|
+
/// @param exceptChainId The destination chain, excluded from the gathered peer records.
|
|
76
87
|
/// @param remoteToken The remote token bytes32 address.
|
|
77
88
|
/// @param amount The amount of terminal tokens to bridge.
|
|
78
89
|
/// @param nonce The outbox nonce for this send.
|
|
79
90
|
/// @param root The merkle root of the outbox tree.
|
|
80
91
|
/// @param messageVersion The message format version.
|
|
81
|
-
/// @param sourceTimestamp The monotonic source freshness key for
|
|
92
|
+
/// @param sourceTimestamp The monotonic source freshness key for the local chain's record.
|
|
82
93
|
/// @return message The constructed JBMessageRoot.
|
|
83
94
|
function buildSnapshotMessage(
|
|
84
95
|
IJBDirectory directory,
|
|
96
|
+
IJBSuckerRegistry registry,
|
|
85
97
|
uint256 projectId,
|
|
98
|
+
uint256 exceptChainId,
|
|
86
99
|
bytes32 remoteToken,
|
|
87
100
|
uint256 amount,
|
|
88
101
|
uint64 nonce,
|
|
@@ -94,26 +107,22 @@ library JBSuckerLib {
|
|
|
94
107
|
view
|
|
95
108
|
returns (JBMessageRoot memory message)
|
|
96
109
|
{
|
|
97
|
-
//
|
|
98
|
-
(uint256 localTotalSupply, JBSourceContext[] memory sourceContexts) =
|
|
99
|
-
_snapshotAccountsOf({directory: directory, projectId: projectId});
|
|
100
|
-
|
|
101
|
-
// Construct the cross-chain message with the per-context snapshot data.
|
|
110
|
+
// Construct the cross-chain message with the outbox root and the accounting gossip bundle.
|
|
102
111
|
message = JBMessageRoot({
|
|
103
112
|
version: messageVersion,
|
|
104
113
|
token: remoteToken,
|
|
105
114
|
amount: amount,
|
|
106
115
|
remoteRoot: JBInboxTreeRoot({nonce: nonce, root: root}),
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
accounts: _buildGossipBundle({
|
|
117
|
+
directory: directory,
|
|
118
|
+
registry: registry,
|
|
119
|
+
projectId: projectId,
|
|
120
|
+
exceptChainId: exceptChainId,
|
|
121
|
+
sourceTimestamp: sourceTimestamp
|
|
122
|
+
})
|
|
110
123
|
});
|
|
111
124
|
}
|
|
112
125
|
|
|
113
|
-
//*********************************************************************//
|
|
114
|
-
// ------------------------- external views -------------------------- //
|
|
115
|
-
//*********************************************************************//
|
|
116
|
-
|
|
117
126
|
/// @notice Compute a branch root from a leaf, branch, and index. Wraps MerkleLib.branchRoot so its
|
|
118
127
|
/// ~170 lines of unrolled assembly live in the library's bytecode instead of each sucker's.
|
|
119
128
|
/// @param item The leaf hash.
|
|
@@ -177,10 +186,130 @@ library JBSuckerLib {
|
|
|
177
186
|
}
|
|
178
187
|
}
|
|
179
188
|
|
|
189
|
+
/// @notice Folds a peer chain's raw source contexts into per-currency surplus and balance, resolving each to a
|
|
190
|
+
/// local currency.
|
|
191
|
+
/// @dev Extracted from `JBSucker.peerChainContextsOf` to reduce child contract bytecode. Called via DELEGATECALL.
|
|
192
|
+
/// Each `localTokens[i]` is the local token the sucker resolved `rawContexts[i].token` to (via its token mapping or
|
|
193
|
+
/// identity); this derives that token's authoritative accounting-context currency and merges entries that share
|
|
194
|
+
/// BOTH currency AND decimals. The accounting-context currency is immutable, so re-resolving on each read is safe.
|
|
195
|
+
/// Entries that share a currency but carry different decimals stay separate, since the raw amounts are on different
|
|
196
|
+
/// scales. No price oracle is consulted.
|
|
197
|
+
/// @param directory The JB directory to look up the project's terminals.
|
|
198
|
+
/// @param projectId The project whose accounting contexts to read.
|
|
199
|
+
/// @param localTokens The local token each raw context resolves to, parallel to `rawContexts`.
|
|
200
|
+
/// @param rawContexts The peer chain's raw per-context surplus and balance.
|
|
201
|
+
/// @return contexts The per-currency surplus and balance for the chain.
|
|
202
|
+
function foldPeerContexts(
|
|
203
|
+
IJBDirectory directory,
|
|
204
|
+
uint256 projectId,
|
|
205
|
+
address[] memory localTokens,
|
|
206
|
+
JBSourceContext[] memory rawContexts
|
|
207
|
+
)
|
|
208
|
+
external
|
|
209
|
+
view
|
|
210
|
+
returns (JBPeerChainContext[] memory contexts)
|
|
211
|
+
{
|
|
212
|
+
uint256 numRaw = rawContexts.length;
|
|
213
|
+
|
|
214
|
+
// The folded set is no larger than the raw set, so allocate to that upper bound and track the populated length.
|
|
215
|
+
JBPeerChainContext[] memory buf = new JBPeerChainContext[](numRaw);
|
|
216
|
+
uint256 count;
|
|
217
|
+
|
|
218
|
+
for (uint256 i; i < numRaw;) {
|
|
219
|
+
uint8 ctxDecimals = rawContexts[i].decimals;
|
|
220
|
+
uint128 ctxSurplus = rawContexts[i].surplus;
|
|
221
|
+
uint128 ctxBalance = rawContexts[i].balance;
|
|
222
|
+
uint32 ctxCurrency = _currencyOf({directory: directory, projectId: projectId, token: localTokens[i]});
|
|
223
|
+
|
|
224
|
+
// Fold into an existing entry that matches on BOTH currency AND decimals, or append a new one.
|
|
225
|
+
bool merged;
|
|
226
|
+
for (uint256 j; j < count;) {
|
|
227
|
+
if (buf[j].currency == ctxCurrency && buf[j].decimals == ctxDecimals) {
|
|
228
|
+
buf[j].surplus = _saturatingAddU128(buf[j].surplus, ctxSurplus);
|
|
229
|
+
buf[j].balance = _saturatingAddU128(buf[j].balance, ctxBalance);
|
|
230
|
+
merged = true;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
unchecked {
|
|
234
|
+
++j;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!merged) {
|
|
238
|
+
buf[count++] = JBPeerChainContext({
|
|
239
|
+
currency: ctxCurrency, decimals: ctxDecimals, surplus: ctxSurplus, balance: ctxBalance
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
unchecked {
|
|
244
|
+
++i;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Trim the over-allocated buffer to the folded length.
|
|
249
|
+
contexts = new JBPeerChainContext[](count);
|
|
250
|
+
for (uint256 k; k < count;) {
|
|
251
|
+
contexts[k] = buf[k];
|
|
252
|
+
unchecked {
|
|
253
|
+
++k;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
180
258
|
//*********************************************************************//
|
|
181
259
|
// ------------------------- internal views -------------------------- //
|
|
182
260
|
//*********************************************************************//
|
|
183
261
|
|
|
262
|
+
/// @notice Assemble a cross-chain accounting gossip bundle: the local chain's own record plus every peer-chain
|
|
263
|
+
/// record the project's suckers currently hold, excluding the destination chain.
|
|
264
|
+
/// @dev The local record is taken fresh from `_snapshotAccountsOf` (including any data-hook adjusted accounts). The
|
|
265
|
+
/// peer records are gathered from the registry, which is the only contract that sees a hub chain's per-peer
|
|
266
|
+
/// suckers together; it dedups them to the freshest per chain. A reverting or unset registry yields a local-only
|
|
267
|
+
/// bundle, so a standalone sucker still propagates its own record. Forwarded peer records keep their own origin
|
|
268
|
+
/// chain and freshness key so the receiver gates each chain independently.
|
|
269
|
+
/// @param directory The JB directory to look up controllers and terminals.
|
|
270
|
+
/// @param registry The sucker registry that aggregates the project's per-chain records.
|
|
271
|
+
/// @param projectId The project to snapshot.
|
|
272
|
+
/// @param exceptChainId The destination chain, excluded from the gathered peer records.
|
|
273
|
+
/// @param sourceTimestamp The local record's freshness key.
|
|
274
|
+
/// @return accounts The assembled gossip bundle, with the local chain's record first.
|
|
275
|
+
function _buildGossipBundle(
|
|
276
|
+
IJBDirectory directory,
|
|
277
|
+
IJBSuckerRegistry registry,
|
|
278
|
+
uint256 projectId,
|
|
279
|
+
uint256 exceptChainId,
|
|
280
|
+
uint256 sourceTimestamp
|
|
281
|
+
)
|
|
282
|
+
internal
|
|
283
|
+
view
|
|
284
|
+
returns (JBChainAccounting[] memory accounts)
|
|
285
|
+
{
|
|
286
|
+
// Snapshot the local chain's own supply and per-context surplus/balance, un-valued. No price oracle is read.
|
|
287
|
+
(uint256 localTotalSupply, JBSourceContext[] memory localContexts) =
|
|
288
|
+
_snapshotAccountsOf({directory: directory, projectId: projectId});
|
|
289
|
+
|
|
290
|
+
// Gather every other chain's record the project knows, deduped per chain and minus the destination. The
|
|
291
|
+
// accounting gossip is best-effort, so a reverting registry must never break the essential root/token bridge:
|
|
292
|
+
// catch the failure and propagate just this chain's own record.
|
|
293
|
+
JBChainAccounting[] memory peers;
|
|
294
|
+
try registry.peerChainAccountsOf({projectId: projectId, exceptChainId: exceptChainId}) returns (
|
|
295
|
+
JBChainAccounting[] memory gathered
|
|
296
|
+
) {
|
|
297
|
+
peers = gathered;
|
|
298
|
+
} catch {}
|
|
299
|
+
|
|
300
|
+
// The local record leads; forwarded peer records follow verbatim, keeping their own origin chain and freshness.
|
|
301
|
+
accounts = new JBChainAccounting[](peers.length + 1);
|
|
302
|
+
accounts[0] = JBChainAccounting({
|
|
303
|
+
chainId: block.chainid, totalSupply: localTotalSupply, contexts: localContexts, timestamp: sourceTimestamp
|
|
304
|
+
});
|
|
305
|
+
for (uint256 i; i < peers.length;) {
|
|
306
|
+
accounts[i + 1] = peers[i];
|
|
307
|
+
unchecked {
|
|
308
|
+
++i;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
184
313
|
/// @notice Builds the project's per-accounting-context surplus and balance, each in the context's own currency,
|
|
185
314
|
/// with no price-feed valuation.
|
|
186
315
|
/// @dev Loops every terminal and accounting context, reading the raw per-token surplus (requested in the token's
|
|
@@ -253,6 +382,42 @@ library JBSuckerLib {
|
|
|
253
382
|
} catch {}
|
|
254
383
|
}
|
|
255
384
|
|
|
385
|
+
/// @notice The project's authoritative accounting-context currency for a local token, or a convention fallback.
|
|
386
|
+
/// @dev Reads the token's accounting context from its primary terminal via length-guarded staticcalls, so a missing
|
|
387
|
+
/// or non-conforming directory/terminal just yields the fallback. Falls back to `uint32(uint160(token))` when the
|
|
388
|
+
/// project has no local accounting context for the token yet. The accounting-context currency is immutable.
|
|
389
|
+
/// @param directory The JB directory to look up the project's primary terminal for the token.
|
|
390
|
+
/// @param projectId The project whose accounting context to read.
|
|
391
|
+
/// @param token The local token to resolve the currency of.
|
|
392
|
+
/// @return currency The project's accounting-context currency for the token.
|
|
393
|
+
function _currencyOf(
|
|
394
|
+
IJBDirectory directory,
|
|
395
|
+
uint256 projectId,
|
|
396
|
+
address token
|
|
397
|
+
)
|
|
398
|
+
internal
|
|
399
|
+
view
|
|
400
|
+
returns (uint32 currency)
|
|
401
|
+
{
|
|
402
|
+
// Resolve the project's primary terminal for the token. An `address` return needs a full word.
|
|
403
|
+
(bool terminalOk, bytes memory terminalData) =
|
|
404
|
+
address(directory).staticcall(abi.encodeCall(IJBDirectory.primaryTerminalOf, (projectId, token)));
|
|
405
|
+
if (terminalOk && terminalData.length >= 32) {
|
|
406
|
+
address terminal = abi.decode(terminalData, (address));
|
|
407
|
+
if (terminal != address(0)) {
|
|
408
|
+
// Read the token's accounting context. The struct encodes to three words.
|
|
409
|
+
(bool contextOk, bytes memory contextData) =
|
|
410
|
+
terminal.staticcall(abi.encodeCall(IJBTerminal.accountingContextForTokenOf, (projectId, token)));
|
|
411
|
+
if (contextOk && contextData.length >= 96) {
|
|
412
|
+
JBAccountingContext memory accountingContext = abi.decode(contextData, (JBAccountingContext));
|
|
413
|
+
if (accountingContext.currency != 0) return accountingContext.currency;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
418
|
+
return uint32(uint160(token));
|
|
419
|
+
}
|
|
420
|
+
|
|
256
421
|
/// @notice Optional project-specific adjusted accounts to add to peer-chain snapshots.
|
|
257
422
|
/// @dev Reads the current ruleset's data hook and asks it for extra supply plus per-context surplus/balance, each
|
|
258
423
|
/// in the context's own currency. Non-supporting or broken hooks are ignored so a project's baseline snapshot stays
|
|
@@ -339,6 +504,21 @@ library JBSuckerLib {
|
|
|
339
504
|
});
|
|
340
505
|
}
|
|
341
506
|
|
|
507
|
+
/// @notice Adds two `uint128` amounts, saturating at `type(uint128).max` instead of overflowing.
|
|
508
|
+
/// @dev Saturation keeps a pathological peer record from reverting the read path; the cap can only under-report a
|
|
509
|
+
/// remote amount, the safe direction.
|
|
510
|
+
/// @param a The first amount.
|
|
511
|
+
/// @param b The second amount.
|
|
512
|
+
/// @return The saturated sum.
|
|
513
|
+
function _saturatingAddU128(uint128 a, uint128 b) internal pure returns (uint128) {
|
|
514
|
+
unchecked {
|
|
515
|
+
uint256 sum = uint256(a) + uint256(b);
|
|
516
|
+
// The cast only runs when `sum <= type(uint128).max`, so it cannot truncate.
|
|
517
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
518
|
+
return sum > type(uint128).max ? type(uint128).max : uint128(sum);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
342
522
|
/// @notice Builds the local accounting values used in outbound peer-chain snapshots.
|
|
343
523
|
/// @dev Project token supply stays a single currency-agnostic scalar. Surplus and balance are emitted per context
|
|
344
524
|
/// in that context's currency, with no price-feed valuation. The receiving chain folds each context into its
|
|
@@ -372,6 +552,15 @@ library JBSuckerLib {
|
|
|
372
552
|
if (address(controller) != address(0) && address(controller).code.length != 0) {
|
|
373
553
|
(additionalSupply, hookContexts) =
|
|
374
554
|
_peerChainAdjustedAccountsOf({controller: controller, projectId: projectId});
|
|
555
|
+
// Fail soft to the baseline snapshot. A malformed hook that returns a `supply` which would overflow the
|
|
556
|
+
// controller supply contributes no extra supply or contexts — the documented fail-soft model — instead
|
|
557
|
+
// of
|
|
558
|
+
// reverting the whole snapshot and bricking every outbound send (`toRemote` / `syncAccountingData`) while
|
|
559
|
+
// the hook stays active.
|
|
560
|
+
if (additionalSupply > type(uint256).max - localTotalSupply) {
|
|
561
|
+
additionalSupply = 0;
|
|
562
|
+
hookContexts = new JBSourceContext[](0);
|
|
563
|
+
}
|
|
375
564
|
localTotalSupply += additionalSupply;
|
|
376
565
|
}
|
|
377
566
|
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {JBChainAccounting} from "./JBChainAccounting.sol";
|
|
5
5
|
|
|
6
|
-
/// @notice A
|
|
6
|
+
/// @notice A cross-chain accounting gossip bundle, sent without any token-local merkle root or transported value.
|
|
7
|
+
/// @dev Carries the sending chain's own accounting record plus every peer-chain record the sender currently holds,
|
|
8
|
+
/// each stamped with its originating chain's freshness key. The receiving chain stores the freshest record per source
|
|
9
|
+
/// chain, so accounting propagates across a hub-and-spoke sucker mesh without a direct sucker between every pair of
|
|
10
|
+
/// chains.
|
|
7
11
|
/// @custom:member version The message format version. Used to reject incompatible messages.
|
|
8
|
-
/// @custom:member
|
|
9
|
-
///
|
|
10
|
-
/// @custom:member sourceContexts The source chain's surplus and balance per accounting context, each in the context's
|
|
11
|
-
/// own currency and decimals, un-valued.
|
|
12
|
-
/// @custom:member sourceTimestamp A monotonic source-chain freshness key for the snapshot. Used by the receiving
|
|
13
|
-
/// chain to reject stale surplus/balance/supply updates.
|
|
12
|
+
/// @custom:member accounts One accounting record per source chain known to the sender: its own chain plus every peer
|
|
13
|
+
/// chain it has heard about, excluding the destination chain.
|
|
14
14
|
struct JBAccountingSnapshot {
|
|
15
15
|
uint8 version;
|
|
16
|
-
|
|
17
|
-
JBSourceContext[] sourceContexts;
|
|
18
|
-
uint256 sourceTimestamp;
|
|
16
|
+
JBChainAccounting[] accounts;
|
|
19
17
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {JBSourceContext} from "./JBSourceContext.sol";
|
|
5
|
+
|
|
6
|
+
/// @notice One source chain's project-wide accounting, carried as a record in a cross-chain gossip bundle.
|
|
7
|
+
/// @dev A sucker sends its own chain's record alongside every peer-chain record it already holds, each stamped with
|
|
8
|
+
/// the originating chain's own freshness key. The receiving chain stores the freshest record per source chain, so a
|
|
9
|
+
/// project's accounting propagates across a hub-and-spoke sucker mesh (L2s bridged only through mainnet) without a
|
|
10
|
+
/// direct sucker between every pair of chains. `contexts` carry the source chain's own token addresses, so each
|
|
11
|
+
/// receiver resolves them to its own local currencies independently. Trust is transitive across the mesh: a receiver
|
|
12
|
+
/// authenticates only the directly-bridged peer that delivers a bundle, not the origin of each forwarded record, so
|
|
13
|
+
/// any authenticated peer can forward a record for any other chain. A record is therefore only as trustworthy as the
|
|
14
|
+
/// project's same-address sucker invariant — the same CREATE2 same-bytecode assumption every paired sucker already
|
|
15
|
+
/// relies on — and a peer running adversarial bytecode could forge another chain's record. The freshest-per-chain
|
|
16
|
+
/// gate bounds rollback, not authorship; the supply view it feeds is clamped downstream by each chain's own local
|
|
17
|
+
/// surplus, so a forged record cannot by itself over-credit a cash out.
|
|
18
|
+
/// @custom:member chainId The source chain this record describes. A receiver ignores a record for its own chain, since
|
|
19
|
+
/// it reads its own local accounting directly.
|
|
20
|
+
/// @custom:member totalSupply The total token supply (including reserved tokens) on the source chain when the record
|
|
21
|
+
/// was taken. Used by the receiving chain to track cross-chain supply for cash out tax calculations.
|
|
22
|
+
/// @custom:member contexts The source chain's surplus and balance per accounting context, each in the context's own
|
|
23
|
+
/// currency and decimals, un-valued. The receiver resolves each entry to its same-asset local context and folds it in
|
|
24
|
+
/// at par.
|
|
25
|
+
/// @custom:member timestamp A monotonic source-chain freshness key for the record. The receiver keeps the freshest
|
|
26
|
+
/// record per source chain, so stale relays cannot roll back surplus, balance, or supply.
|
|
27
|
+
struct JBChainAccounting {
|
|
28
|
+
uint256 chainId;
|
|
29
|
+
uint256 totalSupply;
|
|
30
|
+
JBSourceContext[] contexts;
|
|
31
|
+
uint256 timestamp;
|
|
32
|
+
}
|
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.0;
|
|
3
3
|
|
|
4
|
+
import {JBChainAccounting} from "./JBChainAccounting.sol";
|
|
4
5
|
import {JBInboxTreeRoot} from "./JBInboxTreeRoot.sol";
|
|
5
|
-
import {JBSourceContext} from "./JBSourceContext.sol";
|
|
6
6
|
|
|
7
|
-
/// @notice Information about the remote (inbox) tree's root
|
|
8
|
-
///
|
|
9
|
-
///
|
|
10
|
-
///
|
|
11
|
-
///
|
|
7
|
+
/// @notice Information about the remote (inbox) tree's root passed in a message from the remote chain, carried
|
|
8
|
+
/// alongside a cross-chain accounting gossip bundle.
|
|
9
|
+
/// @dev The accounting bundle carries the sending chain's own record plus every peer-chain record the sender holds,
|
|
10
|
+
/// each in the originating chain's own currency and decimals, un-valued. The receiving chain folds each context into
|
|
11
|
+
/// its same-asset local context at par and stores the freshest record per source chain, so no price oracle is
|
|
12
|
+
/// consulted in the cross-chain surplus path and accounting propagates across the sucker mesh as roots are relayed.
|
|
13
|
+
/// Project token supply stays a single currency-agnostic scalar per source chain.
|
|
12
14
|
/// @custom:member version The message format version. Used to reject incompatible messages.
|
|
13
15
|
/// @custom:member token The remote token address (bytes32 for cross-VM compatibility with SVM).
|
|
14
16
|
/// @custom:member amount The amount of tokens to send.
|
|
15
17
|
/// @custom:member remoteRoot The root of the merkle tree.
|
|
16
|
-
/// @custom:member
|
|
17
|
-
///
|
|
18
|
-
///
|
|
19
|
-
/// own currency and decimals, un-valued. The receiving chain resolves each entry to its same-asset local context and
|
|
20
|
-
/// folds it in at par.
|
|
21
|
-
/// @custom:member sourceTimestamp A monotonic source-chain freshness key for the snapshot. Used by the receiving
|
|
22
|
-
/// chain to reject stale surplus/balance/supply updates without blocking token-local inbox root updates.
|
|
18
|
+
/// @custom:member accounts One accounting record per source chain known to the sender: its own chain plus every peer
|
|
19
|
+
/// chain it has heard about, excluding the destination chain. Used by the receiving chain to track cross-chain
|
|
20
|
+
/// supply, surplus, and balance for cash out tax calculations.
|
|
23
21
|
struct JBMessageRoot {
|
|
24
22
|
uint8 version;
|
|
25
23
|
bytes32 token;
|
|
26
24
|
uint256 amount;
|
|
27
25
|
JBInboxTreeRoot remoteRoot;
|
|
28
|
-
|
|
29
|
-
JBSourceContext[] sourceContexts;
|
|
30
|
-
uint256 sourceTimestamp;
|
|
26
|
+
JBChainAccounting[] accounts;
|
|
31
27
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.0;
|
|
3
|
+
|
|
4
|
+
import {JBChainAccounting} from "./JBChainAccounting.sol";
|
|
5
|
+
|
|
6
|
+
/// @notice Scratch space used while gathering the freshest accounting record per peer chain across a project's suckers.
|
|
7
|
+
/// @dev Sized to the total records across the project's suckers for in-memory de-duplication; `chainCount` tracks
|
|
8
|
+
/// populated entries. Bundled into one struct so the gather helpers stay under the stack-slot limit.
|
|
9
|
+
/// @custom:member chainIds The peer chain IDs that have been observed.
|
|
10
|
+
/// @custom:member records The selected record for each observed peer chain.
|
|
11
|
+
/// @custom:member hasActiveRecord Whether the selected record came from an active sucker instead of a deprecated one.
|
|
12
|
+
/// @custom:member chainCount The number of populated peer-chain entries.
|
|
13
|
+
struct PeerAccountScratch {
|
|
14
|
+
uint256[] chainIds;
|
|
15
|
+
JBChainAccounting[] records;
|
|
16
|
+
bool[] hasActiveRecord;
|
|
17
|
+
uint256 chainCount;
|
|
18
|
+
}
|