@bananapus/core-v6 0.0.42 → 0.0.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -1
- package/README.md +3 -3
- package/package.json +2 -2
- package/references/entrypoints.md +3 -1
- package/references/types-errors-events.md +2 -3
- package/src/JBChainlinkV3PriceFeed.sol +14 -6
- package/src/JBChainlinkV3SequencerPriceFeed.sol +5 -6
- package/src/JBController.sol +84 -68
- package/src/JBDirectory.sol +4 -7
- package/src/JBERC20.sol +8 -7
- package/src/JBFeelessAddresses.sol +32 -13
- package/src/JBFundAccessLimits.sol +12 -4
- package/src/JBMultiTerminal.sol +39 -61
- package/src/JBPermissions.sol +6 -3
- package/src/JBPrices.sol +18 -12
- package/src/JBRulesets.sol +4 -25
- package/src/JBSplits.sol +13 -5
- package/src/JBTerminalStore.sol +46 -82
- package/src/JBTokens.sol +11 -13
- package/src/abstract/JBControlled.sol +1 -2
- package/src/interfaces/IJBController.sol +0 -6
- package/src/interfaces/IJBFeelessAddresses.sol +17 -7
- package/src/libraries/JBCashOuts.sol +6 -2
- package/src/libraries/JBCurrencyIds.sol +3 -0
- package/src/libraries/JBMetadataResolver.sol +20 -12
- package/src/libraries/JBPayoutSplitGroupLib.sol +8 -3
- package/src/libraries/JBRulesetMetadataResolver.sol +4 -4
- package/src/libraries/JBSplitGroupIds.sol +1 -0
- package/src/periphery/JBDeadline1Day.sol +1 -0
- package/src/periphery/JBDeadline3Days.sol +1 -0
- package/src/periphery/JBDeadline3Hours.sol +1 -0
- package/src/periphery/JBDeadline7Days.sol +1 -0
- package/src/structs/JBBeforeCashOutRecordedContext.sol +3 -2
- package/src/structs/JBRulesetMetadata.sol +3 -3
- package/test/helpers/JBTest.sol +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -19,7 +19,7 @@ This file describes the verified change from `nana-core-v5` to the current `nana
|
|
|
19
19
|
- Token metadata is more editable than in v5. The controller now exposes a dedicated token-metadata update path.
|
|
20
20
|
- Approval-hook handling is safer. The v6 codebase and tests are built around preventing a bad approval hook from freezing project behavior.
|
|
21
21
|
- Fee accounting is tighter than in v5, especially around fee-free surplus behavior and cross-flow bookkeeping.
|
|
22
|
-
- The repo carries much broader test coverage than the v5 tree, including dedicated
|
|
22
|
+
- The repo carries much broader test coverage than the v5 tree, including dedicated review, invariant, fork, and formal-style suites.
|
|
23
23
|
- The implementation baseline moved from the v5 `0.8.23` world to `0.8.28`.
|
|
24
24
|
|
|
25
25
|
## Verified deltas
|
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ When a flow is unclear, read the contract that owns the state before the contrac
|
|
|
89
89
|
2. `test/TestTerminalPreviewParity.sol`
|
|
90
90
|
3. `test/invariants/TerminalStoreInvariant.t.sol`
|
|
91
91
|
4. `test/invariants/RulesetsInvariant.t.sol`
|
|
92
|
-
5. `test/
|
|
92
|
+
5. `test/regression/CrossTerminalSurplusSpoof.t.sol`
|
|
93
93
|
|
|
94
94
|
## Install
|
|
95
95
|
|
|
@@ -123,7 +123,7 @@ This repo contains the main core deployments and periphery deployment helpers. M
|
|
|
123
123
|
src/
|
|
124
124
|
core contracts, periphery helpers, interfaces, libraries, enums, structs, and abstract bases
|
|
125
125
|
test/
|
|
126
|
-
unit, integration, fork, invariant,
|
|
126
|
+
unit, integration, fork, invariant, review, formal, and regression coverage
|
|
127
127
|
script/
|
|
128
128
|
Deploy.s.sol
|
|
129
129
|
DeployPeriphery.s.sol
|
|
@@ -135,7 +135,7 @@ script/
|
|
|
135
135
|
- Hooks can materially change payment and cash-out behavior.
|
|
136
136
|
- Permissions are flexible, which makes broad or wildcard grants risky.
|
|
137
137
|
- Multi-terminal and multi-token accounting is powerful, but it is easy to misuse if an integration assumes a single-terminal model.
|
|
138
|
-
- Fee, surplus, and reclaim logic stay high-priority
|
|
138
|
+
- Fee, surplus, and reclaim logic stay high-priority review areas.
|
|
139
139
|
|
|
140
140
|
The easiest way to misread V6 is to treat core like a simple crowdfunding terminal. It is closer to a configurable accounting and settlement layer.
|
|
141
141
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.44",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-core-v6'"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@bananapus/permission-ids-v6": "0.0.22",
|
|
41
|
+
"@bananapus/permission-ids-v6": "^0.0.22",
|
|
42
42
|
"@chainlink/contracts": "1.5.0",
|
|
43
43
|
"@openzeppelin/contracts": "5.6.1",
|
|
44
44
|
"@prb/math": "4.1.1",
|
|
@@ -155,6 +155,8 @@ The core Juicebox V6 protocol on EVM: a modular system for launching treasury-ba
|
|
|
155
155
|
|
|
156
156
|
| Function | What it does |
|
|
157
157
|
|----------|--------------|
|
|
158
|
-
| `setFeelessAddress(address addr, bool flag)` | Adds or removes an address from the fee exemption list. Owner-only. (`JBFeelessAddresses`) |
|
|
158
|
+
| `setFeelessAddress(address addr, bool flag)` | Adds or removes an address from the global (all-project) fee exemption list. Owner-only. (`JBFeelessAddresses`) |
|
|
159
|
+
| `setFeelessAddressFor(uint256 projectId, address addr, bool flag)` | Adds or removes an address from a project's fee exemption list. `projectId = 0` = global wildcard. Owner-only. (`JBFeelessAddresses`) |
|
|
160
|
+
| `isFeelessFor(address addr, uint256 projectId)` | Returns whether an address is feeless for a project (checks both wildcard and project-specific). (`JBFeelessAddresses`) |
|
|
159
161
|
| `setControllerAllowed(uint256 projectId)` | Returns whether a project's controller can currently be set. (`IJBDirectoryAccessControl`) |
|
|
160
162
|
| `setTerminalsAllowed(uint256 projectId)` | Returns whether a project's terminals can currently be set. (`IJBDirectoryAccessControl`) |
|
|
@@ -8,7 +8,7 @@ Use this file when you need deeper protocol reference material after the repo-lo
|
|
|
8
8
|
|-------------|------------|---------|
|
|
9
9
|
| `JBRuleset` | `cycleNumber (uint48)`, `id (uint48)`, `basedOnId (uint48)`, `start (uint48)`, `duration (uint32)`, `weight (uint112)`, `weightCutPercent (uint32)`, `approvalHook`, `metadata (uint256)` | `currentOf()`, `recordPaymentFrom()`, `recordCashOutFor()` return values |
|
|
10
10
|
| `JBRulesetConfig` | `mustStartAtOrAfter (uint48)`, `duration (uint32)`, `weight (uint112)`, `weightCutPercent (uint32)`, `approvalHook`, `metadata (JBRulesetMetadata)`, `splitGroups[]`, `fundAccessLimitGroups[]` | `launchProjectFor()`, `queueRulesetsOf()` input |
|
|
11
|
-
| `JBRulesetMetadata` | `reservedPercent (uint16)`, `cashOutTaxRate (uint16)`, `baseCurrency (uint32)`, `pausePay`, `pauseCreditTransfers`, `allowOwnerMinting`, `allowSetCustomToken`, `allowTerminalMigration`, `allowSetTerminals`, `allowSetController`, `allowAddAccountingContext`, `allowAddPriceFeed`, `ownerMustSendPayouts`, `holdFees`, `
|
|
11
|
+
| `JBRulesetMetadata` | `reservedPercent (uint16)`, `cashOutTaxRate (uint16)`, `baseCurrency (uint32)`, `pausePay`, `pauseCreditTransfers`, `allowOwnerMinting`, `allowSetCustomToken`, `allowTerminalMigration`, `allowSetTerminals`, `allowSetController`, `allowAddAccountingContext`, `allowAddPriceFeed`, `ownerMustSendPayouts`, `holdFees`, `scopeCashOutsToLocalBalances`, `useDataHookForPay`, `useDataHookForCashOut`, `dataHook (address)`, `metadata (uint16)` | Packed into `JBRuleset.metadata` |
|
|
12
12
|
| `JBSplit` | `percent (uint32)`, `projectId (uint64)`, `beneficiary (address payable)`, `preferAddToBalance`, `lockedUntil (uint48)`, `hook (IJBSplitHook)` | `splitsOf()`, `setSplitGroupsOf()` |
|
|
13
13
|
| `JBSplitGroup` | `groupId (uint256)`, `splits (JBSplit[])` | `JBRulesetConfig.splitGroups`, `setSplitGroupsOf()` |
|
|
14
14
|
| `JBAccountingContext` | `token (address)`, `decimals (uint8)`, `currency (uint32)` | Terminal token accounting, surplus/reclaim calculations |
|
|
@@ -28,7 +28,7 @@ Use this file when you need deeper protocol reference material after the repo-lo
|
|
|
28
28
|
| Struct | Key Fields | Used In |
|
|
29
29
|
|--------|------------|---------|
|
|
30
30
|
| `JBBeforePayRecordedContext` | `terminal`, `payer`, `amount (JBTokenAmount)`, `projectId`, `rulesetId`, `beneficiary`, `weight`, `reservedPercent`, `metadata` | `IJBRulesetDataHook.beforePayRecordedWith()` input |
|
|
31
|
-
| `JBBeforeCashOutRecordedContext` | `terminal`, `holder`, `projectId`, `rulesetId`, `cashOutCount`, `totalSupply`, `surplus (JBTokenAmount)`, `
|
|
31
|
+
| `JBBeforeCashOutRecordedContext` | `terminal`, `holder`, `projectId`, `rulesetId`, `cashOutCount`, `totalSupply`, `surplus (JBTokenAmount)`, `scopeCashOutsToLocalBalances`, `cashOutTaxRate`, `beneficiaryIsFeeless`, `metadata` | `IJBRulesetDataHook.beforeCashOutRecordedWith()` input |
|
|
32
32
|
| `JBAfterPayRecordedContext` | `payer`, `projectId`, `rulesetId`, `amount (JBTokenAmount)`, `forwardedAmount (JBTokenAmount)`, `weight`, `newlyIssuedTokenCount`, `beneficiary`, `hookMetadata`, `payerMetadata` | `IJBPayHook.afterPayRecordedWith()` input |
|
|
33
33
|
| `JBAfterCashOutRecordedContext` | `holder`, `projectId`, `rulesetId`, `cashOutCount`, `reclaimedAmount (JBTokenAmount)`, `forwardedAmount (JBTokenAmount)`, `cashOutTaxRate`, `beneficiary`, `hookMetadata`, `cashOutMetadata` | `IJBCashOutHook.afterCashOutRecordedWith()` input |
|
|
34
34
|
| `JBPayHookSpecification` | `hook (IJBPayHook)`, `noop (bool)`, `amount`, `metadata` | Returned by data hook; specifies which pay hooks to call and how much to forward. `noop = true` means informational-only (callback skipped, amount must be 0). |
|
|
@@ -182,7 +182,6 @@ Errors an agent is most likely to encounter. All are custom errors (revert with
|
|
|
182
182
|
| `JBSplits_TotalPercentExceeds100` | `JBSplits` | Split percentages sum exceeds `SPLITS_TOTAL_PERCENT`. |
|
|
183
183
|
| `JBPrices_PriceFeedAlreadyExists` | `JBPrices` | Feed already set for that currency pair (immutable). |
|
|
184
184
|
| `JBPrices_PriceFeedNotFound` | `JBPrices` | No feed found for the requested currency pair. |
|
|
185
|
-
| `JBPermissions_CantSetRootPermissionForWildcardProject` | `JBPermissions` | Tried to grant ROOT with `projectId = 0` (wildcard). |
|
|
186
185
|
| `JBRulesets_InvalidWeight` | `JBRulesets` | Weight exceeds `uint112.max`. |
|
|
187
186
|
| `JBRulesets_InvalidWeightCutPercent` | `JBRulesets` | `weightCutPercent` exceeds `MAX_WEIGHT_CUT_PERCENT`. |
|
|
188
187
|
| `JBFundAccessLimits_InvalidPayoutLimitCurrencyOrdering` | `JBFundAccessLimits` | Payout limit currencies not in strictly increasing order. |
|
|
@@ -17,7 +17,7 @@ contract JBChainlinkV3PriceFeed is IJBPriceFeed {
|
|
|
17
17
|
// --------------------------- custom errors ------------------------- //
|
|
18
18
|
//*********************************************************************//
|
|
19
19
|
|
|
20
|
-
error JBChainlinkV3PriceFeed_IncompleteRound();
|
|
20
|
+
error JBChainlinkV3PriceFeed_IncompleteRound(uint80 roundId, uint80 answeredInRound, uint256 updatedAt);
|
|
21
21
|
error JBChainlinkV3PriceFeed_NegativePrice(int256 price);
|
|
22
22
|
error JBChainlinkV3PriceFeed_StalePrice(uint256 timestamp, uint256 threshold, uint256 updatedAt);
|
|
23
23
|
|
|
@@ -51,20 +51,28 @@ contract JBChainlinkV3PriceFeed is IJBPriceFeed {
|
|
|
51
51
|
/// @return The current unit price from the feed, as a fixed point number with the specified number of decimals.
|
|
52
52
|
function currentUnitPrice(uint256 decimals) public view virtual override returns (uint256) {
|
|
53
53
|
// Get the latest round information from the feed.
|
|
54
|
-
// slither-disable-next-line unused-return
|
|
55
54
|
(uint80 roundId, int256 price,, uint256 updatedAt, uint80 answeredInRound) = FEED.latestRoundData();
|
|
56
55
|
|
|
57
56
|
// Make sure the round is finished (check before stale price to avoid false stale on incomplete rounds).
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
if (updatedAt == 0) {
|
|
58
|
+
revert JBChainlinkV3PriceFeed_IncompleteRound({
|
|
59
|
+
roundId: roundId, answeredInRound: answeredInRound, updatedAt: updatedAt
|
|
60
|
+
});
|
|
61
|
+
}
|
|
60
62
|
|
|
61
63
|
// Make sure the answer was provided in the current round.
|
|
62
|
-
if (answeredInRound < roundId)
|
|
64
|
+
if (answeredInRound < roundId) {
|
|
65
|
+
revert JBChainlinkV3PriceFeed_IncompleteRound({
|
|
66
|
+
roundId: roundId, answeredInRound: answeredInRound, updatedAt: updatedAt
|
|
67
|
+
});
|
|
68
|
+
}
|
|
63
69
|
|
|
64
70
|
// Make sure the price's update threshold is met.
|
|
65
71
|
// forge-lint: disable-next-line(block-timestamp)
|
|
66
72
|
if (block.timestamp > THRESHOLD + updatedAt) {
|
|
67
|
-
revert JBChainlinkV3PriceFeed_StalePrice(
|
|
73
|
+
revert JBChainlinkV3PriceFeed_StalePrice({
|
|
74
|
+
timestamp: block.timestamp, threshold: THRESHOLD, updatedAt: updatedAt
|
|
75
|
+
});
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
// Make sure the price is positive.
|
|
@@ -14,7 +14,7 @@ contract JBChainlinkV3SequencerPriceFeed is JBChainlinkV3PriceFeed {
|
|
|
14
14
|
// --------------------------- custom errors ------------------------- //
|
|
15
15
|
//*********************************************************************//
|
|
16
16
|
|
|
17
|
-
error JBChainlinkV3SequencerPriceFeed_InvalidRound();
|
|
17
|
+
error JBChainlinkV3SequencerPriceFeed_InvalidRound(uint256 startedAt);
|
|
18
18
|
error JBChainlinkV3SequencerPriceFeed_SequencerDownOrRestarting(
|
|
19
19
|
uint256 timestamp, uint256 gracePeriodTime, uint256 startedAt
|
|
20
20
|
);
|
|
@@ -58,18 +58,17 @@ contract JBChainlinkV3SequencerPriceFeed is JBChainlinkV3PriceFeed {
|
|
|
58
58
|
/// @return The current unit price from the feed, as a fixed point number with the specified number of decimals.
|
|
59
59
|
function currentUnitPrice(uint256 decimals) public view override returns (uint256) {
|
|
60
60
|
// Fetch sequencer status.
|
|
61
|
-
// slither-disable-next-line unused-return
|
|
62
61
|
(, int256 answer, uint256 startedAt,,) = SEQUENCER_FEED.latestRoundData();
|
|
63
62
|
|
|
64
63
|
// Check if round is valid to prevent an edge-case where Arbitrum uptime contract is not init.
|
|
65
|
-
if (startedAt == 0) revert JBChainlinkV3SequencerPriceFeed_InvalidRound();
|
|
64
|
+
if (startedAt == 0) revert JBChainlinkV3SequencerPriceFeed_InvalidRound({startedAt: startedAt});
|
|
66
65
|
|
|
67
66
|
// Revert if sequencer has too recently restarted or is currently down.
|
|
68
67
|
// forge-lint: disable-next-line(block-timestamp)
|
|
69
68
|
if (block.timestamp <= GRACE_PERIOD_TIME + startedAt || answer != 0) {
|
|
70
|
-
revert JBChainlinkV3SequencerPriceFeed_SequencerDownOrRestarting(
|
|
71
|
-
block.timestamp, GRACE_PERIOD_TIME, startedAt
|
|
72
|
-
);
|
|
69
|
+
revert JBChainlinkV3SequencerPriceFeed_SequencerDownOrRestarting({
|
|
70
|
+
timestamp: block.timestamp, gracePeriodTime: GRACE_PERIOD_TIME, startedAt: startedAt
|
|
71
|
+
});
|
|
73
72
|
}
|
|
74
73
|
|
|
75
74
|
return super.currentUnitPrice(decimals);
|
package/src/JBController.sol
CHANGED
|
@@ -60,19 +60,19 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
60
60
|
//*********************************************************************//
|
|
61
61
|
|
|
62
62
|
error JBController_AddingPriceFeedNotAllowed(uint256 projectId);
|
|
63
|
-
error JBController_CreditTransfersPaused();
|
|
63
|
+
error JBController_CreditTransfersPaused(uint256 projectId, uint256 rulesetId);
|
|
64
64
|
error JBController_InvalidCashOutTaxRate(uint256 rate, uint256 limit);
|
|
65
65
|
error JBController_InvalidReservedPercent(uint256 percent, uint256 limit);
|
|
66
66
|
error JBController_MintNotAllowedAndNotTerminalOrHook(address caller);
|
|
67
|
-
error JBController_NoReservedTokens();
|
|
67
|
+
error JBController_NoReservedTokens(uint256 projectId);
|
|
68
68
|
error JBController_OnlyDirectory(address sender, IJBDirectory directory);
|
|
69
69
|
error JBController_PendingReservedTokens(uint256 pendingReservedTokenBalance);
|
|
70
70
|
error JBController_RulesetsAlreadyLaunched(uint256 projectId);
|
|
71
|
-
error JBController_RulesetsArrayEmpty();
|
|
71
|
+
error JBController_RulesetsArrayEmpty(uint256 projectId, uint256 rulesetConfigurationCount);
|
|
72
72
|
error JBController_RulesetSetTokenNotAllowed(uint256 projectId);
|
|
73
|
-
error JBController_TerminalTokensNotTransferred();
|
|
74
|
-
error JBController_ZeroTokensToBurn();
|
|
75
|
-
error JBController_ZeroTokensToMint();
|
|
73
|
+
error JBController_TerminalTokensNotTransferred(address terminal, address token, uint256 allowance);
|
|
74
|
+
error JBController_ZeroTokensToBurn(uint256 projectId, address holder);
|
|
75
|
+
error JBController_ZeroTokensToMint(uint256 projectId, address beneficiary);
|
|
76
76
|
|
|
77
77
|
//*********************************************************************//
|
|
78
78
|
// --------------- public immutable stored properties ---------------- //
|
|
@@ -163,7 +163,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
163
163
|
RULESETS = rulesets;
|
|
164
164
|
SPLITS = splits;
|
|
165
165
|
TOKENS = tokens;
|
|
166
|
-
// slither-disable-next-line missing-zero-check
|
|
167
166
|
OMNICHAIN_RULESET_OPERATOR = omnichainRulesetOperator;
|
|
168
167
|
}
|
|
169
168
|
|
|
@@ -189,7 +188,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
189
188
|
{
|
|
190
189
|
// Enforce permissions.
|
|
191
190
|
_requirePermissionFrom({
|
|
192
|
-
account:
|
|
191
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.ADD_PRICE_FEED
|
|
193
192
|
});
|
|
194
193
|
|
|
195
194
|
JBRuleset memory ruleset = _currentRulesetOf(projectId);
|
|
@@ -235,7 +234,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
235
234
|
from.supportsInterface(type(IJBController).interfaceId)
|
|
236
235
|
&& IJBController(address(from)).pendingReservedTokenBalanceOf(projectId) > 0
|
|
237
236
|
) {
|
|
238
|
-
// slither-disable-next-line unused-return
|
|
239
237
|
IJBController(address(from)).sendReservedTokensToSplitsOf(projectId);
|
|
240
238
|
}
|
|
241
239
|
}
|
|
@@ -261,11 +259,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
261
259
|
account: holder,
|
|
262
260
|
projectId: projectId,
|
|
263
261
|
permissionId: JBPermissionIds.BURN_TOKENS,
|
|
264
|
-
alsoGrantAccessIf: _isTerminalOf(projectId, _msgSender())
|
|
262
|
+
alsoGrantAccessIf: _isTerminalOf({projectId: projectId, terminal: _msgSender()})
|
|
265
263
|
});
|
|
266
264
|
|
|
267
265
|
// There must be tokens to burn.
|
|
268
|
-
if (tokenCount == 0) revert JBController_ZeroTokensToBurn();
|
|
266
|
+
if (tokenCount == 0) revert JBController_ZeroTokensToBurn({projectId: projectId, holder: holder});
|
|
269
267
|
|
|
270
268
|
emit BurnTokens({
|
|
271
269
|
holder: holder, projectId: projectId, tokenCount: tokenCount, memo: memo, caller: _msgSender()
|
|
@@ -319,7 +317,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
319
317
|
{
|
|
320
318
|
// Enforce permissions.
|
|
321
319
|
_requirePermissionFrom({
|
|
322
|
-
account:
|
|
320
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.DEPLOY_ERC20
|
|
323
321
|
});
|
|
324
322
|
|
|
325
323
|
// If a salt is provided, use it.
|
|
@@ -359,7 +357,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
359
357
|
// Approve the tokens being paid.
|
|
360
358
|
IERC20(address(token)).forceApprove({spender: address(terminal), value: splitTokenCount});
|
|
361
359
|
|
|
362
|
-
// slither-disable-next-line unused-return
|
|
363
360
|
terminal.pay({
|
|
364
361
|
projectId: projectId,
|
|
365
362
|
token: address(token),
|
|
@@ -371,8 +368,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
371
368
|
});
|
|
372
369
|
|
|
373
370
|
// Make sure that the terminal received the tokens.
|
|
374
|
-
|
|
375
|
-
|
|
371
|
+
uint256 allowance = IERC20(address(token)).allowance({owner: address(this), spender: address(terminal)});
|
|
372
|
+
if (allowance != 0) {
|
|
373
|
+
revert JBController_TerminalTokensNotTransferred({
|
|
374
|
+
terminal: address(terminal), token: address(token), allowance: allowance
|
|
375
|
+
});
|
|
376
376
|
}
|
|
377
377
|
}
|
|
378
378
|
|
|
@@ -400,7 +400,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
400
400
|
returns (uint256 projectId)
|
|
401
401
|
{
|
|
402
402
|
// Mint the project ERC-721 into the owner's wallet.
|
|
403
|
-
// slither-disable-next-line reentrancy-benign
|
|
404
403
|
projectId = PROJECTS.createFor(owner);
|
|
405
404
|
|
|
406
405
|
// If provided, set the project's metadata URI.
|
|
@@ -415,7 +414,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
415
414
|
_configureTerminals({projectId: projectId, terminalConfigurations: terminalConfigurations});
|
|
416
415
|
|
|
417
416
|
// Queue the rulesets.
|
|
418
|
-
// slither-disable-next-line reentrancy-events
|
|
419
417
|
uint256 rulesetId = _queueRulesets({projectId: projectId, rulesetConfigurations: rulesetConfigurations});
|
|
420
418
|
|
|
421
419
|
emit LaunchProject({
|
|
@@ -445,39 +443,41 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
445
443
|
returns (uint256 rulesetId)
|
|
446
444
|
{
|
|
447
445
|
// Make sure there are rulesets being queued.
|
|
448
|
-
if (rulesetConfigurations.length == 0)
|
|
446
|
+
if (rulesetConfigurations.length == 0) {
|
|
447
|
+
revert JBController_RulesetsArrayEmpty({
|
|
448
|
+
projectId: projectId, rulesetConfigurationCount: rulesetConfigurations.length
|
|
449
|
+
});
|
|
450
|
+
}
|
|
449
451
|
|
|
450
452
|
// Keep a reference to the sender.
|
|
451
453
|
address sender = _msgSender();
|
|
452
454
|
|
|
453
455
|
// Enforce permissions.
|
|
456
|
+
bool isOmnichainOperator = sender == OMNICHAIN_RULESET_OPERATOR;
|
|
454
457
|
_requirePermissionAllowingOverrideFrom({
|
|
455
|
-
account:
|
|
458
|
+
account: _ownerOf(projectId),
|
|
456
459
|
projectId: projectId,
|
|
457
460
|
permissionId: JBPermissionIds.LAUNCH_RULESETS,
|
|
458
|
-
alsoGrantAccessIf:
|
|
461
|
+
alsoGrantAccessIf: isOmnichainOperator
|
|
459
462
|
});
|
|
460
|
-
|
|
461
|
-
// Enforce permissions.
|
|
462
463
|
_requirePermissionAllowingOverrideFrom({
|
|
463
|
-
account:
|
|
464
|
+
account: _ownerOf(projectId),
|
|
464
465
|
projectId: projectId,
|
|
465
466
|
permissionId: JBPermissionIds.SET_TERMINALS,
|
|
466
|
-
alsoGrantAccessIf:
|
|
467
|
+
alsoGrantAccessIf: isOmnichainOperator
|
|
467
468
|
});
|
|
468
|
-
|
|
469
469
|
if (bytes(projectUri).length > 0) {
|
|
470
470
|
_requirePermissionAllowingOverrideFrom({
|
|
471
|
-
account:
|
|
471
|
+
account: _ownerOf(projectId),
|
|
472
472
|
projectId: projectId,
|
|
473
473
|
permissionId: JBPermissionIds.SET_PROJECT_URI,
|
|
474
|
-
alsoGrantAccessIf:
|
|
474
|
+
alsoGrantAccessIf: isOmnichainOperator
|
|
475
475
|
});
|
|
476
476
|
}
|
|
477
477
|
|
|
478
478
|
// If the project has already had rulesets, use `queueRulesetsOf(...)` instead.
|
|
479
479
|
if (RULESETS.latestRulesetIdOf(projectId) > 0) {
|
|
480
|
-
revert JBController_RulesetsAlreadyLaunched(projectId);
|
|
480
|
+
revert JBController_RulesetsAlreadyLaunched({projectId: projectId});
|
|
481
481
|
}
|
|
482
482
|
|
|
483
483
|
// If provided, set the project's metadata URI.
|
|
@@ -492,7 +492,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
492
492
|
_configureTerminals({projectId: projectId, terminalConfigurations: terminalConfigurations});
|
|
493
493
|
|
|
494
494
|
// Queue the first ruleset.
|
|
495
|
-
// slither-disable-next-line reentrancy-events
|
|
496
495
|
rulesetId = _queueRulesets({projectId: projectId, rulesetConfigurations: rulesetConfigurations});
|
|
497
496
|
|
|
498
497
|
emit LaunchRulesets({
|
|
@@ -539,7 +538,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
539
538
|
returns (uint256 beneficiaryTokenCount)
|
|
540
539
|
{
|
|
541
540
|
// There should be tokens to mint.
|
|
542
|
-
if (tokenCount == 0) revert JBController_ZeroTokensToMint();
|
|
541
|
+
if (tokenCount == 0) revert JBController_ZeroTokensToMint({projectId: projectId, beneficiary: beneficiary});
|
|
543
542
|
|
|
544
543
|
// Keep a reference to the reserved percent.
|
|
545
544
|
uint256 reservedPercent;
|
|
@@ -549,16 +548,16 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
549
548
|
|
|
550
549
|
// Cache common values used in both permission checks.
|
|
551
550
|
address sender = _msgSender();
|
|
552
|
-
bool senderIsTerminal = _isTerminalOf(projectId, sender);
|
|
551
|
+
bool senderIsTerminal = _isTerminalOf({projectId: projectId, terminal: sender});
|
|
553
552
|
bool senderIsTerminalOrDataHook = senderIsTerminal || sender == ruleset.dataHook();
|
|
554
553
|
// Only query the data hook if the sender isn't already a terminal or the data hook itself.
|
|
555
|
-
bool senderHasDataHookMintPermission =
|
|
556
|
-
|
|
554
|
+
bool senderHasDataHookMintPermission = !senderIsTerminalOrDataHook
|
|
555
|
+
&& _hasDataHookMintPermissionFor({projectId: projectId, ruleset: ruleset, addr: sender});
|
|
557
556
|
|
|
558
557
|
// Minting is restricted to: the project's owner, addresses with permission to `MINT_TOKENS`, the project's
|
|
559
558
|
// terminals, and the project's data hook.
|
|
560
559
|
_requirePermissionAllowingOverrideFrom({
|
|
561
|
-
account:
|
|
560
|
+
account: _ownerOf(projectId),
|
|
562
561
|
projectId: projectId,
|
|
563
562
|
permissionId: JBPermissionIds.MINT_TOKENS,
|
|
564
563
|
alsoGrantAccessIf: senderIsTerminalOrDataHook || senderHasDataHookMintPermission
|
|
@@ -570,7 +569,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
570
569
|
ruleset.id != 0 && !ruleset.allowOwnerMinting() && !senderIsTerminalOrDataHook
|
|
571
570
|
&& !senderHasDataHookMintPermission
|
|
572
571
|
) {
|
|
573
|
-
revert JBController_MintNotAllowedAndNotTerminalOrHook(sender);
|
|
572
|
+
revert JBController_MintNotAllowedAndNotTerminalOrHook({caller: sender});
|
|
574
573
|
}
|
|
575
574
|
|
|
576
575
|
// Determine the reserved percent to use.
|
|
@@ -581,7 +580,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
581
580
|
|
|
582
581
|
if (beneficiaryTokenCount != 0) {
|
|
583
582
|
// Mint the tokens.
|
|
584
|
-
// slither-disable-next-line reentrancy-benign,reentrancy-events,unused-return
|
|
585
583
|
TOKENS.mintFor({holder: beneficiary, projectId: projectId, count: beneficiaryTokenCount});
|
|
586
584
|
}
|
|
587
585
|
|
|
@@ -618,18 +616,21 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
618
616
|
returns (uint256 rulesetId)
|
|
619
617
|
{
|
|
620
618
|
// Make sure there are rulesets being queued.
|
|
621
|
-
if (rulesetConfigurations.length == 0)
|
|
619
|
+
if (rulesetConfigurations.length == 0) {
|
|
620
|
+
revert JBController_RulesetsArrayEmpty({
|
|
621
|
+
projectId: projectId, rulesetConfigurationCount: rulesetConfigurations.length
|
|
622
|
+
});
|
|
623
|
+
}
|
|
622
624
|
|
|
623
625
|
// Enforce permissions.
|
|
624
626
|
_requirePermissionAllowingOverrideFrom({
|
|
625
|
-
account:
|
|
627
|
+
account: _ownerOf(projectId),
|
|
626
628
|
projectId: projectId,
|
|
627
629
|
permissionId: JBPermissionIds.QUEUE_RULESETS,
|
|
628
630
|
alsoGrantAccessIf: _msgSender() == OMNICHAIN_RULESET_OPERATOR
|
|
629
631
|
});
|
|
630
632
|
|
|
631
633
|
// Queue the rulesets.
|
|
632
|
-
// slither-disable-next-line reentrancy-events
|
|
633
634
|
rulesetId = _queueRulesets({projectId: projectId, rulesetConfigurations: rulesetConfigurations});
|
|
634
635
|
|
|
635
636
|
emit QueueRulesets({rulesetId: rulesetId, projectId: projectId, memo: memo, caller: _msgSender()});
|
|
@@ -662,7 +663,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
662
663
|
{
|
|
663
664
|
// Enforce permissions.
|
|
664
665
|
_requirePermissionFrom({
|
|
665
|
-
account:
|
|
666
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_SPLIT_GROUPS
|
|
666
667
|
});
|
|
667
668
|
|
|
668
669
|
// Set the split groups.
|
|
@@ -676,7 +677,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
676
677
|
function setTokenFor(uint256 projectId, IJBToken token) external override {
|
|
677
678
|
// Enforce permissions.
|
|
678
679
|
_requirePermissionFrom({
|
|
679
|
-
account:
|
|
680
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_TOKEN
|
|
680
681
|
});
|
|
681
682
|
|
|
682
683
|
// Get a reference to the current ruleset.
|
|
@@ -700,7 +701,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
700
701
|
function setTokenMetadataOf(uint256 projectId, string calldata name, string calldata symbol) external override {
|
|
701
702
|
// Enforce permissions.
|
|
702
703
|
_requirePermissionFrom({
|
|
703
|
-
account:
|
|
704
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_TOKEN_METADATA
|
|
704
705
|
});
|
|
705
706
|
|
|
706
707
|
TOKENS.setTokenMetadataFor({projectId: projectId, name: name, symbol: symbol});
|
|
@@ -715,7 +716,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
715
716
|
function setUriOf(uint256 projectId, string calldata uri) external override {
|
|
716
717
|
// Enforce permissions.
|
|
717
718
|
_requirePermissionFrom({
|
|
718
|
-
account:
|
|
719
|
+
account: _ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_PROJECT_URI
|
|
719
720
|
});
|
|
720
721
|
|
|
721
722
|
// Set the project's metadata URI.
|
|
@@ -748,7 +749,9 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
748
749
|
JBRuleset memory ruleset = _currentRulesetOf(projectId);
|
|
749
750
|
|
|
750
751
|
// Credit transfers must not be paused.
|
|
751
|
-
if (ruleset.pauseCreditTransfers())
|
|
752
|
+
if (ruleset.pauseCreditTransfers()) {
|
|
753
|
+
revert JBController_CreditTransfersPaused({projectId: projectId, rulesetId: ruleset.id});
|
|
754
|
+
}
|
|
752
755
|
|
|
753
756
|
TOKENS.transferCreditsFrom({holder: holder, projectId: projectId, recipient: recipient, count: creditCount});
|
|
754
757
|
}
|
|
@@ -939,7 +942,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
939
942
|
JBTerminalConfig memory terminalConfig = terminalConfigurations[i];
|
|
940
943
|
|
|
941
944
|
// Add the accounting contexts for the specified tokens.
|
|
942
|
-
// slither-disable-next-line calls-loop
|
|
943
945
|
terminalConfig.terminal
|
|
944
946
|
.addAccountingContextsFor({
|
|
945
947
|
projectId: projectId, accountingContexts: terminalConfig.accountingContextsToAccept
|
|
@@ -975,20 +977,19 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
975
977
|
|
|
976
978
|
// Make sure its reserved percent is valid.
|
|
977
979
|
if (rulesetConfig.metadata.reservedPercent > JBConstants.MAX_RESERVED_PERCENT) {
|
|
978
|
-
revert JBController_InvalidReservedPercent(
|
|
979
|
-
rulesetConfig.metadata.reservedPercent, JBConstants.MAX_RESERVED_PERCENT
|
|
980
|
-
);
|
|
980
|
+
revert JBController_InvalidReservedPercent({
|
|
981
|
+
percent: rulesetConfig.metadata.reservedPercent, limit: JBConstants.MAX_RESERVED_PERCENT
|
|
982
|
+
});
|
|
981
983
|
}
|
|
982
984
|
|
|
983
985
|
// Make sure its cash out tax rate is valid.
|
|
984
986
|
if (rulesetConfig.metadata.cashOutTaxRate > JBConstants.MAX_CASH_OUT_TAX_RATE) {
|
|
985
|
-
revert JBController_InvalidCashOutTaxRate(
|
|
986
|
-
rulesetConfig.metadata.cashOutTaxRate, JBConstants.MAX_CASH_OUT_TAX_RATE
|
|
987
|
-
);
|
|
987
|
+
revert JBController_InvalidCashOutTaxRate({
|
|
988
|
+
rate: rulesetConfig.metadata.cashOutTaxRate, limit: JBConstants.MAX_CASH_OUT_TAX_RATE
|
|
989
|
+
});
|
|
988
990
|
}
|
|
989
991
|
|
|
990
992
|
// Queue its ruleset.
|
|
991
|
-
// slither-disable-next-line calls-loop
|
|
992
993
|
JBRuleset memory ruleset = RULESETS.queueFor({
|
|
993
994
|
projectId: projectId,
|
|
994
995
|
duration: rulesetConfig.duration,
|
|
@@ -1000,13 +1001,11 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1000
1001
|
});
|
|
1001
1002
|
|
|
1002
1003
|
// Set its split groups.
|
|
1003
|
-
// slither-disable-next-line calls-loop
|
|
1004
1004
|
SPLITS.setSplitGroupsOf({
|
|
1005
1005
|
projectId: projectId, rulesetId: ruleset.id, splitGroups: rulesetConfig.splitGroups
|
|
1006
1006
|
});
|
|
1007
1007
|
|
|
1008
1008
|
// Set its fund access limits.
|
|
1009
|
-
// slither-disable-next-line calls-loop
|
|
1010
1009
|
FUND_ACCESS_LIMITS.setFundAccessLimitsFor({
|
|
1011
1010
|
projectId: projectId, rulesetId: ruleset.id, fundAccessLimitGroups: rulesetConfig.fundAccessLimitGroups
|
|
1012
1011
|
});
|
|
@@ -1069,13 +1068,19 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1069
1068
|
|
|
1070
1069
|
// If the split has a hook, call its `processSplitWith` function.
|
|
1071
1070
|
if (split.hook != IJBSplitHook(address(0))) {
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1071
|
+
if (token != IJBToken(address(0))) {
|
|
1072
|
+
// ERC-20: grant the hook an allowance and let it pull tokens via transferFrom.
|
|
1073
|
+
IERC20(address(token)).forceApprove({spender: address(split.hook), value: splitTokenCount});
|
|
1074
|
+
} else {
|
|
1075
|
+
// Credits: send directly — there is no allowance mechanism for credits.
|
|
1076
|
+
TOKENS.transferCreditsFrom({
|
|
1077
|
+
holder: address(this),
|
|
1078
|
+
projectId: projectId,
|
|
1079
|
+
recipient: address(split.hook),
|
|
1080
|
+
count: splitTokenCount
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1077
1083
|
|
|
1078
|
-
// slither-disable-next-line calls-loop,reentrancy-events
|
|
1079
1084
|
try split.hook
|
|
1080
1085
|
.processSplitWith(
|
|
1081
1086
|
JBSplitHookContext({
|
|
@@ -1088,9 +1093,18 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1088
1093
|
})
|
|
1089
1094
|
) {}
|
|
1090
1095
|
catch (bytes memory reason) {
|
|
1091
|
-
// If the hook reverts, the tokens already transferred to it stay with the hook.
|
|
1092
1096
|
emit SplitHookReverted({projectId: projectId, hook: address(split.hook), reason: reason});
|
|
1093
1097
|
}
|
|
1098
|
+
|
|
1099
|
+
// For ERC-20 tokens, burn any tokens the hook did not consume.
|
|
1100
|
+
if (token != IJBToken(address(0))) {
|
|
1101
|
+
uint256 remaining =
|
|
1102
|
+
IERC20(address(token)).allowance({owner: address(this), spender: address(split.hook)});
|
|
1103
|
+
if (remaining > 0) {
|
|
1104
|
+
IERC20(address(token)).forceApprove({spender: address(split.hook), value: 0});
|
|
1105
|
+
TOKENS.burnFrom({holder: address(this), projectId: projectId, count: remaining});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1094
1108
|
// If the split has a project ID, try to pay the project. If that fails, pay the beneficiary.
|
|
1095
1109
|
} else {
|
|
1096
1110
|
// Pay the project using the split's beneficiary if one was provided. Otherwise, use the message
|
|
@@ -1099,7 +1113,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1099
1113
|
|
|
1100
1114
|
if (split.projectId != 0) {
|
|
1101
1115
|
// Get a reference to the receiving project's primary payment terminal for the token.
|
|
1102
|
-
// slither-disable-next-line calls-loop
|
|
1103
1116
|
IJBTerminal terminal = token == IJBToken(address(0))
|
|
1104
1117
|
? IJBTerminal(address(0))
|
|
1105
1118
|
: DIRECTORY.primaryTerminalOf({projectId: split.projectId, token: address(token)});
|
|
@@ -1108,17 +1121,14 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1108
1121
|
// which accepts the token, send the tokens to the beneficiary.
|
|
1109
1122
|
if (address(token) == address(0) || address(terminal) == address(0)) {
|
|
1110
1123
|
// Mint the tokens to the beneficiary.
|
|
1111
|
-
// slither-disable-next-line reentrancy-events
|
|
1112
1124
|
_sendTokens({
|
|
1113
1125
|
projectId: projectId, tokenCount: splitTokenCount, recipient: beneficiary, token: token
|
|
1114
1126
|
});
|
|
1115
1127
|
} else {
|
|
1116
1128
|
// Use the `projectId` in the pay metadata.
|
|
1117
|
-
// slither-disable-next-line reentrancy-events
|
|
1118
1129
|
bytes memory metadata = bytes(abi.encodePacked(projectId));
|
|
1119
1130
|
|
|
1120
1131
|
// Try to fulfill the payment.
|
|
1121
|
-
// slither-disable-next-line calls-loop
|
|
1122
1132
|
try this.executePayReservedTokenToTerminal({
|
|
1123
1133
|
projectId: split.projectId,
|
|
1124
1134
|
terminal: terminal,
|
|
@@ -1142,7 +1152,6 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1142
1152
|
}
|
|
1143
1153
|
} else if (beneficiary == address(0xdead)) {
|
|
1144
1154
|
// If the split has no project ID, and the beneficiary is 0xdead, burn.
|
|
1145
|
-
// slither-disable-next-line calls-loop
|
|
1146
1155
|
TOKENS.burnFrom({holder: address(this), projectId: projectId, count: splitTokenCount});
|
|
1147
1156
|
} else {
|
|
1148
1157
|
// If the split has no project Id, send to beneficiary.
|
|
@@ -1180,13 +1189,13 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1180
1189
|
tokenCount = pendingReservedTokenBalanceOf[projectId];
|
|
1181
1190
|
|
|
1182
1191
|
// Revert if there are no pending reserved tokens
|
|
1183
|
-
if (tokenCount == 0) revert JBController_NoReservedTokens();
|
|
1192
|
+
if (tokenCount == 0) revert JBController_NoReservedTokens({projectId: projectId});
|
|
1184
1193
|
|
|
1185
1194
|
// Get the ruleset to read the reserved percent from.
|
|
1186
1195
|
JBRuleset memory ruleset = _currentRulesetOf(projectId);
|
|
1187
1196
|
|
|
1188
1197
|
// Get a reference to the project's owner.
|
|
1189
|
-
address owner =
|
|
1198
|
+
address owner = _ownerOf(projectId);
|
|
1190
1199
|
|
|
1191
1200
|
// Reset the pending reserved token balance.
|
|
1192
1201
|
pendingReservedTokenBalanceOf[projectId] = 0;
|
|
@@ -1313,6 +1322,13 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
1313
1322
|
return ERC2771Context._msgSender();
|
|
1314
1323
|
}
|
|
1315
1324
|
|
|
1325
|
+
/// @notice The owner of a project.
|
|
1326
|
+
/// @param projectId The ID of the project to get the owner of.
|
|
1327
|
+
/// @return The owner of the project.
|
|
1328
|
+
function _ownerOf(uint256 projectId) internal view returns (address) {
|
|
1329
|
+
return PROJECTS.ownerOf(projectId);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1316
1332
|
/// @notice The project's upcoming ruleset.
|
|
1317
1333
|
/// @param projectId The ID of the project to check.
|
|
1318
1334
|
/// @return The project's upcoming ruleset.
|