@rev-net/core-v6 0.0.4 → 0.0.5
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 +149 -29
- package/SKILLS.md +189 -101
- package/package.json +1 -1
- package/src/REVDeployer.sol +9 -4
- package/src/REVLoans.sol +5 -1
- package/test/TestEmptyBuybackSpecs.t.sol +237 -0
- package/test/TestStageTransitionBorrowable.t.sol +241 -0
- package/test/mock/MockBuybackDataHookMintPath.sol +61 -0
package/README.md
CHANGED
|
@@ -1,53 +1,88 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Revnet Core
|
|
2
2
|
|
|
3
3
|
Deploy and operate Revnets: unowned Juicebox projects that run autonomously according to predefined stages, with built-in token-collateralized loans.
|
|
4
4
|
|
|
5
|
+
[Docs](https://docs.juicebox.money) | [Discord](https://discord.gg/nT3XqbzNEr)
|
|
6
|
+
|
|
5
7
|
## What is a Revnet?
|
|
6
8
|
|
|
7
|
-
A Revnet is a
|
|
9
|
+
A Revnet is a treasury-backed token that runs itself. No owners, no governors, no multisigs. Once deployed, a revnet follows its predefined stages forever, backed by the Juicebox and Uniswap protocols.
|
|
10
|
+
|
|
11
|
+
## Conceptual Overview
|
|
12
|
+
|
|
13
|
+
Revnets are autonomous Juicebox projects with predetermined economic stages. Each stage defines issuance rates, decay schedules, cash-out taxes, reserved splits, and auto-issuance allocations. Once deployed, these parameters cannot be changed -- the revnet operates on its own forever.
|
|
14
|
+
|
|
15
|
+
### Stage-Based Lifecycle
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
1. Deploy revnet with stage configurations
|
|
19
|
+
→ REVDeployer.deployFor(revnetId=0, config, terminals, ...)
|
|
20
|
+
→ Creates Juicebox project owned by REVDeployer (permanently)
|
|
21
|
+
→ Deploys ERC-20 token, configures buyback pools, deploys suckers
|
|
22
|
+
|
|
|
23
|
+
2. Stage 1 begins (startsAtOrAfter or block.timestamp)
|
|
24
|
+
→ Tokens issued at initialIssuance rate per unit of base currency
|
|
25
|
+
→ Issuance decays by issuanceCutPercent every issuanceCutFrequency
|
|
26
|
+
→ splitPercent of new tokens go to reserved splits
|
|
27
|
+
→ Cash outs taxed at cashOutTaxRate (2.5% fee to fee revnet)
|
|
28
|
+
|
|
|
29
|
+
3. Stage transitions happen automatically
|
|
30
|
+
→ When startsAtOrAfter timestamp is reached for next stage
|
|
31
|
+
→ New issuance rate, tax rate, splits, etc. take effect
|
|
32
|
+
→ No governance, no votes, no owner action required
|
|
33
|
+
|
|
|
34
|
+
4. Participants can borrow against their tokens
|
|
35
|
+
→ REVLoans.borrowFrom(revnetId, source, collateral, ...)
|
|
36
|
+
→ Collateral tokens are burned, funds pulled from treasury
|
|
37
|
+
→ Loan is an ERC-721 NFT, liquidates after 10 years
|
|
38
|
+
|
|
|
39
|
+
5. Ongoing operations (permissionless or split operator)
|
|
40
|
+
→ Auto-issuance claims (permissionless)
|
|
41
|
+
→ Buyback pool configuration (split operator)
|
|
42
|
+
→ Sucker deployment (split operator, if ruleset allows)
|
|
43
|
+
→ Split group updates (split operator)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Token-Collateralized Loans
|
|
8
47
|
|
|
9
|
-
|
|
48
|
+
`REVLoans` lets participants borrow against their revnet tokens. Unlike traditional lending:
|
|
10
49
|
|
|
11
|
-
|
|
50
|
+
- **Collateral is burned, not held.** Tokens are destroyed on borrow and re-minted on repay. This keeps the token supply accurate -- collateral tokens don't exist during the loan.
|
|
51
|
+
- **Borrowable amount = cash-out value.** The bonding curve determines how much you can borrow for a given amount of collateral.
|
|
52
|
+
- **Prepaid fee model.** Borrowers choose a prepaid fee (2.5%-50%) that buys an interest-free window. After that window, a time-proportional source fee accrues.
|
|
53
|
+
- **Each loan is an ERC-721 NFT.** Loans can be transferred, and expired loans (10 years) can be liquidated by anyone.
|
|
12
54
|
|
|
13
|
-
|
|
14
|
-
- [Modeling Retailism](https://jango.eth.limo/B762F3CC-AEFE-4DE0-B08C-7C16400AF718/)
|
|
15
|
-
- [Retailism for Devs, Investors, and Customers](https://jango.eth.limo/3EB05292-0376-4B7D-AFCF-042B70673C3D/)
|
|
16
|
-
- [Observations: Network dynamics similar between atoms, cells, organisms, groups, dance parties](https://jango.eth.limo/CF40F5D2-7BFE-43A3-9C15-1C6547FBD15C/)
|
|
55
|
+
### Deployer Variants
|
|
17
56
|
|
|
18
|
-
|
|
57
|
+
- **Basic revnet** -- `deployFor` with stage configurations mapped to Juicebox rulesets.
|
|
58
|
+
- **Tiered 721 revnet** -- `deployWith721sFor` adds a tiered 721 pay hook that mints NFTs as people pay.
|
|
59
|
+
- **Croptop revnet** -- A tiered 721 revnet with Croptop posting criteria, allowing the public to post content.
|
|
19
60
|
|
|
20
61
|
## Architecture
|
|
21
62
|
|
|
22
63
|
| Contract | Description |
|
|
23
64
|
|----------|-------------|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
65
|
+
| `REVDeployer` | Deploys revnets as Juicebox projects owned by the deployer contract itself (no human owner). Translates stage configurations into Juicebox rulesets, manages buyback hooks, tiered 721 hooks, suckers, split operators, auto-issuance, and cash-out fees. Acts as the ruleset data hook and cash-out hook for every revnet it deploys. |
|
|
66
|
+
| `REVLoans` | Lets participants borrow against their revnet tokens. Collateral tokens are burned on borrow and re-minted on repayment. Each loan is an ERC-721 NFT. Charges a prepaid fee (2.5% min, 50% max) that determines the interest-free duration; after that window, a time-proportional source fee accrues. Loans liquidate after 10 years. |
|
|
26
67
|
|
|
27
|
-
### How
|
|
68
|
+
### How They Relate
|
|
28
69
|
|
|
29
70
|
`REVDeployer` owns every revnet's Juicebox project NFT and holds all administrative permissions. During deployment it grants `REVLoans` the `USE_ALLOWANCE` permission so loans can pull funds from the revnet's terminal. `REVLoans` verifies that a revnet was deployed by its expected `REVDeployer` before issuing any loan.
|
|
30
71
|
|
|
31
|
-
###
|
|
32
|
-
|
|
33
|
-
This repo includes several deployer patterns for different use cases:
|
|
34
|
-
|
|
35
|
-
- **Basic revnet** — Deploy a simple revnet with `REVDeployer` using stage configurations that map to Juicebox rulesets.
|
|
36
|
-
- **Pay hook revnet** — Accept additional pay hooks that run throughout the revnet's lifetime as it receives payments.
|
|
37
|
-
- **Tiered 721 revnet** — Deploy a tiered 721 pay hook (NFT tiers) that mints NFTs as people pay into the revnet.
|
|
38
|
-
- **Croptop revnet** — A tiered 721 revnet where the public can post content through the [Croptop](https://croptop.eth.limo) publisher contract.
|
|
72
|
+
### Interfaces
|
|
39
73
|
|
|
40
|
-
|
|
74
|
+
| Interface | Description |
|
|
75
|
+
|-----------|-------------|
|
|
76
|
+
| `IREVDeployer` | Deployment, data hooks, auto-issuance, split operator management, sucker deployment, plus events. |
|
|
77
|
+
| `IREVLoans` | Borrow, repay, refinance, liquidate, views, plus events. |
|
|
41
78
|
|
|
42
79
|
## Install
|
|
43
80
|
|
|
44
81
|
```bash
|
|
45
|
-
npm install @rev-net/core-
|
|
82
|
+
npm install @rev-net/core-v6
|
|
46
83
|
```
|
|
47
84
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
This repo uses [npm](https://www.npmjs.com/) for package management and [Foundry](https://github.com/foundry-rs/foundry) for builds and tests.
|
|
85
|
+
If using Forge directly:
|
|
51
86
|
|
|
52
87
|
```bash
|
|
53
88
|
npm install && forge install
|
|
@@ -55,11 +90,96 @@ npm install && forge install
|
|
|
55
90
|
|
|
56
91
|
If `forge install` has issues, try `git submodule update --init --recursive`.
|
|
57
92
|
|
|
93
|
+
## Develop
|
|
94
|
+
|
|
58
95
|
| Command | Description |
|
|
59
96
|
|---------|-------------|
|
|
60
97
|
| `forge build` | Compile contracts |
|
|
61
|
-
| `forge test` | Run tests |
|
|
98
|
+
| `forge test` | Run tests (20+ test files covering deployment, lifecycle, loans, attacks, invariants) |
|
|
62
99
|
| `forge test -vvvv` | Run tests with full traces |
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
100
|
+
|
|
101
|
+
## Repository Layout
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
src/
|
|
105
|
+
REVDeployer.sol # Revnet deployer + data hook (~1,256 lines)
|
|
106
|
+
REVLoans.sol # Token-collateralized lending (~1,333 lines)
|
|
107
|
+
interfaces/
|
|
108
|
+
IREVDeployer.sol # Deployer interface + events
|
|
109
|
+
IREVLoans.sol # Loans interface + events
|
|
110
|
+
structs/
|
|
111
|
+
REVConfig.sol # Top-level deployment config
|
|
112
|
+
REVDescription.sol # ERC-20 metadata (name, ticker, uri, salt)
|
|
113
|
+
REVStageConfig.sol # Economic stage parameters
|
|
114
|
+
REVAutoIssuance.sol # Per-stage cross-chain premint
|
|
115
|
+
REVLoan.sol # Loan state
|
|
116
|
+
REVLoanSource.sol # Terminal + token pair for loans
|
|
117
|
+
REVDeploy721TiersHookConfig.sol # 721 hook deployment config
|
|
118
|
+
REVCroptopAllowedPost.sol # Croptop posting criteria
|
|
119
|
+
REVSuckerDeploymentConfig.sol # Cross-chain sucker deployment
|
|
120
|
+
test/
|
|
121
|
+
REV.integrations.t.sol # Deployment, payments, cash-outs
|
|
122
|
+
REVLifecycle.t.sol # Stage transitions, weight decay
|
|
123
|
+
REVAutoIssuanceFuzz.t.sol # Auto-issuance fuzz tests
|
|
124
|
+
REVInvincibility.t.sol # Economic property fuzzing
|
|
125
|
+
REVInvincibilityHandler.sol # Fuzz handler
|
|
126
|
+
REVDeployerAuditRegressions.t.sol # Deployer audit regressions
|
|
127
|
+
REVLoansSourced.t.sol # Multi-source loan tests
|
|
128
|
+
REVLoansUnSourced.t.sol # Loan error cases
|
|
129
|
+
REVLoansFeeRecovery.t.sol # Fee calculation tests
|
|
130
|
+
REVLoansAttacks.t.sol # Flash loan, reentrancy scenarios
|
|
131
|
+
REVLoans.invariants.t.sol # Loan fuzzing invariants
|
|
132
|
+
REVLoansAuditRegressions.t.sol # Loan audit regressions
|
|
133
|
+
TestPR09-32_*.t.sol # Per-PR regression tests
|
|
134
|
+
helpers/
|
|
135
|
+
MaliciousContracts.sol # Attack contract mocks
|
|
136
|
+
mock/
|
|
137
|
+
MockBuybackDataHook.sol # Mock for buyback hook tests
|
|
138
|
+
script/
|
|
139
|
+
Deploy.s.sol # Sphinx multi-chain deployment
|
|
140
|
+
helpers/
|
|
141
|
+
RevnetCoreDeploymentLib.sol # Deployment artifact reader
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Permissions
|
|
145
|
+
|
|
146
|
+
### Split Operator (per-revnet)
|
|
147
|
+
|
|
148
|
+
The split operator has these default permissions:
|
|
149
|
+
|
|
150
|
+
| Permission | Purpose |
|
|
151
|
+
|------------|---------|
|
|
152
|
+
| `SET_SPLIT_GROUPS` | Change reserved token splits |
|
|
153
|
+
| `SET_BUYBACK_POOL` | Configure Uniswap buyback pool |
|
|
154
|
+
| `SET_BUYBACK_TWAP` | Adjust TWAP window for buyback |
|
|
155
|
+
| `SET_PROJECT_URI` | Update project metadata |
|
|
156
|
+
| `ADD_PRICE_FEED` | Add price oracle |
|
|
157
|
+
| `SUCKER_SAFETY` | Emergency sucker functions |
|
|
158
|
+
| `SET_BUYBACK_HOOK` | Swap buyback hook |
|
|
159
|
+
| `SET_ROUTER_TERMINAL` | Swap terminal |
|
|
160
|
+
|
|
161
|
+
Plus optional from 721 hook config: `ADJUST_721_TIERS`, `SET_721_METADATA`, `MINT_721`, `SET_721_DISCOUNT_PERCENT`.
|
|
162
|
+
|
|
163
|
+
### Global Permissions
|
|
164
|
+
|
|
165
|
+
| Grantee | Permission | Scope |
|
|
166
|
+
|---------|------------|-------|
|
|
167
|
+
| `SUCKER_REGISTRY` | `MAP_SUCKER_TOKEN` | All revnets (wildcard projectId=0) |
|
|
168
|
+
| `REVLoans` | `USE_ALLOWANCE` | All revnets (wildcard projectId=0) |
|
|
169
|
+
|
|
170
|
+
### Permissionless Operations
|
|
171
|
+
|
|
172
|
+
- `autoIssueFor` -- claim auto-issuance tokens (anyone)
|
|
173
|
+
- `burnHeldTokensOf` -- burn reserved tokens held by deployer (anyone)
|
|
174
|
+
|
|
175
|
+
## Risks
|
|
176
|
+
|
|
177
|
+
- **No human owner.** `REVDeployer` permanently holds the project NFT. There is no function to release it. This is by design -- revnets are ownerless. But it means bugs in stage configurations cannot be fixed after deployment.
|
|
178
|
+
- **Loan flash-loan exposure.** `borrowableAmountFrom` reads live surplus, which can be inflated via flash loans. A borrower could temporarily inflate the treasury to borrow more than the sustained value would support.
|
|
179
|
+
- **uint112 truncation.** `REVLoan.amount` and `REVLoan.collateral` are `uint112` -- values above ~5.19e33 truncate silently.
|
|
180
|
+
- **Cash-out fee stacking.** Cash outs incur both the Juicebox terminal fee (2.5%) and the revnet cash-out fee (2.5% to fee revnet). These compound.
|
|
181
|
+
- **Auto-issuance stage ID mismatch.** Stage IDs are computed as `block.timestamp + i` during deployment, but actual Juicebox ruleset IDs depend on queuing logic. If timestamps don't align, auto-issuance for later stages may be unclaimed.
|
|
182
|
+
- **NATIVE_TOKEN on non-ETH chains.** Using `JBConstants.NATIVE_TOKEN` on Celo or Polygon means CELO/MATIC, not ETH. Use ERC-20 WETH instead. The matching hash does NOT catch this -- it covers economic parameters but NOT terminal configurations.
|
|
183
|
+
- **30-day cash-out delay.** When deploying an existing revnet to a new chain where the first stage has already started, a 30-day delay is imposed before cash outs are allowed, preventing cross-chain liquidity arbitrage.
|
|
184
|
+
- **Loan source array growth.** `_loanSourcesOf[revnetId]` is unbounded. If an attacker creates loans from many different terminals/tokens, the array grows without limit.
|
|
185
|
+
- **10-year loan liquidation.** Expired loans (10+ years) can be liquidated by anyone. The burned collateral is permanently lost -- it was destroyed at borrow time.
|
package/SKILLS.md
CHANGED
|
@@ -1,45 +1,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Revnet Core
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
4
|
|
|
5
|
-
Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged issuance schedules and token-collateralized lending.
|
|
5
|
+
Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged issuance schedules, built-in Uniswap buyback pools, cross-chain suckers, and token-collateralized lending.
|
|
6
6
|
|
|
7
7
|
## Contracts
|
|
8
8
|
|
|
9
9
|
| Contract | Role |
|
|
10
10
|
|----------|------|
|
|
11
|
-
| `REVDeployer` | Deploys revnets,
|
|
12
|
-
| `REVLoans` | Issues token-collateralized loans from revnet treasuries. Each loan is an ERC-721. Burns collateral on borrow, re-mints on repay. Charges tiered fees (REV protocol fee + source fee + prepaid fee). |
|
|
11
|
+
| `REVDeployer` | Deploys revnets, permanently owns the project NFT, acts as data hook and cash-out hook. Manages stages, splits, auto-issuance, buyback hooks, 721 hooks, suckers, and split operators. (~1,256 lines) |
|
|
12
|
+
| `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). (~1,333 lines) |
|
|
13
13
|
|
|
14
14
|
## Key Functions
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
|
19
|
-
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `deploySuckersFor` |
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
|
|
16
|
+
### Deployment
|
|
17
|
+
|
|
18
|
+
| Function | What it does |
|
|
19
|
+
|----------|-------------|
|
|
20
|
+
| `REVDeployer.deployFor(revnetId, config, terminals, buybackHookConfig, suckerConfig)` | Deploy a new revnet (`revnetId=0`) or convert an existing Juicebox project. Encodes stage configs into rulesets, deploys ERC-20 token, sets up split operator, buyback pools, suckers, and loans permissions. |
|
|
21
|
+
| `REVDeployer.deployWith721sFor(revnetId, config, terminals, buybackHookConfig, suckerConfig, hookConfig, allowedPosts)` | Same as `deployFor` but also deploys a tiered ERC-721 hook. Optionally configures Croptop posting criteria and grants publisher permission to add tiers. |
|
|
22
|
+
| `REVDeployer.deploySuckersFor(revnetId, suckerConfig)` | Deploy new cross-chain suckers post-launch. Split operator only. Validates ruleset allows sucker deployment (bit 2 of `extraMetadata`). Uses stored config hash for cross-chain matching. |
|
|
23
|
+
|
|
24
|
+
### Data Hooks
|
|
25
|
+
|
|
26
|
+
| Function | What it does |
|
|
27
|
+
|----------|-------------|
|
|
28
|
+
| `REVDeployer.beforePayRecordedWith(context)` | Returns adjusted weight from buyback hook and assembles pay hook specs (721 hook if present + buyback hook). |
|
|
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. |
|
|
32
|
+
|
|
33
|
+
### Split Operator
|
|
34
|
+
|
|
35
|
+
| Function | What it does |
|
|
36
|
+
|----------|-------------|
|
|
37
|
+
| `REVDeployer.setSplitOperatorOf(revnetId, newOperator)` | Replace the current split operator. Only callable by the current split operator. Revokes old permissions, grants new ones. |
|
|
38
|
+
|
|
39
|
+
### Auto-Issuance
|
|
40
|
+
|
|
41
|
+
| Function | 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
|
+
|
|
46
|
+
### Loans -- Borrowing
|
|
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, maxRepayAmount, collateralToReturn, beneficiary, allowance)` | 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, collateralToRemove, source, minBorrowAmount, collateralToAdd, beneficiary, prepaidFeePercent)` | 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
|
+
|
|
55
|
+
### Loans -- Views
|
|
56
|
+
|
|
57
|
+
| Function | What it does |
|
|
58
|
+
|----------|-------------|
|
|
59
|
+
| `REVLoans.borrowableAmountFrom(revnetId, collateralCount, decimals, currency)` | Calculate how much can be borrowed for a given collateral amount. Aggregates surplus from all terminals, applies bonding curve. |
|
|
60
|
+
| `REVLoans.determineSourceFeeAmount(loan, amount)` | Calculate the time-proportional source fee for a loan repayment. Zero during prepaid window, linear accrual after. |
|
|
61
|
+
| `REVLoans.loanOf(loanId)` | Returns the full `REVLoan` struct for a loan. |
|
|
62
|
+
| `REVLoans.loanSourcesOf(revnetId)` | Returns all `(terminal, token)` pairs used for loans by a revnet. |
|
|
63
|
+
| `REVLoans.revnetIdOfLoanWith(loanId)` | Decode the revnet ID from a loan ID (`loanId / 1_000_000_000_000`). |
|
|
33
64
|
|
|
34
65
|
## Integration Points
|
|
35
66
|
|
|
36
67
|
| Dependency | Import | Used For |
|
|
37
68
|
|------------|--------|----------|
|
|
38
|
-
| `@bananapus/core-v6` | `IJBController`, `IJBDirectory`, `IJBPermissions`, `IJBProjects`, `IJBTerminal`, `IJBPrices` | Project lifecycle, rulesets, token minting/burning, fund access, terminal payments, price feeds |
|
|
69
|
+
| `@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 |
|
|
39
70
|
| `@bananapus/721-hook-v6` | `IJB721TiersHook`, `IJB721TiersHookDeployer` | Deploying and registering tiered ERC-721 pay hooks |
|
|
40
71
|
| `@bananapus/buyback-hook-v6` | `IJBBuybackHook` | Configuring Uniswap buyback pools per revnet |
|
|
41
72
|
| `@bananapus/suckers-v6` | `IJBSuckerRegistry` | Deploying cross-chain suckers, checking sucker status for fee exemption |
|
|
42
|
-
| `@croptop/core-v6` | `CTPublisher` | Configuring
|
|
73
|
+
| `@croptop/core-v6` | `CTPublisher` | Configuring Croptop posting criteria for 721 tiers |
|
|
43
74
|
| `@bananapus/permission-ids-v6` | `JBPermissionIds` | Permission ID constants (SET_SPLIT_GROUPS, USE_ALLOWANCE, etc.) |
|
|
44
75
|
| `@openzeppelin/contracts` | `ERC721`, `ERC2771Context`, `Ownable`, `SafeERC20` | Loan NFTs, meta-transactions, ownership, safe token transfers |
|
|
45
76
|
| `@uniswap/permit2` | `IPermit2`, `IAllowanceTransfer` | Gasless token approvals for loan repayments |
|
|
@@ -47,45 +78,86 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
47
78
|
|
|
48
79
|
## Key Types
|
|
49
80
|
|
|
50
|
-
| Struct
|
|
51
|
-
|
|
52
|
-
| `REVConfig` | `description
|
|
53
|
-
| `REVStageConfig` | `startsAtOrAfter` (uint48), `initialIssuance` (uint112), `issuanceCutFrequency` (uint32), `issuanceCutPercent` (uint32), `cashOutTaxRate` (uint16), `splitPercent` (uint16), `splits[]`, `autoIssuances[]`, `extraMetadata` (uint16) | Translated into `JBRulesetConfig`
|
|
81
|
+
| Struct | Key Fields | Used In |
|
|
82
|
+
|--------|------------|---------|
|
|
83
|
+
| `REVConfig` | `description` (REVDescription), `baseCurrency`, `splitOperator`, `stageConfigurations[]`, `loanSources[]`, `loans` | `deployFor`, `deployWith721sFor` |
|
|
84
|
+
| `REVStageConfig` | `startsAtOrAfter` (uint48), `initialIssuance` (uint112), `issuanceCutFrequency` (uint32), `issuanceCutPercent` (uint32), `cashOutTaxRate` (uint16), `splitPercent` (uint16), `splits[]`, `autoIssuances[]`, `extraMetadata` (uint16) | Translated into `JBRulesetConfig` |
|
|
54
85
|
| `REVDescription` | `name`, `ticker`, `uri`, `salt` | ERC-20 token deployment and project metadata |
|
|
55
|
-
| `REVAutoIssuance` | `chainId` (uint32), `count` (uint104), `beneficiary` | Per-stage token auto-minting
|
|
56
|
-
| `REVLoan` | `amount` (uint112), `collateral` (uint112), `createdAt` (uint48), `prepaidFeePercent` (uint16), `prepaidDuration` (uint32), `source` |
|
|
86
|
+
| `REVAutoIssuance` | `chainId` (uint32), `count` (uint104), `beneficiary` | Per-stage cross-chain token auto-minting |
|
|
87
|
+
| `REVLoan` | `amount` (uint112), `collateral` (uint112), `createdAt` (uint48), `prepaidFeePercent` (uint16), `prepaidDuration` (uint32), `source` (REVLoanSource) | Per-loan state in `REVLoans` |
|
|
57
88
|
| `REVLoanSource` | `token`, `terminal` (IJBPayoutTerminal) | Identifies which terminal and token a loan draws from |
|
|
58
|
-
| `
|
|
59
|
-
| `REVBuybackPoolConfig` | `token`, `fee` (uint24), `twapWindow` (uint32) | Uniswap pool configuration for buyback |
|
|
60
|
-
| `REVSuckerDeploymentConfig` | `deployerConfigurations[]`, `salt` | Cross-chain sucker deployment |
|
|
61
|
-
| `REVDeploy721TiersHookConfig` | `baseline721HookConfiguration`, `salt`, `splitOperatorCanAdjustTiers`, `splitOperatorCanUpdateMetadata`, `splitOperatorCanMint`, `splitOperatorCanIncreaseDiscountPercent` | 721 hook deployment with operator permissions |
|
|
89
|
+
| `REVDeploy721TiersHookConfig` | `baseline721HookConfiguration`, `salt`, `splitOperatorCanAdjustTiers`, `CanUpdateMetadata`, `CanMint`, `CanIncreaseDiscountPercent` | 721 hook deployment with operator permissions |
|
|
62
90
|
| `REVCroptopAllowedPost` | `category` (uint24), `minimumPrice` (uint104), `minimumTotalSupply` (uint32), `maximumTotalSupply` (uint32), `allowedAddresses[]` | Croptop posting criteria |
|
|
91
|
+
| `REVSuckerDeploymentConfig` | `deployerConfigurations[]`, `salt` | Cross-chain sucker deployment |
|
|
63
92
|
|
|
64
|
-
##
|
|
93
|
+
## Constants
|
|
94
|
+
|
|
95
|
+
### REVDeployer
|
|
96
|
+
|
|
97
|
+
| Constant | Value | Purpose |
|
|
98
|
+
|----------|-------|---------|
|
|
99
|
+
| `CASH_OUT_DELAY` | 2,592,000 (30 days) | Prevents cross-chain liquidity arbitrage on new chain deployments |
|
|
100
|
+
| `FEE` | 25 (of MAX_FEE=1000) | 2.5% cash-out fee paid to fee revnet |
|
|
101
|
+
| `DEFAULT_BUYBACK_POOL_FEE` | 10,000 | 1% Uniswap fee tier for default buyback pools |
|
|
102
|
+
| `DEFAULT_BUYBACK_TWAP_WINDOW` | 2 days | TWAP observation window for buyback price |
|
|
103
|
+
|
|
104
|
+
### REVLoans
|
|
65
105
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
- Cash out delay is 30 days (`CASH_OUT_DELAY = 2_592_000`), applied only when deploying an existing revnet to a new chain (first stage already started).
|
|
74
|
-
- Loan IDs encode the revnet ID: `loanId = revnetId * 1_000_000_000_000 + loanNumber`. Use `revnetIdOfLoanWith(loanId)` to decode.
|
|
75
|
-
- Loan liquidation duration is 10 years (`LOAN_LIQUIDATION_DURATION = 3650 days`). After this, collateral is forfeit.
|
|
76
|
-
- The split operator has 6 default permissions (SET_SPLIT_GROUPS, SET_BUYBACK_POOL, SET_BUYBACK_TWAP, SET_PROJECT_URI, ADD_PRICE_FEED, SUCKER_SAFETY) plus any extras from 721 hook config.
|
|
77
|
-
- `REVLoans` uses `permit2` for ERC-20 transfers as a fallback when standard allowance is insufficient.
|
|
78
|
-
- The `FEE` constant is `25` (out of `MAX_FEE = 1000`), meaning a 2.5% cash out fee paid to the fee revnet.
|
|
79
|
-
- `REV_PREPAID_FEE_PERCENT` is `10` (1%) -- this is the protocol-level fee on loans paid to the $REV revnet.
|
|
80
|
-
- `MIN_PREPAID_FEE_PERCENT` is `25` (2.5%) and `MAX_PREPAID_FEE_PERCENT` is `500` (50%) -- bounds on the borrower-chosen prepaid fee.
|
|
106
|
+
| Constant | Value | Purpose |
|
|
107
|
+
|----------|-------|---------|
|
|
108
|
+
| `LOAN_LIQUIDATION_DURATION` | 3,650 days (10 years) | After this, collateral is forfeit |
|
|
109
|
+
| `MIN_PREPAID_FEE_PERCENT` | 25 (2.5%) | Minimum upfront fee borrowers must pay |
|
|
110
|
+
| `MAX_PREPAID_FEE_PERCENT` | 500 (50%) | Maximum upfront fee |
|
|
111
|
+
| `REV_PREPAID_FEE_PERCENT` | 10 (1%) | Protocol-level fee to $REV revnet |
|
|
112
|
+
| `_ONE_TRILLION` | 1,000,000,000,000 | Loan ID generator base: `revnetId * 1T + loanNumber` |
|
|
81
113
|
|
|
82
|
-
|
|
114
|
+
## Storage
|
|
83
115
|
|
|
84
|
-
|
|
116
|
+
### REVDeployer
|
|
85
117
|
|
|
86
|
-
|
|
118
|
+
| Mapping | Type | Purpose |
|
|
119
|
+
|---------|------|---------|
|
|
120
|
+
| `amountToAutoIssue` | `revnetId => stageId => beneficiary => uint256` | Premint tokens per stage per beneficiary |
|
|
121
|
+
| `cashOutDelayOf` | `revnetId => uint256` | Timestamp when cash outs unlock (0 = no delay) |
|
|
122
|
+
| `hashedEncodedConfigurationOf` | `revnetId => bytes32` | Config hash for cross-chain sucker validation |
|
|
123
|
+
| `tiered721HookOf` | `revnetId => address` | Deployed 721 hook address (if any) |
|
|
124
|
+
| `_extraOperatorPermissions` | `revnetId => uint256[]` | Custom permissions for split operator |
|
|
87
125
|
|
|
88
|
-
|
|
126
|
+
### REVLoans
|
|
127
|
+
|
|
128
|
+
| Mapping | Type | Purpose |
|
|
129
|
+
|---------|------|---------|
|
|
130
|
+
| `isLoanSourceOf` | `revnetId => terminal => token => bool` | Is this (terminal, token) pair used for loans? |
|
|
131
|
+
| `numberOfLoansFor` | `revnetId => uint256` | Counter for loan numbering |
|
|
132
|
+
| `totalBorrowedFrom` | `revnetId => terminal => token => uint256` | Tracks debt per loan source |
|
|
133
|
+
| `totalCollateralOf` | `revnetId => uint256` | Sum of all burned collateral |
|
|
134
|
+
| `_loanOf` | `loanId => REVLoan` | Per-loan state |
|
|
135
|
+
| `_loanSourcesOf` | `revnetId => REVLoanSource[]` | Array of all loan sources used |
|
|
136
|
+
|
|
137
|
+
## Gotchas
|
|
138
|
+
|
|
139
|
+
1. **Revnets are permanently ownerless.** `REVDeployer` holds the project NFT forever. There is no function to release it. Stage parameters cannot be changed after deployment.
|
|
140
|
+
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.
|
|
141
|
+
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.
|
|
142
|
+
4. **Loan ID encoding.** `loanId = revnetId * 1_000_000_000_000 + loanNumber`. Each revnet supports ~1 trillion loans. Use `revnetIdOfLoanWith(loanId)` to decode.
|
|
143
|
+
5. **uint112 truncation risk.** `REVLoan.amount` and `REVLoan.collateral` are `uint112`. Values above ~5.19e33 truncate silently.
|
|
144
|
+
6. **Auto-issuance stage IDs.** Computed as `block.timestamp + i` during deployment, but actual Juicebox ruleset IDs depend on queuing logic. Stage 1+ auto-issuance may be unclaimed if IDs don't match exactly.
|
|
145
|
+
7. **Cash-out fee stacking.** Cash outs incur both the Juicebox terminal fee (2.5%) and the revnet cash-out fee (2.5% to fee revnet). These compound.
|
|
146
|
+
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.
|
|
147
|
+
9. **`cashOutTaxRate` cannot be MAX.** Must be strictly less than `MAX_CASH_OUT_TAX_RATE` (10,000). Revnets cannot fully disable cash outs.
|
|
148
|
+
10. **Split operator is singular.** Only ONE address can be split operator at a time. The operator can replace itself via `setSplitOperatorOf` but cannot delegate or multi-sig.
|
|
149
|
+
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 configuration differences.
|
|
150
|
+
12. **Loan source array is unbounded.** `_loanSourcesOf[revnetId]` grows without limit. No validation that a terminal is actually registered for the project.
|
|
151
|
+
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.
|
|
152
|
+
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).
|
|
153
|
+
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.
|
|
154
|
+
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.
|
|
155
|
+
17. **Loan fee model.** Three layers: (1) REV protocol fee (1%) taken when funds pulled, (2) terminal fee (2.5%) charged by `useAllowanceOf`, (3) prepaid source fee (2.5%-50%, borrower-chosen) that buys an interest-free window. After the prepaid window, time-proportional source fee accrues linearly over the remaining 10-year loan duration.
|
|
156
|
+
18. **Permit2 fallback.** `REVLoans` uses permit2 for ERC-20 transfers as a fallback when standard allowance is insufficient. Wrapped in try-catch.
|
|
157
|
+
|
|
158
|
+
### NATIVE_TOKEN Accounting on Non-ETH Chains
|
|
159
|
+
|
|
160
|
+
When deploying to a chain where the native token is NOT ETH (Celo, Polygon), the terminal must NOT use `JBConstants.NATIVE_TOKEN` as its accounting context. `NATIVE_TOKEN` represents whatever is native on that chain, but `baseCurrency=1` (ETH) assumes ETH-denominated value.
|
|
89
161
|
|
|
90
162
|
**Correct (Celo):**
|
|
91
163
|
```solidity
|
|
@@ -105,8 +177,6 @@ JBAccountingContext({
|
|
|
105
177
|
})
|
|
106
178
|
```
|
|
107
179
|
|
|
108
|
-
See also: `SECURITY.md` in this repo and INTEROP-6 in `AUDIT_FINDINGS.md`.
|
|
109
|
-
|
|
110
180
|
## Example Integration
|
|
111
181
|
|
|
112
182
|
```solidity
|
|
@@ -117,50 +187,68 @@ import {REVBuybackHookConfig} from "@rev-net/core-v6/src/structs/REVBuybackHookC
|
|
|
117
187
|
import {REVSuckerDeploymentConfig} from "@rev-net/core-v6/src/structs/REVSuckerDeploymentConfig.sol";
|
|
118
188
|
import {IREVDeployer} from "@rev-net/core-v6/src/interfaces/IREVDeployer.sol";
|
|
119
189
|
|
|
120
|
-
// Deploy a simple revnet with one stage
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
190
|
+
// --- Deploy a simple revnet with one stage ---
|
|
191
|
+
|
|
192
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
193
|
+
stages[0] = REVStageConfig({
|
|
194
|
+
startsAtOrAfter: 0, // Start immediately (uses block.timestamp)
|
|
195
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
196
|
+
splitPercent: 2000, // 20% of new tokens go to splits
|
|
197
|
+
splits: splits, // Reserved token split destinations
|
|
198
|
+
initialIssuance: 1_000_000e18, // 1M tokens per unit of base currency
|
|
199
|
+
issuanceCutFrequency: 30 days, // Decay period
|
|
200
|
+
issuanceCutPercent: 100_000_000, // 10% cut per period (out of 1e9)
|
|
201
|
+
cashOutTaxRate: 2000, // 20% tax on cash outs
|
|
202
|
+
extraMetadata: 0
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
REVConfig memory config = REVConfig({
|
|
206
|
+
description: REVDescription({
|
|
207
|
+
name: "My Revnet Token",
|
|
208
|
+
ticker: "MYREV",
|
|
209
|
+
uri: "ipfs://...",
|
|
210
|
+
salt: bytes32(0)
|
|
211
|
+
}),
|
|
212
|
+
baseCurrency: 1, // USD
|
|
213
|
+
splitOperator: msg.sender,
|
|
214
|
+
stageConfigurations: stages,
|
|
215
|
+
loanSources: new REVLoanSource[](0),
|
|
216
|
+
loans: address(0) // No loans contract
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
deployer.deployFor({
|
|
220
|
+
revnetId: 0, // 0 = deploy new
|
|
221
|
+
configuration: config,
|
|
222
|
+
terminalConfigurations: terminals,
|
|
223
|
+
buybackHookConfiguration: REVBuybackHookConfig({
|
|
224
|
+
dataHook: IJBRulesetDataHook(address(0)),
|
|
225
|
+
hookToConfigure: IJBBuybackHook(address(0)),
|
|
226
|
+
poolConfigurations: new REVBuybackPoolConfig[](0)
|
|
227
|
+
}),
|
|
228
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
229
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0),
|
|
230
|
+
salt: bytes32(0)
|
|
231
|
+
})
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// --- Borrow against revnet tokens ---
|
|
235
|
+
|
|
236
|
+
loans.borrowFrom({
|
|
237
|
+
revnetId: revnetId,
|
|
238
|
+
source: REVLoanSource({ token: JBConstants.NATIVE_TOKEN, terminal: terminal }),
|
|
239
|
+
minBorrowAmount: 0,
|
|
240
|
+
collateralCount: 1000e18, // Burn 1000 tokens as collateral
|
|
241
|
+
beneficiary: msg.sender, // Receive borrowed funds
|
|
242
|
+
prepaidFeePercent: 25 // 2.5% prepaid fee (minimum)
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// --- Repay a loan ---
|
|
246
|
+
|
|
247
|
+
loans.repayLoan({
|
|
248
|
+
loanId: loanId,
|
|
249
|
+
maxRepayAmount: type(uint256).max, // Repay in full
|
|
250
|
+
collateralToReturn: loan.collateral, // Return all collateral
|
|
251
|
+
beneficiary: msg.sender, // Receive re-minted tokens
|
|
252
|
+
allowance: IAllowanceTransfer.PermitSingle({ ... }) // Optional permit2
|
|
253
|
+
});
|
|
166
254
|
```
|
package/package.json
CHANGED
package/src/REVDeployer.sol
CHANGED
|
@@ -249,8 +249,11 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
249
249
|
// Is there a tiered ERC-721 hook?
|
|
250
250
|
bool usesTiered721Hook = address(tiered721Hook) != address(0);
|
|
251
251
|
|
|
252
|
-
//
|
|
253
|
-
|
|
252
|
+
// Did the buyback hook return any specifications? (It won't when direct minting is cheaper than swapping.)
|
|
253
|
+
bool usesBuybackHook = buybackHookSpecifications.length > 0;
|
|
254
|
+
|
|
255
|
+
// Initialize the returned specification array with only the hooks that are present.
|
|
256
|
+
hookSpecifications = new JBPayHookSpecification[]((usesTiered721Hook ? 1 : 0) + (usesBuybackHook ? 1 : 0));
|
|
254
257
|
|
|
255
258
|
// If we have a tiered ERC-721 hook, add it to the array.
|
|
256
259
|
if (usesTiered721Hook) {
|
|
@@ -258,8 +261,10 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IJBRulesetDataHook, IJBCas
|
|
|
258
261
|
JBPayHookSpecification({hook: IJBPayHook(address(tiered721Hook)), amount: 0, metadata: bytes("")});
|
|
259
262
|
}
|
|
260
263
|
|
|
261
|
-
// Add the buyback hook specification.
|
|
262
|
-
|
|
264
|
+
// Add the buyback hook specification if present.
|
|
265
|
+
if (usesBuybackHook) {
|
|
266
|
+
hookSpecifications[usesTiered721Hook ? 1 : 0] = buybackHookSpecifications[0];
|
|
267
|
+
}
|
|
263
268
|
}
|
|
264
269
|
|
|
265
270
|
/// @notice Determine how a cash out from a revnet should be processed.
|
package/src/REVLoans.sol
CHANGED
|
@@ -344,7 +344,11 @@ contract REVLoans is ERC721, ERC2771Context, Ownable, IREVLoans {
|
|
|
344
344
|
// Get a refeerence to the collateral being used to secure loans.
|
|
345
345
|
uint256 totalCollateral = totalCollateralOf[revnetId];
|
|
346
346
|
|
|
347
|
-
// Proportional.
|
|
347
|
+
// Proportional — uses the CURRENT stage's cashOutTaxRate.
|
|
348
|
+
// NOTE: When a revnet transitions between stages with different cashOutTaxRate values, the borrowable amount
|
|
349
|
+
// for the same collateral changes. A lower cashOutTaxRate in a later stage means more borrowable value per
|
|
350
|
+
// collateral. This is by design: loan value tracks the current bonding curve parameters, just as cash-out
|
|
351
|
+
// value does. Borrowers benefit from decreasing tax rates and are constrained by increasing ones.
|
|
348
352
|
return JBCashOuts.cashOutFrom({
|
|
349
353
|
surplus: totalSurplus + totalBorrowed,
|
|
350
354
|
cashOutCount: collateralCount,
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
6
|
+
import /* {*} from */ "./../src/REVDeployer.sol";
|
|
7
|
+
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
8
|
+
import {MockBuybackDataHookMintPath} from "./mock/MockBuybackDataHookMintPath.sol";
|
|
9
|
+
import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
|
|
10
|
+
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
11
|
+
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
12
|
+
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
13
|
+
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
14
|
+
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
15
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
16
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
17
|
+
import {REVLoans} from "../src/REVLoans.sol";
|
|
18
|
+
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
19
|
+
import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
|
|
20
|
+
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
21
|
+
import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
|
|
22
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
23
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
24
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
25
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
26
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
27
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
28
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
29
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
30
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
31
|
+
import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
|
|
32
|
+
|
|
33
|
+
/// @notice Regression tests for the empty buyback hook specifications fix (C-0).
|
|
34
|
+
/// When JBBuybackHook determines minting is cheaper than swapping, it returns an empty
|
|
35
|
+
/// hookSpecifications array. Before the fix, REVDeployer.beforePayRecordedWith would
|
|
36
|
+
/// Panic(0x32) (array out-of-bounds) when accessing buybackHookSpecifications[0].
|
|
37
|
+
contract TestEmptyBuybackSpecs is TestBaseWorkflow, JBTest {
|
|
38
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
39
|
+
|
|
40
|
+
REVDeployer REV_DEPLOYER;
|
|
41
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
42
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
43
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
44
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
45
|
+
IREVLoans LOANS_CONTRACT;
|
|
46
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
47
|
+
CTPublisher PUBLISHER;
|
|
48
|
+
MockBuybackDataHookMintPath MOCK_BUYBACK_MINT_PATH;
|
|
49
|
+
|
|
50
|
+
uint256 FEE_PROJECT_ID;
|
|
51
|
+
|
|
52
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
53
|
+
address USER = makeAddr("user");
|
|
54
|
+
|
|
55
|
+
function setUp() public override {
|
|
56
|
+
super.setUp();
|
|
57
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
58
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
59
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
60
|
+
EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
|
|
61
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
62
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
63
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
64
|
+
MOCK_BUYBACK_MINT_PATH = new MockBuybackDataHookMintPath();
|
|
65
|
+
LOANS_CONTRACT = new REVLoans({
|
|
66
|
+
controller: jbController(),
|
|
67
|
+
projects: jbProjects(),
|
|
68
|
+
revId: FEE_PROJECT_ID,
|
|
69
|
+
owner: address(this),
|
|
70
|
+
permit2: permit2(),
|
|
71
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
72
|
+
});
|
|
73
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
74
|
+
jbController(),
|
|
75
|
+
SUCKER_REGISTRY,
|
|
76
|
+
FEE_PROJECT_ID,
|
|
77
|
+
HOOK_DEPLOYER,
|
|
78
|
+
PUBLISHER,
|
|
79
|
+
IJBRulesetDataHook(address(MOCK_BUYBACK_MINT_PATH)),
|
|
80
|
+
address(LOANS_CONTRACT),
|
|
81
|
+
TRUSTED_FORWARDER
|
|
82
|
+
);
|
|
83
|
+
vm.prank(multisig());
|
|
84
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function _buildMinimalConfig()
|
|
88
|
+
internal
|
|
89
|
+
view
|
|
90
|
+
returns (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc)
|
|
91
|
+
{
|
|
92
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
93
|
+
acc[0] = JBAccountingContext({
|
|
94
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
95
|
+
});
|
|
96
|
+
tc = new JBTerminalConfig[](1);
|
|
97
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
98
|
+
|
|
99
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
100
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
101
|
+
splits[0].beneficiary = payable(multisig());
|
|
102
|
+
splits[0].percent = 10_000;
|
|
103
|
+
stages[0] = REVStageConfig({
|
|
104
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
105
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
106
|
+
splitPercent: 0,
|
|
107
|
+
splits: splits,
|
|
108
|
+
initialIssuance: uint112(1000e18),
|
|
109
|
+
issuanceCutFrequency: 0,
|
|
110
|
+
issuanceCutPercent: 0,
|
|
111
|
+
cashOutTaxRate: 5000,
|
|
112
|
+
extraMetadata: 0
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
cfg = REVConfig({
|
|
116
|
+
description: REVDescription("Test", "TST", "ipfs://test", "TEST_SALT"),
|
|
117
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
118
|
+
splitOperator: multisig(),
|
|
119
|
+
stageConfigurations: stages
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
sdc = REVSuckerDeploymentConfig({
|
|
123
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("TEST"))
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _deployFeeAndRevnet() internal returns (uint256 revnetId) {
|
|
128
|
+
(REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
|
|
129
|
+
_buildMinimalConfig();
|
|
130
|
+
|
|
131
|
+
vm.prank(multisig());
|
|
132
|
+
REV_DEPLOYER.deployFor({
|
|
133
|
+
revnetId: FEE_PROJECT_ID,
|
|
134
|
+
configuration: feeCfg,
|
|
135
|
+
terminalConfigurations: feeTc,
|
|
136
|
+
suckerDeploymentConfiguration: feeSdc
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
|
|
140
|
+
_buildMinimalConfig();
|
|
141
|
+
cfg.description = REVDescription("Test2", "TS2", "ipfs://test2", "TEST_SALT_2");
|
|
142
|
+
|
|
143
|
+
revnetId = REV_DEPLOYER.deployFor({
|
|
144
|
+
revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// @notice REGRESSION: Payment to revnet must succeed when buyback hook returns empty specs (mint path).
|
|
149
|
+
/// Before the fix, this would Panic(0x32) due to accessing buybackHookSpecifications[0] on an empty array.
|
|
150
|
+
function test_payRevnet_emptyBuybackSpecs_succeeds() public {
|
|
151
|
+
uint256 revnetId = _deployFeeAndRevnet();
|
|
152
|
+
|
|
153
|
+
vm.deal(USER, 1 ether);
|
|
154
|
+
vm.prank(USER);
|
|
155
|
+
jbMultiTerminal().pay{value: 1 ether}({
|
|
156
|
+
projectId: revnetId,
|
|
157
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
158
|
+
amount: 1 ether,
|
|
159
|
+
beneficiary: USER,
|
|
160
|
+
minReturnedTokens: 0,
|
|
161
|
+
memo: "payment with mint path buyback",
|
|
162
|
+
metadata: ""
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
uint256 balance = jbTokens().totalBalanceOf(USER, revnetId);
|
|
166
|
+
assertGt(balance, 0, "Should have received tokens when buyback hook takes mint path");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// @notice Payment with various amounts should work when buyback hook returns empty specs.
|
|
170
|
+
function test_payRevnet_emptyBuybackSpecs_variousAmounts(uint96 amount) public {
|
|
171
|
+
vm.assume(amount > 0.001 ether && amount < 100 ether);
|
|
172
|
+
uint256 revnetId = _deployFeeAndRevnet();
|
|
173
|
+
|
|
174
|
+
vm.deal(USER, amount);
|
|
175
|
+
vm.prank(USER);
|
|
176
|
+
jbMultiTerminal().pay{value: amount}({
|
|
177
|
+
projectId: revnetId,
|
|
178
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
179
|
+
amount: amount,
|
|
180
|
+
beneficiary: USER,
|
|
181
|
+
minReturnedTokens: 0,
|
|
182
|
+
memo: "",
|
|
183
|
+
metadata: ""
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
uint256 balance = jbTokens().totalBalanceOf(USER, revnetId);
|
|
187
|
+
assertGt(balance, 0, "Should have received tokens for any valid amount");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// @notice Multiple sequential payments should work with empty buyback specs.
|
|
191
|
+
function test_payRevnet_emptyBuybackSpecs_multiplePayments() public {
|
|
192
|
+
uint256 revnetId = _deployFeeAndRevnet();
|
|
193
|
+
|
|
194
|
+
for (uint256 i; i < 5; i++) {
|
|
195
|
+
address payer = makeAddr(string(abi.encodePacked("payer", i)));
|
|
196
|
+
vm.deal(payer, 1 ether);
|
|
197
|
+
vm.prank(payer);
|
|
198
|
+
jbMultiTerminal().pay{value: 1 ether}({
|
|
199
|
+
projectId: revnetId,
|
|
200
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
201
|
+
amount: 1 ether,
|
|
202
|
+
beneficiary: payer,
|
|
203
|
+
minReturnedTokens: 0,
|
|
204
|
+
memo: "",
|
|
205
|
+
metadata: ""
|
|
206
|
+
});
|
|
207
|
+
assertGt(jbTokens().totalBalanceOf(payer, revnetId), 0, "Each payer should receive tokens");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// @notice Verify beforePayRecordedWith returns empty hookSpecifications when buyback returns empty.
|
|
212
|
+
function test_beforePayRecordedWith_emptyBuybackSpecs_returnsEmptyArray() public {
|
|
213
|
+
uint256 revnetId = _deployFeeAndRevnet();
|
|
214
|
+
|
|
215
|
+
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
216
|
+
terminal: address(jbMultiTerminal()),
|
|
217
|
+
payer: USER,
|
|
218
|
+
amount: JBTokenAmount({
|
|
219
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
220
|
+
value: 1 ether,
|
|
221
|
+
decimals: 18,
|
|
222
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
223
|
+
}),
|
|
224
|
+
projectId: revnetId,
|
|
225
|
+
rulesetId: 0,
|
|
226
|
+
beneficiary: USER,
|
|
227
|
+
weight: 1000e18,
|
|
228
|
+
reservedPercent: 0,
|
|
229
|
+
metadata: ""
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
(uint256 weight, JBPayHookSpecification[] memory specs) = REV_DEPLOYER.beforePayRecordedWith(context);
|
|
233
|
+
|
|
234
|
+
assertEq(weight, context.weight, "Weight should pass through from buyback hook");
|
|
235
|
+
assertEq(specs.length, 0, "Should return empty specs when buyback hook returns empty and no 721 hook");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
6
|
+
import /* {*} from */ "./../src/REVDeployer.sol";
|
|
7
|
+
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
8
|
+
import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
|
|
9
|
+
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
10
|
+
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
11
|
+
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
12
|
+
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
13
|
+
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
14
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
15
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
16
|
+
import {REVLoans} from "../src/REVLoans.sol";
|
|
17
|
+
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
18
|
+
import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
|
|
19
|
+
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
20
|
+
import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
|
|
21
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
22
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
23
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
24
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
25
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
26
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
27
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
28
|
+
|
|
29
|
+
/// @notice Documents and verifies that stage transitions change the borrowable amount for the same collateral.
|
|
30
|
+
/// This is by design: loan value tracks the current bonding curve parameters (cashOutTaxRate),
|
|
31
|
+
/// just as cash-out value does.
|
|
32
|
+
contract TestStageTransitionBorrowable is TestBaseWorkflow, JBTest {
|
|
33
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
34
|
+
|
|
35
|
+
REVDeployer REV_DEPLOYER;
|
|
36
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
37
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
38
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
39
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
40
|
+
IREVLoans LOANS_CONTRACT;
|
|
41
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
42
|
+
CTPublisher PUBLISHER;
|
|
43
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
44
|
+
|
|
45
|
+
uint256 FEE_PROJECT_ID;
|
|
46
|
+
uint256 REVNET_ID;
|
|
47
|
+
|
|
48
|
+
address USER = makeAddr("user");
|
|
49
|
+
|
|
50
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
51
|
+
|
|
52
|
+
/// @notice Stage 1 starts now with 60% cashOutTaxRate, stage 2 starts after 30 days with 20% cashOutTaxRate.
|
|
53
|
+
function _buildConfig()
|
|
54
|
+
internal
|
|
55
|
+
view
|
|
56
|
+
returns (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc)
|
|
57
|
+
{
|
|
58
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
59
|
+
acc[0] = JBAccountingContext({
|
|
60
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
61
|
+
});
|
|
62
|
+
tc = new JBTerminalConfig[](1);
|
|
63
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
64
|
+
|
|
65
|
+
REVStageConfig[] memory stages = new REVStageConfig[](2);
|
|
66
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
67
|
+
splits[0].beneficiary = payable(multisig());
|
|
68
|
+
splits[0].percent = 10_000;
|
|
69
|
+
|
|
70
|
+
// Stage 1: high cashOutTaxRate (60%)
|
|
71
|
+
stages[0] = REVStageConfig({
|
|
72
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
73
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
74
|
+
splitPercent: 0,
|
|
75
|
+
splits: splits,
|
|
76
|
+
initialIssuance: uint112(1000e18),
|
|
77
|
+
issuanceCutFrequency: 0,
|
|
78
|
+
issuanceCutPercent: 0,
|
|
79
|
+
cashOutTaxRate: 6000, // 60%
|
|
80
|
+
extraMetadata: 0
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Stage 2: low cashOutTaxRate (20%) — starts after 30 days
|
|
84
|
+
stages[1] = REVStageConfig({
|
|
85
|
+
startsAtOrAfter: uint40(block.timestamp + 30 days),
|
|
86
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
87
|
+
splitPercent: 0,
|
|
88
|
+
splits: splits,
|
|
89
|
+
initialIssuance: uint112(1000e18),
|
|
90
|
+
issuanceCutFrequency: 0,
|
|
91
|
+
issuanceCutPercent: 0,
|
|
92
|
+
cashOutTaxRate: 2000, // 20%
|
|
93
|
+
extraMetadata: 0
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
cfg = REVConfig({
|
|
97
|
+
description: REVDescription("StageTest", "STG", "ipfs://test", "STG_SALT"),
|
|
98
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
99
|
+
splitOperator: multisig(),
|
|
100
|
+
stageConfigurations: stages
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
sdc = REVSuckerDeploymentConfig({
|
|
104
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("STG"))
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function setUp() public override {
|
|
109
|
+
super.setUp();
|
|
110
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
111
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
112
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
113
|
+
EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
|
|
114
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
115
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
116
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
117
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
118
|
+
LOANS_CONTRACT = new REVLoans({
|
|
119
|
+
controller: jbController(),
|
|
120
|
+
projects: jbProjects(),
|
|
121
|
+
revId: FEE_PROJECT_ID,
|
|
122
|
+
owner: address(this),
|
|
123
|
+
permit2: permit2(),
|
|
124
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
125
|
+
});
|
|
126
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
127
|
+
jbController(),
|
|
128
|
+
SUCKER_REGISTRY,
|
|
129
|
+
FEE_PROJECT_ID,
|
|
130
|
+
HOOK_DEPLOYER,
|
|
131
|
+
PUBLISHER,
|
|
132
|
+
IJBRulesetDataHook(address(MOCK_BUYBACK)),
|
|
133
|
+
address(LOANS_CONTRACT),
|
|
134
|
+
TRUSTED_FORWARDER
|
|
135
|
+
);
|
|
136
|
+
vm.prank(multisig());
|
|
137
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
138
|
+
|
|
139
|
+
// Deploy the fee project first.
|
|
140
|
+
(REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
|
|
141
|
+
_buildConfig();
|
|
142
|
+
vm.prank(multisig());
|
|
143
|
+
REV_DEPLOYER.deployFor({
|
|
144
|
+
revnetId: FEE_PROJECT_ID,
|
|
145
|
+
configuration: feeCfg,
|
|
146
|
+
terminalConfigurations: feeTc,
|
|
147
|
+
suckerDeploymentConfiguration: feeSdc
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Deploy the test revnet.
|
|
151
|
+
(REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) = _buildConfig();
|
|
152
|
+
cfg.description = REVDescription("StageTest2", "ST2", "ipfs://test2", "STG_SALT_2");
|
|
153
|
+
REVNET_ID = REV_DEPLOYER.deployFor({
|
|
154
|
+
revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
vm.deal(USER, 100 ether);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// @notice BY DESIGN: Borrowable amount increases when transitioning to a stage with lower cashOutTaxRate.
|
|
161
|
+
/// This documents that loan value tracks the current bonding curve, just as cash-out value does.
|
|
162
|
+
/// @dev The bonding curve only applies a tax discount when cashOutCount < totalSupply,
|
|
163
|
+
/// so we need multiple payers to see the effect.
|
|
164
|
+
function test_borrowableAmount_increasesWhenCashOutTaxRateDecreases() public {
|
|
165
|
+
// Two payers so the bonding curve tax rate has a visible effect (count < supply).
|
|
166
|
+
address otherPayer = makeAddr("otherPayer");
|
|
167
|
+
vm.deal(otherPayer, 10 ether);
|
|
168
|
+
vm.prank(otherPayer);
|
|
169
|
+
jbMultiTerminal().pay{value: 10 ether}({
|
|
170
|
+
projectId: REVNET_ID,
|
|
171
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
172
|
+
amount: 10 ether,
|
|
173
|
+
beneficiary: otherPayer,
|
|
174
|
+
minReturnedTokens: 0,
|
|
175
|
+
memo: "",
|
|
176
|
+
metadata: ""
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
vm.prank(USER);
|
|
180
|
+
uint256 tokens = jbMultiTerminal().pay{value: 10 ether}({
|
|
181
|
+
projectId: REVNET_ID,
|
|
182
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
183
|
+
amount: 10 ether,
|
|
184
|
+
beneficiary: USER,
|
|
185
|
+
minReturnedTokens: 0,
|
|
186
|
+
memo: "",
|
|
187
|
+
metadata: ""
|
|
188
|
+
});
|
|
189
|
+
assertGt(tokens, 0, "Should receive tokens");
|
|
190
|
+
|
|
191
|
+
// Check borrowable amount during stage 1 (60% cashOutTaxRate).
|
|
192
|
+
uint256 borrowableStage1 =
|
|
193
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
194
|
+
assertGt(borrowableStage1, 0, "Borrowable amount should be positive in stage 1");
|
|
195
|
+
|
|
196
|
+
// Warp to stage 2 (20% cashOutTaxRate).
|
|
197
|
+
vm.warp(block.timestamp + 31 days);
|
|
198
|
+
|
|
199
|
+
// Check borrowable amount during stage 2.
|
|
200
|
+
uint256 borrowableStage2 =
|
|
201
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
202
|
+
|
|
203
|
+
// Borrowable amount should be HIGHER with a lower cashOutTaxRate — by design.
|
|
204
|
+
assertGt(borrowableStage2, borrowableStage1, "Borrowable amount should increase with lower cashOutTaxRate");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// @notice Verifies that the bonding curve formula applies the tax rate correctly when count < supply.
|
|
208
|
+
function test_borrowableAmount_taxRateReducesPartialCashOut() public {
|
|
209
|
+
// Two payers so USER holds a fraction of total supply.
|
|
210
|
+
address otherPayer = makeAddr("otherPayer");
|
|
211
|
+
vm.deal(otherPayer, 10 ether);
|
|
212
|
+
vm.prank(otherPayer);
|
|
213
|
+
jbMultiTerminal().pay{value: 10 ether}({
|
|
214
|
+
projectId: REVNET_ID,
|
|
215
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
216
|
+
amount: 10 ether,
|
|
217
|
+
beneficiary: otherPayer,
|
|
218
|
+
minReturnedTokens: 0,
|
|
219
|
+
memo: "",
|
|
220
|
+
metadata: ""
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
vm.prank(USER);
|
|
224
|
+
uint256 tokens = jbMultiTerminal().pay{value: 10 ether}({
|
|
225
|
+
projectId: REVNET_ID,
|
|
226
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
227
|
+
amount: 10 ether,
|
|
228
|
+
beneficiary: USER,
|
|
229
|
+
minReturnedTokens: 0,
|
|
230
|
+
memo: "",
|
|
231
|
+
metadata: ""
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// With 60% tax rate and ~50% of supply, borrowable should be meaningfully less than pro-rata share.
|
|
235
|
+
uint256 borrowable =
|
|
236
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
237
|
+
assertGt(borrowable, 0, "Borrowable amount should be positive");
|
|
238
|
+
// Pro-rata share would be ~10 ether (half of 20 ether surplus). With 60% tax, it should be less.
|
|
239
|
+
assertLt(borrowable, 10 ether, "Borrowable should be less than pro-rata share due to tax rate");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
5
|
+
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
6
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
7
|
+
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
8
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
9
|
+
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
10
|
+
import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
|
|
11
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
12
|
+
import {IUniswapV3Pool} from "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol";
|
|
13
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
14
|
+
|
|
15
|
+
/// @notice Mock buyback hook that simulates the "mint path" — returns EMPTY hookSpecifications.
|
|
16
|
+
/// This is what the real JBBuybackHook does when direct minting is cheaper than swapping
|
|
17
|
+
/// (i.e., tokenCountWithoutHook >= minimumSwapAmountOut).
|
|
18
|
+
contract MockBuybackDataHookMintPath is IJBRulesetDataHook, IJBPayHook {
|
|
19
|
+
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
20
|
+
external
|
|
21
|
+
view
|
|
22
|
+
override
|
|
23
|
+
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
24
|
+
{
|
|
25
|
+
weight = context.weight;
|
|
26
|
+
// Return EMPTY hookSpecifications — simulating the mint path where no swap is needed.
|
|
27
|
+
hookSpecifications = new JBPayHookSpecification[](0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
31
|
+
external
|
|
32
|
+
view
|
|
33
|
+
override
|
|
34
|
+
returns (
|
|
35
|
+
uint256 cashOutTaxRate,
|
|
36
|
+
uint256 cashOutCount,
|
|
37
|
+
uint256 totalSupply,
|
|
38
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
39
|
+
)
|
|
40
|
+
{
|
|
41
|
+
cashOutTaxRate = context.cashOutTaxRate;
|
|
42
|
+
cashOutCount = context.cashOutCount;
|
|
43
|
+
totalSupply = context.totalSupply;
|
|
44
|
+
hookSpecifications = new JBCashOutHookSpecification[](0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure override returns (bool) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function afterPayRecordedWith(JBAfterPayRecordedContext calldata) external payable override {}
|
|
52
|
+
|
|
53
|
+
function setPoolFor(uint256, uint24, uint256, address) external pure returns (IUniswapV3Pool) {
|
|
54
|
+
return IUniswapV3Pool(address(0));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
|
|
58
|
+
return interfaceId == type(IJBRulesetDataHook).interfaceId || interfaceId == type(IJBPayHook).interfaceId
|
|
59
|
+
|| interfaceId == type(IERC165).interfaceId;
|
|
60
|
+
}
|
|
61
|
+
}
|