@rev-net/core-v6 0.0.72 → 0.0.74
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/package.json +2 -2
- package/references/operations.md +2 -2
- package/references/runtime.md +1 -1
- package/script/Deploy.s.sol +2 -2
- package/script/helpers/CoreDeploymentLib.sol +172 -0
- package/src/REVDeployer.sol +47 -6
- package/src/REVLoans.sol +38 -11
- package/src/REVOwner.sol +11 -11
- package/src/interfaces/IREVDeployer.sol +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rev-net/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.74",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@bananapus/721-hook-v6": "^0.0.55",
|
|
28
28
|
"@bananapus/buyback-hook-v6": "^0.0.51",
|
|
29
|
-
"@bananapus/core-v6": "^0.0.
|
|
29
|
+
"@bananapus/core-v6": "^0.0.64",
|
|
30
30
|
"@bananapus/ownable-v6": "^0.0.28",
|
|
31
31
|
"@bananapus/permission-ids-v6": "^0.0.27",
|
|
32
32
|
"@bananapus/router-terminal-v6": "^0.0.49",
|
package/references/operations.md
CHANGED
|
@@ -91,7 +91,7 @@ Use this file when you need revnet-specific risks, state reads, constants, or ex
|
|
|
91
91
|
| Constant | Value | Purpose |
|
|
92
92
|
|----------|-------|---------|
|
|
93
93
|
| `CASH_OUT_DELAY` | 2,592,000 (30 days) | Prevents cross-chain liquidity arbitrage on new chain deployments |
|
|
94
|
-
| `FEE` | 25 (of MAX_FEE=1000) | 2.5% cash-out fee paid to fee revnet |
|
|
94
|
+
| `FEE` | 25 (of MAX_FEE=1000) | 2.5% cash-out fee paid to fee revnet for non-zero-tax ordinary cash-outs |
|
|
95
95
|
| `DEFAULT_BUYBACK_POOL_FEE` | 10,000 | 1% Uniswap fee tier for default buyback pools |
|
|
96
96
|
| `DEFAULT_BUYBACK_TWAP_WINDOW` | 2 days | TWAP observation window for buyback price |
|
|
97
97
|
| `DEFAULT_BUYBACK_TICK_SPACING` | 200 | Tick spacing for default buyback V4 pools |
|
|
@@ -147,7 +147,7 @@ Use this file when you need revnet-specific risks, state reads, constants, or ex
|
|
|
147
147
|
4. **Loan ID encoding.** `loanId = revnetId * 1_000_000_000_000 + loanNumber`. Each revnet supports ~1 trillion loans. Use `revnetIdOfLoanWith(loanId)` to decode.
|
|
148
148
|
5. **uint112 truncation risk.** `REVLoan.amount` and `REVLoan.collateral` are `uint112`. Values above ~5.19e33 truncate silently.
|
|
149
149
|
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
|
-
7. **Cash-out fee stacking.**
|
|
150
|
+
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
151
|
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
152
|
9. **`cashOutTaxRate` cannot be MAX.** Must be strictly less than `MAX_CASH_OUT_TAX_RATE` (10,000). Revnets cannot fully disable cash outs.
|
|
153
153
|
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.
|
package/references/runtime.md
CHANGED
|
@@ -29,7 +29,7 @@ Deploy and manage Revnets -- autonomous, unowned Juicebox projects with staged i
|
|
|
29
29
|
| Function | Permissions | What it does |
|
|
30
30
|
|----------|------------|-------------|
|
|
31
31
|
| `REVOwner.beforePayRecordedWith(context)` | Terminal callback | Calls the 721 hook first for split specs, then calls the buyback hook with a reduced amount context (payment minus split amount). Adjusts the returned weight proportionally for splits (`weight = mulDiv(weight, amount - splitAmount, amount)`) so the terminal only mints tokens for the amount entering the project. Assembles pay hook specs (721 hook specs first, then buyback spec). Reads `tiered721HookOf` from REVOwner storage. |
|
|
32
|
-
| `REVOwner.beforeCashOutRecordedWith(context)` | Terminal callback | If sucker: returns full amount with 0 tax (fee exempt). Otherwise:
|
|
32
|
+
| `REVOwner.beforeCashOutRecordedWith(context)` | Terminal callback | If sucker: returns full amount with 0 tax (fee exempt). Otherwise: enforces the 30-day cash-out delay, routes zero-tax ordinary cash-outs through the buyback hook without a revnet fee, and only calculates the 2.5% revnet fee when `cashOutTaxRate != 0`. |
|
|
33
33
|
| `REVOwner.afterCashOutRecordedWith(context)` | Permissionless | Cash-out hook callback. Receives fee amount and pays it to the fee revnet's terminal. Falls back to returning funds if fee payment fails. |
|
|
34
34
|
| `REVOwner.hasMintPermissionFor(revnetId, ruleset, addr)` | View | Returns `true` for: loans contract, buyback hook, buyback hook delegates, or suckers. |
|
|
35
35
|
| `REVOwner.cashOutDelayOf(revnetId)` | View | Returns the cash-out delay timestamp from REVOwner storage. Exposed for REVLoans compatibility (REVLoans imports IREVOwner for this). |
|
package/script/Deploy.s.sol
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
BuybackDeployment,
|
|
7
7
|
BuybackDeploymentLib
|
|
8
8
|
} from "@bananapus/buyback-hook-v6/script/helpers/BuybackDeploymentLib.sol";
|
|
9
|
-
import {CoreDeployment, CoreDeploymentLib} from "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
10
9
|
import {SuckerDeployment, SuckerDeploymentLib} from "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
11
10
|
import {
|
|
12
11
|
RouterTerminalDeployment,
|
|
@@ -44,6 +43,7 @@ import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721Init
|
|
|
44
43
|
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
45
44
|
import {REVBaseline721HookConfig} from "../src/structs/REVBaseline721HookConfig.sol";
|
|
46
45
|
import {REV721TiersHookFlags} from "../src/structs/REV721TiersHookFlags.sol";
|
|
46
|
+
import {CoreDeployment, CoreDeploymentLib} from "./helpers/CoreDeploymentLib.sol";
|
|
47
47
|
|
|
48
48
|
struct FeeProjectConfig {
|
|
49
49
|
REVConfig configuration;
|
|
@@ -426,7 +426,7 @@ contract DeployScript is Script, Sphinx {
|
|
|
426
426
|
}
|
|
427
427
|
}
|
|
428
428
|
if (!_foundExisting) {
|
|
429
|
-
feeProjectId = core.projects.createFor(safeAddress());
|
|
429
|
+
feeProjectId = core.projects.createFor{value: core.projects.creationFee()}(safeAddress());
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
432
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
import {stdJson} from "forge-std/Script.sol";
|
|
5
|
+
import {Vm} from "forge-std/Vm.sol";
|
|
6
|
+
|
|
7
|
+
import {JBController} from "@bananapus/core-v6/src/JBController.sol";
|
|
8
|
+
import {JBDirectory} from "@bananapus/core-v6/src/JBDirectory.sol";
|
|
9
|
+
import {JBFeelessAddresses} from "@bananapus/core-v6/src/JBFeelessAddresses.sol";
|
|
10
|
+
import {JBFundAccessLimits} from "@bananapus/core-v6/src/JBFundAccessLimits.sol";
|
|
11
|
+
import {JBMultiTerminal} from "@bananapus/core-v6/src/JBMultiTerminal.sol";
|
|
12
|
+
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
13
|
+
import {JBPrices} from "@bananapus/core-v6/src/JBPrices.sol";
|
|
14
|
+
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
15
|
+
import {JBRulesets} from "@bananapus/core-v6/src/JBRulesets.sol";
|
|
16
|
+
import {JBSplits} from "@bananapus/core-v6/src/JBSplits.sol";
|
|
17
|
+
import {JBTerminalStore} from "@bananapus/core-v6/src/JBTerminalStore.sol";
|
|
18
|
+
import {JBTokens} from "@bananapus/core-v6/src/JBTokens.sol";
|
|
19
|
+
|
|
20
|
+
import {SphinxConstants, NetworkInfo} from "@sphinx-labs/contracts/contracts/foundry/SphinxConstants.sol";
|
|
21
|
+
|
|
22
|
+
struct CoreDeployment {
|
|
23
|
+
JBPermissions permissions;
|
|
24
|
+
JBProjects projects;
|
|
25
|
+
JBDirectory directory;
|
|
26
|
+
JBSplits splits;
|
|
27
|
+
JBRulesets rulesets;
|
|
28
|
+
JBController controller;
|
|
29
|
+
JBMultiTerminal terminal;
|
|
30
|
+
JBTerminalStore terminalStore;
|
|
31
|
+
JBPrices prices;
|
|
32
|
+
JBFeelessAddresses feeless;
|
|
33
|
+
JBFundAccessLimits fundAccess;
|
|
34
|
+
JBTokens tokens;
|
|
35
|
+
address trustedForwarder;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
library CoreDeploymentLib {
|
|
39
|
+
address internal constant VM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code"))));
|
|
40
|
+
// forge-lint: disable-next-line(screaming-snake-case-const)
|
|
41
|
+
Vm internal constant vm = Vm(VM_ADDRESS);
|
|
42
|
+
string internal constant PROJECT_NAME = "nana-core-v6";
|
|
43
|
+
|
|
44
|
+
function getDeployment(string memory path) internal returns (CoreDeployment memory deployment) {
|
|
45
|
+
uint256 chainId = block.chainid;
|
|
46
|
+
|
|
47
|
+
SphinxConstants sphinxConstants = new SphinxConstants();
|
|
48
|
+
NetworkInfo[] memory networks = sphinxConstants.getNetworkInfoArray();
|
|
49
|
+
|
|
50
|
+
for (uint256 i; i < networks.length; i++) {
|
|
51
|
+
if (networks[i].chainId == chainId) return getDeployment({path: path, networkName: networks[i].name});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
revert("ChainID is not (currently) supported by Sphinx.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getDeployment(
|
|
58
|
+
string memory path,
|
|
59
|
+
string memory networkName
|
|
60
|
+
)
|
|
61
|
+
internal
|
|
62
|
+
returns (CoreDeployment memory deployment)
|
|
63
|
+
{
|
|
64
|
+
deployment.permissions = JBPermissions(
|
|
65
|
+
_getDeploymentAddress({
|
|
66
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBPermissions"
|
|
67
|
+
})
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
deployment.projects = JBProjects(
|
|
71
|
+
_getDeploymentAddress({
|
|
72
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBProjects"
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
deployment.directory = JBDirectory(
|
|
77
|
+
_getDeploymentAddress({
|
|
78
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBDirectory"
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
deployment.splits = JBSplits(
|
|
83
|
+
_getDeploymentAddress({
|
|
84
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBSplits"
|
|
85
|
+
})
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
deployment.rulesets = JBRulesets(
|
|
89
|
+
_getDeploymentAddress({
|
|
90
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBRulesets"
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
deployment.controller = JBController(
|
|
95
|
+
_tryGetDeploymentAddress({
|
|
96
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBController"
|
|
97
|
+
})
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
deployment.terminal = JBMultiTerminal(
|
|
101
|
+
_getDeploymentAddress({
|
|
102
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBMultiTerminal"
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
deployment.terminalStore = JBTerminalStore(
|
|
107
|
+
_getDeploymentAddress({
|
|
108
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBTerminalStore"
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
deployment.prices = JBPrices(
|
|
113
|
+
_getDeploymentAddress({
|
|
114
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBPrices"
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
deployment.feeless = JBFeelessAddresses(
|
|
119
|
+
_getDeploymentAddress({
|
|
120
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBFeelessAddresses"
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
deployment.fundAccess = JBFundAccessLimits(
|
|
125
|
+
_getDeploymentAddress({
|
|
126
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBFundAccessLimits"
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
deployment.tokens = JBTokens(
|
|
131
|
+
_getDeploymentAddress({
|
|
132
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "JBTokens"
|
|
133
|
+
})
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
deployment.trustedForwarder = _getDeploymentAddress({
|
|
137
|
+
path: path, projectName: PROJECT_NAME, networkName: networkName, contractName: "ERC2771Forwarder"
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function _getDeploymentAddress(
|
|
142
|
+
string memory path,
|
|
143
|
+
string memory projectName,
|
|
144
|
+
string memory networkName,
|
|
145
|
+
string memory contractName
|
|
146
|
+
)
|
|
147
|
+
internal
|
|
148
|
+
view
|
|
149
|
+
returns (address)
|
|
150
|
+
{
|
|
151
|
+
string memory deploymentJson =
|
|
152
|
+
// forge-lint: disable-next-line(unsafe-cheatcode)
|
|
153
|
+
vm.readFile(string.concat(path, projectName, "/", networkName, "/", contractName, ".json"));
|
|
154
|
+
return stdJson.readAddress({json: deploymentJson, key: ".address"});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _tryGetDeploymentAddress(
|
|
158
|
+
string memory path,
|
|
159
|
+
string memory projectName,
|
|
160
|
+
string memory networkName,
|
|
161
|
+
string memory contractName
|
|
162
|
+
)
|
|
163
|
+
internal
|
|
164
|
+
returns (address)
|
|
165
|
+
{
|
|
166
|
+
string memory filePath = string.concat(path, projectName, "/", networkName, "/", contractName, ".json");
|
|
167
|
+
// forge-lint: disable-next-line(unsafe-cheatcode)
|
|
168
|
+
if (!vm.exists(filePath)) return address(0);
|
|
169
|
+
// forge-lint: disable-next-line(unsafe-cheatcode)
|
|
170
|
+
return stdJson.readAddress({json: vm.readFile(filePath), key: ".address"});
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/REVDeployer.sol
CHANGED
|
@@ -24,6 +24,7 @@ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.so
|
|
|
24
24
|
import {JBRulesetMetadata} from "@bananapus/core-v6/src/structs/JBRulesetMetadata.sol";
|
|
25
25
|
import {JBSplitGroup} from "@bananapus/core-v6/src/structs/JBSplitGroup.sol";
|
|
26
26
|
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
27
|
+
import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
|
|
27
28
|
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
28
29
|
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
29
30
|
import {CTPublisher} from "@croptop/core-v6/src/CTPublisher.sol";
|
|
@@ -63,6 +64,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
63
64
|
error REVDeployer_AutoIssuanceBeneficiaryZeroAddress(uint256 stageIndex, uint256 autoIssuanceIndex);
|
|
64
65
|
error REVDeployer_CashOutsCantBeTurnedOffCompletely(uint256 cashOutTaxRate, uint256 maxCashOutTaxRate);
|
|
65
66
|
error REVDeployer_MustHaveSplits(uint256 stageIndex, uint256 splitPercent);
|
|
67
|
+
error REVDeployer_ProjectCreationFeeNotNeeded(uint256 revnetId, uint256 value);
|
|
66
68
|
error REVDeployer_RulesetDoesNotAllowDeployingSuckers(uint256 revnetId);
|
|
67
69
|
error REVDeployer_StagesRequired(uint256 stageCount);
|
|
68
70
|
error REVDeployer_StageTimesMustIncrease(uint256 stageIndex, uint256 previousStageStart, uint256 effectiveStart);
|
|
@@ -345,6 +347,27 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
345
347
|
}
|
|
346
348
|
}
|
|
347
349
|
|
|
350
|
+
/// @notice Calculate a Uniswap V4 sqrt price, returning zero if the ratio is out of range.
|
|
351
|
+
/// @param numerator The numerator of the raw token price ratio.
|
|
352
|
+
/// @param denominator The denominator of the raw token price ratio.
|
|
353
|
+
/// @return sqrtPriceX96 The encoded sqrt price, or zero when it cannot be represented.
|
|
354
|
+
function _sqrtPriceX96From(uint256 numerator, uint256 denominator) internal pure returns (uint160 sqrtPriceX96) {
|
|
355
|
+
// Q192 is the fixed-point scale Uniswap uses before taking the square root.
|
|
356
|
+
uint256 q192 = 1 << 192;
|
|
357
|
+
|
|
358
|
+
// `mulDiv` reverts if `numerator * Q192 / denominator` exceeds uint256.
|
|
359
|
+
uint256 maxRatio = type(uint256).max / q192;
|
|
360
|
+
|
|
361
|
+
// Cap the numerator at a conservative bound that keeps the scaled ratio representable.
|
|
362
|
+
uint256 maxNumerator = denominator > type(uint256).max / maxRatio ? type(uint256).max : maxRatio * denominator;
|
|
363
|
+
|
|
364
|
+
// A zero denominator is invalid, and an out-of-range numerator means this pool price should be skipped.
|
|
365
|
+
if (denominator == 0 || numerator > maxNumerator) return 0;
|
|
366
|
+
|
|
367
|
+
// The bounded ratio fits in uint256, and its square root always fits in uint160.
|
|
368
|
+
sqrtPriceX96 = uint160(sqrt(mulDiv({x: numerator, y: q192, denominator: denominator})));
|
|
369
|
+
}
|
|
370
|
+
|
|
348
371
|
/// @notice Try to initialize a Uniswap V4 buyback pool for a terminal token at its fair issuance price.
|
|
349
372
|
/// @dev Called after the ERC-20 token is deployed so the pool can be initialized in the PoolManager.
|
|
350
373
|
/// Computes `sqrtPriceX96` from `initialIssuance` so the pool starts at the same price as the bonding curve.
|
|
@@ -406,13 +429,15 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
406
429
|
sqrtPriceX96 = uint160(1 << 96);
|
|
407
430
|
} else if (normalizedTerminalToken < projectToken) {
|
|
408
431
|
// token0 = terminal, token1 = project → price = adjustedIssuance / terminalTokenUnit
|
|
409
|
-
sqrtPriceX96 =
|
|
410
|
-
uint160(sqrt(mulDiv({x: adjustedInitialIssuance, y: 1 << 192, denominator: terminalTokenUnit})));
|
|
432
|
+
sqrtPriceX96 = _sqrtPriceX96From({numerator: adjustedInitialIssuance, denominator: terminalTokenUnit});
|
|
411
433
|
} else {
|
|
412
434
|
// token0 = project, token1 = terminal → price = terminalTokenUnit / adjustedIssuance
|
|
413
|
-
sqrtPriceX96 =
|
|
414
|
-
uint160(sqrt(mulDiv({x: terminalTokenUnit, y: 1 << 192, denominator: adjustedInitialIssuance})));
|
|
435
|
+
sqrtPriceX96 = _sqrtPriceX96From({numerator: terminalTokenUnit, denominator: adjustedInitialIssuance});
|
|
415
436
|
}
|
|
437
|
+
|
|
438
|
+
// Some extreme cross-currency prices are outside Uniswap's usable sqrt-price range. In those cases,
|
|
439
|
+
// leave the pool uninitialized instead of reverting the whole revnet deployment.
|
|
440
|
+
if (sqrtPriceX96 == 0) return;
|
|
416
441
|
}
|
|
417
442
|
|
|
418
443
|
try BUYBACK_HOOK.initializePoolFor({
|
|
@@ -468,6 +493,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
468
493
|
REVCroptopAllowedPost[] calldata allowedPosts
|
|
469
494
|
)
|
|
470
495
|
external
|
|
496
|
+
payable
|
|
471
497
|
override
|
|
472
498
|
returns (uint256, IJB721TiersHook hook)
|
|
473
499
|
{
|
|
@@ -475,7 +501,11 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
475
501
|
bool shouldDeployNewRevnet = revnetId == 0;
|
|
476
502
|
|
|
477
503
|
// If the caller is deploying a new revnet, reserve its project ID before deriving hook/sucker config.
|
|
478
|
-
if (shouldDeployNewRevnet)
|
|
504
|
+
if (shouldDeployNewRevnet) {
|
|
505
|
+
revnetId = PROJECTS.createFor{value: msg.value}(address(this));
|
|
506
|
+
} else if (msg.value != 0) {
|
|
507
|
+
revert REVDeployer_ProjectCreationFeeNotNeeded({revnetId: revnetId, value: msg.value});
|
|
508
|
+
}
|
|
479
509
|
|
|
480
510
|
// Deploy the revnet with the specified tiered ERC-721 hook and croptop posting criteria.
|
|
481
511
|
hook = _deploy721RevnetFor({
|
|
@@ -500,11 +530,16 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
500
530
|
REVSuckerDeploymentConfig calldata suckerDeploymentConfiguration
|
|
501
531
|
)
|
|
502
532
|
external
|
|
533
|
+
payable
|
|
503
534
|
override
|
|
504
535
|
returns (uint256, IJB721TiersHook hook)
|
|
505
536
|
{
|
|
506
537
|
bool shouldDeployNewRevnet = revnetId == 0;
|
|
507
|
-
if (shouldDeployNewRevnet)
|
|
538
|
+
if (shouldDeployNewRevnet) {
|
|
539
|
+
revnetId = PROJECTS.createFor{value: msg.value}(address(this));
|
|
540
|
+
} else if (msg.value != 0) {
|
|
541
|
+
revert REVDeployer_ProjectCreationFeeNotNeeded({revnetId: revnetId, value: msg.value});
|
|
542
|
+
}
|
|
508
543
|
|
|
509
544
|
// Deploy the revnet (project, rulesets, ERC-20, suckers, etc.).
|
|
510
545
|
(bytes32 encodedConfigurationHash, REVOwnerRevnetInit memory ownerInit) = _deployRevnetFor({
|
|
@@ -528,6 +563,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
528
563
|
});
|
|
529
564
|
}
|
|
530
565
|
|
|
566
|
+
// Scope the hook's permissions to REVOwner, where the operator permissions are granted.
|
|
567
|
+
JBOwnable(address(hook)).transferOwnership(OWNER);
|
|
568
|
+
|
|
531
569
|
// Grant the operator all 721 permissions (no prevent* flags for default config).
|
|
532
570
|
ownerInit.extraOperatorPermissionIds = new uint256[](4);
|
|
533
571
|
ownerInit.extraOperatorPermissionIds[0] = JBPermissionIds.ADJUST_721_TIERS;
|
|
@@ -630,6 +668,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
630
668
|
salt: keccak256(abi.encode(tiered721HookConfiguration.salt, encodedConfigurationHash, _msgSender()))
|
|
631
669
|
});
|
|
632
670
|
|
|
671
|
+
// Scope the hook's permissions to REVOwner, where the operator permissions are granted.
|
|
672
|
+
JBOwnable(address(hook)).transferOwnership(OWNER);
|
|
673
|
+
|
|
633
674
|
// Build the 721 permission additions based on the deployer's `preventOperator*` flags.
|
|
634
675
|
{
|
|
635
676
|
uint256 extraCount;
|
package/src/REVLoans.sol
CHANGED
|
@@ -4,6 +4,7 @@ pragma solidity 0.8.28;
|
|
|
4
4
|
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
5
5
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
6
6
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
|
+
import {IJBMultiTerminal} from "@bananapus/core-v6/src/interfaces/IJBMultiTerminal.sol";
|
|
7
8
|
import {IJBPayoutTerminal} from "@bananapus/core-v6/src/interfaces/IJBPayoutTerminal.sol";
|
|
8
9
|
import {IJBPermissioned} from "@bananapus/core-v6/src/interfaces/IJBPermissioned.sol";
|
|
9
10
|
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
@@ -58,6 +59,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
58
59
|
|
|
59
60
|
error REVLoans_CashOutDelayNotFinished(uint256 cashOutDelay, uint256 blockTimestamp);
|
|
60
61
|
error REVLoans_CollateralExceedsLoan(uint256 collateralToReturn, uint256 loanCollateral);
|
|
62
|
+
error REVLoans_FeeOnTransferSourceUnsupported(address token, uint256 expectedAmount, uint256 creditedAmount);
|
|
61
63
|
error REVLoans_InvalidAccountingContext(uint256 revnetId, address token);
|
|
62
64
|
error REVLoans_InvalidPrepaidFeePercent(uint256 prepaidFeePercent, uint256 min, uint256 max);
|
|
63
65
|
error REVLoans_LoanExpired(uint256 timeSinceLoanCreated, uint256 loanLiquidationDuration);
|
|
@@ -682,11 +684,20 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
682
684
|
override
|
|
683
685
|
nonReentrantLoanAction
|
|
684
686
|
{
|
|
685
|
-
//
|
|
686
|
-
|
|
687
|
-
|
|
687
|
+
// No loans are checked when count is zero.
|
|
688
|
+
if (count == 0) return;
|
|
689
|
+
|
|
690
|
+
// Prevent cross-revnet accounting corruption: every iterated loan number must stay within the revnet's ID
|
|
691
|
+
// namespace.
|
|
692
|
+
if (startingLoanId > _ONE_TRILLION) {
|
|
688
693
|
revert REVLoans_LoanIdOverflow({
|
|
689
|
-
revnetId: revnetId, loanNumber:
|
|
694
|
+
revnetId: revnetId, loanNumber: startingLoanId, maxLoanNumber: _ONE_TRILLION
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
uint256 maxCount = _ONE_TRILLION - startingLoanId + 1;
|
|
698
|
+
if (count > maxCount) {
|
|
699
|
+
revert REVLoans_LoanIdOverflow({
|
|
700
|
+
revnetId: revnetId, loanNumber: _ONE_TRILLION + 1, maxLoanNumber: _ONE_TRILLION
|
|
690
701
|
});
|
|
691
702
|
}
|
|
692
703
|
|
|
@@ -1196,6 +1207,12 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1196
1207
|
|
|
1197
1208
|
// INTERACTIONS: Execute external calls with pre-computed deltas.
|
|
1198
1209
|
|
|
1210
|
+
// Burn newly added collateral before pulling source funds so fee-project token mints from the borrow cannot
|
|
1211
|
+
// be used as same-transaction collateral.
|
|
1212
|
+
if (addedCollateralCount > 0) {
|
|
1213
|
+
_addCollateralTo({revnetId: revnetId, amount: addedCollateralCount, holder: holder});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1199
1216
|
// Add to the loan if needed...
|
|
1200
1217
|
if (addedBorrowAmount > 0) {
|
|
1201
1218
|
_addTo({
|
|
@@ -1210,11 +1227,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1210
1227
|
_removeFrom({loan: loan, revnetId: revnetId, repaidBorrowAmount: repaidBorrowAmount});
|
|
1211
1228
|
}
|
|
1212
1229
|
|
|
1213
|
-
//
|
|
1214
|
-
if (
|
|
1215
|
-
_addCollateralTo({revnetId: revnetId, amount: addedCollateralCount, holder: holder});
|
|
1216
|
-
// ... or return collateral if needed.
|
|
1217
|
-
} else if (returnedCollateralCount > 0) {
|
|
1230
|
+
// Return collateral if needed.
|
|
1231
|
+
if (returnedCollateralCount > 0) {
|
|
1218
1232
|
_returnCollateralFrom({
|
|
1219
1233
|
revnetId: revnetId, collateralCount: returnedCollateralCount, beneficiary: beneficiary
|
|
1220
1234
|
});
|
|
@@ -1477,8 +1491,10 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1477
1491
|
function _removeFrom(REVLoan memory loan, uint256 revnetId, uint256 repaidBorrowAmount) internal {
|
|
1478
1492
|
address sourceToken = loan.sourceToken;
|
|
1479
1493
|
|
|
1480
|
-
|
|
1481
|
-
|
|
1494
|
+
IJBMultiTerminal terminal = IJBMultiTerminal(address(TERMINAL));
|
|
1495
|
+
|
|
1496
|
+
// Snapshot the credited terminal balance so fee-on-transfer source tokens cannot under-repay the revnet.
|
|
1497
|
+
uint256 balanceBefore = terminal.STORE().balanceOf(address(TERMINAL), revnetId, sourceToken);
|
|
1482
1498
|
|
|
1483
1499
|
// Increase the allowance for the beneficiary.
|
|
1484
1500
|
uint256 payValue = _beforeTransferTo({to: address(TERMINAL), token: sourceToken, amount: repaidBorrowAmount});
|
|
@@ -1493,6 +1509,17 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1493
1509
|
metadata: bytes(abi.encodePacked(REV_ID))
|
|
1494
1510
|
});
|
|
1495
1511
|
|
|
1512
|
+
uint256 creditedAmount = terminal.STORE().balanceOf(address(TERMINAL), revnetId, sourceToken) - balanceBefore;
|
|
1513
|
+
|
|
1514
|
+
if (creditedAmount != repaidBorrowAmount) {
|
|
1515
|
+
revert REVLoans_FeeOnTransferSourceUnsupported({
|
|
1516
|
+
token: sourceToken, expectedAmount: repaidBorrowAmount, creditedAmount: creditedAmount
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Decrement the total amount of a token being loaned out by the revnet.
|
|
1521
|
+
totalBorrowedFrom[revnetId][sourceToken] -= repaidBorrowAmount;
|
|
1522
|
+
|
|
1496
1523
|
_afterTransferTo({to: address(TERMINAL), token: sourceToken});
|
|
1497
1524
|
}
|
|
1498
1525
|
|
package/src/REVOwner.sol
CHANGED
|
@@ -41,8 +41,8 @@ import {REVOwnerRevnetInit} from "./structs/REVOwnerRevnetInit.sol";
|
|
|
41
41
|
/// @notice The runtime hook for all revnets — set as every 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 outs
|
|
45
|
-
/// proceeds to the fee revnet via `afterCashOutRecordedWith`.
|
|
44
|
+
/// loan debt and collateral), grants suckers 0% tax, splits a 2.5% fee from non-sucker cash outs with a non-zero
|
|
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.
|
|
48
48
|
contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChainAdjustedAccounts, IERC721Receiver {
|
|
@@ -167,12 +167,12 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
167
167
|
|
|
168
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
|
-
/// splits a 2.5% fee from the cashed-out token count
|
|
171
|
-
/// and the fee portion, then delegates to the buyback hook for potential
|
|
172
|
-
///
|
|
173
|
-
///
|
|
174
|
-
/// fee (2.5%) applies on top of the rev fee. The fee hook spec amount sent to
|
|
175
|
-
/// the protocol fee deducted by the terminal before reaching this contract.
|
|
170
|
+
/// splits a 2.5% fee from the cashed-out token count when cash-out tax is non-zero, computes bonding curve
|
|
171
|
+
/// reclaims for both the holder's portion and the fee portion, then delegates to the buyback hook for potential
|
|
172
|
+
/// swap routing.
|
|
173
|
+
/// @dev Part of `IJBRulesetDataHook`. In the non-zero-tax fee path, REVOwner is intentionally not registered as a
|
|
174
|
+
/// feeless address — the protocol fee (2.5%) applies on top of the rev fee. The fee hook spec amount sent to
|
|
175
|
+
/// `afterCashOutRecordedWith` will have the protocol fee deducted by the terminal before reaching this contract.
|
|
176
176
|
/// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
|
|
177
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.
|
|
@@ -232,9 +232,9 @@ contract REVOwner is IREVOwner, IJBRulesetDataHook, IJBCashOutHook, IJBPeerChain
|
|
|
232
232
|
});
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
// If there's no cash out tax
|
|
236
|
-
//
|
|
237
|
-
//
|
|
235
|
+
// If there's no cash out tax, if there's no fee terminal, or if the beneficiary is feeless (e.g. the router
|
|
236
|
+
// terminal routing value between projects), proxy to the buyback hook with our totalSupply and
|
|
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
|
|
240
240
|
// for its swap-vs-passthrough routing decision.
|
|
@@ -156,6 +156,7 @@ interface IREVDeployer {
|
|
|
156
156
|
REVCroptopAllowedPost[] memory allowedPosts
|
|
157
157
|
)
|
|
158
158
|
external
|
|
159
|
+
payable
|
|
159
160
|
returns (uint256, IJB721TiersHook hook);
|
|
160
161
|
|
|
161
162
|
/// @notice Deploy a revnet with a default empty tiered ERC-721 hook.
|
|
@@ -173,6 +174,7 @@ interface IREVDeployer {
|
|
|
173
174
|
REVSuckerDeploymentConfig memory suckerDeploymentConfiguration
|
|
174
175
|
)
|
|
175
176
|
external
|
|
177
|
+
payable
|
|
176
178
|
returns (uint256, IJB721TiersHook hook);
|
|
177
179
|
|
|
178
180
|
/// @notice Deploy new suckers for an existing revnet.
|