@rev-net/core-v6 0.0.76 → 0.0.77
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -1
- package/references/operations.md +8 -7
- package/references/runtime.md +2 -2
- package/script/Deploy.s.sol +13 -26
- package/src/REVDeployer.sol +33 -34
- package/src/REVLoans.sol +20 -15
- package/src/REVOwner.sol +36 -36
- package/src/interfaces/IREVDeployer.sol +9 -9
- package/src/interfaces/IREVOwner.sol +3 -3
- package/src/structs/REVCroptopAllowedPost.sol +2 -1
- package/src/structs/REVDeploy721TiersHookConfig.sol +6 -9
- package/src/structs/REVStageConfig.sol +3 -3
- package/src/structs/REVSuckerDeploymentConfig.sol +2 -2
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ Use this repo when the product is a treasury-backed network with encoded stage t
|
|
|
31
31
|
| `REVDeployer` | Launches and configures Revnets, stages, operators, and optional auxiliary features. |
|
|
32
32
|
| `REVOwner` | Runtime data-hook and cash-out-hook surface used by active Revnets. |
|
|
33
33
|
| `REVLoans` | Loan surface that lets users borrow against Revnet tokens with burned collateral and NFT loan positions. |
|
|
34
|
+
|
|
34
35
|
## Mental Model
|
|
35
36
|
|
|
36
37
|
Read the package in two halves:
|
|
@@ -59,6 +60,7 @@ Most mistakes come from assuming a deploy-time parameter can be changed later or
|
|
|
59
60
|
- deployment-time configuration and operator envelope live in `REVDeployer`
|
|
60
61
|
- runtime pay and cash-out behavior live in `REVOwner`
|
|
61
62
|
- loan positions and loan-specific state live in `REVLoans`
|
|
63
|
+
|
|
62
64
|
## High-Signal Tests
|
|
63
65
|
|
|
64
66
|
1. `test/REVLifecycle.t.sol`
|
package/package.json
CHANGED
package/references/operations.md
CHANGED
|
@@ -69,7 +69,7 @@ Use this file when you need revnet-specific risks, state reads, constants, or ex
|
|
|
69
69
|
| `REVLoans_InvalidPrepaidFeePercent(prepaidFeePercent, min, max)` | When `prepaidFeePercent` is outside the allowed range (2.5%--50%). |
|
|
70
70
|
| `REVLoans_InvalidTerminal(terminal, revnetId)` | When the specified terminal is not registered for the revnet. |
|
|
71
71
|
| `REVLoans_LoanExpired(timeSinceLoanCreated, loanLiquidationDuration)` | When trying to repay or reallocate an expired loan. |
|
|
72
|
-
| `REVLoans_LoanIdOverflow()` | When the loan ID counter exceeds the per-revnet
|
|
72
|
+
| `REVLoans_LoanIdOverflow()` | When the loan ID counter exceeds the per-revnet 1e18-ID namespace. |
|
|
73
73
|
| `REVLoans_NewBorrowAmountGreaterThanLoanAmount(newBorrowAmount, loanAmount)` | When a reallocation would produce a reduced loan with a larger borrow amount than the original. |
|
|
74
74
|
| `REVLoans_NoMsgValueAllowed()` | When `msg.value > 0` on a non-native-token repayment. |
|
|
75
75
|
| `REVLoans_NotEnoughCollateral()` | When the caller does not have enough tokens for the requested collateral. |
|
|
@@ -104,7 +104,8 @@ Use this file when you need revnet-specific risks, state reads, constants, or ex
|
|
|
104
104
|
| `MIN_PREPAID_FEE_PERCENT` | 25 (2.5%) | Minimum upfront fee borrowers must pay |
|
|
105
105
|
| `MAX_PREPAID_FEE_PERCENT` | 500 (50%) | Maximum upfront fee |
|
|
106
106
|
| `REV_PREPAID_FEE_PERCENT` | 10 (1%) | Protocol-level fee to $REV revnet |
|
|
107
|
-
| `
|
|
107
|
+
| `_LOAN_ID_NAMESPACE_SIZE` | 1,000,000,000,000,000,000 | Loan ID generator base: `revnetId * namespaceSize + loanNumber` |
|
|
108
|
+
| `_MAX_LOAN_NUMBER` | 999,999,999,999,999,999 | Largest loan number that stays in the current revnet's ID namespace |
|
|
108
109
|
|
|
109
110
|
## Storage
|
|
110
111
|
|
|
@@ -144,19 +145,19 @@ Use this file when you need revnet-specific risks, state reads, constants, or ex
|
|
|
144
145
|
1. **Revnets are permanently ownerless.** `REVOwner` holds the project NFT for every Revnet and exposes no function to release it. Stage parameters cannot be changed after deployment.
|
|
145
146
|
2. **Collateral is burned, not held.** Unlike traditional lending, collateral tokens are destroyed at borrow time and re-minted on repay. If a loan liquidates after 10 years, the collateral is permanently lost.
|
|
146
147
|
3. **100% LTV by design.** Borrowable amount equals the pro-rata cash-out value. No safety margin unless the stage has `cashOutTaxRate > 0`. A tax of 20% creates ~20% effective collateral buffer.
|
|
147
|
-
4. **Loan ID encoding.** `loanId = revnetId *
|
|
148
|
+
4. **Loan ID encoding.** `loanId = revnetId * 1_000_000_000_000_000_000 + loanNumber`. Each revnet supports loan numbers up to `999_999_999_999_999_999`; `1_000_000_000_000_000_000` is the next revnet's namespace. Use `revnetIdOfLoanWith(loanId)` to decode.
|
|
148
149
|
5. **uint112 truncation risk.** `REVLoan.amount` and `REVLoan.collateral` are `uint112`. Values above ~5.19e33 truncate silently.
|
|
149
150
|
6. **Auto-issuance stage IDs.** Computed as `block.timestamp + i` during deployment. These match the Juicebox ruleset IDs because `JBRulesets` assigns IDs the same way (`latestId >= block.timestamp ? latestId + 1 : block.timestamp`), producing identical sequential IDs when all stages are queued in a single `deployFor()` call.
|
|
150
151
|
7. **Cash-out fee stacking.** Non-zero-tax ordinary cash-outs incur both the Juicebox terminal fee (2.5%) and the revnet cash-out fee (2.5% to fee revnet). These compound. The 2.5% revnet fee is deducted from the TOKEN AMOUNT being cashed out, not from the reclaim value. 2.5% of the tokens are redirected to the fee revnet, which then redeems them at the bonding curve independently. The net reclaim to the caller is based on 97.5% of the tokens, not 97.5% of the computed ETH value. Zero-tax ordinary cash-outs route through the buyback hook without adding the revnet fee hook, matching current code behavior. This is by design.
|
|
151
152
|
8. **30-day cash-out delay.** Applied when deploying an existing revnet to a new chain where the first stage has already started. Prevents cross-chain liquidity arbitrage. Enforced in both `beforeCashOutRecordedWith` (direct cash outs) and `REVLoans.borrowFrom` / `borrowableAmountFrom` (loans). The delay is stored on REVOwner (`cashOutDelayOf(revnetId)`) and populated by REVDeployer during deployment via the bundled `initializeRevnet()` call. REVLoans imports IREVOwner (not IREVDeployer) to read it.
|
|
152
153
|
9. **`cashOutTaxRate` cannot be MAX.** Must be strictly less than `MAX_CASH_OUT_TAX_RATE` (10,000). Revnets cannot fully disable cash outs.
|
|
153
154
|
10. **Split operator is singular.** Only ONE address can be operator at a time. The operator can replace itself via `setOperatorOf` but cannot delegate or multi-sig.
|
|
154
|
-
11. **NATIVE_TOKEN on non-ETH chains.** `JBConstants.NATIVE_TOKEN` on Celo means CELO, on Polygon means MATIC -- not ETH. Use ERC-20 WETH instead. The config matching hash does
|
|
155
|
+
11. **NATIVE_TOKEN on non-ETH chains.** `JBConstants.NATIVE_TOKEN` on Celo means CELO, on Polygon means MATIC -- not ETH. Use ERC-20 WETH instead. The config matching hash does not catch terminal or accounting-context differences.
|
|
155
156
|
12. **Loan source array is unbounded.** `_loanSourceTokensOf[revnetId]` grows without limit, bounded in practice by the token accounting contexts accepted by the canonical `MULTI_TERMINAL`.
|
|
156
157
|
13. **Flash-loan surplus exposure.** `borrowableAmountFrom` reads live surplus. A flash loan can temporarily inflate the treasury to borrow more than the sustained value supports.
|
|
157
158
|
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).
|
|
158
159
|
15. **Buyback hook is immutable per deployer.** `BUYBACK_HOOK` is set at construction time on both REVDeployer and REVOwner. All revnets deployed by the same deployer share the same buyback hook.
|
|
159
|
-
16. **Cross-chain config matching.** `hashedEncodedConfigurationOf` covers
|
|
160
|
+
16. **Cross-chain config matching.** `hashedEncodedConfigurationOf` covers deploy-time revnet identity and economics: base currency, scope flag, name, ticker, salt, stage timing, issuance, cash-out taxes, extra metadata, and auto-issuances. It does not cover terminal configurations, accounting contexts, sucker token mappings, or mutable split recipients. Two deployments with identical hashes can still differ in those external surfaces.
|
|
160
161
|
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.
|
|
161
162
|
18. **Permit2 fallback.** `REVLoans` uses permit2 for ERC-20 transfers as a fallback when standard allowance is insufficient. Wrapped in try-catch.
|
|
162
163
|
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`.
|
|
@@ -214,7 +215,7 @@ Quick-reference for common read operations. All functions are `view`/`pure` and
|
|
|
214
215
|
|
|
215
216
|
| What | Call | Returns |
|
|
216
217
|
|------|------|---------|
|
|
217
|
-
| Remaining auto-issuance for beneficiary | `
|
|
218
|
+
| Remaining auto-issuance for beneficiary | `REVOwner.amountToAutoIssue(revnetId, stageId, beneficiary)` | `uint256` (0 if already claimed) |
|
|
218
219
|
|
|
219
220
|
### Loans
|
|
220
221
|
|
|
@@ -315,7 +316,7 @@ loans.borrowFrom({
|
|
|
315
316
|
beneficiary: payable(msg.sender), // Receive new loan proceeds
|
|
316
317
|
prepaidFeePercent: 25 // 2.5% prepaid fee on new loan
|
|
317
318
|
});
|
|
318
|
-
// Result: original loan
|
|
319
|
+
// Result: original loan has 500 fewer collateral tokens (reallocatedLoanId),
|
|
319
320
|
// new loan has 700 tokens of collateral (newLoanId).
|
|
320
321
|
|
|
321
322
|
// --- Repay a loan ---
|
package/references/runtime.md
CHANGED
|
@@ -11,7 +11,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
11
11
|
| Contract | Role |
|
|
12
12
|
|----------|------|
|
|
13
13
|
| `REVDeployer` | Deploys revnets. Configures stages, splits, auto-issuance amounts, buyback hooks, suckers, operators, and stores `hashedEncodedConfigurationOf`. Exposes `OWNER()` view returning the REVOwner address. Hands the JBProjects NFT to REVOwner mid-deploy and finishes by bundling every per-revnet runtime write into a single `REVOwner.initializeRevnet(revnetId, init)` call (cash-out delay, tiered 721 hook, auto-issuance allocations, extra operator permissions, the initial operator, and any integration grants). |
|
|
14
|
-
| `REVOwner` | Project NFT owner
|
|
14
|
+
| `REVOwner` | Project NFT owner and runtime hook for every revnet. Implements `IJBRulesetDataHook` + `IJBCashOutHook` + `IERC721Receiver`. Set as the `dataHook` in each revnet's ruleset metadata. Holds the JBProjects NFT (project owner of record). Manages operator permissions (grants `DEPLOY_SUCKERS` + `MAP_SUCKER_TOKEN` to REVDeployer in `setDeployer` so the deployer can continue driving sucker setup). Exposes `initializeRevnet` (deployer-only single-call setup), `autoIssueFor`, `burnHeldTokensOf`, `setOperatorOf`, `isOperatorOf`. Handles pay hooks, cash-out hooks, mint permissions, and sucker verification. Stores `cashOutDelayOf`, `tiered721HookOf`, `amountToAutoIssue`, and extra-operator-permission state. |
|
|
15
15
|
| `REVLoans` | Issues token-collateralized loans from revnet treasuries. Each loan is an ERC-721 NFT. Burns collateral on borrow, re-mints on repay. Charges tiered fees (REV protocol fee + source fee + prepaid fee). |
|
|
16
16
|
|
|
17
17
|
## Key Functions
|
|
@@ -66,7 +66,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
66
66
|
| `REVLoans.determineSourceFeeAmount(loan, amount)` | Calculate the time-proportional source fee for a loan repayment. Zero during prepaid window, linear accrual after. |
|
|
67
67
|
| `REVLoans.loanOf(loanId)` | Returns the full `REVLoan` struct for a loan. |
|
|
68
68
|
| `REVLoans.loanSourceTokensOf(revnetId)` | Returns all token sources used for loans by a revnet. Loans always source funds from the canonical `MULTI_TERMINAL`. |
|
|
69
|
-
| `REVLoans.revnetIdOfLoanWith(loanId)` | Decode the revnet ID from a loan ID (`loanId /
|
|
69
|
+
| `REVLoans.revnetIdOfLoanWith(loanId)` | Decode the revnet ID from a loan ID (`loanId / 1_000_000_000_000_000_000`). |
|
|
70
70
|
## Integration Points
|
|
71
71
|
|
|
72
72
|
| Dependency | Import | Used For |
|
package/script/Deploy.s.sol
CHANGED
|
@@ -54,17 +54,17 @@ struct FeeProjectConfig {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
contract DeployScript is Script, Sphinx {
|
|
57
|
-
/// @notice
|
|
57
|
+
/// @notice Tracks the deployment of the buyback hook.
|
|
58
58
|
BuybackDeployment buybackHook;
|
|
59
|
-
/// @notice
|
|
59
|
+
/// @notice Tracks the deployment of the core contracts for the chain we are deploying to.
|
|
60
60
|
CoreDeployment core;
|
|
61
|
-
/// @notice
|
|
61
|
+
/// @notice Tracks the deployment of the Croptop contracts for the chain we are deploying to.
|
|
62
62
|
CroptopDeployment croptop;
|
|
63
|
-
/// @notice
|
|
63
|
+
/// @notice Tracks the deployment of the 721 hook contracts for the chain we are deploying to.
|
|
64
64
|
Hook721Deployment hook;
|
|
65
|
-
/// @notice
|
|
65
|
+
/// @notice Tracks the deployment of the sucker contracts for the chain we are deploying to.
|
|
66
66
|
SuckerDeployment suckers;
|
|
67
|
-
/// @notice
|
|
67
|
+
/// @notice Tracks the deployment of the router terminal registry package.
|
|
68
68
|
RouterTerminalDeployment routerTerminal;
|
|
69
69
|
uint32 private constant _PREMINT_CHAIN_ID = 1;
|
|
70
70
|
string private constant _NAME = "Revnet";
|
|
@@ -102,7 +102,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
102
102
|
// Get the loans owner address.
|
|
103
103
|
loansOwner = safeAddress();
|
|
104
104
|
|
|
105
|
-
// Get the deployment addresses for
|
|
105
|
+
// Get the core deployment addresses for this chain.
|
|
106
106
|
// We want to do this outside of the `sphinx` modifier.
|
|
107
107
|
core = CoreDeploymentLib.getDeployment(
|
|
108
108
|
vm.envOr({
|
|
@@ -116,13 +116,13 @@ contract DeployScript is Script, Sphinx {
|
|
|
116
116
|
defaultValue: string("node_modules/@bananapus/suckers-v6/deployments/")
|
|
117
117
|
})
|
|
118
118
|
);
|
|
119
|
-
// Get the deployment addresses for
|
|
119
|
+
// Get the Croptop deployment addresses for this chain.
|
|
120
120
|
croptop = CroptopDeploymentLib.getDeployment(
|
|
121
121
|
vm.envOr({
|
|
122
122
|
name: "CROPTOP_CORE_DEPLOYMENT_PATH", defaultValue: string("node_modules/@croptop/core-v6/deployments/")
|
|
123
123
|
})
|
|
124
124
|
);
|
|
125
|
-
// Get the deployment addresses for
|
|
125
|
+
// Get the buyback hook deployment addresses for this chain.
|
|
126
126
|
hook = Hook721DeploymentLib.getDeployment(
|
|
127
127
|
vm.envOr({
|
|
128
128
|
name: "NANA_721_DEPLOYMENT_PATH",
|
|
@@ -223,7 +223,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
223
223
|
autoIssuances: new REVAutoIssuance[](0),
|
|
224
224
|
splitPercent: 3800, // 38%
|
|
225
225
|
splits: splits,
|
|
226
|
-
initialIssuance: 0, // no more
|
|
226
|
+
initialIssuance: 0, // no more issuance.
|
|
227
227
|
issuanceCutFrequency: 0,
|
|
228
228
|
issuanceCutPercent: 0,
|
|
229
229
|
cashOutTaxRate: 1000, // 0.1
|
|
@@ -316,24 +316,11 @@ contract DeployScript is Script, Sphinx {
|
|
|
316
316
|
}
|
|
317
317
|
|
|
318
318
|
function deploy() public sphinx {
|
|
319
|
-
// Check
|
|
320
|
-
// This prevents
|
|
319
|
+
// Check whether the deterministic singletons already exist before creating a new fee project.
|
|
320
|
+
// This prevents orphan projects on script restarts.
|
|
321
321
|
uint256 feeProjectId;
|
|
322
322
|
|
|
323
|
-
//
|
|
324
|
-
// We can't predict without a fee project ID, so we first check the REVDeployer which also stores it.
|
|
325
|
-
// Try a two-step approach: compute addresses assuming singletons exist, then check.
|
|
326
|
-
|
|
327
|
-
// First, check if REVLoans is already deployed by trying with project ID = 0 (placeholder).
|
|
328
|
-
// We need to iterate: if either singleton exists, extract the fee project ID from it.
|
|
329
|
-
// Since both encode feeProjectId, we check if any code exists at the predicted address
|
|
330
|
-
// for sequential project IDs starting from 1.
|
|
331
|
-
|
|
332
|
-
// A simpler approach: predict the REVDeployer address for each possible fee project ID
|
|
333
|
-
// until we find one that's deployed, or give up and create a new one.
|
|
334
|
-
// In practice, the fee project is always one of the first few projects created.
|
|
335
|
-
|
|
336
|
-
// Check the next project ID that would be created — if singletons were deployed with a previous ID,
|
|
323
|
+
// Check the next project ID that would be created. If singletons were deployed with a previous ID,
|
|
337
324
|
// that ID must be less than the current count.
|
|
338
325
|
uint256 _nextProjectId = core.projects.count() + 1;
|
|
339
326
|
|
package/src/REVDeployer.sol
CHANGED
|
@@ -47,13 +47,12 @@ import {REVOwnerRevnetInit} from "./structs/REVOwnerRevnetInit.sol";
|
|
|
47
47
|
import {REVStageConfig} from "./structs/REVStageConfig.sol";
|
|
48
48
|
import {REVSuckerDeploymentConfig} from "./structs/REVSuckerDeploymentConfig.sol";
|
|
49
49
|
|
|
50
|
-
/// @notice Deploys and configures Revnets
|
|
51
|
-
///
|
|
52
|
-
///
|
|
53
|
-
///
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
/// address can modify the revnet's rules.
|
|
50
|
+
/// @notice Deploys and configures Revnets: autonomous Juicebox projects with immutable staged tokenomics.
|
|
51
|
+
/// @dev Each revnet progresses through stages that define issuance rate, decay schedule, cash-out tax, split
|
|
52
|
+
/// allocations, and auto-issuances. The deployer translates these stage configurations into Juicebox rulesets, sets up
|
|
53
|
+
/// a buyback hook for secondary market routing, deploys a tiered 721 hook, optionally configures Croptop posting, and
|
|
54
|
+
/// can deploy cross-chain suckers. Once deployed, the project NFT is held by this contract — no single address can
|
|
55
|
+
/// modify the revnet's rules.
|
|
57
56
|
/// @dev Revnets are unowned Juicebox projects which operate autonomously after deployment. Runtime data hook logic
|
|
58
57
|
/// (pay/cash-out callbacks) is handled by the separate `REVOwner` contract to stay within EIP-170 size limits.
|
|
59
58
|
contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
@@ -106,21 +105,21 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
106
105
|
/// @notice The directory of terminals and controllers for Juicebox projects (and revnets).
|
|
107
106
|
IJBDirectory public immutable override DIRECTORY;
|
|
108
107
|
|
|
109
|
-
/// @notice The Juicebox project ID of the revnet that receives cash
|
|
108
|
+
/// @notice The Juicebox project ID of the revnet that receives cash-out fees.
|
|
110
109
|
uint256 public immutable override FEE_REVNET_ID;
|
|
111
110
|
|
|
112
111
|
/// @notice Deploys tiered ERC-721 hooks for revnets.
|
|
113
112
|
IJB721TiersHookDeployer public immutable override HOOK_DEPLOYER;
|
|
114
113
|
|
|
115
|
-
/// @notice The loan contract used by
|
|
114
|
+
/// @notice The loan contract used by every revnet.
|
|
116
115
|
/// @dev Revnets can offer loans to their participants, collateralized by their tokens.
|
|
117
|
-
/// Participants can borrow up to the current cash
|
|
116
|
+
/// Participants can borrow up to the current cash-out value of their tokens.
|
|
118
117
|
IREVLoans public immutable override LOANS;
|
|
119
118
|
|
|
120
119
|
/// @notice The canonical terminal that holds revnet treasury balances.
|
|
121
120
|
IJBTerminal public immutable override MULTI_TERMINAL;
|
|
122
121
|
|
|
123
|
-
/// @notice The runtime data hook contract that handles pay and cash
|
|
122
|
+
/// @notice The runtime data hook contract that handles pay and cash-out callbacks for revnets.
|
|
124
123
|
/// @dev Set as `dataHook` in each revnet's ruleset metadata. Implements `IJBRulesetDataHook` and `IJBCashOutHook`.
|
|
125
124
|
address public immutable override OWNER;
|
|
126
125
|
|
|
@@ -162,11 +161,11 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
162
161
|
/// @param suckerRegistry The registry to use for deploying and tracking each revnet's suckers.
|
|
163
162
|
/// @param feeRevnetId The Juicebox project ID of the revnet that will receive fees.
|
|
164
163
|
/// @param hookDeployer The deployer to use for revnet's tiered ERC-721 hooks.
|
|
165
|
-
/// @param publisher The
|
|
164
|
+
/// @param publisher The Croptop publisher revnets can use to publish ERC-721 posts to their tiered ERC-721 hooks.
|
|
166
165
|
/// @param buybackHook The buyback hook used as a data hook to route payments through buyback pools.
|
|
167
|
-
/// @param loans The loan contract used by
|
|
166
|
+
/// @param loans The loan contract used by every revnet.
|
|
168
167
|
/// @param trustedForwarder The trusted forwarder for the ERC2771Context.
|
|
169
|
-
/// @param owner The runtime data hook contract (REVOwner) that handles pay and cash
|
|
168
|
+
/// @param owner The runtime data hook contract (REVOwner) that handles pay and cash-out callbacks.
|
|
170
169
|
constructor(
|
|
171
170
|
IJBController controller,
|
|
172
171
|
IJBTerminal multiTerminal,
|
|
@@ -301,11 +300,11 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
301
300
|
metadata.cashOutTaxRate = stageConfiguration.cashOutTaxRate;
|
|
302
301
|
metadata.baseCurrency = baseCurrency;
|
|
303
302
|
metadata.scopeCashOutsToLocalBalances = scopeCashOutsToLocalBalances;
|
|
304
|
-
|
|
305
|
-
metadata.
|
|
306
|
-
metadata.
|
|
307
|
-
|
|
308
|
-
metadata.dataHook = OWNER;
|
|
303
|
+
// Allow `REVOwner` to auto-mint configured allocations and handle pay/cash-out callbacks.
|
|
304
|
+
metadata.allowOwnerMinting = true;
|
|
305
|
+
metadata.useDataHookForPay = true;
|
|
306
|
+
metadata.useDataHookForCashOut = true;
|
|
307
|
+
metadata.dataHook = OWNER;
|
|
309
308
|
metadata.metadata = stageConfiguration.extraMetadata;
|
|
310
309
|
|
|
311
310
|
// Package the reserved token splits.
|
|
@@ -449,7 +448,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
449
448
|
sqrtPriceX96: sqrtPriceX96
|
|
450
449
|
}) {}
|
|
451
450
|
catch {
|
|
452
|
-
// Two failure modes both end up here and both must
|
|
451
|
+
// Two failure modes both end up here and both must not block the revnet deploy:
|
|
453
452
|
// 1. The V4 pool is already initialized at the expected price (idempotent re-deploy). The buyback
|
|
454
453
|
// hook's strict price check inside `initializePoolFor` would still call `_setPoolFor`, so the
|
|
455
454
|
// try-branch already covered this. We reach this catch only when the check rejected the existing
|
|
@@ -470,7 +469,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
470
469
|
/// @dev When initializing an existing project (revnetId != 0):
|
|
471
470
|
/// - The project must not yet have a controller or rulesets. `JBController.launchRulesetsFor` enforces this —
|
|
472
471
|
/// it reverts if rulesets have already been launched, and `JBDirectory.setControllerOf` only allows setting the
|
|
473
|
-
/// first controller. This means conversion only works for blank projects
|
|
472
|
+
/// first controller. This means conversion only works for blank projects: an ID with no on-chain state.
|
|
474
473
|
/// - This is useful in deploy scripts where the project ID is needed before configuration (e.g. for cross-chain
|
|
475
474
|
/// sucker peer mappings): create the project first, then initialize it as a revnet here.
|
|
476
475
|
/// - Initialization is a one-way operation: the project's ownership NFT is permanently transferred to this
|
|
@@ -480,7 +479,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
480
479
|
/// @param accountingContextsToAccept The accounting contexts the canonical multi terminal should accept.
|
|
481
480
|
/// @param suckerDeploymentConfiguration The suckers to set up for cross-chain token transfers.
|
|
482
481
|
/// @param tiered721HookConfiguration How to configure the tiered ERC-721 hook for the revnet.
|
|
483
|
-
/// @param allowedPosts Restrictions on which
|
|
482
|
+
/// @param allowedPosts Restrictions on which Croptop posts to allow on the revnet's ERC-721 tiers.
|
|
484
483
|
/// @return revnetId The ID of the newly created revnet.
|
|
485
484
|
/// @return hook The address of the tiered ERC-721 hook deployed for the revnet.
|
|
486
485
|
// The deployment flow makes external setup calls, but any observed state is revnet-scoped and reverts atomically.
|
|
@@ -507,7 +506,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
507
506
|
revert REVDeployer_ProjectCreationFeeNotNeeded({revnetId: revnetId, value: msg.value});
|
|
508
507
|
}
|
|
509
508
|
|
|
510
|
-
// Deploy the revnet with the specified tiered ERC-721 hook and
|
|
509
|
+
// Deploy the revnet with the specified tiered ERC-721 hook and Croptop posting criteria.
|
|
511
510
|
hook = _deploy721RevnetFor({
|
|
512
511
|
revnetId: revnetId,
|
|
513
512
|
shouldDeployNewRevnet: shouldDeployNewRevnet,
|
|
@@ -619,7 +618,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
619
618
|
// --------------------- internal transactions ----------------------- //
|
|
620
619
|
//*********************************************************************//
|
|
621
620
|
|
|
622
|
-
/// @notice Deploy a revnet which sells tiered ERC-721s and
|
|
621
|
+
/// @notice Deploy a revnet which sells tiered ERC-721s and optionally allows Croptop posts to its ERC-721 tiers.
|
|
623
622
|
// The helper performs external hook/post setup after core revnet setup; any failure reverts the whole deployment.
|
|
624
623
|
function _deploy721RevnetFor(
|
|
625
624
|
uint256 revnetId,
|
|
@@ -726,7 +725,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
726
725
|
// deployer passes that check directly only before ownership is scoped to REVOwner below.
|
|
727
726
|
PUBLISHER.configurePostingCriteriaFor({allowedPosts: formattedAllowedPosts});
|
|
728
727
|
|
|
729
|
-
// Give the
|
|
728
|
+
// Give the Croptop publisher permission to post new ERC-721 tiers on the revnet's behalf.
|
|
730
729
|
ownerInit.extraGrants = new REVOwnerExtraGrant[](1);
|
|
731
730
|
ownerInit.extraGrants[0] =
|
|
732
731
|
REVOwnerExtraGrant({operator: address(PUBLISHER), permissionId: JBPermissionIds.ADJUST_721_TIERS});
|
|
@@ -798,7 +797,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
798
797
|
memo: ""
|
|
799
798
|
});
|
|
800
799
|
|
|
801
|
-
// Compute the cash
|
|
800
|
+
// Compute the cash-out delay if the revnet's stages are already in progress. This prevents cash-out
|
|
802
801
|
// liquidity/arbitrage issues for existing revnets which are deploying to a new chain.
|
|
803
802
|
ownerInit.cashOutDelay =
|
|
804
803
|
_computeCashOutDelayIfNeeded({revnetId: revnetId, firstStageConfig: configuration.stageConfigurations[0]});
|
|
@@ -951,11 +950,11 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
951
950
|
|
|
952
951
|
// Iterate through each stage to set up its ruleset.
|
|
953
952
|
for (uint256 i; i < configuration.stageConfigurations.length;) {
|
|
954
|
-
//
|
|
953
|
+
// Read the stage being converted into a ruleset.
|
|
955
954
|
REVStageConfig calldata stageConfiguration = configuration.stageConfigurations[i];
|
|
956
955
|
|
|
957
956
|
// Make sure the revnet has at least one split if it has a split percent.
|
|
958
|
-
// Otherwise, the
|
|
957
|
+
// Otherwise, the reserved-token residue would stay with this contract as the revnet's owner.
|
|
959
958
|
if (stageConfiguration.splitPercent > 0 && stageConfiguration.splits.length == 0) {
|
|
960
959
|
revert REVDeployer_MustHaveSplits({stageIndex: i, splitPercent: stageConfiguration.splitPercent});
|
|
961
960
|
}
|
|
@@ -975,7 +974,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
975
974
|
// Store for the next iteration's ordering check.
|
|
976
975
|
previousStageStart = effectiveStart;
|
|
977
976
|
|
|
978
|
-
// Make sure the revnet
|
|
977
|
+
// Make sure the revnet does not disable cash-outs completely.
|
|
979
978
|
if (stageConfiguration.cashOutTaxRate >= JBConstants.MAX_CASH_OUT_TAX_RATE) {
|
|
980
979
|
revert REVDeployer_CashOutsCantBeTurnedOffCompletely({
|
|
981
980
|
cashOutTaxRate: stageConfiguration.cashOutTaxRate,
|
|
@@ -1055,12 +1054,12 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
1055
1054
|
encodedConfigurationHash = keccak256(encodedConfiguration);
|
|
1056
1055
|
}
|
|
1057
1056
|
|
|
1058
|
-
/// @notice Compute the cash
|
|
1057
|
+
/// @notice Compute the cash-out delay for the revnet's first stage if its stages are already in progress.
|
|
1059
1058
|
/// @dev Returns 0 when no delay is needed. Emits `SetCashOutDelay` so deployer-side telemetry stays unchanged
|
|
1060
1059
|
/// even though the actual storage write happens later inside `REVOwner.initializeRevnet`.
|
|
1061
|
-
/// @param revnetId The ID of the revnet to compute the cash
|
|
1060
|
+
/// @param revnetId The ID of the revnet to compute the cash-out delay for.
|
|
1062
1061
|
/// @param firstStageConfig The revnet's first stage.
|
|
1063
|
-
/// @return cashOutDelay The timestamp after which cash
|
|
1062
|
+
/// @return cashOutDelay The timestamp after which cash-outs are allowed, or 0 if no delay applies.
|
|
1064
1063
|
function _computeCashOutDelayIfNeeded(
|
|
1065
1064
|
uint256 revnetId,
|
|
1066
1065
|
REVStageConfig calldata firstStageConfig
|
|
@@ -1069,11 +1068,11 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
1069
1068
|
returns (uint256 cashOutDelay)
|
|
1070
1069
|
{
|
|
1071
1070
|
// If this is the first revnet being deployed (with a `startsAtOrAfter` of 0),
|
|
1072
|
-
// or if the first stage hasn't started yet, we don't need to set a cash
|
|
1071
|
+
// or if the first stage hasn't started yet, we don't need to set a cash-out delay.
|
|
1073
1072
|
// forge-lint: disable-next-line(block-timestamp)
|
|
1074
1073
|
if (firstStageConfig.startsAtOrAfter == 0 || firstStageConfig.startsAtOrAfter >= block.timestamp) return 0;
|
|
1075
1074
|
|
|
1076
|
-
// Calculate the timestamp at which the cash
|
|
1075
|
+
// Calculate the timestamp at which the cash-out delay ends.
|
|
1077
1076
|
cashOutDelay = block.timestamp + CASH_OUT_DELAY;
|
|
1078
1077
|
|
|
1079
1078
|
emit SetCashOutDelay({revnetId: revnetId, cashOutDelay: cashOutDelay, caller: _msgSender()});
|
package/src/REVLoans.sol
CHANGED
|
@@ -41,8 +41,8 @@ import {REVLoan} from "./structs/REVLoan.sol";
|
|
|
41
41
|
/// revnet's token structure orderly. Each loan is represented as an ERC-721 NFT that can be transferred.
|
|
42
42
|
/// @dev Fee structure: an upfront fee is taken at borrow time. 2.5% goes to the source revnet
|
|
43
43
|
/// (MIN_PREPAID_FEE_PERCENT), 1% goes to the $REV revnet (REV_PREPAID_FEE_PERCENT), and a variable amount chosen by the
|
|
44
|
-
/// borrower determines the
|
|
45
|
-
///
|
|
44
|
+
/// borrower determines the prepaid duration — the more paid upfront, the longer the borrower can hold without
|
|
45
|
+
/// additional cost. After the
|
|
46
46
|
/// prepaid duration expires, the repayment cost increases linearly until the loan liquidates at 10 years
|
|
47
47
|
/// (LOAN_LIQUIDATION_DURATION), at which point the collateral is permanently lost.
|
|
48
48
|
/// @dev The loaned amounts include the fees taken, meaning the amount paid back is the amount borrowed plus the fees.
|
|
@@ -104,9 +104,12 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
104
104
|
// ------------------------ private constants ------------------------ //
|
|
105
105
|
//*********************************************************************//
|
|
106
106
|
|
|
107
|
-
/// @notice
|
|
108
|
-
/// @dev
|
|
109
|
-
uint256 private constant
|
|
107
|
+
/// @notice The number of loan IDs reserved for each revnet.
|
|
108
|
+
/// @dev Loan IDs are encoded as `revnetId * _LOAN_ID_NAMESPACE_SIZE + loanNumber`.
|
|
109
|
+
uint256 private constant _LOAN_ID_NAMESPACE_SIZE = 1_000_000_000_000_000_000;
|
|
110
|
+
|
|
111
|
+
/// @dev This is the largest loan number that stays in the current revnet's ID namespace.
|
|
112
|
+
uint256 private constant _MAX_LOAN_NUMBER = _LOAN_ID_NAMESPACE_SIZE - 1;
|
|
110
113
|
|
|
111
114
|
//*********************************************************************//
|
|
112
115
|
// --------------- public immutable stored properties ---------------- //
|
|
@@ -171,8 +174,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
171
174
|
// --------------------- internal stored properties ------------------ //
|
|
172
175
|
//*********************************************************************//
|
|
173
176
|
|
|
174
|
-
/// @notice The
|
|
175
|
-
/// @custom:
|
|
177
|
+
/// @notice The loan state for each loan ID.
|
|
178
|
+
/// @custom:param loanId The ID of the loan.
|
|
176
179
|
mapping(uint256 loanId => REVLoan) internal _loanOf;
|
|
177
180
|
|
|
178
181
|
/// @notice The sources of each revnet's loan.
|
|
@@ -308,7 +311,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
308
311
|
/// @param loanId The loan ID to look up.
|
|
309
312
|
/// @return The ID of the revnet.
|
|
310
313
|
function revnetIdOfLoanWith(uint256 loanId) public pure override returns (uint256) {
|
|
311
|
-
return loanId /
|
|
314
|
+
return loanId / _LOAN_ID_NAMESPACE_SIZE;
|
|
312
315
|
}
|
|
313
316
|
|
|
314
317
|
/// @notice Returns the URI where the ERC-721 standard JSON of a loan is hosted.
|
|
@@ -518,7 +521,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
518
521
|
/// @param loanNumber The loan number within the revnet.
|
|
519
522
|
/// @return The token ID of the 721.
|
|
520
523
|
function _generateLoanId(uint256 revnetId, uint256 loanNumber) internal pure returns (uint256) {
|
|
521
|
-
return (revnetId *
|
|
524
|
+
return (revnetId * _LOAN_ID_NAMESPACE_SIZE) + loanNumber;
|
|
522
525
|
}
|
|
523
526
|
|
|
524
527
|
/// @notice The calldata. Preferred to use over `msg.data`.
|
|
@@ -689,15 +692,15 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
689
692
|
|
|
690
693
|
// Prevent cross-revnet accounting corruption: every iterated loan number must stay within the revnet's ID
|
|
691
694
|
// namespace.
|
|
692
|
-
if (startingLoanId >
|
|
695
|
+
if (startingLoanId > _MAX_LOAN_NUMBER) {
|
|
693
696
|
revert REVLoans_LoanIdOverflow({
|
|
694
|
-
revnetId: revnetId, loanNumber: startingLoanId, maxLoanNumber:
|
|
697
|
+
revnetId: revnetId, loanNumber: startingLoanId, maxLoanNumber: _MAX_LOAN_NUMBER
|
|
695
698
|
});
|
|
696
699
|
}
|
|
697
|
-
uint256 maxCount =
|
|
700
|
+
uint256 maxCount = _MAX_LOAN_NUMBER - startingLoanId + 1;
|
|
698
701
|
if (count > maxCount) {
|
|
699
702
|
revert REVLoans_LoanIdOverflow({
|
|
700
|
-
revnetId: revnetId, loanNumber:
|
|
703
|
+
revnetId: revnetId, loanNumber: _LOAN_ID_NAMESPACE_SIZE, maxLoanNumber: _MAX_LOAN_NUMBER
|
|
701
704
|
});
|
|
702
705
|
}
|
|
703
706
|
|
|
@@ -1388,8 +1391,10 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1388
1391
|
/// @return loanId The allocated loan ID.
|
|
1389
1392
|
function _nextLoanIdFor(uint256 revnetId) internal returns (uint256 loanId) {
|
|
1390
1393
|
uint256 loanNumber = totalLoansBorrowedFor[revnetId] + 1;
|
|
1391
|
-
if (loanNumber >
|
|
1392
|
-
revert REVLoans_LoanIdOverflow({
|
|
1394
|
+
if (loanNumber > _MAX_LOAN_NUMBER) {
|
|
1395
|
+
revert REVLoans_LoanIdOverflow({
|
|
1396
|
+
revnetId: revnetId, loanNumber: loanNumber, maxLoanNumber: _MAX_LOAN_NUMBER
|
|
1397
|
+
});
|
|
1393
1398
|
}
|
|
1394
1399
|
totalLoansBorrowedFor[revnetId] = loanNumber;
|
|
1395
1400
|
return _generateLoanId({revnetId: revnetId, loanNumber: loanNumber});
|
package/src/REVOwner.sol
CHANGED
|
@@ -38,10 +38,10 @@ import {REVOwnerAutoIssuance} from "./structs/REVOwnerAutoIssuance.sol";
|
|
|
38
38
|
import {REVOwnerExtraGrant} from "./structs/REVOwnerExtraGrant.sol";
|
|
39
39
|
import {REVOwnerRevnetInit} from "./structs/REVOwnerRevnetInit.sol";
|
|
40
40
|
|
|
41
|
-
/// @notice The runtime hook for
|
|
41
|
+
/// @notice The runtime hook for every revnet — set as each revnet's `dataHook` in ruleset metadata. At pay time, it
|
|
42
42
|
/// coordinates the 721 hook (NFT tier minting) with the buyback hook (secondary market swap routing) and scales weight
|
|
43
43
|
/// for split deductions. At cash-out time, it aggregates cross-chain total supply and surplus (including outstanding
|
|
44
|
-
/// loan debt and collateral), grants suckers 0% tax, splits a 2.5% fee from non-sucker cash
|
|
44
|
+
/// loan debt and collateral), grants suckers 0% tax, splits a 2.5% fee from non-sucker cash-outs with a non-zero
|
|
45
45
|
/// cash-out tax, and routes fee proceeds to the fee revnet via `afterCashOutRecordedWith`.
|
|
46
46
|
/// @dev Separated from `REVDeployer` to stay within the EIP-170 contract size limit. Also implements
|
|
47
47
|
/// `IJBPeerChainAdjustedAccounts` to expose loan state to peer-chain supply/surplus snapshots.
|
|
@@ -73,10 +73,10 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
73
73
|
/// @notice The directory of terminals and controllers for Juicebox projects.
|
|
74
74
|
IJBDirectory public immutable override DIRECTORY;
|
|
75
75
|
|
|
76
|
-
/// @notice The Juicebox project ID of the revnet that receives cash
|
|
76
|
+
/// @notice The Juicebox project ID of the revnet that receives cash-out fees.
|
|
77
77
|
uint256 public immutable override FEE_REVNET_ID;
|
|
78
78
|
|
|
79
|
-
/// @notice The loan contract used by
|
|
79
|
+
/// @notice The loan contract used by every revnet.
|
|
80
80
|
IREVLoans public immutable override LOANS;
|
|
81
81
|
|
|
82
82
|
/// @notice Deploys and tracks suckers for revnets.
|
|
@@ -86,9 +86,9 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
86
86
|
// --------------------- public stored properties -------------------- //
|
|
87
87
|
//*********************************************************************//
|
|
88
88
|
|
|
89
|
-
/// @notice The timestamp
|
|
89
|
+
/// @notice The timestamp when cash-outs become available to a specific revnet's participants.
|
|
90
90
|
/// @dev Only applies to existing revnets deploying onto a new network.
|
|
91
|
-
/// @custom:param revnetId The ID of the revnet to check the cash
|
|
91
|
+
/// @custom:param revnetId The ID of the revnet to check the cash-out delay for.
|
|
92
92
|
mapping(uint256 revnetId => uint256 cashOutDelay) public override cashOutDelayOf;
|
|
93
93
|
|
|
94
94
|
/// @notice Each revnet's tiered ERC-721 hook.
|
|
@@ -107,7 +107,7 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
107
107
|
/// @dev Set once via `setDeployer()` using the precomputed canonical REVDeployer address.
|
|
108
108
|
IREVDeployer public override deployer;
|
|
109
109
|
|
|
110
|
-
/// @notice The controller used by
|
|
110
|
+
/// @notice The controller used by every revnet to manage its project.
|
|
111
111
|
/// @dev Cached from the deployer at `setDeployer()` time.
|
|
112
112
|
IJBController public override CONTROLLER;
|
|
113
113
|
|
|
@@ -165,7 +165,7 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
165
165
|
// ------------------------- external views -------------------------- //
|
|
166
166
|
//*********************************************************************//
|
|
167
167
|
|
|
168
|
-
/// @notice Called before a cash
|
|
168
|
+
/// @notice Called before a cash-out is recorded. Suckers get 0% tax (bridged tokens redeem at face value). For
|
|
169
169
|
/// regular holders, aggregates cross-chain total supply and surplus (including outstanding loan debt/collateral),
|
|
170
170
|
/// splits a 2.5% fee from the cashed-out token count when cash-out tax is non-zero, computes bonding curve
|
|
171
171
|
/// reclaims for both the holder's portion and the fee portion, then delegates to the buyback hook for potential
|
|
@@ -173,12 +173,12 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
173
173
|
/// @dev Part of `IJBRulesetDataHook`. In the non-zero-tax fee path, REVOwner is intentionally not registered as a
|
|
174
174
|
/// feeless address — the protocol fee (2.5%) applies on top of the rev fee. The fee hook spec amount sent to
|
|
175
175
|
/// `afterCashOutRecordedWith` will have the protocol fee deducted by the terminal before reaching this contract.
|
|
176
|
-
/// @param context Standard Juicebox cash
|
|
177
|
-
/// @return cashOutTaxRate The cash
|
|
176
|
+
/// @param context Standard Juicebox cash-out context. See `JBBeforeCashOutRecordedContext`.
|
|
177
|
+
/// @return cashOutTaxRate The cash-out tax rate, which influences the amount of terminal tokens reclaimed.
|
|
178
178
|
/// @return cashOutCount The number of revnet tokens to cash out.
|
|
179
179
|
/// @return totalSupply The total token supply across all chains (for both proportional reclaim and tax).
|
|
180
180
|
/// @return effectiveSurplusValue The global surplus across all chains for proportional reclaim.
|
|
181
|
-
/// @return hookSpecifications The amount of funds and data to send to cash
|
|
181
|
+
/// @return hookSpecifications The amount of funds and data to send to cash-out hooks (this contract).
|
|
182
182
|
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
183
183
|
external
|
|
184
184
|
view
|
|
@@ -201,7 +201,7 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
201
201
|
totalSupply = context.totalSupply + totalCollateral;
|
|
202
202
|
effectiveSurplusValue = context.surplus.value + totalBorrowed;
|
|
203
203
|
|
|
204
|
-
// If the cash
|
|
204
|
+
// If the cash-out is from a sucker, return the full cash-out amount without taxes or fees.
|
|
205
205
|
// Sucker cash-outs are the bridge accounting path: the value moving out of this chain must stay proportional
|
|
206
206
|
// to this chain's local backing. Do not add remote supply/surplus here, even for unscoped revnets.
|
|
207
207
|
// This relies on the sucker registry to only contain trusted sucker contracts deployed via
|
|
@@ -210,16 +210,16 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
210
210
|
return (0, context.cashOutCount, totalSupply, effectiveSurplusValue, hookSpecifications);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
// Keep a reference to the cash
|
|
213
|
+
// Keep a reference to the cash-out delay of the revnet.
|
|
214
214
|
uint256 cashOutDelay = cashOutDelayOf[context.projectId];
|
|
215
215
|
|
|
216
|
-
// Enforce the cash
|
|
216
|
+
// Enforce the cash-out delay.
|
|
217
217
|
// forge-lint: disable-next-line(block-timestamp)
|
|
218
218
|
if (cashOutDelay > block.timestamp) {
|
|
219
219
|
revert REVOwner_CashOutDelayNotFinished({cashOutDelay: cashOutDelay, blockTimestamp: block.timestamp});
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
// Get the terminal that will receive the cash
|
|
222
|
+
// Get the terminal that will receive the cash-out fee.
|
|
223
223
|
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf({projectId: FEE_REVNET_ID, token: context.surplus.token});
|
|
224
224
|
|
|
225
225
|
// If the ruleset aggregates cross-chain state, add remote supply and surplus.
|
|
@@ -232,8 +232,8 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
// If there
|
|
236
|
-
//
|
|
235
|
+
// If there is no cash-out tax, no fee terminal, or a feeless beneficiary (e.g. the router terminal routing
|
|
236
|
+
// value between projects), proxy to the buyback hook with our totalSupply and
|
|
237
237
|
// effectiveSurplusValue. Zero-tax ordinary cash-outs do not add the revnet fee hook.
|
|
238
238
|
if (context.cashOutTaxRate == 0 || address(feeTerminal) == address(0) || context.beneficiaryIsFeeless) {
|
|
239
239
|
// Build a modified context with cross-chain-adjusted values so the buyback hook sees the global state
|
|
@@ -249,8 +249,8 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
249
249
|
// The fee is applied to TOKEN COUNT (2.5% of tokens), not to value. The fee revnet receives the bonding-curve
|
|
250
250
|
// reclaim of its 2.5% token share regardless of whether the remaining 97.5% routes through a buyback pool at
|
|
251
251
|
// a better price. This is by design.
|
|
252
|
-
// Micro cash
|
|
253
|
-
//
|
|
252
|
+
// Micro cash-outs (< 40 wei at 2.5% fee) round feeCashOutCount to zero. This accepted floor is economically
|
|
253
|
+
// negligible because gas dominates the avoided fee.
|
|
254
254
|
uint256 feeCashOutCount = JBFees.standardFeeAmountFrom(context.cashOutCount);
|
|
255
255
|
uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
|
|
256
256
|
|
|
@@ -311,7 +311,7 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
311
311
|
buybackHookContext.totalSupply = totalSupply;
|
|
312
312
|
buybackHookContext.surplus.value = buybackSurplus;
|
|
313
313
|
|
|
314
|
-
// Let the buyback hook adjust the cash
|
|
314
|
+
// Let the buyback hook adjust the cash-out parameters and optionally return a hook specification.
|
|
315
315
|
JBCashOutHookSpecification[] memory buybackHookSpecifications;
|
|
316
316
|
(cashOutTaxRate, cashOutCount,,, buybackHookSpecifications) =
|
|
317
317
|
BUYBACK_HOOK.beforeCashOutRecordedWith(buybackHookContext);
|
|
@@ -325,11 +325,11 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
325
325
|
// totalSupply, cashOutTaxRate)` and add the fee spec on top. When local liquidity is the binding cap, that
|
|
326
326
|
// sum can exceed local surplus and revert. `cashOutFrom` is linear in `surplus`, so scale the surplus we
|
|
327
327
|
// report so the store-side reclaim is at most `localSurplus - feeAmount`, preserving room for the fee.
|
|
328
|
-
// The fee
|
|
329
|
-
//
|
|
328
|
+
// The fee was already scaled, if needed, by the local-liquidity block above. Only the store-facing surplus is
|
|
329
|
+
// touched; the buyback hook already received the full pre-cap value for its routing decision.
|
|
330
330
|
//
|
|
331
331
|
// Worked example (local=10, global=100, 500 of 1000 tokens at 50% tax):
|
|
332
|
-
//
|
|
332
|
+
// local-liquidity scaling above:
|
|
333
333
|
// unscaledReclaim = cashOutFrom(100, 487.5, 1000, 5000) ≈ 36 ETH (global)
|
|
334
334
|
// feeAmount = cashOutFrom(64, 12.5, 512.5, 5000) ≈ 0.78 ETH (global)
|
|
335
335
|
// grossOutflow ≈ 36.78 > 10 → scale both proportionally to local liquidity:
|
|
@@ -343,10 +343,9 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
343
343
|
// storeReclaim = 36 × (27.18 / 100) ≈ 9.786 ETH
|
|
344
344
|
// balanceDiff = 9.786 + 0.214 = 10 ETH = localSurplus ✓ no revert
|
|
345
345
|
//
|
|
346
|
-
// Underflow safety
|
|
347
|
-
//
|
|
348
|
-
// `
|
|
349
|
-
// in the else branch because `feeAmount ≤ grossOutflow ≤ localSurplus` already.
|
|
346
|
+
// Underflow safety: `feeAmount <= localSurplus` holds in both branches.
|
|
347
|
+
// In the scaling branch, `feeAmount <= grossOutflow` and the multiplier is
|
|
348
|
+
// `localSurplus / grossOutflow <= 1`. In the else branch, `feeAmount <= grossOutflow <= localSurplus`.
|
|
350
349
|
uint256 reclaimCap = context.surplus.value - feeAmount;
|
|
351
350
|
if (unscaledReclaim > reclaimCap) {
|
|
352
351
|
effectiveSurplusValue = mulDiv({x: effectiveSurplusValue, y: reclaimCap, denominator: unscaledReclaim});
|
|
@@ -486,11 +485,11 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
486
485
|
// --------------------- external transactions ----------------------- //
|
|
487
486
|
//*********************************************************************//
|
|
488
487
|
|
|
489
|
-
/// @notice Process the fee from a cash
|
|
488
|
+
/// @notice Process the fee from a cash-out.
|
|
490
489
|
/// @param context Cash out context passed in by the terminal.
|
|
491
490
|
function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context) external payable override {
|
|
492
491
|
// No caller validation needed — this hook only pays fees to the fee project using funds forwarded by the
|
|
493
|
-
// caller. A non-terminal caller would
|
|
492
|
+
// caller. A non-terminal caller would donate their own funds as fees. There's nothing to exploit.
|
|
494
493
|
|
|
495
494
|
if (context.forwardedAmount.token == JBConstants.NATIVE_TOKEN) {
|
|
496
495
|
// Native fee processing must be value-balanced by the current call. Otherwise a non-terminal caller could
|
|
@@ -568,14 +567,14 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
568
567
|
}
|
|
569
568
|
|
|
570
569
|
// Store the cash-out delay if the deployer computed one (existing revnet landing on a new chain). A zero
|
|
571
|
-
// delay means cash
|
|
570
|
+
// delay means cash-outs are unlocked immediately, so skip the storage write to save gas in the common case.
|
|
572
571
|
if (init.cashOutDelay != 0) {
|
|
573
572
|
cashOutDelayOf[revnetId] = init.cashOutDelay;
|
|
574
573
|
}
|
|
575
574
|
|
|
576
|
-
// Bind the tiered ERC-721 hook the deployer
|
|
577
|
-
//
|
|
578
|
-
//
|
|
575
|
+
// Bind the tiered ERC-721 hook the deployer created for this revnet so `beforePayRecordedWith` can route
|
|
576
|
+
// NFT-tier mints through it. The deployer always deploys a hook (empty or pre-tiered), so the zero guard is
|
|
577
|
+
// defensive.
|
|
579
578
|
if (address(init.tiered721Hook) != address(0)) {
|
|
580
579
|
tiered721HookOf[revnetId] = init.tiered721Hook;
|
|
581
580
|
}
|
|
@@ -647,7 +646,7 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
647
646
|
PROJECTS = newDeployer.PROJECTS();
|
|
648
647
|
|
|
649
648
|
// Grant the wildcard operator permissions used by every revnet, scoped on this contract's account.
|
|
650
|
-
// The loan contract is the singleton shared by
|
|
649
|
+
// The loan contract is the singleton shared by every revnet and uses the surplus allowance of each
|
|
651
650
|
// revnet's terminal. The buyback hook registry configures pools on every revnet.
|
|
652
651
|
uint8[] memory loanPermissionIds = new uint8[](1);
|
|
653
652
|
loanPermissionIds[0] = JBPermissionIds.USE_ALLOWANCE;
|
|
@@ -774,8 +773,9 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
774
773
|
|
|
775
774
|
/// @notice Indicates if this contract adheres to the specified interface.
|
|
776
775
|
/// @dev See `IERC165.supportsInterface`.
|
|
777
|
-
/// @
|
|
778
|
-
|
|
776
|
+
/// @param interfaceId The interface ID to check.
|
|
777
|
+
/// @return flag A flag indicating whether the interface is supported.
|
|
778
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool flag) {
|
|
779
779
|
return interfaceId == type(IERC165).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
|
|
780
780
|
|| interfaceId == type(IJBCashOutHook).interfaceId
|
|
781
781
|
|| interfaceId == type(IJBPeerChainAdjustedAccounts).interfaceId;
|
|
@@ -52,9 +52,9 @@ interface IREVDeployer {
|
|
|
52
52
|
address caller
|
|
53
53
|
);
|
|
54
54
|
|
|
55
|
-
/// @notice Emitted when the cash
|
|
55
|
+
/// @notice Emitted when the cash-out delay is set for a revnet.
|
|
56
56
|
/// @param revnetId The ID of the revnet.
|
|
57
|
-
/// @param cashOutDelay The cash
|
|
57
|
+
/// @param cashOutDelay The cash-out delay in seconds.
|
|
58
58
|
/// @param caller The address that set the delay.
|
|
59
59
|
event SetCashOutDelay(uint256 indexed revnetId, uint256 cashOutDelay, address caller);
|
|
60
60
|
|
|
@@ -73,7 +73,7 @@ interface IREVDeployer {
|
|
|
73
73
|
function BUYBACK_HOOK() external view returns (IJBBuybackHookRegistry);
|
|
74
74
|
|
|
75
75
|
/// @notice The number of seconds until a revnet's participants can cash out after deploying to a new network.
|
|
76
|
-
/// @return The cash
|
|
76
|
+
/// @return The cash-out delay in seconds.
|
|
77
77
|
function CASH_OUT_DELAY() external view returns (uint256);
|
|
78
78
|
|
|
79
79
|
/// @notice The controller used to create and manage Juicebox projects for revnets.
|
|
@@ -92,7 +92,7 @@ interface IREVDeployer {
|
|
|
92
92
|
/// @return The directory contract.
|
|
93
93
|
function DIRECTORY() external view returns (IJBDirectory);
|
|
94
94
|
|
|
95
|
-
/// @notice The Juicebox project ID of the revnet that receives cash
|
|
95
|
+
/// @notice The Juicebox project ID of the revnet that receives cash-out fees.
|
|
96
96
|
/// @return The fee revnet ID.
|
|
97
97
|
function FEE_REVNET_ID() external view returns (uint256);
|
|
98
98
|
|
|
@@ -105,7 +105,7 @@ interface IREVDeployer {
|
|
|
105
105
|
/// @return The hook deployer contract.
|
|
106
106
|
function HOOK_DEPLOYER() external view returns (IJB721TiersHookDeployer);
|
|
107
107
|
|
|
108
|
-
/// @notice The loan contract used by
|
|
108
|
+
/// @notice The loan contract used by every revnet.
|
|
109
109
|
/// @return The loans contract address.
|
|
110
110
|
function LOANS() external view returns (IREVLoans);
|
|
111
111
|
|
|
@@ -113,7 +113,7 @@ interface IREVDeployer {
|
|
|
113
113
|
/// @return The multi terminal contract.
|
|
114
114
|
function MULTI_TERMINAL() external view returns (IJBTerminal);
|
|
115
115
|
|
|
116
|
-
/// @notice The runtime data hook contract that handles pay and cash
|
|
116
|
+
/// @notice The runtime data hook contract that handles pay and cash-out callbacks for revnets.
|
|
117
117
|
/// @return The owner contract address.
|
|
118
118
|
function OWNER() external view returns (address);
|
|
119
119
|
|
|
@@ -125,7 +125,7 @@ interface IREVDeployer {
|
|
|
125
125
|
/// @return The projects contract.
|
|
126
126
|
function PROJECTS() external view returns (IJBProjects);
|
|
127
127
|
|
|
128
|
-
/// @notice The
|
|
128
|
+
/// @notice The Croptop publisher revnets can use to publish ERC-721 posts to their tiered ERC-721 hooks.
|
|
129
129
|
/// @return The publisher contract.
|
|
130
130
|
function PUBLISHER() external view returns (CTPublisher);
|
|
131
131
|
|
|
@@ -137,14 +137,14 @@ interface IREVDeployer {
|
|
|
137
137
|
/// @return The sucker registry contract.
|
|
138
138
|
function SUCKER_REGISTRY() external view returns (IJBSuckerRegistry);
|
|
139
139
|
|
|
140
|
-
/// @notice Deploy a revnet with a tiered ERC-721 hook and optional
|
|
140
|
+
/// @notice Deploy a revnet with a tiered ERC-721 hook and optional Croptop posting support.
|
|
141
141
|
/// @dev Every revnet gets a 721 hook — pass an empty config if no tiers are needed initially.
|
|
142
142
|
/// @param revnetId The ID of the Juicebox project to initialize. Send 0 to deploy a new revnet.
|
|
143
143
|
/// @param configuration Core revnet configuration.
|
|
144
144
|
/// @param accountingContextsToAccept The accounting contexts the canonical multi terminal should accept.
|
|
145
145
|
/// @param suckerDeploymentConfiguration The suckers to set up for cross-chain token transfers.
|
|
146
146
|
/// @param tiered721HookConfiguration How to configure the tiered ERC-721 hook for the revnet.
|
|
147
|
-
/// @param allowedPosts Restrictions on which
|
|
147
|
+
/// @param allowedPosts Restrictions on which Croptop posts to allow on the revnet's ERC-721 tiers.
|
|
148
148
|
/// @return The ID of the newly created or initialized revnet.
|
|
149
149
|
/// @return hook The tiered ERC-721 hook deployed for the revnet.
|
|
150
150
|
function deployFor(
|
|
@@ -54,7 +54,7 @@ interface IREVOwner {
|
|
|
54
54
|
/// @return The directory instance.
|
|
55
55
|
function DIRECTORY() external view returns (IJBDirectory);
|
|
56
56
|
|
|
57
|
-
/// @notice The Juicebox project ID of the revnet that receives cash
|
|
57
|
+
/// @notice The Juicebox project ID of the revnet that receives cash-out fees.
|
|
58
58
|
/// @return The fee revnet's Juicebox project ID.
|
|
59
59
|
function FEE_REVNET_ID() external view returns (uint256);
|
|
60
60
|
|
|
@@ -81,9 +81,9 @@ interface IREVOwner {
|
|
|
81
81
|
/// @return The number of tokens available to auto-issue.
|
|
82
82
|
function amountToAutoIssue(uint256 revnetId, uint256 stageId, address beneficiary) external view returns (uint256);
|
|
83
83
|
|
|
84
|
-
/// @notice The timestamp at which cash
|
|
84
|
+
/// @notice The timestamp at which cash-outs become available for a specific revnet's participants.
|
|
85
85
|
/// @param revnetId The ID of the revnet.
|
|
86
|
-
/// @return The cash
|
|
86
|
+
/// @return The cash-out delay timestamp (0 if no delay applies).
|
|
87
87
|
function cashOutDelayOf(uint256 revnetId) external view returns (uint256);
|
|
88
88
|
|
|
89
89
|
/// @notice The canonical deployer that manages runtime hook state.
|
|
@@ -5,7 +5,8 @@ pragma solidity ^0.8.0;
|
|
|
5
5
|
/// @custom:member category The category to allow posts for.
|
|
6
6
|
/// @custom:member minimumPrice The minimum price a post to the specified category must cost.
|
|
7
7
|
/// @custom:member minimumTotalSupply The minimum total supply of NFTs to make available when minting.
|
|
8
|
-
/// @custom:member
|
|
8
|
+
/// @custom:member maximumTotalSupply The max total supply of NFTs to make available when minting. Leave as 0 for
|
|
9
|
+
/// unlimited.
|
|
9
10
|
/// @custom:member maximumSplitPercent The maximum split percent (out of JBConstants.SPLITS_TOTAL_PERCENT) a poster can
|
|
10
11
|
/// set. 0 means splits are not allowed.
|
|
11
12
|
/// @custom:member allowedAddresses The addresses allowed to post on the category through Croptop.
|
|
@@ -3,16 +3,13 @@ pragma solidity ^0.8.0;
|
|
|
3
3
|
|
|
4
4
|
import {REVBaseline721HookConfig} from "./REVBaseline721HookConfig.sol";
|
|
5
5
|
|
|
6
|
-
/// @custom:member baseline721HookConfiguration The
|
|
6
|
+
/// @custom:member baseline721HookConfiguration The base tiered ERC-721 hook config.
|
|
7
7
|
/// @custom:member salt The salt to derive the collection's address from.
|
|
8
|
-
/// @custom:member preventOperatorAdjustingTiers Whether to prevent the operator from adding and removing
|
|
9
|
-
///
|
|
10
|
-
/// @custom:member
|
|
11
|
-
///
|
|
12
|
-
///
|
|
13
|
-
/// allow it.
|
|
14
|
-
/// @custom:member preventOperatorIncreasingDiscountPercent Whether to prevent the operator from increasing
|
|
15
|
-
/// the discount of a tier.
|
|
8
|
+
/// @custom:member preventOperatorAdjustingTiers Whether to prevent the operator from adding and removing tiers.
|
|
9
|
+
/// @custom:member preventOperatorUpdatingMetadata Whether to prevent the operator from updating the 721's metadata.
|
|
10
|
+
/// @custom:member preventOperatorMinting Whether to prevent the operator from minting 721s from tiers that allow it.
|
|
11
|
+
/// @custom:member preventOperatorIncreasingDiscountPercent Whether to prevent the operator from increasing a tier's
|
|
12
|
+
/// discount.
|
|
16
13
|
struct REVDeploy721TiersHookConfig {
|
|
17
14
|
REVBaseline721HookConfig baseline721HookConfiguration;
|
|
18
15
|
bytes32 salt;
|
|
@@ -7,10 +7,10 @@ import {REVAutoIssuance} from "./REVAutoIssuance.sol";
|
|
|
7
7
|
|
|
8
8
|
/// @notice A stage in a revnet's lifecycle. Each stage defines the token issuance rate, how quickly it decays, what
|
|
9
9
|
/// percentage goes to splits, and the cash-out tax rate. Stages are processed in order — each one activates at or
|
|
10
|
-
/// after
|
|
11
|
-
/// its `startsAtOrAfter` timestamp.
|
|
10
|
+
/// after its `startsAtOrAfter` timestamp.
|
|
12
11
|
/// @custom:member startsAtOrAfter The earliest timestamp this stage can begin. Must be strictly increasing across
|
|
13
|
-
/// stages.
|
|
12
|
+
/// stages.
|
|
13
|
+
/// @custom:member autoIssuances Tokens to mint without payment during this stage (per-chain, per-beneficiary).
|
|
14
14
|
/// @custom:member splitPercent The percentage of newly issued tokens routed to splits, out of 10,000.
|
|
15
15
|
/// @custom:member splits The split recipients for this stage's production allocation.
|
|
16
16
|
/// @custom:member initialIssuance Tokens per unit of base currency at stage start (18-decimal fixed point).
|
|
@@ -3,8 +3,8 @@ pragma solidity ^0.8.0;
|
|
|
3
3
|
|
|
4
4
|
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
5
5
|
|
|
6
|
-
/// @custom:member deployerConfigurations The
|
|
7
|
-
/// @custom:member salt
|
|
6
|
+
/// @custom:member deployerConfigurations The sucker deployers and token bridge mappings to configure.
|
|
7
|
+
/// @custom:member salt Extra caller-provided entropy mixed with the revnet config hash and deployer caller.
|
|
8
8
|
struct REVSuckerDeploymentConfig {
|
|
9
9
|
JBSuckerDeployerConfig[] deployerConfigurations;
|
|
10
10
|
bytes32 salt;
|