@bananapus/ownable-v6 0.0.7 → 0.0.9
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/STYLE_GUIDE.md +148 -44
- package/foundry.lock +5 -0
- package/foundry.toml +0 -3
- package/package.json +4 -3
- package/src/JBOwnableOverrides.sol +7 -6
- package/src/structs/JBOwner.sol +1 -0
- package/test/Ownable.t.sol +39 -30
- package/test/OwnableAttacks.t.sol +23 -18
- package/test/OwnableEdgeCases.t.sol +41 -35
- package/test/OwnableInvariantTests.sol +0 -9
- package/test/handlers/OwnableHandler.sol +1 -4
- package/test/mocks/MockOwnable.sol +1 -1
- package/test/regression/{L65_BurnLockProtection.t.sol → BurnLockProtection.t.sol} +19 -17
- package/test/regression/{L66_ZeroAddressValidation.t.sol → ZeroAddressValidation.t.sol} +15 -14
package/STYLE_GUIDE.md
CHANGED
|
@@ -17,8 +17,6 @@ src/
|
|
|
17
17
|
|
|
18
18
|
One contract/interface/struct/enum per file. Name the file after the type it contains.
|
|
19
19
|
|
|
20
|
-
**Structs, enums, libraries, and interfaces always go in their subdirectories** (`src/structs/`, `src/enums/`, `src/libraries/`, `src/interfaces/`) — never inline in contract files or placed in `src/` root. This keeps type definitions discoverable and import paths consistent across repos.
|
|
21
|
-
|
|
22
20
|
## Pragma Versions
|
|
23
21
|
|
|
24
22
|
```solidity
|
|
@@ -106,14 +104,6 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
106
104
|
// -------------------------- constructor ---------------------------- //
|
|
107
105
|
//*********************************************************************//
|
|
108
106
|
|
|
109
|
-
//*********************************************************************//
|
|
110
|
-
// ---------------------- receive / fallback ------------------------- //
|
|
111
|
-
//*********************************************************************//
|
|
112
|
-
|
|
113
|
-
//*********************************************************************//
|
|
114
|
-
// --------------------------- modifiers ----------------------------- //
|
|
115
|
-
//*********************************************************************//
|
|
116
|
-
|
|
117
107
|
//*********************************************************************//
|
|
118
108
|
// ---------------------- external transactions ---------------------- //
|
|
119
109
|
//*********************************************************************//
|
|
@@ -141,28 +131,23 @@ contract JBExample is JBPermissioned, IJBExample {
|
|
|
141
131
|
```
|
|
142
132
|
|
|
143
133
|
**Section order:**
|
|
144
|
-
1.
|
|
145
|
-
2.
|
|
146
|
-
3.
|
|
147
|
-
4.
|
|
148
|
-
5.
|
|
149
|
-
6.
|
|
150
|
-
7.
|
|
151
|
-
8.
|
|
152
|
-
9.
|
|
153
|
-
10.
|
|
154
|
-
11.
|
|
155
|
-
12.
|
|
156
|
-
13.
|
|
157
|
-
14.
|
|
158
|
-
15. Internal helpers
|
|
159
|
-
16. Internal views
|
|
160
|
-
17. Private helpers
|
|
134
|
+
1. Custom errors
|
|
135
|
+
2. Public constants
|
|
136
|
+
3. Internal constants
|
|
137
|
+
4. Public immutable stored properties
|
|
138
|
+
5. Internal immutable stored properties
|
|
139
|
+
6. Public stored properties
|
|
140
|
+
7. Internal stored properties
|
|
141
|
+
8. Constructor
|
|
142
|
+
9. External transactions
|
|
143
|
+
10. External views
|
|
144
|
+
11. Public transactions
|
|
145
|
+
12. Internal helpers
|
|
146
|
+
13. Internal views
|
|
147
|
+
14. Private helpers
|
|
161
148
|
|
|
162
149
|
Functions are alphabetized within each section.
|
|
163
150
|
|
|
164
|
-
**Events:** Events are declared in interfaces only, never in implementation contracts. Implementations inherit events from their interface and emit them unqualified. This keeps the ABI definition in one place and allows tests to use interface-qualified event expectations (e.g., `emit IJBController.LaunchProject(...)`).
|
|
165
|
-
|
|
166
151
|
## Interface Structure
|
|
167
152
|
|
|
168
153
|
```solidity
|
|
@@ -212,7 +197,7 @@ interface IJBExample is IJBBase {
|
|
|
212
197
|
| Public/external function | `camelCase` | `cashOutTokensOf` |
|
|
213
198
|
| Internal/private function | `_camelCase` | `_processFee` |
|
|
214
199
|
| Internal storage | `_camelCase` | `_accountingContextForTokenOf` |
|
|
215
|
-
| Function parameter | `camelCase` | `projectId`, `cashOutCount` |
|
|
200
|
+
| Function parameter | `camelCase` (no underscores) | `projectId`, `cashOutCount` |
|
|
216
201
|
|
|
217
202
|
## NatSpec
|
|
218
203
|
|
|
@@ -268,9 +253,12 @@ uint256 public constant MAX_RESERVED_PERCENT = 10_000;
|
|
|
268
253
|
|
|
269
254
|
## Function Calls
|
|
270
255
|
|
|
271
|
-
Use named
|
|
256
|
+
Use named arguments for all function calls with 2 or more arguments — in both `src/` and `script/`:
|
|
272
257
|
|
|
273
258
|
```solidity
|
|
259
|
+
// Good — named arguments
|
|
260
|
+
token.mint({account: beneficiary, amount: count});
|
|
261
|
+
_transferOwnership({newOwner: address(0), projectId: 0});
|
|
274
262
|
PERMISSIONS.hasPermission({
|
|
275
263
|
operator: sender,
|
|
276
264
|
account: account,
|
|
@@ -279,8 +267,18 @@ PERMISSIONS.hasPermission({
|
|
|
279
267
|
includeRoot: true,
|
|
280
268
|
includeWildcardProjectId: true
|
|
281
269
|
});
|
|
270
|
+
|
|
271
|
+
// Bad — positional arguments with 2+ args
|
|
272
|
+
token.mint(beneficiary, count);
|
|
273
|
+
_transferOwnership(address(0), 0);
|
|
282
274
|
```
|
|
283
275
|
|
|
276
|
+
Single-argument calls use positional style: `_burn(amount)`.
|
|
277
|
+
|
|
278
|
+
This also applies to constructor calls, struct literals, and inherited/library calls (e.g., OZ `_mint`, `_safeMint`, `safeTransfer`, `allowance`, `Clones.cloneDeterministic`).
|
|
279
|
+
|
|
280
|
+
Named argument keys must use **camelCase** — never underscores. If a function's parameter names use underscores, rename them to camelCase first.
|
|
281
|
+
|
|
284
282
|
## Multiline Signatures
|
|
285
283
|
|
|
286
284
|
```solidity
|
|
@@ -334,9 +332,6 @@ optimizer_runs = 200
|
|
|
334
332
|
libs = ["node_modules", "lib"]
|
|
335
333
|
fs_permissions = [{ access = "read-write", path = "./"}]
|
|
336
334
|
|
|
337
|
-
[profile.ci_sizes]
|
|
338
|
-
optimizer_runs = 200
|
|
339
|
-
|
|
340
335
|
[fuzz]
|
|
341
336
|
runs = 4096
|
|
342
337
|
|
|
@@ -351,10 +346,14 @@ multiline_func_header = "all"
|
|
|
351
346
|
wrap_comments = true
|
|
352
347
|
```
|
|
353
348
|
|
|
354
|
-
**
|
|
355
|
-
- `
|
|
356
|
-
- `
|
|
357
|
-
|
|
349
|
+
**Optional sections (add only when needed):**
|
|
350
|
+
- `[rpc_endpoints]` — repos with fork tests. Maps named endpoints to env vars (e.g. `ethereum = "${RPC_ETHEREUM_MAINNET}"`).
|
|
351
|
+
- `[profile.ci_sizes]` — only when CI needs different optimizer settings than defaults for the size check step (e.g. `optimizer_runs = 200` when the default profile uses a lower value).
|
|
352
|
+
|
|
353
|
+
**Common variations:**
|
|
354
|
+
- `via_ir = true` when hitting stack-too-deep
|
|
355
|
+
- `optimizer = false` when optimization causes stack-too-deep
|
|
356
|
+
- `optimizer_runs` reduced when deep struct nesting causes stack-too-deep at 200 runs
|
|
358
357
|
|
|
359
358
|
### CI Workflows
|
|
360
359
|
|
|
@@ -384,8 +383,10 @@ jobs:
|
|
|
384
383
|
uses: foundry-rs/foundry-toolchain@v1
|
|
385
384
|
- name: Run tests
|
|
386
385
|
run: forge test --fail-fast --summary --detailed --skip "*/script/**"
|
|
386
|
+
env:
|
|
387
|
+
RPC_ETHEREUM_MAINNET: ${{ secrets.RPC_ETHEREUM_MAINNET }}
|
|
387
388
|
- name: Check contract sizes
|
|
388
|
-
run:
|
|
389
|
+
run: forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
|
|
389
390
|
```
|
|
390
391
|
|
|
391
392
|
**lint.yml:**
|
|
@@ -407,11 +408,60 @@ jobs:
|
|
|
407
408
|
run: forge fmt --check
|
|
408
409
|
```
|
|
409
410
|
|
|
411
|
+
**slither.yml** (repos with `src/` contracts only):
|
|
412
|
+
```yaml
|
|
413
|
+
name: slither
|
|
414
|
+
on:
|
|
415
|
+
pull_request:
|
|
416
|
+
branches:
|
|
417
|
+
- main
|
|
418
|
+
push:
|
|
419
|
+
branches:
|
|
420
|
+
- main
|
|
421
|
+
jobs:
|
|
422
|
+
analyze:
|
|
423
|
+
runs-on: ubuntu-latest
|
|
424
|
+
steps:
|
|
425
|
+
- uses: actions/checkout@v4
|
|
426
|
+
with:
|
|
427
|
+
submodules: recursive
|
|
428
|
+
- uses: actions/setup-node@v4
|
|
429
|
+
with:
|
|
430
|
+
node-version: latest
|
|
431
|
+
- name: Install npm dependencies
|
|
432
|
+
run: npm install --omit=dev
|
|
433
|
+
- name: Install Foundry
|
|
434
|
+
uses: foundry-rs/foundry-toolchain@v1
|
|
435
|
+
- name: Run slither
|
|
436
|
+
uses: crytic/slither-action@v0.3.1
|
|
437
|
+
with:
|
|
438
|
+
slither-config: slither-ci.config.json
|
|
439
|
+
fail-on: medium
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**slither-ci.config.json:**
|
|
443
|
+
```json
|
|
444
|
+
{
|
|
445
|
+
"detectors_to_exclude": "timestamp,uninitialized-local,naming-convention,solc-version,shadowing-local",
|
|
446
|
+
"exclude_informational": true,
|
|
447
|
+
"exclude_low": false,
|
|
448
|
+
"exclude_medium": false,
|
|
449
|
+
"exclude_high": false,
|
|
450
|
+
"disable_color": false,
|
|
451
|
+
"filter_paths": "(mocks/|test/|node_modules/|lib/)",
|
|
452
|
+
"legacy_ast": false
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Variations:**
|
|
457
|
+
- Deployer-only repos (no `src/`, only `script/`) skip slither entirely — the action's internal `forge build` skips `test/` and `script/` by default, leaving nothing to compile.
|
|
458
|
+
- Use inline `// slither-disable-next-line <detector>` to suppress known false positives rather than adding to `detectors_to_exclude` in the config. The comment must be on the line immediately before the flagged expression.
|
|
459
|
+
|
|
410
460
|
### package.json
|
|
411
461
|
|
|
412
462
|
```json
|
|
413
463
|
{
|
|
414
|
-
"name": "@bananapus/
|
|
464
|
+
"name": "@bananapus/package-name-v6",
|
|
415
465
|
"version": "x.x.x",
|
|
416
466
|
"license": "MIT",
|
|
417
467
|
"repository": { "type": "git", "url": "git+https://github.com/Org/repo.git" },
|
|
@@ -431,13 +481,62 @@ jobs:
|
|
|
431
481
|
|
|
432
482
|
### remappings.txt
|
|
433
483
|
|
|
434
|
-
Every repo has a `remappings.txt
|
|
484
|
+
Every repo has a `remappings.txt` as the **single source of truth** for import remappings. Never add remappings to `foundry.toml`.
|
|
485
|
+
|
|
486
|
+
**Principle:** Import paths in Solidity source must match npm package names exactly. With `libs = ["node_modules", "lib"]`, Foundry auto-resolves `@scope/package/path/File.sol` → `node_modules/@scope/package/path/File.sol`. No remapping needed for packages installed as real directories.
|
|
487
|
+
|
|
488
|
+
**Note:** Auto-resolution does **not** work for symlinked packages (e.g. npm workspace links). Workspace repos like `deploy-all-v6` and `nana-cli-v6` need explicit `@scope/package/=node_modules/@scope/package/` remappings for each symlinked dependency.
|
|
489
|
+
|
|
490
|
+
**Minimal content** (most repos):
|
|
435
491
|
|
|
436
492
|
```
|
|
437
|
-
|
|
493
|
+
forge-std/=lib/forge-std/src/
|
|
438
494
|
```
|
|
439
495
|
|
|
440
|
-
|
|
496
|
+
Only add extra remappings for:
|
|
497
|
+
- **`forge-std`** — always needed (git submodule with `src/` subdirectory)
|
|
498
|
+
- **Repo-specific `lib/` submodules** that have no npm package (e.g., `hookmate/=lib/hookmate/src/`)
|
|
499
|
+
- **Symlinked npm packages** — need explicit `@scope/package/=node_modules/@scope/package/` entries
|
|
500
|
+
- **Nested transitive deps** — e.g., `@chainlink/contracts-ccip/` nested inside `@bananapus/suckers-v6/node_modules/`
|
|
501
|
+
|
|
502
|
+
**Never add remappings for:**
|
|
503
|
+
- npm packages that match their import path and are installed as real directories — they auto-resolve
|
|
504
|
+
- Short-form aliases (e.g., `@bananapus/core/` → `@bananapus/core-v6/src/`) — fix the import instead
|
|
505
|
+
- Packages available via npm that are also git submodules — remove the submodule, use npm
|
|
506
|
+
|
|
507
|
+
**Import path convention:**
|
|
508
|
+
|
|
509
|
+
| Package | Import path | Resolves to |
|
|
510
|
+
|---------|------------|-------------|
|
|
511
|
+
| `@bananapus/core-v6` | `@bananapus/core-v6/src/libraries/JBConstants.sol` | `node_modules/@bananapus/core-v6/src/...` |
|
|
512
|
+
| `@openzeppelin/contracts` | `@openzeppelin/contracts/token/ERC20/IERC20.sol` | `node_modules/@openzeppelin/contracts/...` |
|
|
513
|
+
| `@uniswap/v4-core` | `@uniswap/v4-core/src/interfaces/IPoolManager.sol` | `node_modules/@uniswap/v4-core/src/...` |
|
|
514
|
+
|
|
515
|
+
### Linting
|
|
516
|
+
|
|
517
|
+
Solar (Foundry's built-in linter) runs automatically during `forge build`. It scans all `.sol` files in `libs` directories, including `node_modules`.
|
|
518
|
+
|
|
519
|
+
**All test helpers must use relative imports** (e.g. `../../src/structs/JBRuleset.sol`), not bare `src/` imports. This ensures solar can resolve paths when the helper is consumed via npm in downstream repos.
|
|
520
|
+
|
|
521
|
+
### Fork Tests
|
|
522
|
+
|
|
523
|
+
Fork tests use named RPC endpoints defined in `[rpc_endpoints]` of `foundry.toml`. No skip guards — fork tests should hard-fail if the RPC endpoint is unavailable, making CI failures explicit.
|
|
524
|
+
|
|
525
|
+
```solidity
|
|
526
|
+
function setUp() public {
|
|
527
|
+
vm.createSelectFork("ethereum");
|
|
528
|
+
// ... setup code
|
|
529
|
+
}
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
The endpoint name (e.g. `"ethereum"`) maps to an env var via `foundry.toml`:
|
|
533
|
+
|
|
534
|
+
```toml
|
|
535
|
+
[rpc_endpoints]
|
|
536
|
+
ethereum = "${RPC_ETHEREUM_MAINNET}"
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
For multi-chain fork tests, add all needed endpoints.
|
|
441
540
|
|
|
442
541
|
### Formatting
|
|
443
542
|
|
|
@@ -465,4 +564,9 @@ CI checks formatting via `forge fmt --check`.
|
|
|
465
564
|
|
|
466
565
|
### Contract Size Checks
|
|
467
566
|
|
|
468
|
-
CI runs `
|
|
567
|
+
CI runs `forge build --sizes` to catch contracts approaching the 24KB limit. When the repo's default `optimizer_runs` differs from what you want for size checking, use `FOUNDRY_PROFILE=ci_sizes forge build --sizes` with a `[profile.ci_sizes]` section in `foundry.toml`.
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
## Repo-Specific Deviations
|
|
571
|
+
|
|
572
|
+
None. This repo follows the standard configuration exactly.
|
package/foundry.lock
ADDED
package/foundry.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/ownable-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
"node": ">=20.0.0"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@bananapus/core-v6": "^0.0.
|
|
14
|
-
"@
|
|
13
|
+
"@bananapus/core-v6": "^0.0.16",
|
|
14
|
+
"@bananapus/permission-ids-v6": "^0.0.9",
|
|
15
|
+
"@openzeppelin/contracts": "^5.6.1"
|
|
15
16
|
},
|
|
16
17
|
"scripts": {
|
|
17
18
|
"test": "forge test",
|
|
@@ -75,7 +75,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
75
75
|
revert JBOwnableOverrides_InvalidNewOwner();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
_transferOwnership(initialOwner, initialProjectIdOwner);
|
|
78
|
+
_transferOwnership({newOwner: initialOwner, projectId: initialProjectIdOwner});
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
//*********************************************************************//
|
|
@@ -140,7 +140,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
140
140
|
/// @dev This can only be called by the current owner.
|
|
141
141
|
function renounceOwnership() public virtual override {
|
|
142
142
|
_checkOwner();
|
|
143
|
-
_transferOwnership(address(0), 0);
|
|
143
|
+
_transferOwnership({newOwner: address(0), projectId: 0});
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/// @notice Sets the permission ID the owner can use to give other addresses owner access.
|
|
@@ -162,7 +162,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
162
162
|
revert JBOwnableOverrides_InvalidNewOwner();
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
_transferOwnership(newOwner, 0);
|
|
165
|
+
_transferOwnership({newOwner: newOwner, projectId: 0});
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/// @notice Transfer ownership of this contract to a new Juicebox project.
|
|
@@ -181,7 +181,8 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
181
181
|
revert JBOwnableOverrides_ProjectDoesNotExist();
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
185
|
+
_transferOwnership({newOwner: address(0), projectId: uint88(projectId)});
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
//*********************************************************************//
|
|
@@ -207,7 +208,7 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
207
208
|
/// @notice Helper to allow for drop-in replacement of OpenZeppelin `Ownable`.
|
|
208
209
|
/// @param newOwner The address that should receive ownership of this contract.
|
|
209
210
|
function _transferOwnership(address newOwner) internal virtual {
|
|
210
|
-
_transferOwnership(newOwner, 0);
|
|
211
|
+
_transferOwnership({newOwner: newOwner, projectId: 0});
|
|
211
212
|
}
|
|
212
213
|
|
|
213
214
|
/// @notice Transfers this contract's ownership to an address (`newOwner`) OR a Juicebox project (`projectId`).
|
|
@@ -238,6 +239,6 @@ abstract contract JBOwnableOverrides is Context, JBPermissioned, IJBOwnable {
|
|
|
238
239
|
// This is to prevent permissions clashes for the new user/owner.
|
|
239
240
|
jbOwner = JBOwner({owner: newOwner, projectId: projectId, permissionId: 0});
|
|
240
241
|
// Emit a transfer event with the new owner's address.
|
|
241
|
-
_emitTransferEvent(oldOwner, newOwner, projectId);
|
|
242
|
+
_emitTransferEvent({previousOwner: oldOwner, newOwner: newOwner, newProjectId: projectId});
|
|
242
243
|
}
|
|
243
244
|
}
|
package/src/structs/JBOwner.sol
CHANGED
|
@@ -7,6 +7,7 @@ pragma solidity ^0.8.0;
|
|
|
7
7
|
/// `owner` address has owner access.
|
|
8
8
|
/// @custom:member permissionId The permission ID which corresponds to owner access. See `JBPermissions` in `nana-core`
|
|
9
9
|
/// and `nana-permission-ids`.
|
|
10
|
+
// forge-lint: disable-next-line(pascal-case-struct)
|
|
10
11
|
struct JBOwner {
|
|
11
12
|
address owner;
|
|
12
13
|
uint88 projectId;
|
package/test/Ownable.t.sol
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: UNLICENSED
|
|
2
2
|
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
|
-
import "forge-std/Test.sol";
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {MockOwnable} from "./mocks/MockOwnable.sol";
|
|
6
6
|
import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
|
|
7
7
|
|
|
@@ -13,8 +13,8 @@ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsDat
|
|
|
13
13
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
14
14
|
|
|
15
15
|
contract OwnableTest is Test {
|
|
16
|
-
IJBProjects
|
|
17
|
-
IJBPermissions
|
|
16
|
+
IJBProjects projects;
|
|
17
|
+
IJBPermissions permissions;
|
|
18
18
|
|
|
19
19
|
modifier isNotContract(address a) {
|
|
20
20
|
uint256 size;
|
|
@@ -27,9 +27,9 @@ contract OwnableTest is Test {
|
|
|
27
27
|
|
|
28
28
|
function setUp() public {
|
|
29
29
|
// Deploy the permissions contract.
|
|
30
|
-
|
|
30
|
+
permissions = new JBPermissions(address(0));
|
|
31
31
|
// Deploy the projects contract.
|
|
32
|
-
|
|
32
|
+
projects = new JBProjects(address(123), address(0), address(0));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function testDeployerDoesNotBecomeOwner(address deployer, address owner) public isNotContract(owner) {
|
|
@@ -37,7 +37,7 @@ contract OwnableTest is Test {
|
|
|
37
37
|
vm.assume(owner != address(0));
|
|
38
38
|
|
|
39
39
|
vm.prank(deployer);
|
|
40
|
-
MockOwnable ownable = new MockOwnable(
|
|
40
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
|
|
41
41
|
|
|
42
42
|
assertEq(owner, ownable.owner(), "Deployer did not become the owner.");
|
|
43
43
|
}
|
|
@@ -56,17 +56,18 @@ contract OwnableTest is Test {
|
|
|
56
56
|
vm.assume(newProjectOwner != address(0));
|
|
57
57
|
|
|
58
58
|
// Create a project for the owner.
|
|
59
|
-
uint256 projectId =
|
|
59
|
+
uint256 projectId = projects.createFor(projectOwner);
|
|
60
60
|
|
|
61
61
|
// Create the `Ownable` contract.
|
|
62
|
-
|
|
62
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
63
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
63
64
|
|
|
64
65
|
// Make sure the deployer owns it.
|
|
65
66
|
assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
|
|
66
67
|
|
|
67
68
|
// Transfer the project's ownership.
|
|
68
69
|
vm.prank(projectOwner);
|
|
69
|
-
|
|
70
|
+
projects.transferFrom(projectOwner, newProjectOwner, projectId);
|
|
70
71
|
|
|
71
72
|
// Make sure the `Ownable` contract has also been transferred to the new project owner.
|
|
72
73
|
assertEq(newProjectOwner, ownable.owner(), "Ownable did not follow the Project owner.");
|
|
@@ -86,10 +87,11 @@ contract OwnableTest is Test {
|
|
|
86
87
|
vm.assume(projectOwner != address(0));
|
|
87
88
|
|
|
88
89
|
// Create a project for the owner.
|
|
89
|
-
uint256 _projectId =
|
|
90
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
90
91
|
|
|
91
92
|
// Create the `Ownable` contract.
|
|
92
|
-
|
|
93
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
94
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
|
|
93
95
|
|
|
94
96
|
// Make sure the project owner owns it.
|
|
95
97
|
assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
|
|
@@ -100,7 +102,7 @@ contract OwnableTest is Test {
|
|
|
100
102
|
// Make sure it was transferred to the new owner.
|
|
101
103
|
assertEq(newOwnableOwner, ownable.owner());
|
|
102
104
|
// Sanity check to make sure it only the `Ownable` changed, and that the project did not.
|
|
103
|
-
assertEq(
|
|
105
|
+
assertEq(projects.ownerOf(_projectId), projectOwner);
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
function testCantTransferToProjectZero(address owner) public {
|
|
@@ -108,7 +110,7 @@ contract OwnableTest is Test {
|
|
|
108
110
|
vm.startPrank(owner);
|
|
109
111
|
|
|
110
112
|
// Create the `Ownable` contract.
|
|
111
|
-
MockOwnable ownable = new MockOwnable(
|
|
113
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, owner, 0);
|
|
112
114
|
|
|
113
115
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
114
116
|
|
|
@@ -122,7 +124,7 @@ contract OwnableTest is Test {
|
|
|
122
124
|
vm.startPrank(owner);
|
|
123
125
|
|
|
124
126
|
// Create the `Ownable` contract.
|
|
125
|
-
MockOwnable ownable = new MockOwnable(
|
|
127
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
|
|
126
128
|
|
|
127
129
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
128
130
|
|
|
@@ -145,18 +147,19 @@ contract OwnableTest is Test {
|
|
|
145
147
|
vm.assume(newProjectOwner != address(0));
|
|
146
148
|
|
|
147
149
|
// Create a project for the owner.
|
|
148
|
-
uint256 _projectId =
|
|
150
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
149
151
|
|
|
150
152
|
// Create the `Ownable` contract.
|
|
151
|
-
|
|
153
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
154
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
|
|
152
155
|
|
|
153
156
|
// Make sure the project owner owns it.
|
|
154
157
|
assertEq(projectOwner, ownable.owner(), "Deployer is not the owner.");
|
|
155
158
|
|
|
156
159
|
// Transfer the project ownership.
|
|
157
160
|
vm.prank(projectOwner);
|
|
158
|
-
|
|
159
|
-
assertEq(
|
|
161
|
+
projects.transferFrom(projectOwner, newProjectOwner, _projectId);
|
|
162
|
+
assertEq(projects.ownerOf(_projectId), newProjectOwner);
|
|
160
163
|
|
|
161
164
|
// Make sure the `Ownable` contract has also been transferred to the new project owner.
|
|
162
165
|
assertEq(newProjectOwner, ownable.owner());
|
|
@@ -167,7 +170,7 @@ contract OwnableTest is Test {
|
|
|
167
170
|
vm.assume(deployer != owner);
|
|
168
171
|
|
|
169
172
|
// Create the `Ownable` contract.
|
|
170
|
-
MockOwnable ownable = new MockOwnable(
|
|
173
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, owner, uint88(0));
|
|
171
174
|
|
|
172
175
|
// Transfer ownership to the project owner.
|
|
173
176
|
vm.prank(owner);
|
|
@@ -185,11 +188,12 @@ contract OwnableTest is Test {
|
|
|
185
188
|
vm.assume(projectOwner != address(0));
|
|
186
189
|
|
|
187
190
|
// Create a project for the owner.
|
|
188
|
-
uint256 _projectId =
|
|
191
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
189
192
|
|
|
190
193
|
// Create the `Ownable` contract.
|
|
191
194
|
vm.prank(deployer);
|
|
192
|
-
|
|
195
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
196
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
|
|
193
197
|
|
|
194
198
|
// Renounce the ownership.
|
|
195
199
|
vm.prank(projectOwner);
|
|
@@ -218,10 +222,11 @@ contract OwnableTest is Test {
|
|
|
218
222
|
}
|
|
219
223
|
|
|
220
224
|
// Create a project for the owner.
|
|
221
|
-
uint256 _projectId =
|
|
225
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
222
226
|
|
|
223
227
|
// Create the `Ownable` contract.
|
|
224
|
-
|
|
228
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
229
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
|
|
225
230
|
|
|
226
231
|
// Set the required permission.
|
|
227
232
|
vm.prank(projectOwner);
|
|
@@ -254,8 +259,9 @@ contract OwnableTest is Test {
|
|
|
254
259
|
|
|
255
260
|
// The owner gives permission to the caller.
|
|
256
261
|
vm.prank(projectOwner);
|
|
257
|
-
|
|
262
|
+
permissions.setPermissionsFor(
|
|
258
263
|
projectOwner,
|
|
264
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
259
265
|
JBPermissionsData({operator: callerAddress, projectId: uint56(_projectId), permissionIds: _permissionIds})
|
|
260
266
|
);
|
|
261
267
|
|
|
@@ -296,10 +302,11 @@ contract OwnableTest is Test {
|
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
// Create a project for the owner.
|
|
299
|
-
uint256 _projectId =
|
|
305
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
300
306
|
|
|
301
307
|
// Create the `Ownable` contract.
|
|
302
|
-
|
|
308
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
309
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(_projectId));
|
|
303
310
|
|
|
304
311
|
// Set the permission that is required.
|
|
305
312
|
ownable.setPermission(requiredPermissionId);
|
|
@@ -331,8 +338,9 @@ contract OwnableTest is Test {
|
|
|
331
338
|
|
|
332
339
|
// The owner gives permission to the caller.
|
|
333
340
|
vm.prank(projectOwner);
|
|
334
|
-
|
|
341
|
+
permissions.setPermissionsFor(
|
|
335
342
|
projectOwner,
|
|
343
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
336
344
|
JBPermissionsData({operator: callerAddress, projectId: uint56(_projectId), permissionIds: _permissionIds})
|
|
337
345
|
);
|
|
338
346
|
|
|
@@ -356,19 +364,20 @@ contract OwnableTest is Test {
|
|
|
356
364
|
vm.assume(owner != address(0) && projectOwner != address(0));
|
|
357
365
|
|
|
358
366
|
// Create a project for the owner.
|
|
359
|
-
uint256 _projectId =
|
|
367
|
+
uint256 _projectId = projects.createFor(projectOwner);
|
|
360
368
|
|
|
361
369
|
// Should revert because we set both a owner and a projectOwner
|
|
362
370
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
363
371
|
|
|
364
372
|
// Create the `Ownable` contract.
|
|
365
|
-
|
|
373
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
374
|
+
new MockOwnable(projects, permissions, address(owner), uint88(_projectId));
|
|
366
375
|
}
|
|
367
376
|
|
|
368
377
|
function testCantInitializeAsRenounced() public {
|
|
369
378
|
// Should revert because we set both a owner and a projectOwner
|
|
370
379
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
371
380
|
// Create the `Ownable` contract.
|
|
372
|
-
new MockOwnable(
|
|
381
|
+
new MockOwnable(projects, permissions, address(0), uint88(0));
|
|
373
382
|
}
|
|
374
383
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: UNLICENSED
|
|
2
2
|
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
|
-
import "forge-std/Test.sol";
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {MockOwnable} from "./mocks/MockOwnable.sol";
|
|
6
6
|
import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
|
|
7
7
|
|
|
@@ -16,8 +16,8 @@ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsDat
|
|
|
16
16
|
/// @notice Adversarial security tests for JBOwnable covering edge cases
|
|
17
17
|
/// around dual ownership, permission semantics, and renounced contracts.
|
|
18
18
|
contract OwnableAttacks is Test {
|
|
19
|
-
IJBProjects
|
|
20
|
-
IJBPermissions
|
|
19
|
+
IJBProjects projects;
|
|
20
|
+
IJBPermissions permissions;
|
|
21
21
|
|
|
22
22
|
address alice = makeAddr("alice");
|
|
23
23
|
address bob = makeAddr("bob");
|
|
@@ -33,25 +33,26 @@ contract OwnableAttacks is Test {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function setUp() public {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
permissions = new JBPermissions(address(0));
|
|
37
|
+
projects = new JBProjects(address(123), address(0), address(0));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// =========================================================================
|
|
41
41
|
// Test 1: Constructor rejects both owner AND projectId set
|
|
42
42
|
// =========================================================================
|
|
43
43
|
function test_bothOwnerAndProjectId_constructorReverts() public {
|
|
44
|
-
uint256 projectId =
|
|
44
|
+
uint256 projectId = projects.createFor(alice);
|
|
45
45
|
|
|
46
46
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
47
|
-
|
|
47
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
48
|
+
new MockOwnable(projects, permissions, bob, uint88(projectId));
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
// =========================================================================
|
|
51
52
|
// Test 2: Renounced contract — protectedMethod always reverts
|
|
52
53
|
// =========================================================================
|
|
53
54
|
function test_renounced_protectedMethodAlwaysReverts() public {
|
|
54
|
-
MockOwnable ownable = new MockOwnable(
|
|
55
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
55
56
|
|
|
56
57
|
// Owner can call.
|
|
57
58
|
vm.prank(alice);
|
|
@@ -82,8 +83,9 @@ contract OwnableAttacks is Test {
|
|
|
82
83
|
/// @notice After any ownership transfer, permissionId should reset to 0.
|
|
83
84
|
/// This prevents stale permission delegation.
|
|
84
85
|
function test_permissionIdResetOnTransfer() public {
|
|
85
|
-
uint256 projectId =
|
|
86
|
-
|
|
86
|
+
uint256 projectId = projects.createFor(alice);
|
|
87
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
88
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
87
89
|
|
|
88
90
|
// Set permission ID.
|
|
89
91
|
vm.prank(alice);
|
|
@@ -105,8 +107,9 @@ contract OwnableAttacks is Test {
|
|
|
105
107
|
// =========================================================================
|
|
106
108
|
/// @notice After transferring project NFT, old owner should lose access.
|
|
107
109
|
function test_staleOwner_afterNFTTransfer() public {
|
|
108
|
-
uint256 projectId =
|
|
109
|
-
|
|
110
|
+
uint256 projectId = projects.createFor(alice);
|
|
111
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
112
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
110
113
|
|
|
111
114
|
// Alice is current owner.
|
|
112
115
|
assertEq(ownable.owner(), alice);
|
|
@@ -115,7 +118,7 @@ contract OwnableAttacks is Test {
|
|
|
115
118
|
|
|
116
119
|
// Transfer project NFT to bob.
|
|
117
120
|
vm.prank(alice);
|
|
118
|
-
|
|
121
|
+
projects.transferFrom(alice, bob, projectId);
|
|
119
122
|
|
|
120
123
|
// Alice should no longer be owner.
|
|
121
124
|
assertEq(ownable.owner(), bob, "Bob should be new owner");
|
|
@@ -135,7 +138,7 @@ contract OwnableAttacks is Test {
|
|
|
135
138
|
// =========================================================================
|
|
136
139
|
/// @notice transferOwnershipToProject with projectId > type(uint88).max should revert.
|
|
137
140
|
function test_transferOwnershipToProject_overflowReverts() public {
|
|
138
|
-
MockOwnable ownable = new MockOwnable(
|
|
141
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
139
142
|
|
|
140
143
|
// type(uint88).max + 1 = 309485009821345068724781056
|
|
141
144
|
uint256 overflowId = uint256(type(uint88).max) + 1;
|
|
@@ -152,11 +155,12 @@ contract OwnableAttacks is Test {
|
|
|
152
155
|
/// doesn't grant access to a different project's JBOwnable.
|
|
153
156
|
function test_rootOnWrongProject_noAccess() public {
|
|
154
157
|
// Create two projects.
|
|
155
|
-
uint256 aliceProject =
|
|
156
|
-
uint256 attackerProject =
|
|
158
|
+
uint256 aliceProject = projects.createFor(alice);
|
|
159
|
+
uint256 attackerProject = projects.createFor(attacker);
|
|
157
160
|
|
|
158
161
|
// Ownable is owned by alice's project.
|
|
159
|
-
|
|
162
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
163
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(aliceProject));
|
|
160
164
|
|
|
161
165
|
// Set permission ID so delegated access is possible.
|
|
162
166
|
vm.prank(alice);
|
|
@@ -167,8 +171,9 @@ contract OwnableAttacks is Test {
|
|
|
167
171
|
rootPerms[0] = 1; // ROOT
|
|
168
172
|
|
|
169
173
|
vm.prank(attacker);
|
|
170
|
-
|
|
174
|
+
permissions.setPermissionsFor(
|
|
171
175
|
attacker,
|
|
176
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
172
177
|
JBPermissionsData({operator: attacker, projectId: uint56(attackerProject), permissionIds: rootPerms})
|
|
173
178
|
);
|
|
174
179
|
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: UNLICENSED
|
|
2
2
|
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
|
-
import "forge-std/Test.sol";
|
|
4
|
+
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {MockOwnable} from "./mocks/MockOwnable.sol";
|
|
6
6
|
import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
|
|
7
|
-
import {JBOwner} from "../src/structs/JBOwner.sol";
|
|
8
7
|
import {IJBOwnable} from "../src/interfaces/IJBOwnable.sol";
|
|
9
8
|
|
|
10
9
|
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
11
|
-
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
12
10
|
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
13
11
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
14
12
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
@@ -18,8 +16,8 @@ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsDat
|
|
|
18
16
|
/// @notice Edge case and gap tests for JBOwnable: multi-hop NFT transfers,
|
|
19
17
|
/// project-to-project ownership, permissionId lifecycle, and nonexistent projects.
|
|
20
18
|
contract OwnableEdgeCases is Test {
|
|
21
|
-
IJBProjects
|
|
22
|
-
IJBPermissions
|
|
19
|
+
IJBProjects projects;
|
|
20
|
+
IJBPermissions permissions;
|
|
23
21
|
|
|
24
22
|
address alice = makeAddr("alice");
|
|
25
23
|
address bob = makeAddr("bob");
|
|
@@ -36,32 +34,33 @@ contract OwnableEdgeCases is Test {
|
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
function setUp() public {
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
permissions = new JBPermissions(address(0));
|
|
38
|
+
projects = new JBProjects(address(123), address(0), address(0));
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
// =========================================================================
|
|
44
42
|
// Test 1: Multi-hop NFT transfer — ownership follows through A→B→C→D
|
|
45
43
|
// =========================================================================
|
|
46
44
|
function test_multiHopNFTTransfer_ownerFollows() public {
|
|
47
|
-
uint256 projectId =
|
|
48
|
-
|
|
45
|
+
uint256 projectId = projects.createFor(alice);
|
|
46
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
47
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
49
48
|
|
|
50
49
|
assertEq(ownable.owner(), alice);
|
|
51
50
|
|
|
52
51
|
// Transfer NFT: alice → bob
|
|
53
52
|
vm.prank(alice);
|
|
54
|
-
|
|
53
|
+
projects.transferFrom(alice, bob, projectId);
|
|
55
54
|
assertEq(ownable.owner(), bob, "Should follow to bob");
|
|
56
55
|
|
|
57
56
|
// Transfer NFT: bob → charlie
|
|
58
57
|
vm.prank(bob);
|
|
59
|
-
|
|
58
|
+
projects.transferFrom(bob, charlie, projectId);
|
|
60
59
|
assertEq(ownable.owner(), charlie, "Should follow to charlie");
|
|
61
60
|
|
|
62
61
|
// Transfer NFT: charlie → dave
|
|
63
62
|
vm.prank(charlie);
|
|
64
|
-
|
|
63
|
+
projects.transferFrom(charlie, dave, projectId);
|
|
65
64
|
assertEq(ownable.owner(), dave, "Should follow to dave");
|
|
66
65
|
|
|
67
66
|
// dave can call protectedMethod, alice/bob/charlie cannot
|
|
@@ -85,10 +84,11 @@ contract OwnableEdgeCases is Test {
|
|
|
85
84
|
// Test 2: Transfer project → different project
|
|
86
85
|
// =========================================================================
|
|
87
86
|
function test_transferProjectToProject() public {
|
|
88
|
-
uint256 projectA =
|
|
89
|
-
uint256 projectB =
|
|
87
|
+
uint256 projectA = projects.createFor(alice);
|
|
88
|
+
uint256 projectB = projects.createFor(bob);
|
|
90
89
|
|
|
91
|
-
|
|
90
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
91
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectA));
|
|
92
92
|
assertEq(ownable.owner(), alice);
|
|
93
93
|
|
|
94
94
|
// Transfer ownership from project A to project B.
|
|
@@ -112,11 +112,11 @@ contract OwnableEdgeCases is Test {
|
|
|
112
112
|
// Test 3: Full ownership cycle: address → project → address → project
|
|
113
113
|
// =========================================================================
|
|
114
114
|
function test_fullOwnershipCycle() public {
|
|
115
|
-
uint256 projectA =
|
|
116
|
-
uint256 projectB =
|
|
115
|
+
uint256 projectA = projects.createFor(alice);
|
|
116
|
+
uint256 projectB = projects.createFor(bob);
|
|
117
117
|
|
|
118
118
|
// Start with address ownership.
|
|
119
|
-
MockOwnable ownable = new MockOwnable(
|
|
119
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, charlie, 0);
|
|
120
120
|
assertEq(ownable.owner(), charlie);
|
|
121
121
|
|
|
122
122
|
// charlie → project A (alice)
|
|
@@ -137,6 +137,7 @@ contract OwnableEdgeCases is Test {
|
|
|
137
137
|
// Verify jbOwner struct is correct (projectId set, owner zeroed).
|
|
138
138
|
(address storedOwner, uint88 storedProjectId, uint8 storedPermId) = ownable.jbOwner();
|
|
139
139
|
assertEq(storedOwner, address(0), "owner field should be zero in project mode");
|
|
140
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
140
141
|
assertEq(storedProjectId, uint88(projectB), "projectId should be projectB");
|
|
141
142
|
assertEq(storedPermId, 0, "permissionId should be 0");
|
|
142
143
|
}
|
|
@@ -145,9 +146,10 @@ contract OwnableEdgeCases is Test {
|
|
|
145
146
|
// Test 4: permissionId lifecycle through multiple transfers
|
|
146
147
|
// =========================================================================
|
|
147
148
|
function test_permissionIdLifecycle() public {
|
|
148
|
-
uint256 projectA =
|
|
149
|
+
uint256 projectA = projects.createFor(alice);
|
|
149
150
|
|
|
150
|
-
|
|
151
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
152
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectA));
|
|
151
153
|
|
|
152
154
|
// Set permissionId to 42.
|
|
153
155
|
vm.prank(alice);
|
|
@@ -192,7 +194,7 @@ contract OwnableEdgeCases is Test {
|
|
|
192
194
|
function test_nonOwnerCannotSetPermissionId(address nonOwner) public {
|
|
193
195
|
vm.assume(nonOwner != alice && nonOwner != address(0));
|
|
194
196
|
|
|
195
|
-
MockOwnable ownable = new MockOwnable(
|
|
197
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
196
198
|
|
|
197
199
|
vm.prank(nonOwner);
|
|
198
200
|
vm.expectRevert();
|
|
@@ -204,8 +206,8 @@ contract OwnableEdgeCases is Test {
|
|
|
204
206
|
// =========================================================================
|
|
205
207
|
function test_transferToNonexistentProject_reverts() public {
|
|
206
208
|
// Create one project so count == 1.
|
|
207
|
-
|
|
208
|
-
MockOwnable ownable = new MockOwnable(
|
|
209
|
+
projects.createFor(alice);
|
|
210
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
209
211
|
|
|
210
212
|
// Project 2 doesn't exist.
|
|
211
213
|
vm.prank(alice);
|
|
@@ -223,8 +225,9 @@ contract OwnableEdgeCases is Test {
|
|
|
223
225
|
// transferred, old delegate loses access
|
|
224
226
|
// =========================================================================
|
|
225
227
|
function test_delegatedAccess_lostAfterNFTTransfer() public {
|
|
226
|
-
uint256 projectId =
|
|
227
|
-
|
|
228
|
+
uint256 projectId = projects.createFor(alice);
|
|
229
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
230
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
228
231
|
|
|
229
232
|
// Set permissionId so delegation is possible.
|
|
230
233
|
vm.prank(alice);
|
|
@@ -234,8 +237,10 @@ contract OwnableEdgeCases is Test {
|
|
|
234
237
|
uint8[] memory permIds = new uint8[](1);
|
|
235
238
|
permIds[0] = 42;
|
|
236
239
|
vm.prank(alice);
|
|
237
|
-
|
|
238
|
-
|
|
240
|
+
permissions.setPermissionsFor(
|
|
241
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
242
|
+
alice,
|
|
243
|
+
JBPermissionsData({operator: charlie, projectId: uint56(projectId), permissionIds: permIds})
|
|
239
244
|
);
|
|
240
245
|
|
|
241
246
|
// Charlie can call protectedMethod (delegated via permissions).
|
|
@@ -244,7 +249,7 @@ contract OwnableEdgeCases is Test {
|
|
|
244
249
|
|
|
245
250
|
// Transfer NFT to bob.
|
|
246
251
|
vm.prank(alice);
|
|
247
|
-
|
|
252
|
+
projects.transferFrom(alice, bob, projectId);
|
|
248
253
|
|
|
249
254
|
// Charlie's delegation was from alice. Now owner is bob.
|
|
250
255
|
// Charlie should lose access because _checkOwner resolves to bob,
|
|
@@ -262,7 +267,7 @@ contract OwnableEdgeCases is Test {
|
|
|
262
267
|
// Test 8: OwnershipTransferred event emitted correctly
|
|
263
268
|
// =========================================================================
|
|
264
269
|
function test_ownershipTransferredEvent() public {
|
|
265
|
-
MockOwnable ownable = new MockOwnable(
|
|
270
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
266
271
|
|
|
267
272
|
// Transfer to bob — expect event.
|
|
268
273
|
vm.expectEmit(true, true, false, true);
|
|
@@ -276,7 +281,7 @@ contract OwnableEdgeCases is Test {
|
|
|
276
281
|
// Test 9: PermissionIdChanged event emitted correctly
|
|
277
282
|
// =========================================================================
|
|
278
283
|
function test_permissionIdChangedEvent() public {
|
|
279
|
-
MockOwnable ownable = new MockOwnable(
|
|
284
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
280
285
|
|
|
281
286
|
vm.expectEmit(true, true, false, true);
|
|
282
287
|
emit IJBOwnable.PermissionIdChanged(42, alice);
|
|
@@ -291,8 +296,8 @@ contract OwnableEdgeCases is Test {
|
|
|
291
296
|
function testFuzz_transferToProject(address projectOwner) public isNotContract(projectOwner) {
|
|
292
297
|
vm.assume(projectOwner != address(0));
|
|
293
298
|
|
|
294
|
-
uint256 projectId =
|
|
295
|
-
MockOwnable ownable = new MockOwnable(
|
|
299
|
+
uint256 projectId = projects.createFor(projectOwner);
|
|
300
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
296
301
|
|
|
297
302
|
vm.prank(alice);
|
|
298
303
|
ownable.transferOwnershipToProject(projectId);
|
|
@@ -302,6 +307,7 @@ contract OwnableEdgeCases is Test {
|
|
|
302
307
|
// Verify jbOwner struct.
|
|
303
308
|
(address storedOwner, uint88 storedProjectId,) = ownable.jbOwner();
|
|
304
309
|
assertEq(storedOwner, address(0), "stored owner should be zero");
|
|
310
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
305
311
|
assertEq(storedProjectId, uint88(projectId), "stored projectId should match");
|
|
306
312
|
}
|
|
307
313
|
|
|
@@ -311,8 +317,8 @@ contract OwnableEdgeCases is Test {
|
|
|
311
317
|
/// @notice After renouncing, no one can call transferOwnership, transferOwnershipToProject,
|
|
312
318
|
/// setPermissionId, or renounceOwnership again.
|
|
313
319
|
function test_renouncedContract_cannotReclaim() public {
|
|
314
|
-
uint256 projectId =
|
|
315
|
-
MockOwnable ownable = new MockOwnable(
|
|
320
|
+
uint256 projectId = projects.createFor(alice);
|
|
321
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
316
322
|
|
|
317
323
|
vm.prank(alice);
|
|
318
324
|
ownable.renounceOwnership();
|
|
@@ -349,7 +355,7 @@ contract OwnableEdgeCases is Test {
|
|
|
349
355
|
/// NOT ERC2771Context. This test documents that a trusted forwarder
|
|
350
356
|
/// appending a sender address to calldata does NOT affect _checkOwner.
|
|
351
357
|
function test_noERC2771_trustedForwarderHasNoEffect() public {
|
|
352
|
-
MockOwnable ownable = new MockOwnable(
|
|
358
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
353
359
|
|
|
354
360
|
// Simulate what a trusted forwarder would do: call with alice's address
|
|
355
361
|
// appended to calldata. Since JBOwnable uses plain Context, this has no effect.
|
|
@@ -4,15 +4,6 @@ pragma solidity ^0.8.26;
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {OwnableHandler} from "./handlers/OwnableHandler.sol";
|
|
6
6
|
|
|
7
|
-
import {MockOwnable} from "./mocks/MockOwnable.sol";
|
|
8
|
-
import {JBOwnableOverrides} from "../src/JBOwnableOverrides.sol";
|
|
9
|
-
import {JBOwner} from "../src/structs/JBOwner.sol";
|
|
10
|
-
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
11
|
-
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
12
|
-
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
13
|
-
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
14
|
-
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
15
|
-
|
|
16
7
|
contract OwnableInvariantTests is Test {
|
|
17
8
|
OwnableHandler handler;
|
|
18
9
|
|
|
@@ -5,12 +5,9 @@ pragma solidity ^0.8.26;
|
|
|
5
5
|
import {CommonBase} from "forge-std/Base.sol";
|
|
6
6
|
import {StdCheats} from "forge-std/StdCheats.sol";
|
|
7
7
|
import {StdUtils} from "forge-std/StdUtils.sol";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import {MockOwnable, JBOwnableOverrides} from "../mocks/MockOwnable.sol";
|
|
8
|
+
import {MockOwnable} from "../mocks/MockOwnable.sol";
|
|
11
9
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
12
10
|
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
13
|
-
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
14
11
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
15
12
|
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
16
13
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: UNLICENSED
|
|
2
2
|
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
|
-
import {JBOwnable
|
|
4
|
+
import {JBOwnable} from "../../src/JBOwnable.sol";
|
|
5
5
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
6
6
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
7
7
|
|
|
@@ -3,7 +3,6 @@ pragma solidity ^0.8.26;
|
|
|
3
3
|
|
|
4
4
|
import {Test} from "forge-std/Test.sol";
|
|
5
5
|
import {MockOwnable} from "../mocks/MockOwnable.sol";
|
|
6
|
-
import {JBOwnableOverrides} from "../../src/JBOwnableOverrides.sol";
|
|
7
6
|
|
|
8
7
|
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
9
8
|
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
@@ -11,34 +10,35 @@ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.s
|
|
|
11
10
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
12
11
|
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
13
12
|
|
|
14
|
-
/// @title
|
|
13
|
+
/// @title BurnLockProtection
|
|
15
14
|
/// @notice Verifies that if a project NFT is burned/invalidated,
|
|
16
15
|
/// owner() returns address(0) and _checkOwner() reverts gracefully instead of
|
|
17
16
|
/// permanently locking the contract with an unrecoverable revert.
|
|
18
|
-
contract
|
|
19
|
-
IJBProjects
|
|
20
|
-
IJBPermissions
|
|
17
|
+
contract BurnLockProtection is Test {
|
|
18
|
+
IJBProjects projects;
|
|
19
|
+
IJBPermissions permissions;
|
|
21
20
|
|
|
22
21
|
address alice = makeAddr("alice");
|
|
23
22
|
address bob = makeAddr("bob");
|
|
24
23
|
|
|
25
24
|
function setUp() public {
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
permissions = new JBPermissions(address(0));
|
|
26
|
+
projects = new JBProjects(address(123), address(0), address(0));
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
/// @notice When a project NFT is burned (simulated via mockCallRevert), owner() should
|
|
31
30
|
/// return address(0) instead of reverting — contract degrades to "renounced" state.
|
|
32
31
|
function test_burnedProjectNFT_ownerReturnsZero() public {
|
|
33
|
-
uint256 projectId =
|
|
34
|
-
|
|
32
|
+
uint256 projectId = projects.createFor(alice);
|
|
33
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
34
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
35
35
|
|
|
36
36
|
// Verify normal operation first.
|
|
37
37
|
assertEq(ownable.owner(), alice, "Owner should be alice before burn");
|
|
38
38
|
|
|
39
39
|
// Simulate project NFT burn by making ownerOf revert for this projectId.
|
|
40
40
|
vm.mockCallRevert(
|
|
41
|
-
address(
|
|
41
|
+
address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, projectId), "ERC721: invalid token ID"
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
// After burn, owner() should return address(0) — NOT revert.
|
|
@@ -50,8 +50,9 @@ contract L65_BurnLockProtection is Test {
|
|
|
50
50
|
/// Unauthorized error (not an unrecoverable ownerOf revert), making the contract
|
|
51
51
|
/// behave as if ownership was renounced.
|
|
52
52
|
function test_burnedProjectNFT_checkOwnerRevertsGracefully() public {
|
|
53
|
-
uint256 projectId =
|
|
54
|
-
|
|
53
|
+
uint256 projectId = projects.createFor(alice);
|
|
54
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
55
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
55
56
|
|
|
56
57
|
// Alice can call the protected method before burn.
|
|
57
58
|
vm.prank(alice);
|
|
@@ -59,7 +60,7 @@ contract L65_BurnLockProtection is Test {
|
|
|
59
60
|
|
|
60
61
|
// Simulate project NFT burn.
|
|
61
62
|
vm.mockCallRevert(
|
|
62
|
-
address(
|
|
63
|
+
address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, projectId), "ERC721: invalid token ID"
|
|
63
64
|
);
|
|
64
65
|
|
|
65
66
|
// After burn, nobody can call protected methods — but the revert is graceful
|
|
@@ -75,7 +76,7 @@ contract L65_BurnLockProtection is Test {
|
|
|
75
76
|
|
|
76
77
|
/// @notice Address-based ownership is unaffected by the try-catch change.
|
|
77
78
|
function test_addressBasedOwnership_unaffectedByTryCatch() public {
|
|
78
|
-
MockOwnable ownable = new MockOwnable(
|
|
79
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, alice, 0);
|
|
79
80
|
|
|
80
81
|
assertEq(ownable.owner(), alice, "Owner should be alice");
|
|
81
82
|
|
|
@@ -93,14 +94,15 @@ contract L65_BurnLockProtection is Test {
|
|
|
93
94
|
|
|
94
95
|
/// @notice Normal project-based ownership still works correctly after the fix.
|
|
95
96
|
function test_normalProjectOwnership_stillWorks() public {
|
|
96
|
-
uint256 projectId =
|
|
97
|
-
|
|
97
|
+
uint256 projectId = projects.createFor(alice);
|
|
98
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
99
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
98
100
|
|
|
99
101
|
assertEq(ownable.owner(), alice);
|
|
100
102
|
|
|
101
103
|
// Transfer project NFT.
|
|
102
104
|
vm.prank(alice);
|
|
103
|
-
|
|
105
|
+
projects.transferFrom(alice, bob, projectId);
|
|
104
106
|
|
|
105
107
|
assertEq(ownable.owner(), bob, "Owner should follow project NFT transfer");
|
|
106
108
|
|
|
@@ -10,19 +10,19 @@ import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
|
10
10
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
11
11
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
12
12
|
|
|
13
|
-
/// @title
|
|
14
|
-
/// @notice Verifies that deploying with a zero-address
|
|
13
|
+
/// @title ZeroAddressValidation
|
|
14
|
+
/// @notice Verifies that deploying with a zero-address projects
|
|
15
15
|
/// contract and a non-zero projectId reverts at construction time, preventing
|
|
16
16
|
/// permanently broken project-based ownership.
|
|
17
|
-
contract
|
|
18
|
-
IJBProjects
|
|
19
|
-
IJBPermissions
|
|
17
|
+
contract ZeroAddressValidation is Test {
|
|
18
|
+
IJBProjects projects;
|
|
19
|
+
IJBPermissions permissions;
|
|
20
20
|
|
|
21
21
|
address alice = makeAddr("alice");
|
|
22
22
|
|
|
23
23
|
function setUp() public {
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
permissions = new JBPermissions(address(0));
|
|
25
|
+
projects = new JBProjects(address(123), address(0), address(0));
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/// @notice Deploying with projects=address(0) and non-zero projectId must revert.
|
|
@@ -30,7 +30,7 @@ contract L66_ZeroAddressValidation is Test {
|
|
|
30
30
|
vm.expectRevert(
|
|
31
31
|
abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner.selector)
|
|
32
32
|
);
|
|
33
|
-
new MockOwnable(IJBProjects(address(0)),
|
|
33
|
+
new MockOwnable(IJBProjects(address(0)), permissions, address(0), uint88(1));
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/// @notice Fuzz: any non-zero projectId with projects=address(0) must revert.
|
|
@@ -40,27 +40,28 @@ contract L66_ZeroAddressValidation is Test {
|
|
|
40
40
|
vm.expectRevert(
|
|
41
41
|
abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_ZeroAddressProjectsWithProjectOwner.selector)
|
|
42
42
|
);
|
|
43
|
-
new MockOwnable(IJBProjects(address(0)),
|
|
43
|
+
new MockOwnable(IJBProjects(address(0)), permissions, address(0), projectId);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/// @notice Deploying with projects=address(0) and projectId=0 (address-based ownership)
|
|
47
47
|
/// should NOT revert for this error — it's valid as long as initialOwner != address(0).
|
|
48
48
|
function test_zeroProjectsWithAddressOwnership_succeeds() public {
|
|
49
49
|
// This is valid: address-based ownership with projects=address(0).
|
|
50
|
-
MockOwnable ownable = new MockOwnable(IJBProjects(address(0)),
|
|
50
|
+
MockOwnable ownable = new MockOwnable(IJBProjects(address(0)), permissions, alice, uint88(0));
|
|
51
51
|
assertEq(ownable.owner(), alice, "Owner should be alice with address-based ownership");
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/// @notice Normal deployment with valid
|
|
54
|
+
/// @notice Normal deployment with valid projects contract and projectId succeeds.
|
|
55
55
|
function test_validProjectsWithProjectId_succeeds() public {
|
|
56
|
-
uint256 projectId =
|
|
57
|
-
|
|
56
|
+
uint256 projectId = projects.createFor(alice);
|
|
57
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
58
|
+
MockOwnable ownable = new MockOwnable(projects, permissions, address(0), uint88(projectId));
|
|
58
59
|
assertEq(ownable.owner(), alice, "Owner should be alice via project NFT");
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
/// @notice The existing check for both zero owner and zero projectId is still enforced.
|
|
62
63
|
function test_bothZero_stillReverts() public {
|
|
63
64
|
vm.expectRevert(abi.encodeWithSelector(JBOwnableOverrides.JBOwnableOverrides_InvalidNewOwner.selector));
|
|
64
|
-
new MockOwnable(
|
|
65
|
+
new MockOwnable(projects, permissions, address(0), uint88(0));
|
|
65
66
|
}
|
|
66
67
|
}
|