@rev-net/core-v6 0.0.14 → 0.0.16
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/ADMINISTRATION.md +5 -1
- package/ARCHITECTURE.md +69 -11
- package/AUDIT_INSTRUCTIONS.md +90 -7
- package/CHANGE_LOG.md +16 -3
- package/README.md +32 -7
- package/RISKS.md +26 -14
- package/SKILLS.md +168 -46
- package/STYLE_GUIDE.md +1 -1
- package/USER_JOURNEYS.md +20 -6
- package/foundry.toml +7 -0
- package/package.json +9 -10
- package/script/Deploy.s.sol +80 -16
- package/script/helpers/RevnetCoreDeploymentLib.sol +1 -1
- package/src/REVDeployer.sol +73 -21
- package/src/REVLoans.sol +27 -6
- package/test/REV.integrations.t.sol +1 -1
- package/test/REVAutoIssuanceFuzz.t.sol +1 -1
- package/test/REVDeployerRegressions.t.sol +7 -4
- package/test/REVInvincibility.t.sol +7 -19
- package/test/REVInvincibilityHandler.sol +1 -1
- package/test/REVLifecycle.t.sol +1 -1
- package/test/REVLoans.invariants.t.sol +1 -1
- package/test/REVLoansAttacks.t.sol +20 -12
- package/test/REVLoansFeeRecovery.t.sol +20 -12
- package/test/REVLoansFindings.t.sol +20 -12
- package/test/REVLoansRegressions.t.sol +20 -12
- package/test/REVLoansSourceFeeRecovery.t.sol +1 -1
- package/test/REVLoansSourced.t.sol +1 -9
- package/test/REVLoansUnSourced.t.sol +1 -1
- package/test/TestBurnHeldTokens.t.sol +1 -1
- package/test/TestCEIPattern.t.sol +1 -1
- package/test/TestCashOutCallerValidation.t.sol +75 -1
- package/test/TestConversionDocumentation.t.sol +1 -1
- package/test/TestCrossCurrencyReclaim.t.sol +1 -1
- package/test/TestCrossSourceReallocation.t.sol +1 -1
- package/test/TestERC2771MetaTx.t.sol +1 -1
- package/test/TestEmptyBuybackSpecs.t.sol +1 -1
- package/test/TestFlashLoanSurplus.t.sol +1 -1
- package/test/TestHookArrayOOB.t.sol +1 -1
- package/test/TestLiquidationBehavior.t.sol +1 -1
- package/test/TestLoanSourceRotation.t.sol +1 -1
- package/test/TestLongTailEconomics.t.sol +1 -1
- package/test/TestLowFindings.t.sol +4 -2
- package/test/TestMixedFixes.t.sol +7 -5
- package/test/TestPermit2Signatures.t.sol +1 -1
- package/test/TestReallocationSandwich.t.sol +1 -1
- package/test/TestRevnetRegressions.t.sol +1 -1
- package/test/TestSplitWeightAdjustment.t.sol +11 -6
- package/test/TestSplitWeightE2E.t.sol +1 -1
- package/test/TestSplitWeightFork.t.sol +9 -10
- package/test/TestStageTransitionBorrowable.t.sol +1 -1
- package/test/TestSwapTerminalPermission.t.sol +1 -1
- package/test/TestUint112Overflow.t.sol +1 -1
- package/test/TestZeroRepayment.t.sol +1 -1
- package/test/audit/LoanIdOverflowGuard.t.sol +497 -0
- package/test/fork/ForkTestBase.sol +8 -11
- package/test/fork/TestAutoIssuanceFork.t.sol +148 -0
- package/test/fork/TestCashOutFork.t.sol +23 -22
- package/test/fork/TestIssuanceDecayFork.t.sol +158 -0
- package/test/fork/TestLoanBorrowFork.t.sol +1 -1
- package/test/fork/TestLoanCrossRulesetFork.t.sol +1 -1
- package/test/fork/TestLoanERC20Fork.t.sol +463 -0
- package/test/fork/TestLoanLiquidationFork.t.sol +1 -1
- package/test/fork/TestLoanReallocateFork.t.sol +1 -1
- package/test/fork/TestLoanRepayFork.t.sol +3 -3
- package/test/fork/TestLoanTransferFork.t.sol +1 -1
- package/test/fork/TestPermit2PaymentFork.t.sol +299 -0
- package/test/fork/TestSplitWeightFork.t.sol +1 -1
- package/test/helpers/MaliciousContracts.sol +37 -23
- package/test/mock/MockBuybackCashOutRecorder.sol +82 -0
- package/test/mock/MockBuybackDataHook.sol +51 -7
- package/test/mock/MockBuybackDataHookMintPath.sol +1 -1
- package/test/regression/TestBurnPermissionRequired.t.sol +1 -1
- package/test/regression/TestCashOutBuybackFeeLeak.t.sol +205 -0
- package/test/regression/TestCrossRevnetLiquidation.t.sol +1 -1
- package/test/regression/TestCumulativeLoanCounter.t.sol +1 -1
- package/test/regression/TestLiquidateGapHandling.t.sol +1 -1
- package/test/regression/TestZeroPriceFeed.t.sol +1 -1
package/SKILLS.md
CHANGED
|
@@ -15,42 +15,43 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
15
15
|
|
|
16
16
|
### Deployment
|
|
17
17
|
|
|
18
|
-
| Function | What it does |
|
|
19
|
-
|
|
20
|
-
| `REVDeployer.deployFor(revnetId, config, terminals, suckerConfig)` | Deploy a new revnet (`revnetId=0`) or convert an existing Juicebox project. Encodes stage configs into rulesets, deploys ERC-20 token, initializes buyback pool at 1:1 price, sets up split operator, suckers, loans permissions, and deploys a default empty tiered ERC-721 hook. |
|
|
21
|
-
| `REVDeployer.deployFor(revnetId, config, terminals, suckerConfig, hookConfig, allowedPosts)` | Same as `deployFor` but deploys a tiered ERC-721 hook with pre-configured tiers. Optionally configures Croptop posting criteria and grants publisher permission to add tiers. |
|
|
22
|
-
| `REVDeployer.deploySuckersFor(revnetId, suckerConfig)` | Deploy new cross-chain suckers post-launch.
|
|
18
|
+
| Function | Permissions | What it does |
|
|
19
|
+
|----------|------------|-------------|
|
|
20
|
+
| `REVDeployer.deployFor(revnetId, config, terminals, suckerConfig)` | Permissionless | Deploy a new revnet (`revnetId=0`) or convert an existing Juicebox project. Encodes stage configs into rulesets, deploys ERC-20 token, initializes buyback pool at 1:1 price, sets up split operator, suckers, loans permissions, and deploys a default empty tiered ERC-721 hook. |
|
|
21
|
+
| `REVDeployer.deployFor(revnetId, config, terminals, suckerConfig, hookConfig, allowedPosts)` | Permissionless | Same as `deployFor` but deploys a tiered ERC-721 hook with pre-configured tiers. Optionally configures Croptop posting criteria and grants publisher permission to add tiers. |
|
|
22
|
+
| `REVDeployer.deploySuckersFor(revnetId, suckerConfig)` | Split operator | Deploy new cross-chain suckers post-launch. Validates ruleset allows sucker deployment (bit 2 of `extraMetadata`). Uses stored config hash for cross-chain matching. |
|
|
23
23
|
|
|
24
24
|
### Data Hooks
|
|
25
25
|
|
|
26
|
-
| Function | What it does |
|
|
27
|
-
|
|
28
|
-
| `REVDeployer.beforePayRecordedWith(context)` | Calls the 721 hook first for split specs, then calls the buyback hook with a reduced amount context (payment minus split amount). Adjusts the returned weight proportionally for splits (`weight = mulDiv(weight, amount - splitAmount, amount)`) so the terminal only mints tokens for the amount entering the project. Assembles pay hook specs (721 hook specs first, then buyback spec). |
|
|
29
|
-
| `REVDeployer.beforeCashOutRecordedWith(context)` | If sucker: returns full amount with 0 tax (fee exempt). Otherwise: calculates 2.5% fee, enforces 30-day cash-out delay, returns modified count + fee hook spec. |
|
|
30
|
-
| `REVDeployer.afterCashOutRecordedWith(context)` | Cash-out hook callback. Receives fee amount and pays it to the fee revnet's terminal. Falls back to returning funds if fee payment fails. |
|
|
31
|
-
| `REVDeployer.hasMintPermissionFor(revnetId, ruleset, addr)` | Returns `true` for: loans contract, buyback hook, buyback hook delegates, or suckers. |
|
|
26
|
+
| Function | Permissions | What it does |
|
|
27
|
+
|----------|------------|-------------|
|
|
28
|
+
| `REVDeployer.beforePayRecordedWith(context)` | Terminal callback | Calls the 721 hook first for split specs, then calls the buyback hook with a reduced amount context (payment minus split amount). Adjusts the returned weight proportionally for splits (`weight = mulDiv(weight, amount - splitAmount, amount)`) so the terminal only mints tokens for the amount entering the project. Assembles pay hook specs (721 hook specs first, then buyback spec). |
|
|
29
|
+
| `REVDeployer.beforeCashOutRecordedWith(context)` | Terminal callback | If sucker: returns full amount with 0 tax (fee exempt). Otherwise: calculates 2.5% fee, enforces 30-day cash-out delay, returns modified count + fee hook spec. |
|
|
30
|
+
| `REVDeployer.afterCashOutRecordedWith(context)` | Permissionless | Cash-out hook callback. Receives fee amount and pays it to the fee revnet's terminal. Falls back to returning funds if fee payment fails. |
|
|
31
|
+
| `REVDeployer.hasMintPermissionFor(revnetId, ruleset, addr)` | View | Returns `true` for: loans contract, buyback hook, buyback hook delegates, or suckers. |
|
|
32
32
|
|
|
33
33
|
### Split Operator
|
|
34
34
|
|
|
35
|
-
| Function | What it does |
|
|
36
|
-
|
|
37
|
-
| `REVDeployer.setSplitOperatorOf(revnetId, newOperator)` |
|
|
35
|
+
| Function | Permissions | What it does |
|
|
36
|
+
|----------|------------|-------------|
|
|
37
|
+
| `REVDeployer.setSplitOperatorOf(revnetId, newOperator)` | Split operator | Replace the current split operator. Revokes old permissions, grants new ones. |
|
|
38
38
|
|
|
39
39
|
### Auto-Issuance
|
|
40
40
|
|
|
41
|
-
| Function | What it does |
|
|
42
|
-
|
|
43
|
-
| `REVDeployer.autoIssueFor(revnetId, stageId, beneficiary)` |
|
|
44
|
-
| `REVDeployer.burnHeldTokensOf(revnetId)` |
|
|
41
|
+
| Function | Permissions | What it does |
|
|
42
|
+
|----------|------------|-------------|
|
|
43
|
+
| `REVDeployer.autoIssueFor(revnetId, stageId, beneficiary)` | Permissionless | Mint pre-configured auto-issuance tokens for a beneficiary once a stage has started. One-time per stage per beneficiary. |
|
|
44
|
+
| `REVDeployer.burnHeldTokensOf(revnetId)` | Permissionless | Burn any reserved tokens held by the deployer (when splits < 100%). |
|
|
45
45
|
|
|
46
46
|
### Loans -- Borrowing
|
|
47
47
|
|
|
48
|
-
| Function | What it does |
|
|
49
|
-
|
|
50
|
-
| `REVLoans.borrowFrom(revnetId, source, minBorrowAmount, collateralCount, beneficiary, prepaidFeePercent)` | Open a loan: burn collateral tokens, pull funds from revnet via `useAllowanceOf`, pay REV fee (1%) + terminal fee (2.5%), transfer remainder to beneficiary, mint loan NFT. |
|
|
51
|
-
| `REVLoans.repayLoan(loanId,
|
|
52
|
-
| `REVLoans.reallocateCollateralFromLoan(loanId,
|
|
53
|
-
| `REVLoans.liquidateExpiredLoansFrom(revnetId, startingLoanId, count)` |
|
|
48
|
+
| Function | Permissions | What it does |
|
|
49
|
+
|----------|------------|-------------|
|
|
50
|
+
| `REVLoans.borrowFrom(revnetId, source, minBorrowAmount, collateralCount, beneficiary, prepaidFeePercent)` | Permissionless (caller must grant BURN_TOKENS to REVLoans) | Open a loan: burn collateral tokens, pull funds from revnet via `useAllowanceOf`, pay REV fee (1%) + terminal fee (2.5%), transfer remainder to beneficiary, mint loan NFT. |
|
|
51
|
+
| `REVLoans.repayLoan(loanId, maxRepayBorrowAmount, collateralCountToReturn, beneficiary, allowance)` | Loan NFT owner | Repay fully or partially. Returns funds to revnet via `addToBalanceOf`, re-mints collateral tokens, burns/replaces the loan NFT. Supports permit2 signatures. |
|
|
52
|
+
| `REVLoans.reallocateCollateralFromLoan(loanId, collateralCountToTransfer, source, minBorrowAmount, collateralCountToAdd, beneficiary, prepaidFeePercent)` | Loan NFT owner | Refinance: remove excess collateral from an existing loan and open a new loan with the freed collateral. Burns original, mints two replacements. |
|
|
53
|
+
| `REVLoans.liquidateExpiredLoansFrom(revnetId, startingLoanId, count)` | Permissionless | Clean up loans past the 10-year liquidation duration. Burns NFTs and decrements accounting totals. Collateral is permanently lost. |
|
|
54
|
+
| `REVLoans.setTokenUriResolver(resolver)` | Contract owner (`onlyOwner`) | Set the `IJBTokenUriResolver` used for loan NFT token URIs. |
|
|
54
55
|
|
|
55
56
|
### Loans -- Views
|
|
56
57
|
|
|
@@ -68,7 +69,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
68
69
|
|------------|--------|----------|
|
|
69
70
|
| `@bananapus/core-v6` | `IJBController`, `IJBDirectory`, `IJBPermissions`, `IJBProjects`, `IJBTerminal`, `IJBPrices`, `JBConstants`, `JBCashOuts`, `JBSurplus` | Project lifecycle, rulesets, token minting/burning, fund access, terminal payments, price feeds, bonding curve |
|
|
70
71
|
| `@bananapus/721-hook-v6` | `IJB721TiersHook`, `IJB721TiersHookDeployer` | Deploying and registering tiered ERC-721 pay hooks |
|
|
71
|
-
| `@bananapus/buyback-hook-v6` | `
|
|
72
|
+
| `@bananapus/buyback-hook-v6` | `IJBBuybackHookRegistry` | Configuring Uniswap buyback pools per revnet |
|
|
72
73
|
| `@bananapus/suckers-v6` | `IJBSuckerRegistry` | Deploying cross-chain suckers, checking sucker status for fee exemption |
|
|
73
74
|
| `@croptop/core-v6` | `CTPublisher` | Configuring Croptop posting criteria for 721 tiers |
|
|
74
75
|
| `@bananapus/permission-ids-v6` | `JBPermissionIds` | Permission ID constants (SET_SPLIT_GROUPS, USE_ALLOWANCE, etc.) |
|
|
@@ -92,6 +93,71 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
92
93
|
| `REVCroptopAllowedPost` | `category` (uint24), `minimumPrice` (uint104), `minimumTotalSupply` (uint32), `maximumTotalSupply` (uint32), `allowedAddresses[]` | Croptop posting criteria |
|
|
93
94
|
| `REVSuckerDeploymentConfig` | `deployerConfigurations[]`, `salt` | Cross-chain sucker deployment |
|
|
94
95
|
|
|
96
|
+
## Events
|
|
97
|
+
|
|
98
|
+
### REVDeployer
|
|
99
|
+
|
|
100
|
+
| Event | When It Fires |
|
|
101
|
+
|-------|---------------|
|
|
102
|
+
| `AutoIssue(revnetId, stageId, beneficiary, count, caller)` | When tokens are auto-issued for a beneficiary during a stage via `autoIssueFor`. |
|
|
103
|
+
| `BurnHeldTokens(revnetId, count, caller)` | When held tokens are burned from the deployer contract via `burnHeldTokensOf`. |
|
|
104
|
+
| `DeployRevnet(revnetId, configuration, terminalConfigurations, suckerDeploymentConfiguration, rulesetConfigurations, encodedConfigurationHash, caller)` | When a new revnet is deployed via `deployFor`. |
|
|
105
|
+
| `DeploySuckers(revnetId, encodedConfigurationHash, suckerDeploymentConfiguration, caller)` | When suckers are deployed for a revnet via `deploySuckersFor`. |
|
|
106
|
+
| `ReplaceSplitOperator(revnetId, newSplitOperator, caller)` | When the split operator of a revnet is replaced via `setSplitOperatorOf`. |
|
|
107
|
+
| `SetCashOutDelay(revnetId, cashOutDelay, caller)` | When the cash out delay is set for a revnet during deployment to a new chain. |
|
|
108
|
+
| `StoreAutoIssuanceAmount(revnetId, stageId, beneficiary, count, caller)` | When an auto-issuance amount is stored for a beneficiary during deployment. |
|
|
109
|
+
|
|
110
|
+
### REVLoans
|
|
111
|
+
|
|
112
|
+
| Event | When It Fires |
|
|
113
|
+
|-------|---------------|
|
|
114
|
+
| `Borrow(loanId, revnetId, loan, source, borrowAmount, collateralCount, sourceFeeAmount, beneficiary, caller)` | When a loan is created by borrowing from a revnet via `borrowFrom`. |
|
|
115
|
+
| `Liquidate(loanId, revnetId, loan, caller)` | When a loan is liquidated after exceeding the 10-year liquidation duration via `liquidateExpiredLoansFrom`. |
|
|
116
|
+
| `ReallocateCollateral(loanId, revnetId, reallocatedLoanId, reallocatedLoan, removedCollateralCount, caller)` | When collateral is reallocated from one loan to a new loan via `reallocateCollateralFromLoan`. |
|
|
117
|
+
| `RepayLoan(loanId, revnetId, paidOffLoanId, loan, paidOffLoan, repayBorrowAmount, sourceFeeAmount, collateralCountToReturn, beneficiary, caller)` | When a loan is repaid via `repayLoan`. |
|
|
118
|
+
| `SetTokenUriResolver(resolver, caller)` | When the token URI resolver is changed via `setTokenUriResolver`. |
|
|
119
|
+
|
|
120
|
+
## Errors
|
|
121
|
+
|
|
122
|
+
### REVDeployer
|
|
123
|
+
|
|
124
|
+
| Error | When It Fires |
|
|
125
|
+
|-------|---------------|
|
|
126
|
+
| `REVDeployer_AutoIssuanceBeneficiaryZeroAddress()` | When an auto-issuance config has a zero-address beneficiary. |
|
|
127
|
+
| `REVDeployer_CashOutDelayNotFinished(cashOutDelay, blockTimestamp)` | When a cash out is attempted before the 30-day delay has elapsed. |
|
|
128
|
+
| `REVDeployer_CashOutsCantBeTurnedOffCompletely(cashOutTaxRate, maxCashOutTaxRate)` | When `cashOutTaxRate` equals `MAX_CASH_OUT_TAX_RATE` (10,000). Must be strictly less. |
|
|
129
|
+
| `REVDeployer_MustHaveSplits()` | When a stage with `splitPercent > 0` has no splits configured. |
|
|
130
|
+
| `REVDeployer_NothingToAutoIssue()` | When `autoIssueFor` is called but no tokens are available for auto-issuance. |
|
|
131
|
+
| `REVDeployer_NothingToBurn()` | When `burnHeldTokensOf` is called but the deployer holds no tokens. |
|
|
132
|
+
| `REVDeployer_RulesetDoesNotAllowDeployingSuckers()` | When `deploySuckersFor` is called but the current ruleset's `extraMetadata` bit 2 is not set. |
|
|
133
|
+
| `REVDeployer_StageNotStarted(stageId)` | When `autoIssueFor` is called for a stage that hasn't started yet. |
|
|
134
|
+
| `REVDeployer_StagesRequired()` | When `deployFor` is called with zero stage configurations. |
|
|
135
|
+
| `REVDeployer_StageTimesMustIncrease()` | When stage `startsAtOrAfter` values are not strictly increasing. |
|
|
136
|
+
| `REVDeployer_Unauthorized(revnetId, caller)` | When a non-split-operator calls a split-operator-only function. |
|
|
137
|
+
|
|
138
|
+
### REVLoans
|
|
139
|
+
|
|
140
|
+
| Error | When It Fires |
|
|
141
|
+
|-------|---------------|
|
|
142
|
+
| `REVLoans_CollateralExceedsLoan(collateralToReturn, loanCollateral)` | When trying to return more collateral than the loan holds. |
|
|
143
|
+
| `REVLoans_InvalidPrepaidFeePercent(prepaidFeePercent, min, max)` | When `prepaidFeePercent` is outside the allowed range (2.5%--50%). |
|
|
144
|
+
| `REVLoans_InvalidTerminal(terminal, revnetId)` | When the specified terminal is not registered for the revnet. |
|
|
145
|
+
| `REVLoans_LoanExpired(timeSinceLoanCreated, loanLiquidationDuration)` | When trying to repay or reallocate an expired loan. |
|
|
146
|
+
| `REVLoans_LoanIdOverflow()` | When the loan ID counter exceeds the per-revnet trillion-ID namespace. |
|
|
147
|
+
| `REVLoans_NewBorrowAmountGreaterThanLoanAmount(newBorrowAmount, loanAmount)` | When a reallocation would produce a reduced loan with a larger borrow amount than the original. |
|
|
148
|
+
| `REVLoans_NoMsgValueAllowed()` | When `msg.value > 0` on a non-native-token repayment. |
|
|
149
|
+
| `REVLoans_NotEnoughCollateral()` | When the caller does not have enough tokens for the requested collateral. |
|
|
150
|
+
| `REVLoans_NothingToRepay()` | When `repayLoan` is called with zero repay amount and zero collateral to return. |
|
|
151
|
+
| `REVLoans_OverMaxRepayBorrowAmount(maxRepayBorrowAmount, repayBorrowAmount)` | When the actual repay cost exceeds the caller's `maxRepayBorrowAmount`. |
|
|
152
|
+
| `REVLoans_OverflowAlert(value, limit)` | When a value would overflow `uint112` storage. |
|
|
153
|
+
| `REVLoans_PermitAllowanceNotEnough(allowanceAmount, requiredAmount)` | When the permit2 allowance is insufficient for the repayment. |
|
|
154
|
+
| `REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows(newBorrowAmount, loanAmount)` | When the collateral being transferred out would leave the original loan undercollateralized. |
|
|
155
|
+
| `REVLoans_SourceMismatch()` | When `reallocateCollateralFromLoan` specifies a source that doesn't match the existing loan's source. |
|
|
156
|
+
| `REVLoans_Unauthorized(caller, owner)` | When a non-owner tries to repay or reallocate someone else's loan. |
|
|
157
|
+
| `REVLoans_UnderMinBorrowAmount(minBorrowAmount, borrowAmount)` | When the actual borrow amount is less than the caller's `minBorrowAmount`. |
|
|
158
|
+
| `REVLoans_ZeroBorrowAmount()` | When a borrow or reallocation would result in zero borrowed funds. |
|
|
159
|
+
| `REVLoans_ZeroCollateralLoanIsInvalid()` | When a loan would end up with zero collateral. |
|
|
160
|
+
|
|
95
161
|
## Constants
|
|
96
162
|
|
|
97
163
|
### REVDeployer
|
|
@@ -102,6 +168,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
102
168
|
| `FEE` | 25 (of MAX_FEE=1000) | 2.5% cash-out fee paid to fee revnet |
|
|
103
169
|
| `DEFAULT_BUYBACK_POOL_FEE` | 10,000 | 1% Uniswap fee tier for default buyback pools |
|
|
104
170
|
| `DEFAULT_BUYBACK_TWAP_WINDOW` | 2 days | TWAP observation window for buyback price |
|
|
171
|
+
| `DEFAULT_BUYBACK_TICK_SPACING` | 200 | Tick spacing for default buyback V4 pools |
|
|
105
172
|
|
|
106
173
|
### REVLoans
|
|
107
174
|
|
|
@@ -117,24 +184,25 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
117
184
|
|
|
118
185
|
### REVDeployer
|
|
119
186
|
|
|
120
|
-
| Mapping | Type | Purpose |
|
|
121
|
-
|
|
122
|
-
| `amountToAutoIssue` | `revnetId => stageId => beneficiary => uint256` | Premint tokens per stage per beneficiary |
|
|
123
|
-
| `cashOutDelayOf` | `revnetId => uint256` | Timestamp when cash outs unlock (0 = no delay) |
|
|
124
|
-
| `hashedEncodedConfigurationOf` | `revnetId => bytes32` | Config hash for cross-chain sucker validation |
|
|
125
|
-
| `tiered721HookOf` | `revnetId => address` | Deployed 721 hook address (if any) |
|
|
126
|
-
| `_extraOperatorPermissions` | `revnetId => uint256[]` | Custom permissions for split operator |
|
|
187
|
+
| Mapping | Visibility | Type | Purpose |
|
|
188
|
+
|---------|-----------|------|---------|
|
|
189
|
+
| `amountToAutoIssue` | `public` | `revnetId => stageId => beneficiary => uint256` | Premint tokens per stage per beneficiary |
|
|
190
|
+
| `cashOutDelayOf` | `public` | `revnetId => uint256` | Timestamp when cash outs unlock (0 = no delay) |
|
|
191
|
+
| `hashedEncodedConfigurationOf` | `public` | `revnetId => bytes32` | Config hash for cross-chain sucker validation |
|
|
192
|
+
| `tiered721HookOf` | `public` | `revnetId => address` | Deployed 721 hook address (if any) |
|
|
193
|
+
| `_extraOperatorPermissions` | `internal` | `revnetId => uint256[]` | Custom permissions for split operator (no auto-getter) |
|
|
127
194
|
|
|
128
195
|
### REVLoans
|
|
129
196
|
|
|
130
|
-
| Mapping | Type | Purpose |
|
|
131
|
-
|
|
132
|
-
| `isLoanSourceOf` | `revnetId => terminal => token => bool` | Is this (terminal, token) pair used for loans? |
|
|
133
|
-
| `totalLoansBorrowedFor` | `revnetId => uint256` | Counter for loan numbering |
|
|
134
|
-
| `totalBorrowedFrom` | `revnetId => terminal => token => uint256` | Tracks debt per loan source |
|
|
135
|
-
| `totalCollateralOf` | `revnetId => uint256` | Sum of all burned collateral |
|
|
136
|
-
| `_loanOf` | `loanId => REVLoan` | Per-loan state |
|
|
137
|
-
| `_loanSourcesOf` | `revnetId => REVLoanSource[]` | Array of all loan sources used |
|
|
197
|
+
| Mapping | Visibility | Type | Purpose |
|
|
198
|
+
|---------|-----------|------|---------|
|
|
199
|
+
| `isLoanSourceOf` | `public` | `revnetId => terminal => token => bool` | Is this (terminal, token) pair used for loans? |
|
|
200
|
+
| `totalLoansBorrowedFor` | `public` | `revnetId => uint256` | Counter for loan numbering |
|
|
201
|
+
| `totalBorrowedFrom` | `public` | `revnetId => terminal => token => uint256` | Tracks debt per loan source |
|
|
202
|
+
| `totalCollateralOf` | `public` | `revnetId => uint256` | Sum of all burned collateral |
|
|
203
|
+
| `_loanOf` | `internal` | `loanId => REVLoan` | Per-loan state (use `loanOf(loanId)` view) |
|
|
204
|
+
| `_loanSourcesOf` | `internal` | `revnetId => REVLoanSource[]` | Array of all loan sources used (use `loanSourcesOf(revnetId)` view) |
|
|
205
|
+
| `tokenUriResolver` | `public` | `IJBTokenUriResolver` | Resolver for loan NFT token URIs |
|
|
138
206
|
|
|
139
207
|
## Gotchas
|
|
140
208
|
|
|
@@ -154,10 +222,10 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
154
222
|
14. **Fee revnet must have terminals.** Cash-out fees and loan protocol fees are paid to `FEE_REVNET_ID`. If that project has no terminal for the token, the fee silently fails (try-catch).
|
|
155
223
|
15. **Buyback hook is immutable per deployer.** `REVDeployer.BUYBACK_HOOK` is set at construction time. All revnets deployed by the same deployer share the same buyback hook.
|
|
156
224
|
16. **Cross-chain config matching.** `hashedEncodedConfigurationOf` covers economic parameters (baseCurrency, stages, auto-issuances) but NOT terminal configurations, accounting contexts, or sucker token mappings. Two deployments with identical hashes can have different terminal setups.
|
|
157
|
-
17. **Loan fee model
|
|
225
|
+
17. **Loan fee model has three layers.** See Constants table for exact values: REV protocol fee, terminal fee, and prepaid source fee (borrower-chosen, buys interest-free window). After the prepaid window, source fee accrues linearly over the remaining loan duration.
|
|
158
226
|
18. **Permit2 fallback.** `REVLoans` uses permit2 for ERC-20 transfers as a fallback when standard allowance is insufficient. Wrapped in try-catch.
|
|
159
227
|
19. **39.16% cash-out tax crossover.** Below ~39% cash-out tax, cashing out is more capital-efficient than borrowing. Above ~39%, loans become more efficient because they preserve upside while providing liquidity. Based on CryptoEconLab academic research. Design implication: revnets intended for active token trading should consider this threshold when setting `cashOutTaxRate`.
|
|
160
|
-
20. **REVDeployer always deploys a 721 hook** via `HOOK_DEPLOYER.deployHookFor` — even if `baseline721HookConfiguration` has empty tiers. This is correct by design: it lets the split operator add and sell NFTs later without migration. Non-revnet projects should follow the same pattern by using `JB721TiersHookProjectDeployer.launchProjectFor` (or `JBOmnichainDeployer.
|
|
228
|
+
20. **REVDeployer always deploys a 721 hook** via `HOOK_DEPLOYER.deployHookFor` — even if `baseline721HookConfiguration` has empty tiers. This is correct by design: it lets the split operator add and sell NFTs later without migration. Non-revnet projects should follow the same pattern by using `JB721TiersHookProjectDeployer.launchProjectFor` (or `JBOmnichainDeployer.launchProjectFor`) instead of bare `launchProjectFor`.
|
|
161
229
|
|
|
162
230
|
### NATIVE_TOKEN Accounting on Non-ETH Chains
|
|
163
231
|
|
|
@@ -181,6 +249,60 @@ JBAccountingContext({
|
|
|
181
249
|
})
|
|
182
250
|
```
|
|
183
251
|
|
|
252
|
+
## Reading Revnet State
|
|
253
|
+
|
|
254
|
+
Quick-reference for common read operations. All functions are `view`/`pure` and permissionless.
|
|
255
|
+
|
|
256
|
+
### Current Stage & Ruleset
|
|
257
|
+
|
|
258
|
+
| What | Call | Returns |
|
|
259
|
+
|------|------|---------|
|
|
260
|
+
| Current ruleset (stage) | `IJBController(CONTROLLER).currentRulesetOf(revnetId)` | `(JBRuleset, JBRulesetMetadata)` -- the active stage's parameters |
|
|
261
|
+
| All queued rulesets | `IJBController(CONTROLLER).allRulesetsOf(revnetId, startingId, size)` | `JBRulesetWithMetadata[]` -- paginated list of stages |
|
|
262
|
+
| Specific stage by ID | `IJBController(CONTROLLER).getRulesetOf(revnetId, stageId)` | `(JBRuleset, JBRulesetMetadata)` for that stage |
|
|
263
|
+
|
|
264
|
+
### Split Operator
|
|
265
|
+
|
|
266
|
+
| What | Call | Returns |
|
|
267
|
+
|------|------|---------|
|
|
268
|
+
| Check if address is split operator | `REVDeployer.isSplitOperatorOf(revnetId, addr)` | `bool` |
|
|
269
|
+
|
|
270
|
+
### Token Supply & Surplus
|
|
271
|
+
|
|
272
|
+
| What | Call | Returns |
|
|
273
|
+
|------|------|---------|
|
|
274
|
+
| Total supply (incl. pending reserved) | `IJBController(CONTROLLER).totalTokenSupplyWithReservedTokensOf(revnetId)` | `uint256` |
|
|
275
|
+
| Pending reserved tokens | `IJBController(CONTROLLER).pendingReservedTokenBalanceOf(revnetId)` | `uint256` |
|
|
276
|
+
| Current surplus (single terminal) | `IJBTerminalStore(STORE).currentSurplusOf(terminal, revnetId, configs, decimals, currency)` | `uint256` |
|
|
277
|
+
|
|
278
|
+
### Auto-Issuance
|
|
279
|
+
|
|
280
|
+
| What | Call | Returns |
|
|
281
|
+
|------|------|---------|
|
|
282
|
+
| Remaining auto-issuance for beneficiary | `REVDeployer.amountToAutoIssue(revnetId, stageId, beneficiary)` | `uint256` (0 if already claimed) |
|
|
283
|
+
|
|
284
|
+
### Loans
|
|
285
|
+
|
|
286
|
+
| What | Call | Returns |
|
|
287
|
+
|------|------|---------|
|
|
288
|
+
| Borrowable amount for collateral | `REVLoans.borrowableAmountFrom(revnetId, collateralCount, decimals, currency)` | `uint256` |
|
|
289
|
+
| Total borrowed (per source) | `REVLoans.totalBorrowedFrom(revnetId, terminal, token)` | `uint256` |
|
|
290
|
+
| Total collateral locked | `REVLoans.totalCollateralOf(revnetId)` | `uint256` |
|
|
291
|
+
| Loan details | `REVLoans.loanOf(loanId)` | `REVLoan` struct |
|
|
292
|
+
| All loan sources | `REVLoans.loanSourcesOf(revnetId)` | `REVLoanSource[]` |
|
|
293
|
+
| Loan count | `REVLoans.totalLoansBorrowedFor(revnetId)` | `uint256` |
|
|
294
|
+
| Source fee for repayment | `REVLoans.determineSourceFeeAmount(loan, amount)` | `uint256` |
|
|
295
|
+
| Revnet ID from loan ID | `REVLoans.revnetIdOfLoanWith(loanId)` | `uint256` (pure) |
|
|
296
|
+
| Loan NFT owner | `REVLoans.ownerOf(loanId)` | `address` (ERC-721) |
|
|
297
|
+
|
|
298
|
+
### Deployer Config
|
|
299
|
+
|
|
300
|
+
| What | Call | Returns |
|
|
301
|
+
|------|------|---------|
|
|
302
|
+
| Config hash (cross-chain matching) | `REVDeployer.hashedEncodedConfigurationOf(revnetId)` | `bytes32` |
|
|
303
|
+
| 721 hook address | `REVDeployer.tiered721HookOf(revnetId)` | `IJB721TiersHook` |
|
|
304
|
+
| Cash-out delay timestamp | `REVDeployer.cashOutDelayOf(revnetId)` | `uint256` (0 = no delay) |
|
|
305
|
+
|
|
184
306
|
## Example Integration
|
|
185
307
|
|
|
186
308
|
```solidity
|
|
@@ -258,9 +380,9 @@ loans.borrowFrom({
|
|
|
258
380
|
|
|
259
381
|
loans.repayLoan({
|
|
260
382
|
loanId: loanId,
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
beneficiary: msg.sender,
|
|
264
|
-
allowance:
|
|
383
|
+
maxRepayBorrowAmount: type(uint256).max, // Repay in full
|
|
384
|
+
collateralCountToReturn: loan.collateral, // Return all collateral
|
|
385
|
+
beneficiary: msg.sender, // Receive re-minted tokens
|
|
386
|
+
allowance: JBSingleAllowance({ ... }) // Optional permit2
|
|
265
387
|
});
|
|
266
388
|
```
|
package/STYLE_GUIDE.md
CHANGED
|
@@ -21,7 +21,7 @@ One contract/interface/struct/enum per file. Name the file after the type it con
|
|
|
21
21
|
|
|
22
22
|
```solidity
|
|
23
23
|
// Contracts — pin to exact version
|
|
24
|
-
pragma solidity 0.8.26;
|
|
24
|
+
pragma solidity ^0.8.26;
|
|
25
25
|
|
|
26
26
|
// Interfaces, structs, enums — caret for forward compatibility
|
|
27
27
|
pragma solidity ^0.8.0;
|
package/USER_JOURNEYS.md
CHANGED
|
@@ -74,6 +74,12 @@ Or: `REVDeployer.deployFor(revnetId=0, configuration, terminalConfigurations, su
|
|
|
74
74
|
5. Cash-out delay applied if first stage has already started
|
|
75
75
|
6. Same remaining steps as new deployment (ERC-20, buyback pools, suckers, 721 hook)
|
|
76
76
|
|
|
77
|
+
**Events:**
|
|
78
|
+
- `DeployRevnet(revnetId, configuration, terminalConfigurations, suckerDeploymentConfiguration, rulesetConfigurations, encodedConfigurationHash, caller)`
|
|
79
|
+
- `StoreAutoIssuanceAmount(revnetId, stageId, beneficiary, count, caller)` for each auto-issuance on this chain
|
|
80
|
+
- `DeploySuckers(...)` if suckers are deployed
|
|
81
|
+
- `SetCashOutDelay(revnetId, cashOutDelay, caller)` if applicable
|
|
82
|
+
|
|
77
83
|
**Edge cases:**
|
|
78
84
|
- This is a **one-way operation**. The project NFT is permanently locked in REVDeployer.
|
|
79
85
|
- `launchRulesetsFor` reverts if rulesets already exist. `setControllerOf` reverts if a controller is already set.
|
|
@@ -102,6 +108,10 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
102
108
|
- 721 hook processes tier purchases
|
|
103
109
|
- Buyback hook processes swap (if applicable)
|
|
104
110
|
|
|
111
|
+
**Preview**: Call `JBMultiTerminal.previewPayFor(revnetId, token, amount, beneficiary, metadata)` to simulate the full payment including REVDeployer's data hook effects (buyback routing, 721 tier splits, weight adjustment). Returns the expected token count and hook specifications. When the buyback hook is active, noop specs may carry routing diagnostics (TWAP tick, liquidity, pool ID) even when the protocol mint path wins.
|
|
112
|
+
|
|
113
|
+
**Events:** No revnet-specific events. The payment is handled by `JBMultiTerminal` and `JBController` (see nana-core-v6). REVDeployer's `beforePayRecordedWith` is a `view` function and emits nothing.
|
|
114
|
+
|
|
105
115
|
**Edge cases:**
|
|
106
116
|
- If the buyback hook determines a DEX swap is better, weight = 0 and the buyback hook spec receives the full project amount. The buyback hook buys tokens on the DEX and mints them to the payer.
|
|
107
117
|
- If `totalSplitAmount >= context.amount.value`, `projectAmount = 0`, weight = 0, and no tokens are minted by the terminal. All funds go to 721 tier splits.
|
|
@@ -133,12 +143,16 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
133
143
|
- Pays fee to fee revnet's terminal via `feeTerminal.pay`
|
|
134
144
|
- On failure: returns funds to the originating project via `addToBalanceOf`
|
|
135
145
|
|
|
146
|
+
**Preview**: Call `JBMultiTerminal.previewCashOutFrom(holder, revnetId, cashOutCount, tokenToReclaim, beneficiary, metadata)` to simulate the full cash out including REVDeployer's data hook effects (fee splitting, tax rate). Returns the expected reclaim amount and hook specifications. For a simpler estimate without data hook effects, use `JBTerminalStore.currentTotalReclaimableSurplusOf(revnetId, cashOutCount, decimals, currency)`.
|
|
147
|
+
|
|
148
|
+
**Events:** No revnet-specific events. Cash-out events are emitted by `JBMultiTerminal` and `JBController`. REVDeployer's `beforeCashOutRecordedWith` is a `view` function. The `afterCashOutRecordedWith` hook processes fees but does not emit events.
|
|
149
|
+
|
|
136
150
|
**Edge cases:**
|
|
137
151
|
- Suckers bypass both the cash-out fee AND the cash-out delay. The `_isSuckerOf` check is the only gate.
|
|
138
|
-
- `cashOutTaxRate == 0` means no tax and no revnet fee
|
|
152
|
+
- `cashOutTaxRate == 0` means no tax and no revnet fee. The terminal's 2.5% protocol fee only applies up to the `feeFreeSurplusOf` amount (round-trip prevention), not the full reclaim.
|
|
139
153
|
- Micro cash-outs (< 40 wei at 2.5%) round `feeCashOutCount` to 0, bypassing the fee. Gas cost far exceeds the bypassed fee.
|
|
140
154
|
- The fee is paid to `FEE_REVNET_ID`, not `REV_ID`. These may be different projects.
|
|
141
|
-
-
|
|
155
|
+
- Both the revnet fee and the terminal protocol fee apply. The revnet fee is computed first (at the data hook level, by splitting the cashout token count into fee and non-fee portions), then the terminal's 2.5% protocol fee is applied to all outbound fund amounts (both the beneficiary's reclaim and the hook-forwarded fee amount).
|
|
142
156
|
|
|
143
157
|
---
|
|
144
158
|
|
|
@@ -185,7 +199,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
185
199
|
- Pays REV fee (1%) to `REV_ID` via `feeTerminal.pay` (try-catch; zeroed on failure)
|
|
186
200
|
- Transfers remaining: `netAmountPaidOut - revFeeAmount - sourceFeeAmount` to beneficiary
|
|
187
201
|
- `_addCollateralTo`: increments `totalCollateralOf`, burns collateral via `CONTROLLER.burnTokensOf`
|
|
188
|
-
- Pays source fee to revnet via `terminal.pay` (
|
|
202
|
+
- Pays source fee to revnet via `terminal.pay` (try-catch — on failure, returns fee amount to beneficiary)
|
|
189
203
|
8. **Mint loan ERC-721** to `_msgSender()`
|
|
190
204
|
|
|
191
205
|
**Events:** `Borrow(loanId, revnetId, loan, source, borrowAmount, collateralCount, sourceFeeAmount, beneficiary, caller)`
|
|
@@ -194,7 +208,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
194
208
|
- Revnets always deploy an ERC-20 at creation, so collateral is always ERC-20 tokens (never credits).
|
|
195
209
|
- The `minBorrowAmount` check is against the raw bonding curve output, BEFORE fees are deducted. The actual amount received is less.
|
|
196
210
|
- `prepaidDuration` at minimum (25): `25 * 3650 days / 500 = 182.5 days`. At maximum (500): `500 * 3650 days / 500 = 3650 days`.
|
|
197
|
-
-
|
|
211
|
+
- Both the REV fee payment and the source fee payment failures are non-fatal. If either `feeTerminal.pay` or `source.terminal.pay` reverts, the fee amount is transferred to the beneficiary instead.
|
|
198
212
|
- Loan NFT is minted to `_msgSender()`, not `beneficiary`. The caller owns the loan; the beneficiary receives the funds.
|
|
199
213
|
|
|
200
214
|
---
|
|
@@ -224,7 +238,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
224
238
|
4. **Nothing-to-do check:** Reverts if `repayBorrowAmount == 0 && collateralCountToReturn == 0`
|
|
225
239
|
5. **Source fee calculation:**
|
|
226
240
|
- If within prepaid window (`timeSinceCreated <= prepaidDuration`): fee = 0
|
|
227
|
-
- If expired (`timeSinceCreated
|
|
241
|
+
- If expired (`timeSinceCreated > LOAN_LIQUIDATION_DURATION`): revert `REVLoans_LoanExpired`
|
|
228
242
|
- Otherwise: linear fee based on time elapsed beyond prepaid window
|
|
229
243
|
- `repayBorrowAmount += sourceFeeAmount` (fee added to repayment)
|
|
230
244
|
6. **Accept funds:** `_acceptFundsFor` handles native token (uses `msg.value`) or ERC-20 (with optional permit2)
|
|
@@ -378,7 +392,7 @@ This is a standard Juicebox payment, but REVDeployer intervenes as the data hook
|
|
|
378
392
|
2. Reverts with `REVDeployer_NothingToBurn` if balance is 0
|
|
379
393
|
3. Burns all held tokens via `CONTROLLER.burnTokensOf`
|
|
380
394
|
|
|
381
|
-
**Events:** `BurnHeldTokens(revnetId,
|
|
395
|
+
**Events:** `BurnHeldTokens(revnetId, count, caller)`
|
|
382
396
|
|
|
383
397
|
---
|
|
384
398
|
|
package/foundry.toml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rev-net/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
"node": ">=20.0.0"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"postinstall": "find node_modules -name '*.sol' -type f | xargs grep -l 'pragma solidity 0.8.23;' 2>/dev/null | xargs sed -i '' 's/pragma solidity 0.8.23;/pragma solidity 0.8.26;/g' 2>/dev/null || true",
|
|
14
13
|
"test": "forge test",
|
|
15
14
|
"coverage": "forge coverage --match-path \"./src/*.sol\" --report lcov --report summary",
|
|
16
15
|
"deploy:mainnets": "source ./.env && export START_TIME=$(date +%s) && npx sphinx propose ./script/Deploy.s.sol --networks mainnets",
|
|
@@ -20,14 +19,14 @@
|
|
|
20
19
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'revnet-core-v6'"
|
|
21
20
|
},
|
|
22
21
|
"dependencies": {
|
|
23
|
-
"@bananapus/721-hook-v6": "^0.0.
|
|
24
|
-
"@bananapus/buyback-hook-v6": "^0.0.
|
|
25
|
-
"@bananapus/core-v6": "^0.0.
|
|
26
|
-
"@bananapus/ownable-v6": "^0.0.
|
|
27
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
28
|
-
"@bananapus/router-terminal-v6": "^0.0.
|
|
29
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
30
|
-
"@croptop/core-v6": "^0.0.
|
|
22
|
+
"@bananapus/721-hook-v6": "^0.0.20",
|
|
23
|
+
"@bananapus/buyback-hook-v6": "^0.0.19",
|
|
24
|
+
"@bananapus/core-v6": "^0.0.26",
|
|
25
|
+
"@bananapus/ownable-v6": "^0.0.13",
|
|
26
|
+
"@bananapus/permission-ids-v6": "^0.0.12",
|
|
27
|
+
"@bananapus/router-terminal-v6": "^0.0.19",
|
|
28
|
+
"@bananapus/suckers-v6": "^0.0.16",
|
|
29
|
+
"@croptop/core-v6": "^0.0.21",
|
|
31
30
|
"@openzeppelin/contracts": "^5.6.1",
|
|
32
31
|
"@uniswap/v4-core": "^1.0.2",
|
|
33
32
|
"@uniswap/v4-periphery": "^1.0.3"
|
package/script/Deploy.s.sol
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.26;
|
|
2
|
+
pragma solidity ^0.8.26;
|
|
3
3
|
|
|
4
4
|
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
5
|
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
@@ -342,20 +342,86 @@ contract DeployScript is Script, Sphinx {
|
|
|
342
342
|
}
|
|
343
343
|
|
|
344
344
|
function deploy() public sphinx {
|
|
345
|
-
//
|
|
345
|
+
// Check if singletons are already deployed before creating a new fee project.
|
|
346
|
+
// This prevents creating orphan projects on script restarts.
|
|
346
347
|
// forge-lint: disable-next-line(mixed-case-variable)
|
|
347
|
-
uint256 FEE_PROJECT_ID
|
|
348
|
+
uint256 FEE_PROJECT_ID;
|
|
349
|
+
|
|
350
|
+
// Predict the REVLoans address for an arbitrary fee project ID to check if it exists.
|
|
351
|
+
// We can't predict without a fee project ID, so we first check the REVDeployer which also stores it.
|
|
352
|
+
// Try a two-step approach: compute addresses assuming singletons exist, then check.
|
|
353
|
+
|
|
354
|
+
// First, check if REVLoans is already deployed by trying with project ID = 0 (placeholder).
|
|
355
|
+
// We need to iterate: if either singleton exists, extract the fee project ID from it.
|
|
356
|
+
// Since both encode FEE_PROJECT_ID, we check if any code exists at the predicted address
|
|
357
|
+
// for sequential project IDs starting from 1.
|
|
358
|
+
|
|
359
|
+
// A simpler approach: predict the REVDeployer address for each possible fee project ID
|
|
360
|
+
// until we find one that's deployed, or give up and create a new one.
|
|
361
|
+
// In practice, the fee project is always one of the first few projects created.
|
|
362
|
+
|
|
363
|
+
// Check the next project ID that would be created — if singletons were deployed with a previous ID,
|
|
364
|
+
// that ID must be less than the current count.
|
|
365
|
+
uint256 _nextProjectId = core.projects.count() + 1;
|
|
366
|
+
|
|
367
|
+
// Try to find an existing deployment by checking all project IDs that have already been created.
|
|
368
|
+
// Whether previously deployed singletons were found.
|
|
369
|
+
bool _singletonsExist;
|
|
370
|
+
// The address of the previously deployed REVLoans, if found.
|
|
371
|
+
address _existingRevloansAddr;
|
|
372
|
+
// The address of the previously deployed REVDeployer, if found.
|
|
373
|
+
address _existingDeployerAddr;
|
|
374
|
+
|
|
375
|
+
for (uint256 _candidateId = 1; _candidateId < _nextProjectId; _candidateId++) {
|
|
376
|
+
// Predict REVLoans address for this candidate fee project ID.
|
|
377
|
+
(address _candidateRevloansAddr, bool _candidateRevloansDeployed) = _isDeployed({
|
|
378
|
+
salt: REVLOANS_SALT,
|
|
379
|
+
creationCode: type(REVLoans).creationCode,
|
|
380
|
+
arguments: abi.encode(
|
|
381
|
+
core.controller, core.projects, _candidateId, LOANS_OWNER, PERMIT2, TRUSTED_FORWARDER
|
|
382
|
+
)
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
if (_candidateRevloansDeployed) {
|
|
386
|
+
// Verify the fee project ID matches by reading the immutable.
|
|
387
|
+
if (REVLoans(payable(_candidateRevloansAddr)).REV_ID() == _candidateId) {
|
|
388
|
+
// Record the fee project ID from the existing deployment.
|
|
389
|
+
FEE_PROJECT_ID = _candidateId;
|
|
390
|
+
// Record the existing REVLoans address.
|
|
391
|
+
_existingRevloansAddr = _candidateRevloansAddr;
|
|
392
|
+
// Flag that singletons were found.
|
|
393
|
+
_singletonsExist = true;
|
|
394
|
+
|
|
395
|
+
// Also predict and verify the deployer.
|
|
396
|
+
(_existingDeployerAddr,) = _isDeployed({
|
|
397
|
+
salt: DEPLOYER_SALT,
|
|
398
|
+
creationCode: type(REVDeployer).creationCode,
|
|
399
|
+
arguments: abi.encode(
|
|
400
|
+
core.controller,
|
|
401
|
+
suckers.registry,
|
|
402
|
+
_candidateId,
|
|
403
|
+
hook.hook_deployer,
|
|
404
|
+
croptop.publisher,
|
|
405
|
+
IJBBuybackHookRegistry(address(buybackHook.registry)),
|
|
406
|
+
_candidateRevloansAddr,
|
|
407
|
+
TRUSTED_FORWARDER
|
|
408
|
+
)
|
|
409
|
+
});
|
|
410
|
+
// Stop searching — we found the deployed singletons.
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Only create a new fee project if no singletons were found.
|
|
417
|
+
if (!_singletonsExist) {
|
|
418
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
419
|
+
FEE_PROJECT_ID = core.projects.createFor(safeAddress());
|
|
420
|
+
}
|
|
348
421
|
|
|
349
422
|
// Deploy REVLoans first — it only depends on the controller.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
creationCode: type(REVLoans).creationCode,
|
|
353
|
-
arguments: abi.encode(
|
|
354
|
-
core.controller, core.projects, FEE_PROJECT_ID, LOANS_OWNER, PERMIT2, TRUSTED_FORWARDER
|
|
355
|
-
)
|
|
356
|
-
});
|
|
357
|
-
REVLoans revloans = _revloansIsDeployed
|
|
358
|
-
? REVLoans(payable(_revloansAddr))
|
|
423
|
+
REVLoans revloans = _singletonsExist
|
|
424
|
+
? REVLoans(payable(_existingRevloansAddr))
|
|
359
425
|
: new REVLoans{salt: REVLOANS_SALT}({
|
|
360
426
|
controller: core.controller,
|
|
361
427
|
projects: core.projects,
|
|
@@ -410,6 +476,8 @@ contract DeployScript is Script, Sphinx {
|
|
|
410
476
|
});
|
|
411
477
|
}
|
|
412
478
|
|
|
479
|
+
/// @notice Check whether a contract has already been deployed at its deterministic address.
|
|
480
|
+
/// @dev Uses the Arachnid deterministic-deployment-proxy address to predict the CREATE2 address.
|
|
413
481
|
function _isDeployed(
|
|
414
482
|
bytes32 salt,
|
|
415
483
|
bytes memory creationCode,
|
|
@@ -419,10 +487,6 @@ contract DeployScript is Script, Sphinx {
|
|
|
419
487
|
view
|
|
420
488
|
returns (address deployedTo, bool isDeployed)
|
|
421
489
|
{
|
|
422
|
-
// Note: This uses the Arachnid deterministic-deployment-proxy address, which differs from
|
|
423
|
-
// the Sphinx deployer used at runtime. As a result, the predicted address won't match and
|
|
424
|
-
// _isDeployed will always return false when deploying via Sphinx. This is benign — it just
|
|
425
|
-
// means contracts are always freshly deployed rather than skipped.
|
|
426
490
|
address _deployedTo = vm.computeCreate2Address({
|
|
427
491
|
salt: salt,
|
|
428
492
|
initCodeHash: keccak256(abi.encodePacked(creationCode, arguments)),
|