@bananapus/core-v6 0.0.14 → 0.0.15
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 +134 -41
- package/foundry.toml +3 -3
- package/package.json +4 -4
- package/remappings.txt +1 -1
- package/script/Deploy.s.sol +2 -4
- package/script/DeployPeriphery.s.sol +2 -15
- package/script/helpers/CoreDeploymentLib.sol +1 -2
- package/src/JBController.sol +1 -1
- package/src/interfaces/IJBController.sol +0 -1
- package/src/interfaces/IJBPayoutTerminal.sol +0 -1
- package/test/ComprehensiveInvariant.t.sol +0 -3
- package/test/{AuditExploits.t.sol → CoreExploitTests.t.sol} +1 -3
- package/test/EntryPointPermutations.t.sol +1 -2
- package/test/FlashLoanAttacks.t.sol +0 -1
- package/test/PermissionEscalation.t.sol +1 -12
- package/test/SplitLoopTests.t.sol +1 -3
- package/test/TestCashOut.sol +0 -1
- package/test/TestCashOutCountFor.sol +0 -1
- package/test/TestFeeProcessingFailure.sol +1 -6
- package/test/TestMigrationHeldFees.sol +1 -10
- package/test/TestRulesetQueuingStress.sol +8 -1
- package/test/TestRulesetWeightCaching.sol +0 -1
- package/test/WeirdTokenTests.t.sol +1 -1
- package/test/fork/TestChainlinkPriceFeedFork.sol +249 -0
- package/test/formal/BondingCurveProperties.t.sol +0 -1
- package/test/helpers/JBTest.sol +6 -6
- package/test/invariants/Phase3DeepInvariant.t.sol +0 -3
- package/test/invariants/handlers/Phase3Handler.sol +0 -1
- package/test/invariants/handlers/TokensHandler.sol +0 -1
- package/test/mock/ERC2771ForwarderMock.sol +1 -1
- package/test/mock/MockERC20.sol +1 -1
- package/test/mock/MockMaliciousBeneficiary.sol +0 -1
- package/test/units/static/JBDeadline/TestDeadlineFuzz.sol +3 -3
- package/test/units/static/JBMetadataResolver/{TestMetadataResolverM20M21.sol → TestMetadataResolverEdgeCases.sol} +4 -4
- package/test/units/static/JBRulesets/TestRulesets.sol +0 -1
- package/test/units/static/JBSurplus/TestSurplusFuzz.sol +1 -1
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
|
|
@@ -324,7 +309,7 @@ try hook.afterPayRecordedWith(context) {} catch (bytes memory reason) {
|
|
|
324
309
|
|
|
325
310
|
### foundry.toml
|
|
326
311
|
|
|
327
|
-
Standard config
|
|
312
|
+
Standard config across all repos:
|
|
328
313
|
|
|
329
314
|
```toml
|
|
330
315
|
[profile.default]
|
|
@@ -334,9 +319,6 @@ optimizer_runs = 200
|
|
|
334
319
|
libs = ["node_modules", "lib"]
|
|
335
320
|
fs_permissions = [{ access = "read-write", path = "./"}]
|
|
336
321
|
|
|
337
|
-
[profile.ci_sizes]
|
|
338
|
-
optimizer_runs = 200
|
|
339
|
-
|
|
340
322
|
[fuzz]
|
|
341
323
|
runs = 4096
|
|
342
324
|
|
|
@@ -351,7 +333,14 @@ multiline_func_header = "all"
|
|
|
351
333
|
wrap_comments = true
|
|
352
334
|
```
|
|
353
335
|
|
|
354
|
-
|
|
336
|
+
**Optional sections (add only when needed):**
|
|
337
|
+
- `[rpc_endpoints]` — repos with fork tests. Maps named endpoints to env vars (e.g. `ethereum = "${RPC_ETHEREUM_MAINNET}"`).
|
|
338
|
+
- `[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).
|
|
339
|
+
|
|
340
|
+
**Common variations:**
|
|
341
|
+
- `via_ir = true` when hitting stack-too-deep
|
|
342
|
+
- `optimizer = false` when optimization causes stack-too-deep
|
|
343
|
+
- `optimizer_runs` reduced when deep struct nesting causes stack-too-deep at 200 runs
|
|
355
344
|
|
|
356
345
|
### CI Workflows
|
|
357
346
|
|
|
@@ -381,8 +370,10 @@ jobs:
|
|
|
381
370
|
uses: foundry-rs/foundry-toolchain@v1
|
|
382
371
|
- name: Run tests
|
|
383
372
|
run: forge test --fail-fast --summary --detailed --skip "*/script/**"
|
|
373
|
+
env:
|
|
374
|
+
RPC_ETHEREUM_MAINNET: ${{ secrets.RPC_ETHEREUM_MAINNET }}
|
|
384
375
|
- name: Check contract sizes
|
|
385
|
-
run:
|
|
376
|
+
run: forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
|
|
386
377
|
```
|
|
387
378
|
|
|
388
379
|
**lint.yml:**
|
|
@@ -404,14 +395,63 @@ jobs:
|
|
|
404
395
|
run: forge fmt --check
|
|
405
396
|
```
|
|
406
397
|
|
|
398
|
+
**slither.yml** (repos with `src/` contracts only):
|
|
399
|
+
```yaml
|
|
400
|
+
name: slither
|
|
401
|
+
on:
|
|
402
|
+
pull_request:
|
|
403
|
+
branches:
|
|
404
|
+
- main
|
|
405
|
+
push:
|
|
406
|
+
branches:
|
|
407
|
+
- main
|
|
408
|
+
jobs:
|
|
409
|
+
analyze:
|
|
410
|
+
runs-on: ubuntu-latest
|
|
411
|
+
steps:
|
|
412
|
+
- uses: actions/checkout@v4
|
|
413
|
+
with:
|
|
414
|
+
submodules: recursive
|
|
415
|
+
- uses: actions/setup-node@v4
|
|
416
|
+
with:
|
|
417
|
+
node-version: latest
|
|
418
|
+
- name: Install npm dependencies
|
|
419
|
+
run: npm install --omit=dev
|
|
420
|
+
- name: Install Foundry
|
|
421
|
+
uses: foundry-rs/foundry-toolchain@v1
|
|
422
|
+
- name: Run slither
|
|
423
|
+
uses: crytic/slither-action@v0.3.1
|
|
424
|
+
with:
|
|
425
|
+
slither-config: slither-ci.config.json
|
|
426
|
+
fail-on: medium
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**slither-ci.config.json:**
|
|
430
|
+
```json
|
|
431
|
+
{
|
|
432
|
+
"detectors_to_exclude": "timestamp,uninitialized-local,naming-convention,solc-version,shadowing-local",
|
|
433
|
+
"exclude_informational": true,
|
|
434
|
+
"exclude_low": false,
|
|
435
|
+
"exclude_medium": false,
|
|
436
|
+
"exclude_high": false,
|
|
437
|
+
"disable_color": false,
|
|
438
|
+
"filter_paths": "(mocks/|test/|node_modules/|lib/)",
|
|
439
|
+
"legacy_ast": false
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**Variations:**
|
|
444
|
+
- 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.
|
|
445
|
+
- 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.
|
|
446
|
+
|
|
407
447
|
### package.json
|
|
408
448
|
|
|
409
449
|
```json
|
|
410
450
|
{
|
|
411
|
-
"name": "@bananapus/
|
|
451
|
+
"name": "@bananapus/package-name-v6",
|
|
412
452
|
"version": "x.x.x",
|
|
413
453
|
"license": "MIT",
|
|
414
|
-
"repository": { "type": "git", "url": "git+https://github.com/
|
|
454
|
+
"repository": { "type": "git", "url": "git+https://github.com/Org/repo.git" },
|
|
415
455
|
"engines": { "node": ">=20.0.0" },
|
|
416
456
|
"scripts": {
|
|
417
457
|
"test": "forge test",
|
|
@@ -428,13 +468,62 @@ jobs:
|
|
|
428
468
|
|
|
429
469
|
### remappings.txt
|
|
430
470
|
|
|
431
|
-
Every repo has a `remappings.txt
|
|
471
|
+
Every repo has a `remappings.txt` as the **single source of truth** for import remappings. Never add remappings to `foundry.toml`.
|
|
472
|
+
|
|
473
|
+
**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.
|
|
474
|
+
|
|
475
|
+
**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.
|
|
476
|
+
|
|
477
|
+
**Minimal content** (most repos):
|
|
478
|
+
|
|
479
|
+
```
|
|
480
|
+
forge-std/=lib/forge-std/src/
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
Only add extra remappings for:
|
|
484
|
+
- **`forge-std`** — always needed (git submodule with `src/` subdirectory)
|
|
485
|
+
- **Repo-specific `lib/` submodules** that have no npm package (e.g., `hookmate/=lib/hookmate/src/`)
|
|
486
|
+
- **Symlinked npm packages** — need explicit `@scope/package/=node_modules/@scope/package/` entries
|
|
487
|
+
- **Nested transitive deps** — e.g., `@chainlink/contracts-ccip/` nested inside `@bananapus/suckers-v6/node_modules/`
|
|
488
|
+
|
|
489
|
+
**Never add remappings for:**
|
|
490
|
+
- npm packages that match their import path and are installed as real directories — they auto-resolve
|
|
491
|
+
- Short-form aliases (e.g., `@bananapus/core/` → `@bananapus/core-v6/src/`) — fix the import instead
|
|
492
|
+
- Packages available via npm that are also git submodules — remove the submodule, use npm
|
|
432
493
|
|
|
494
|
+
**Import path convention:**
|
|
495
|
+
|
|
496
|
+
| Package | Import path | Resolves to |
|
|
497
|
+
|---------|------------|-------------|
|
|
498
|
+
| `@bananapus/core-v6` | `@bananapus/core-v6/src/libraries/JBConstants.sol` | `node_modules/@bananapus/core-v6/src/...` |
|
|
499
|
+
| `@openzeppelin/contracts` | `@openzeppelin/contracts/token/ERC20/IERC20.sol` | `node_modules/@openzeppelin/contracts/...` |
|
|
500
|
+
| `@uniswap/v4-core` | `@uniswap/v4-core/src/interfaces/IPoolManager.sol` | `node_modules/@uniswap/v4-core/src/...` |
|
|
501
|
+
|
|
502
|
+
### Linting
|
|
503
|
+
|
|
504
|
+
Solar (Foundry's built-in linter) runs automatically during `forge build`. It scans all `.sol` files in `libs` directories, including `node_modules`.
|
|
505
|
+
|
|
506
|
+
**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.
|
|
507
|
+
|
|
508
|
+
### Fork Tests
|
|
509
|
+
|
|
510
|
+
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.
|
|
511
|
+
|
|
512
|
+
```solidity
|
|
513
|
+
function setUp() public {
|
|
514
|
+
vm.createSelectFork("ethereum");
|
|
515
|
+
// ... setup code
|
|
516
|
+
}
|
|
433
517
|
```
|
|
434
|
-
|
|
518
|
+
|
|
519
|
+
The endpoint name (e.g. `"ethereum"`) maps to an env var via `foundry.toml`:
|
|
520
|
+
|
|
521
|
+
```toml
|
|
522
|
+
[rpc_endpoints]
|
|
523
|
+
ethereum = "${RPC_ETHEREUM_MAINNET}"
|
|
435
524
|
```
|
|
436
525
|
|
|
437
|
-
|
|
526
|
+
For multi-chain fork tests, add all needed endpoints.
|
|
438
527
|
|
|
439
528
|
### Formatting
|
|
440
529
|
|
|
@@ -462,4 +551,8 @@ CI checks formatting via `forge fmt --check`.
|
|
|
462
551
|
|
|
463
552
|
### Contract Size Checks
|
|
464
553
|
|
|
465
|
-
CI runs `
|
|
554
|
+
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`.
|
|
555
|
+
|
|
556
|
+
## Repo-Specific Deviations
|
|
557
|
+
|
|
558
|
+
None. This repo follows the standard configuration exactly.
|
package/foundry.toml
CHANGED
|
@@ -5,9 +5,6 @@ optimizer_runs = 200
|
|
|
5
5
|
libs = ["node_modules", "lib"]
|
|
6
6
|
fs_permissions = [{ access = "read-write", path = "./"}]
|
|
7
7
|
|
|
8
|
-
[profile.ci_sizes]
|
|
9
|
-
optimizer_runs = 200
|
|
10
|
-
|
|
11
8
|
[fuzz]
|
|
12
9
|
runs = 4096
|
|
13
10
|
|
|
@@ -16,6 +13,9 @@ runs = 1024
|
|
|
16
13
|
depth = 100
|
|
17
14
|
fail_on_revert = false
|
|
18
15
|
|
|
16
|
+
[rpc_endpoints]
|
|
17
|
+
ethereum = "${RPC_ETHEREUM_MAINNET}"
|
|
18
|
+
|
|
19
19
|
[fmt]
|
|
20
20
|
number_underscore = "thousands"
|
|
21
21
|
multiline_func_header = "all"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-core-v6'"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
29
|
+
"@bananapus/permission-ids-v6": "^0.0.8",
|
|
30
30
|
"@chainlink/contracts": "^1.3.0",
|
|
31
|
-
"@openzeppelin/contracts": "^5.
|
|
32
|
-
"@prb/math": "^4.1.
|
|
31
|
+
"@openzeppelin/contracts": "^5.6.1",
|
|
32
|
+
"@prb/math": "^4.1.1",
|
|
33
33
|
"@uniswap/permit2": "github:Uniswap/permit2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
package/remappings.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
forge-std/=lib/forge-std/src/
|
package/script/Deploy.s.sol
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
-
import "@sphinx-labs/contracts/SphinxPlugin.sol";
|
|
5
|
-
import {Script
|
|
6
|
-
import {CoreDeploymentLib} from "./helpers/CoreDeploymentLib.sol";
|
|
4
|
+
import "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
|
|
5
|
+
import {Script} from "forge-std/Script.sol";
|
|
7
6
|
|
|
8
7
|
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
9
8
|
import {JBPermissions} from "src/JBPermissions.sol";
|
|
@@ -16,7 +15,6 @@ import {JBTokens} from "src/JBTokens.sol";
|
|
|
16
15
|
import {JBSplits} from "src/JBSplits.sol";
|
|
17
16
|
import {JBFeelessAddresses} from "src/JBFeelessAddresses.sol";
|
|
18
17
|
import {JBFundAccessLimits} from "src/JBFundAccessLimits.sol";
|
|
19
|
-
import {JBController} from "src/JBController.sol";
|
|
20
18
|
import {JBTerminalStore} from "src/JBTerminalStore.sol";
|
|
21
19
|
import {JBMultiTerminal} from "src/JBMultiTerminal.sol";
|
|
22
20
|
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
-
import "@sphinx-labs/contracts/SphinxPlugin.sol";
|
|
5
|
-
import {Script
|
|
4
|
+
import "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
|
|
5
|
+
import {Script} from "forge-std/Script.sol";
|
|
6
6
|
import {CoreDeployment, CoreDeploymentLib} from "./helpers/CoreDeploymentLib.sol";
|
|
7
7
|
|
|
8
|
-
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
9
8
|
import {IJBPriceFeed} from "src/interfaces/IJBPriceFeed.sol";
|
|
10
|
-
import {JBPermissions} from "src/JBPermissions.sol";
|
|
11
|
-
import {JBProjects} from "src/JBProjects.sol";
|
|
12
|
-
import {JBPrices} from "src/JBPrices.sol";
|
|
13
9
|
import {JBDeadline3Hours} from "src/periphery/JBDeadline3Hours.sol";
|
|
14
10
|
import {JBDeadline1Day} from "src/periphery/JBDeadline1Day.sol";
|
|
15
11
|
import {JBDeadline3Days} from "src/periphery/JBDeadline3Days.sol";
|
|
@@ -18,16 +14,7 @@ import {JBMatchingPriceFeed} from "src/periphery/JBMatchingPriceFeed.sol";
|
|
|
18
14
|
import {JBChainlinkV3PriceFeed, AggregatorV3Interface} from "src/JBChainlinkV3PriceFeed.sol";
|
|
19
15
|
import {AggregatorV2V3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV2V3Interface.sol";
|
|
20
16
|
import {JBChainlinkV3SequencerPriceFeed} from "src/JBChainlinkV3SequencerPriceFeed.sol";
|
|
21
|
-
import {JBRulesets} from "src/JBRulesets.sol";
|
|
22
|
-
import {JBDirectory} from "src/JBDirectory.sol";
|
|
23
|
-
import {JBERC20} from "src/JBERC20.sol";
|
|
24
|
-
import {JBTokens} from "src/JBTokens.sol";
|
|
25
|
-
import {JBSplits} from "src/JBSplits.sol";
|
|
26
|
-
import {JBFeelessAddresses} from "src/JBFeelessAddresses.sol";
|
|
27
|
-
import {JBFundAccessLimits} from "src/JBFundAccessLimits.sol";
|
|
28
17
|
import {JBController} from "src/JBController.sol";
|
|
29
|
-
import {JBTerminalStore} from "src/JBTerminalStore.sol";
|
|
30
|
-
import {JBMultiTerminal} from "src/JBMultiTerminal.sol";
|
|
31
18
|
|
|
32
19
|
import {JBConstants} from "src/libraries/JBConstants.sol";
|
|
33
20
|
import {JBCurrencyIds} from "src/libraries/JBCurrencyIds.sol";
|
|
@@ -16,9 +16,8 @@ import {JBFundAccessLimits} from "../../src/JBFundAccessLimits.sol";
|
|
|
16
16
|
import {JBController} from "../../src/JBController.sol";
|
|
17
17
|
import {JBTerminalStore} from "../../src/JBTerminalStore.sol";
|
|
18
18
|
import {JBMultiTerminal} from "../../src/JBMultiTerminal.sol";
|
|
19
|
-
import {ERC2771Forwarder} from "@openzeppelin/contracts/metatx/ERC2771Forwarder.sol";
|
|
20
19
|
|
|
21
|
-
import {SphinxConstants, NetworkInfo} from "@sphinx-labs/contracts/SphinxConstants.sol";
|
|
20
|
+
import {SphinxConstants, NetworkInfo} from "@sphinx-labs/contracts/contracts/foundry/SphinxConstants.sol";
|
|
22
21
|
|
|
23
22
|
struct CoreDeployment {
|
|
24
23
|
JBPermissions permissions;
|
package/src/JBController.sol
CHANGED
|
@@ -187,7 +187,7 @@ contract JBController is JBPermissioned, ERC2771Context, IJBController, IJBMigra
|
|
|
187
187
|
/// @dev Can only be called by the directory.
|
|
188
188
|
/// @param from The controller being migrated from.
|
|
189
189
|
/// @param projectId The ID of the project that migrated to this controller.
|
|
190
|
-
function afterReceiveMigrationFrom(IERC165 from, uint256 projectId) external override {
|
|
190
|
+
function afterReceiveMigrationFrom(IERC165 from, uint256 projectId) external view override {
|
|
191
191
|
from; // Suppress unused variable warning.
|
|
192
192
|
projectId; // Suppress unused variable warning.
|
|
193
193
|
|
|
@@ -12,7 +12,6 @@ import {IJBProjects} from "./IJBProjects.sol";
|
|
|
12
12
|
import {IJBProjectUriRegistry} from "./IJBProjectUriRegistry.sol";
|
|
13
13
|
import {IJBRulesets} from "./IJBRulesets.sol";
|
|
14
14
|
import {IJBSplits} from "./IJBSplits.sol";
|
|
15
|
-
import {IJBTerminal} from "./IJBTerminal.sol";
|
|
16
15
|
import {IJBToken} from "./IJBToken.sol";
|
|
17
16
|
import {IJBTokens} from "./IJBTokens.sol";
|
|
18
17
|
import {JBApprovalStatus} from "./../enums/JBApprovalStatus.sol";
|
|
@@ -266,9 +266,6 @@ contract ComprehensiveInvariant_Local is StdInvariant, TestBaseWorkflow {
|
|
|
266
266
|
uint256 totalOut =
|
|
267
267
|
handler.ghost_totalCashedOut() + handler.ghost_totalPaidOut() + handler.ghost_totalAllowanceUsed();
|
|
268
268
|
|
|
269
|
-
uint256 projectBalance =
|
|
270
|
-
jbTerminalStore().balanceOf(address(jbMultiTerminal()), projectId, JBConstants.NATIVE_TOKEN);
|
|
271
|
-
|
|
272
269
|
// Fees go to project #1, so total funds are conserved within the terminal
|
|
273
270
|
assertGe(totalIn, totalOut, "COMP6: Ghost conservation - total funds in must be >= total funds out");
|
|
274
271
|
}
|
|
@@ -682,7 +682,7 @@ contract EdgeCases_Local is TestBaseWorkflow {
|
|
|
682
682
|
// Cash out all remaining tokens.
|
|
683
683
|
uint256 tokenBalance = _tokens.totalBalanceOf(_beneficiary, projectId);
|
|
684
684
|
vm.prank(_beneficiary);
|
|
685
|
-
|
|
685
|
+
_terminal.cashOutTokensOf({
|
|
686
686
|
holder: _beneficiary,
|
|
687
687
|
projectId: projectId,
|
|
688
688
|
cashOutCount: tokenBalance,
|
|
@@ -1451,8 +1451,6 @@ contract EdgeCases_Local is TestBaseWorkflow {
|
|
|
1451
1451
|
vm.warp(block.timestamp + 2_419_200 + 1); // 28 days + 1 second
|
|
1452
1452
|
|
|
1453
1453
|
// Record terminal ETH before processing.
|
|
1454
|
-
uint256 terminalBalanceBefore = address(_terminal).balance;
|
|
1455
|
-
|
|
1456
1454
|
// Process both held fees.
|
|
1457
1455
|
_terminal.processHeldFeesOf(projectId, JBConstants.NATIVE_TOKEN, 100);
|
|
1458
1456
|
|
|
@@ -4,7 +4,6 @@ pragma solidity ^0.8.6;
|
|
|
4
4
|
import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
|
|
5
5
|
import {JBAccountingContext} from "../src/structs/JBAccountingContext.sol";
|
|
6
6
|
import {JBConstants} from "../src/libraries/JBConstants.sol";
|
|
7
|
-
import {JBFees} from "../src/libraries/JBFees.sol";
|
|
8
7
|
|
|
9
8
|
/// @title EntryPointPermutations
|
|
10
9
|
/// @notice Systematically tests every JBMultiTerminal external function with edge-case parameters
|
|
@@ -452,7 +451,7 @@ contract EntryPointPermutations_Local is TestBaseWorkflow {
|
|
|
452
451
|
);
|
|
453
452
|
|
|
454
453
|
// Held fees should be reduced (partially or fully returned)
|
|
455
|
-
|
|
454
|
+
jbMultiTerminal().heldFeesOf(projectStandard, JBConstants.NATIVE_TOKEN, 100);
|
|
456
455
|
|
|
457
456
|
if (feesBefore.length > 0) {
|
|
458
457
|
// Either fewer fees or smaller amounts
|
|
@@ -254,7 +254,6 @@ contract FlashLoanAttacks_Local is TestBaseWorkflow {
|
|
|
254
254
|
|
|
255
255
|
// Both should get proportional shares (with cashOutTax reducing it)
|
|
256
256
|
// Key check: they should get roughly equal amounts since they have equal tokens
|
|
257
|
-
uint256 diff = aliceReclaim > bobReclaim ? aliceReclaim - bobReclaim : bobReclaim - aliceReclaim;
|
|
258
257
|
// Alice cashes out first, so she gets slightly more due to reduced supply.
|
|
259
258
|
// But the proportional split should be reasonable.
|
|
260
259
|
assertTrue(aliceReclaim > 0, "Alice should get some reclaim");
|
|
@@ -500,7 +500,7 @@ contract PermissionEscalation_Local is TestBaseWorkflow {
|
|
|
500
500
|
// Test 10: ROOT on project 2 has no power over project 3
|
|
501
501
|
// ═══════════════════════════════════════════════════════════════════
|
|
502
502
|
|
|
503
|
-
function test_permission_rootOnProject_doesNotAffectOtherProject() public {
|
|
503
|
+
function test_permission_rootOnProject_doesNotAffectOtherProject() public view {
|
|
504
504
|
// Alice has ROOT on project 2 (from setUp)
|
|
505
505
|
// Alice should NOT have any permissions on project 3
|
|
506
506
|
bool hasPermOnProject3 = jbPermissions()
|
|
@@ -638,17 +638,6 @@ contract PermissionEscalation_Local is TestBaseWorkflow {
|
|
|
638
638
|
// Verify the forwarder is set up correctly
|
|
639
639
|
assertTrue(forwarder != address(0), "Trusted forwarder should be set");
|
|
640
640
|
|
|
641
|
-
// Construct the calldata with Bob's address appended (ERC2771 pattern)
|
|
642
|
-
bytes memory callData = abi.encodeWithSelector(
|
|
643
|
-
IJBPermissions.hasPermission.selector,
|
|
644
|
-
bob,
|
|
645
|
-
projectOwner,
|
|
646
|
-
projectId2,
|
|
647
|
-
JBPermissionIds.CASH_OUT_TOKENS,
|
|
648
|
-
false,
|
|
649
|
-
false
|
|
650
|
-
);
|
|
651
|
-
|
|
652
641
|
// Direct call should return true
|
|
653
642
|
bool hasPerm = jbPermissions()
|
|
654
643
|
.hasPermission({
|
|
@@ -463,10 +463,8 @@ contract SplitLoopTests_Local is TestBaseWorkflow {
|
|
|
463
463
|
|
|
464
464
|
_payProject(pid, address(0xBA1E), 10 ether);
|
|
465
465
|
|
|
466
|
-
uint256 totalBefore = address(jbMultiTerminal()).balance;
|
|
467
|
-
|
|
468
466
|
vm.prank(projectOwner);
|
|
469
|
-
|
|
467
|
+
jbMultiTerminal()
|
|
470
468
|
.sendPayoutsOf({
|
|
471
469
|
projectId: pid,
|
|
472
470
|
token: JBConstants.NATIVE_TOKEN,
|
package/test/TestCashOut.sol
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
pragma solidity ^0.8.6;
|
|
3
3
|
|
|
4
4
|
import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
|
|
5
|
-
import {JBCashOuts} from "../src/libraries/JBCashOuts.sol";
|
|
6
5
|
|
|
7
6
|
// Projects can issue a token, be paid to receieve claimed tokens, burn some of the claimed tokens, cash out the rest
|
|
8
7
|
// of
|
|
@@ -153,9 +153,6 @@ contract TestFeeProcessingFailure_Local is TestBaseWorkflow {
|
|
|
153
153
|
// For this test, verify the FeeReverted event is emitted when fee processing fails.
|
|
154
154
|
// We'll manipulate the directory to return address(0) for fee project's terminal.
|
|
155
155
|
|
|
156
|
-
// Record balance before payout.
|
|
157
|
-
uint256 projectBalanceBefore = _store.balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
|
|
158
|
-
|
|
159
156
|
// The fee is taken during sendPayoutsOf. Under normal conditions, it goes to project #1.
|
|
160
157
|
// The _processFee try-catch handles failures gracefully.
|
|
161
158
|
// We verify that the normal path works (fee is deducted from payout and sent to fee project).
|
|
@@ -168,8 +165,6 @@ contract TestFeeProcessingFailure_Local is TestBaseWorkflow {
|
|
|
168
165
|
minTokensPaidOut: 0
|
|
169
166
|
});
|
|
170
167
|
|
|
171
|
-
uint256 projectBalanceAfter = _store.balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
|
|
172
|
-
|
|
173
168
|
// Fee was deducted from the payout — project balance is now only the surplus (if any).
|
|
174
169
|
// The fee was 2.5% of the payout amount.
|
|
175
170
|
uint256 feeAmount = JBFees.feeAmountFrom({amountBeforeFee: PAY_AMOUNT, feePercent: _terminal.FEE()});
|
|
@@ -178,7 +173,7 @@ contract TestFeeProcessingFailure_Local is TestBaseWorkflow {
|
|
|
178
173
|
|
|
179
174
|
/// @notice Held fee processing: when fee payment reverts, the FeeReverted event is emitted
|
|
180
175
|
/// and the fee amount is credited back to the project balance via _recordAddedBalanceFor.
|
|
181
|
-
function test_heldFeeProcessing_revert_refundsToProject() external {
|
|
176
|
+
function test_heldFeeProcessing_revert_refundsToProject() external view {
|
|
182
177
|
// This test requires holdFees=true ruleset — we test the principle:
|
|
183
178
|
// When _processFee's try block reverts, the catch block calls _recordAddedBalanceFor.
|
|
184
179
|
// This returns the fee amount to the project's terminal store balance.
|
|
@@ -144,11 +144,8 @@ contract TestMigrationHeldFees_Local is TestBaseWorkflow {
|
|
|
144
144
|
assertGt(heldFees.length, 0, "Should have held fees after payout");
|
|
145
145
|
|
|
146
146
|
// Step 3: Migrate balance to terminal2.
|
|
147
|
-
uint256 balanceBefore = _store.balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
|
|
148
|
-
|
|
149
147
|
vm.prank(_projectOwner);
|
|
150
|
-
|
|
151
|
-
_terminal.migrateBalanceOf(_projectId, JBConstants.NATIVE_TOKEN, IJBTerminal(address(_terminal2)));
|
|
148
|
+
_terminal.migrateBalanceOf(_projectId, JBConstants.NATIVE_TOKEN, IJBTerminal(address(_terminal2)));
|
|
152
149
|
|
|
153
150
|
// Step 4: Verify old terminal has no balance but still has held fees.
|
|
154
151
|
uint256 balanceAfter = _store.balanceOf(address(_terminal), _projectId, JBConstants.NATIVE_TOKEN);
|
|
@@ -231,16 +228,10 @@ contract TestMigrationHeldFees_Local is TestBaseWorkflow {
|
|
|
231
228
|
// Warp past holding period.
|
|
232
229
|
vm.warp(block.timestamp + 28 days + 1);
|
|
233
230
|
|
|
234
|
-
// Fee project balance before attempting to process.
|
|
235
|
-
uint256 feeBalanceBefore = _store.balanceOf(address(_terminal), 1, JBConstants.NATIVE_TOKEN);
|
|
236
|
-
|
|
237
231
|
// Try to process held fees — the fees still exist but the old terminal has no ETH.
|
|
238
232
|
// The _processFee try-catch will catch the revert and credit the fee amount back to the project.
|
|
239
233
|
_terminal.processHeldFeesOf(_projectId, JBConstants.NATIVE_TOKEN, 100);
|
|
240
234
|
|
|
241
|
-
// Fee project should NOT have received fees (old terminal had no ETH to transfer).
|
|
242
|
-
uint256 feeBalanceAfter = _store.balanceOf(address(_terminal), 1, JBConstants.NATIVE_TOKEN);
|
|
243
|
-
|
|
244
235
|
// The fee processing attempted but the terminal has no actual ETH.
|
|
245
236
|
// The _recordAddedBalanceFor in the catch block inflates the store balance
|
|
246
237
|
// without actual funds — this is a phantom balance.
|
|
@@ -709,7 +709,14 @@ contract MockApprovalHookConfigurable is IJBRulesetApprovalHook {
|
|
|
709
709
|
}
|
|
710
710
|
|
|
711
711
|
/// @notice Fuzz: deriveStartFrom always returns a value >= mustStartAtOrAfter and aligned to duration.
|
|
712
|
-
function testFuzz_deriveStartFrom_alignment(
|
|
712
|
+
function testFuzz_deriveStartFrom_alignment(
|
|
713
|
+
uint48 baseStart,
|
|
714
|
+
uint32 duration,
|
|
715
|
+
uint48 mustStartAfter
|
|
716
|
+
)
|
|
717
|
+
external
|
|
718
|
+
view
|
|
719
|
+
{
|
|
713
720
|
// Bound to reasonable values.
|
|
714
721
|
baseStart = uint48(bound(baseStart, 1, type(uint48).max / 2));
|
|
715
722
|
duration = uint32(bound(duration, 1, type(uint32).max / 2));
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
pragma solidity >=0.8.6;
|
|
3
3
|
|
|
4
4
|
import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
|
|
5
|
-
import {MockPriceFeed} from "./mock/MockPriceFeed.sol";
|
|
6
5
|
|
|
7
6
|
// A ruleset's weight can be cached to make larger intervals calculable while staying within the gas limit.
|
|
8
7
|
contract TestRulesetWeightCaching_Local is TestBaseWorkflow {
|
|
@@ -3,7 +3,7 @@ pragma solidity ^0.8.6;
|
|
|
3
3
|
|
|
4
4
|
import /* {*} from */ "./helpers/TestBaseWorkflow.sol";
|
|
5
5
|
import {JBAccountingContext} from "../src/structs/JBAccountingContext.sol";
|
|
6
|
-
import {ERC20
|
|
6
|
+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
7
7
|
|
|
8
8
|
/// @notice Tests for weird/non-standard ERC-20 tokens: fee-on-transfer, rebasing, return-false, low/high decimals.
|
|
9
9
|
contract WeirdTokenTests_Local is TestBaseWorkflow {
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";
|
|
7
|
+
|
|
8
|
+
import {JBChainlinkV3PriceFeed} from "../../src/JBChainlinkV3PriceFeed.sol";
|
|
9
|
+
import {JBPrices} from "../../src/JBPrices.sol";
|
|
10
|
+
import {IJBDirectory} from "../../src/interfaces/IJBDirectory.sol";
|
|
11
|
+
import {IJBPermissions} from "../../src/interfaces/IJBPermissions.sol";
|
|
12
|
+
import {IJBPriceFeed} from "../../src/interfaces/IJBPriceFeed.sol";
|
|
13
|
+
import {IJBProjects} from "../../src/interfaces/IJBProjects.sol";
|
|
14
|
+
|
|
15
|
+
/// @notice Fork tests for JBChainlinkV3PriceFeed and JBPrices against live Chainlink oracles on Ethereum mainnet.
|
|
16
|
+
contract TestChainlinkPriceFeedFork is Test {
|
|
17
|
+
// Chainlink feed addresses (Ethereum mainnet).
|
|
18
|
+
address constant ETH_USD_FEED = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419;
|
|
19
|
+
address constant BTC_USD_FEED = 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c;
|
|
20
|
+
|
|
21
|
+
// Staleness threshold (1 hour).
|
|
22
|
+
uint256 constant THRESHOLD = 3600;
|
|
23
|
+
|
|
24
|
+
// Currency identifiers (arbitrary nonzero values for JBPrices mapping keys).
|
|
25
|
+
uint256 constant CURRENCY_ETH = 1;
|
|
26
|
+
uint256 constant CURRENCY_USD = 2;
|
|
27
|
+
uint256 constant CURRENCY_BTC = 3;
|
|
28
|
+
|
|
29
|
+
// Pinned block for reproducibility.
|
|
30
|
+
uint256 constant FORK_BLOCK = 22_000_000;
|
|
31
|
+
|
|
32
|
+
JBChainlinkV3PriceFeed ethUsdPriceFeed;
|
|
33
|
+
JBChainlinkV3PriceFeed btcUsdPriceFeed;
|
|
34
|
+
JBPrices prices;
|
|
35
|
+
address owner;
|
|
36
|
+
|
|
37
|
+
function setUp() public {
|
|
38
|
+
vm.createSelectFork("ethereum", FORK_BLOCK);
|
|
39
|
+
|
|
40
|
+
owner = makeAddr("owner");
|
|
41
|
+
|
|
42
|
+
// Deploy price feeds against real Chainlink aggregators.
|
|
43
|
+
ethUsdPriceFeed = new JBChainlinkV3PriceFeed(AggregatorV3Interface(ETH_USD_FEED), THRESHOLD);
|
|
44
|
+
btcUsdPriceFeed = new JBChainlinkV3PriceFeed(AggregatorV3Interface(BTC_USD_FEED), THRESHOLD);
|
|
45
|
+
|
|
46
|
+
// Deploy JBPrices with mock directory/permissions/projects (only price resolution is tested).
|
|
47
|
+
prices = new JBPrices(
|
|
48
|
+
IJBDirectory(makeAddr("directory")),
|
|
49
|
+
IJBPermissions(makeAddr("permissions")),
|
|
50
|
+
IJBProjects(makeAddr("projects")),
|
|
51
|
+
owner,
|
|
52
|
+
address(0)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ------------------------------------------------------------------
|
|
57
|
+
// Live feed sanity checks
|
|
58
|
+
// ------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/// @notice Verify currentUnitPrice returns a sane ETH/USD price at the pinned block.
|
|
61
|
+
function test_currentUnitPrice_liveEthUsd() public view {
|
|
62
|
+
uint256 price18 = ethUsdPriceFeed.currentUnitPrice(18);
|
|
63
|
+
|
|
64
|
+
// ETH price should be between $500 and $50,000.
|
|
65
|
+
assertGt(price18, 500e18, "ETH price too low");
|
|
66
|
+
assertLt(price18, 50_000e18, "ETH price too high");
|
|
67
|
+
|
|
68
|
+
// Cross-check against raw latestRoundData.
|
|
69
|
+
(, int256 rawPrice,,,) = AggregatorV3Interface(ETH_USD_FEED).latestRoundData();
|
|
70
|
+
uint256 feedDecimals = AggregatorV3Interface(ETH_USD_FEED).decimals();
|
|
71
|
+
uint256 expected18 = uint256(rawPrice) * 10 ** (18 - feedDecimals);
|
|
72
|
+
assertEq(price18, expected18, "Price mismatch vs raw feed");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ------------------------------------------------------------------
|
|
76
|
+
// Decimal scaling
|
|
77
|
+
// ------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
/// @notice Verify correct scaling across 6, 8, 18, and 27 decimals.
|
|
80
|
+
function test_currentUnitPrice_differentDecimals() public view {
|
|
81
|
+
uint256 price6 = ethUsdPriceFeed.currentUnitPrice(6);
|
|
82
|
+
uint256 price8 = ethUsdPriceFeed.currentUnitPrice(8);
|
|
83
|
+
uint256 price18 = ethUsdPriceFeed.currentUnitPrice(18);
|
|
84
|
+
uint256 price27 = ethUsdPriceFeed.currentUnitPrice(27);
|
|
85
|
+
|
|
86
|
+
// Raw feed is 8 decimals — price8 should match it exactly.
|
|
87
|
+
(, int256 rawPrice,,,) = AggregatorV3Interface(ETH_USD_FEED).latestRoundData();
|
|
88
|
+
assertEq(price8, uint256(rawPrice), "8-decimal mismatch");
|
|
89
|
+
|
|
90
|
+
// 6 decimals = raw / 100 (truncated).
|
|
91
|
+
assertEq(price6, uint256(rawPrice) / 1e2, "6-decimal mismatch");
|
|
92
|
+
|
|
93
|
+
// 18 decimals = raw * 1e10.
|
|
94
|
+
assertEq(price18, uint256(rawPrice) * 1e10, "18-decimal mismatch");
|
|
95
|
+
|
|
96
|
+
// 27 decimals = raw * 1e19.
|
|
97
|
+
assertEq(price27, uint256(rawPrice) * 1e19, "27-decimal mismatch");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ------------------------------------------------------------------
|
|
101
|
+
// JBPrices integration — direct feed
|
|
102
|
+
// ------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
/// @notice Register ETH/USD feed and resolve via pricePerUnitOf.
|
|
105
|
+
function test_pricePerUnitOf_directFeed() public {
|
|
106
|
+
vm.prank(owner);
|
|
107
|
+
prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
|
|
108
|
+
|
|
109
|
+
uint256 priceFromPrices = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_ETH, 18);
|
|
110
|
+
uint256 priceFromFeed = ethUsdPriceFeed.currentUnitPrice(18);
|
|
111
|
+
|
|
112
|
+
assertEq(priceFromPrices, priceFromFeed, "Direct feed mismatch");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ------------------------------------------------------------------
|
|
116
|
+
// JBPrices integration — inverse feed
|
|
117
|
+
// ------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
/// @notice Register ETH/USD feed but query USD/ETH — verify inverse calculation.
|
|
120
|
+
function test_pricePerUnitOf_inverseFeed() public {
|
|
121
|
+
vm.prank(owner);
|
|
122
|
+
prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
|
|
123
|
+
|
|
124
|
+
uint256 inversePrice = prices.pricePerUnitOf(0, CURRENCY_ETH, CURRENCY_USD, 18);
|
|
125
|
+
|
|
126
|
+
// inverse = (1e18 * 1e18) / ethUsdPrice. For ETH ~$2000, inverse ~0.0005e18 = 5e14.
|
|
127
|
+
// Sane range: $500–$50k → inverse 2e13–2e15.
|
|
128
|
+
assertGt(inversePrice, 2e13, "Inverse price too low");
|
|
129
|
+
assertLt(inversePrice, 2e15, "Inverse price too high");
|
|
130
|
+
|
|
131
|
+
// Verify round-trip: price * inverse ≈ 1e18 (within mulDiv rounding).
|
|
132
|
+
uint256 directPrice = ethUsdPriceFeed.currentUnitPrice(18);
|
|
133
|
+
uint256 product = (directPrice * inversePrice) / 1e18;
|
|
134
|
+
// mulDiv truncation can cause up to ~1e4 wei error at these magnitudes.
|
|
135
|
+
assertApproxEqAbs(product, 1e18, 1e4, "Round-trip mismatch");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ------------------------------------------------------------------
|
|
139
|
+
// JBPrices — same currency
|
|
140
|
+
// ------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/// @notice pricePerUnitOf(X, X, 18) should return 1e18 without any feed.
|
|
143
|
+
function test_pricePerUnitOf_sameCurrency() public view {
|
|
144
|
+
uint256 price = prices.pricePerUnitOf(0, CURRENCY_ETH, CURRENCY_ETH, 18);
|
|
145
|
+
assertEq(price, 1e18, "Same currency should be 1e18");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ------------------------------------------------------------------
|
|
149
|
+
// JBPrices — default fallback
|
|
150
|
+
// ------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/// @notice Feed registered at projectId=0 should be used when querying projectId=99.
|
|
153
|
+
function test_pricePerUnitOf_defaultFallback() public {
|
|
154
|
+
vm.prank(owner);
|
|
155
|
+
prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
|
|
156
|
+
|
|
157
|
+
uint256 defaultPrice = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_ETH, 18);
|
|
158
|
+
uint256 fallbackPrice = prices.pricePerUnitOf(99, CURRENCY_USD, CURRENCY_ETH, 18);
|
|
159
|
+
|
|
160
|
+
assertEq(fallbackPrice, defaultPrice, "Fallback should match default");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ------------------------------------------------------------------
|
|
164
|
+
// Stale price revert
|
|
165
|
+
// ------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
/// @notice Warping past the threshold should cause a StalePrice revert.
|
|
168
|
+
function test_stalePriceReverts() public {
|
|
169
|
+
// Snapshot updatedAt from the feed at this block.
|
|
170
|
+
(,,, uint256 updatedAt,) = AggregatorV3Interface(ETH_USD_FEED).latestRoundData();
|
|
171
|
+
|
|
172
|
+
// Warp far enough that the feed's updatedAt + threshold < block.timestamp.
|
|
173
|
+
uint256 warpTo = block.timestamp + 7200;
|
|
174
|
+
vm.warp(warpTo);
|
|
175
|
+
|
|
176
|
+
vm.expectRevert(
|
|
177
|
+
abi.encodeWithSelector(
|
|
178
|
+
JBChainlinkV3PriceFeed.JBChainlinkV3PriceFeed_StalePrice.selector, warpTo, THRESHOLD, updatedAt
|
|
179
|
+
)
|
|
180
|
+
);
|
|
181
|
+
ethUsdPriceFeed.currentUnitPrice(18);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ------------------------------------------------------------------
|
|
185
|
+
// Feed immutability
|
|
186
|
+
// ------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
/// @notice Registering the same currency pair twice should revert.
|
|
189
|
+
function test_feedImmutability() public {
|
|
190
|
+
vm.prank(owner);
|
|
191
|
+
prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
|
|
192
|
+
|
|
193
|
+
vm.prank(owner);
|
|
194
|
+
vm.expectRevert(
|
|
195
|
+
abi.encodeWithSelector(
|
|
196
|
+
JBPrices.JBPrices_PriceFeedAlreadyExists.selector, IJBPriceFeed(address(ethUsdPriceFeed))
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(btcUsdPriceFeed)));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ------------------------------------------------------------------
|
|
203
|
+
// Multiple feeds
|
|
204
|
+
// ------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
/// @notice Register both ETH/USD and BTC/USD and verify independent resolution.
|
|
207
|
+
function test_multipleFeeds_ethUsd_btcUsd() public {
|
|
208
|
+
vm.startPrank(owner);
|
|
209
|
+
prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
|
|
210
|
+
prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_BTC, IJBPriceFeed(address(btcUsdPriceFeed)));
|
|
211
|
+
vm.stopPrank();
|
|
212
|
+
|
|
213
|
+
uint256 ethUsd = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_ETH, 18);
|
|
214
|
+
uint256 btcUsd = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_BTC, 18);
|
|
215
|
+
|
|
216
|
+
// ETH/USD: $500–$50,000.
|
|
217
|
+
assertGt(ethUsd, 500e18, "ETH/USD too low");
|
|
218
|
+
assertLt(ethUsd, 50_000e18, "ETH/USD too high");
|
|
219
|
+
|
|
220
|
+
// BTC/USD: $10,000–$500,000.
|
|
221
|
+
assertGt(btcUsd, 10_000e18, "BTC/USD too low");
|
|
222
|
+
assertLt(btcUsd, 500_000e18, "BTC/USD too high");
|
|
223
|
+
|
|
224
|
+
// BTC should be more expensive than ETH.
|
|
225
|
+
assertGt(btcUsd, ethUsd, "BTC should cost more than ETH");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ------------------------------------------------------------------
|
|
229
|
+
// Cross-price derivation
|
|
230
|
+
// ------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
/// @notice Derive ETH/BTC price from ETH/USD and BTC/USD feeds.
|
|
233
|
+
function test_crossPriceDerived() public {
|
|
234
|
+
vm.startPrank(owner);
|
|
235
|
+
prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_ETH, IJBPriceFeed(address(ethUsdPriceFeed)));
|
|
236
|
+
prices.addPriceFeedFor(0, CURRENCY_USD, CURRENCY_BTC, IJBPriceFeed(address(btcUsdPriceFeed)));
|
|
237
|
+
vm.stopPrank();
|
|
238
|
+
|
|
239
|
+
uint256 ethUsd = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_ETH, 18);
|
|
240
|
+
uint256 btcUsd = prices.pricePerUnitOf(0, CURRENCY_USD, CURRENCY_BTC, 18);
|
|
241
|
+
|
|
242
|
+
// ETH/BTC = ethUsd / btcUsd (how many BTC per 1 ETH).
|
|
243
|
+
uint256 ethPerBtc = (ethUsd * 1e18) / btcUsd;
|
|
244
|
+
|
|
245
|
+
// Should be between 0.01 and 0.1 (at 18 decimals: 1e16–1e17).
|
|
246
|
+
assertGt(ethPerBtc, 1e16, "ETH/BTC ratio too low");
|
|
247
|
+
assertLt(ethPerBtc, 1e17, "ETH/BTC ratio too high");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
pragma solidity ^0.8.17;
|
|
3
3
|
|
|
4
4
|
import "forge-std/Test.sol";
|
|
5
|
-
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
6
5
|
import {JBCashOuts} from "../../src/libraries/JBCashOuts.sol";
|
|
7
6
|
import {JBFees} from "../../src/libraries/JBFees.sol";
|
|
8
7
|
import {JBConstants} from "../../src/libraries/JBConstants.sol";
|
package/test/helpers/JBTest.sol
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
-
import {JBRuleset} from "src/structs/JBRuleset.sol";
|
|
5
|
-
import {JBRulesetMetadata} from "src/structs/JBRulesetMetadata.sol";
|
|
6
|
-
import {JBRulesetMetadataResolver} from "src/libraries/JBRulesetMetadataResolver.sol";
|
|
7
|
-
import {JBConstants} from "src/libraries/JBConstants.sol";
|
|
8
|
-
import {IJBRulesetApprovalHook} from "src/interfaces/IJBRulesetApprovalHook.sol";
|
|
4
|
+
import {JBRuleset} from "../../src/structs/JBRuleset.sol";
|
|
5
|
+
import {JBRulesetMetadata} from "../../src/structs/JBRulesetMetadata.sol";
|
|
6
|
+
import {JBRulesetMetadataResolver} from "../../src/libraries/JBRulesetMetadataResolver.sol";
|
|
7
|
+
import {JBConstants} from "../../src/libraries/JBConstants.sol";
|
|
8
|
+
import {IJBRulesetApprovalHook} from "../../src/interfaces/IJBRulesetApprovalHook.sol";
|
|
9
9
|
import "lib/forge-std/src/Test.sol";
|
|
10
10
|
|
|
11
11
|
contract JBTest is Test {
|
|
@@ -65,7 +65,7 @@ contract JBTest is Test {
|
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function generateEmptyMetadata() public
|
|
68
|
+
function generateEmptyMetadata() public pure returns (JBRulesetMetadata memory) {
|
|
69
69
|
return JBRulesetMetadata({
|
|
70
70
|
reservedPercent: 0,
|
|
71
71
|
cashOutTaxRate: 0,
|
|
@@ -6,8 +6,6 @@ import /* {*} from */ "../helpers/TestBaseWorkflow.sol";
|
|
|
6
6
|
import {Phase3Handler} from "./handlers/Phase3Handler.sol";
|
|
7
7
|
import {JBAccountingContext} from "../../src/structs/JBAccountingContext.sol";
|
|
8
8
|
import {JBConstants} from "../../src/libraries/JBConstants.sol";
|
|
9
|
-
import {JBFee} from "../../src/structs/JBFee.sol";
|
|
10
|
-
import {JBSplitGroupIds} from "../../src/libraries/JBSplitGroupIds.sol";
|
|
11
9
|
|
|
12
10
|
/// @title Phase3DeepInvariant
|
|
13
11
|
/// @notice Multi-project deep invariant tests with strict equality checks.
|
|
@@ -362,7 +360,6 @@ contract Phase3DeepInvariant_Local is StdInvariant, TestBaseWorkflow {
|
|
|
362
360
|
/// @notice After addToBalance(shouldReturn=true), returned fees must not exceed held fees.
|
|
363
361
|
function invariant_P3_6_heldFeeReturnBounded() public view {
|
|
364
362
|
uint256 returned = handler.ghost_totalReturnedFees(project2);
|
|
365
|
-
uint256 heldTotal = handler.ghost_totalHeldFeeAmounts(project2);
|
|
366
363
|
|
|
367
364
|
// Returned fees should never exceed what was held
|
|
368
365
|
// (heldTotal may be 0 if we never tracked, so only check when both nonzero)
|
|
@@ -9,7 +9,6 @@ import {IJBMultiTerminal} from "../../../src/interfaces/IJBMultiTerminal.sol";
|
|
|
9
9
|
import {IJBTerminalStore} from "../../../src/interfaces/IJBTerminalStore.sol";
|
|
10
10
|
import {IJBController} from "../../../src/interfaces/IJBController.sol";
|
|
11
11
|
import {IJBTokens} from "../../../src/interfaces/IJBTokens.sol";
|
|
12
|
-
import {JBMultiTerminal} from "../../../src/JBMultiTerminal.sol";
|
|
13
12
|
|
|
14
13
|
/// @title Phase3Handler
|
|
15
14
|
/// @notice Stateful fuzzing handler with ghost variable tracking for strict invariant verification.
|
|
@@ -6,7 +6,6 @@ import {JBConstants} from "../../../src/libraries/JBConstants.sol";
|
|
|
6
6
|
import {IJBMultiTerminal} from "../../../src/interfaces/IJBMultiTerminal.sol";
|
|
7
7
|
import {IJBController} from "../../../src/interfaces/IJBController.sol";
|
|
8
8
|
import {IJBTokens} from "../../../src/interfaces/IJBTokens.sol";
|
|
9
|
-
import {IJBToken} from "../../../src/interfaces/IJBToken.sol";
|
|
10
9
|
|
|
11
10
|
/// @notice Handler contract for JBTokens invariant testing.
|
|
12
11
|
/// @dev Drives mint, burn, claim, and transfer operations; tracks holders for sum-of-balances checks.
|
package/test/mock/MockERC20.sol
CHANGED
|
@@ -3,7 +3,7 @@ pragma solidity 0.8.26;
|
|
|
3
3
|
|
|
4
4
|
import /* {*} from */ "../helpers/TestBaseWorkflow.sol";
|
|
5
5
|
|
|
6
|
-
import {ERC20
|
|
6
|
+
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
7
7
|
|
|
8
8
|
contract MockERC20 is ERC20 {
|
|
9
9
|
function decimals() public view virtual override returns (uint8) {
|
|
@@ -3,7 +3,6 @@ pragma solidity 0.8.26;
|
|
|
3
3
|
|
|
4
4
|
import /* {*} from */ "../helpers/TestBaseWorkflow.sol";
|
|
5
5
|
import {IJBMultiTerminal} from "../../src/interfaces/IJBMultiTerminal.sol";
|
|
6
|
-
import {JBMultiTerminal} from "../../src/JBMultiTerminal.sol";
|
|
7
6
|
import {JBConstants} from "../../src/libraries/JBConstants.sol";
|
|
8
7
|
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
9
8
|
|
|
@@ -46,7 +46,7 @@ contract TestDeadlineFuzz_Local is JBTest {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/// @notice Ruleset queued too close to start returns Failed.
|
|
49
|
-
function test_insufficientGap_isFailed() external {
|
|
49
|
+
function test_insufficientGap_isFailed() external view {
|
|
50
50
|
uint48 start = uint48(block.timestamp + 1 days);
|
|
51
51
|
uint48 queued = start - uint48(DURATION) + 1; // 1 second short of required gap
|
|
52
52
|
|
|
@@ -56,7 +56,7 @@ contract TestDeadlineFuzz_Local is JBTest {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/// @notice Ruleset with exactly enough gap and deadline not yet passed returns ApprovalExpected.
|
|
59
|
-
function test_exactGap_deadlineNotPassed_isApprovalExpected() external {
|
|
59
|
+
function test_exactGap_deadlineNotPassed_isApprovalExpected() external view {
|
|
60
60
|
// queue far enough in advance, and start is still far in the future
|
|
61
61
|
uint48 start = uint48(block.timestamp + 2 * DURATION + 100);
|
|
62
62
|
uint48 queued = start - uint48(DURATION);
|
|
@@ -67,7 +67,7 @@ contract TestDeadlineFuzz_Local is JBTest {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/// @notice Ruleset with enough gap and deadline passed returns Approved.
|
|
70
|
-
function test_gapSufficient_deadlinePassed_isApproved() external {
|
|
70
|
+
function test_gapSufficient_deadlinePassed_isApproved() external view {
|
|
71
71
|
uint48 start = uint48(block.timestamp + 1); // start is very soon
|
|
72
72
|
uint48 queued = start - uint48(DURATION) - 1; // plenty of gap
|
|
73
73
|
|
|
@@ -7,7 +7,7 @@ import {JBMetadataResolver} from "../../../../src/libraries/JBMetadataResolver.s
|
|
|
7
7
|
|
|
8
8
|
/// @notice Harness that exposes JBMetadataResolver internals and adds a combined operation
|
|
9
9
|
/// to test memory corruption from _sliceBytes within a single execution context.
|
|
10
|
-
contract
|
|
10
|
+
contract MetadataResolverEdgeCaseHarness {
|
|
11
11
|
function createMetadata(bytes4[] memory ids, bytes[] memory datas) external pure returns (bytes memory) {
|
|
12
12
|
return JBMetadataResolver.createMetadata(ids, datas);
|
|
13
13
|
}
|
|
@@ -64,11 +64,11 @@ contract M20M21Harness {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/// @notice Tests for _sliceBytes over-copy and addToMetadata offset overflow.
|
|
67
|
-
contract
|
|
68
|
-
|
|
67
|
+
contract TestMetadataResolverEdgeCases is JBTest {
|
|
68
|
+
MetadataResolverEdgeCaseHarness harness;
|
|
69
69
|
|
|
70
70
|
function setUp() external {
|
|
71
|
-
harness = new
|
|
71
|
+
harness = new MetadataResolverEdgeCaseHarness();
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
//*********************************************************************//
|
|
@@ -3,7 +3,6 @@ pragma solidity 0.8.26;
|
|
|
3
3
|
|
|
4
4
|
import /* {*} from */ "../../../helpers/TestBaseWorkflow.sol";
|
|
5
5
|
import {JBTest} from "../../../helpers/JBTest.sol";
|
|
6
|
-
import {JBRulesetWeightCache} from "src/structs/JBRulesetWeightCache.sol";
|
|
7
6
|
|
|
8
7
|
contract TestJBRulesetsUnits_Local is JBTest {
|
|
9
8
|
// Contracts
|
|
@@ -53,7 +53,7 @@ contract MockSurplusTerminal is ERC165, IJBTerminal {
|
|
|
53
53
|
override
|
|
54
54
|
{}
|
|
55
55
|
|
|
56
|
-
function migrateBalanceOf(uint256, address, IJBTerminal) external override returns (uint256) {
|
|
56
|
+
function migrateBalanceOf(uint256, address, IJBTerminal) external pure override returns (uint256) {
|
|
57
57
|
return 0;
|
|
58
58
|
}
|
|
59
59
|
|