@rev-net/core-v6 0.0.36 → 0.0.37
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/RISKS.md +11 -1
- package/package.json +9 -9
- package/src/REVLoans.sol +120 -83
- package/src/REVOwner.sol +3 -3
- package/test/REV.integrations.t.sol +14 -14
- package/test/REVInvincibility.t.sol +16 -16
- package/test/REVLifecycle.t.sol +32 -32
- package/test/REVLoansSourced.t.sol +15 -15
- package/test/TestCashOutCallerValidation.t.sol +8 -8
- package/test/TestConversionDocumentation.t.sol +2 -5
- package/test/TestCrossCurrencyReclaim.t.sol +72 -72
- package/test/TestLongTailEconomics.t.sol +56 -56
- package/test/TestSwapTerminalPermission.t.sol +21 -21
- package/test/audit/HiddenSupplyCashout.t.sol +61 -0
- package/test/audit/NemesisVerification.t.sol +97 -0
- package/test/audit/REVOwnerCurrencyMismatch.t.sol +188 -0
- package/test/audit/{CodexREVOwnerRemoteSurplusCurrencyMismatch.t.sol → REVOwnerRemoteSurplusCurrencyMismatch.t.sol} +4 -6
- package/test/audit/ReallocatePermission.t.sol +363 -0
- package/test/audit/RemoteLoanAccountingGap.t.sol +74 -0
- package/test/fork/TestCashOutFork.t.sol +48 -48
- package/test/fork/TestLoanAdversarialFork.t.sol +744 -0
- package/test/fork/TestLoanERC20Fork.t.sol +2 -8
- package/test/fork/TestPermit2PaymentFork.t.sol +32 -32
- package/test/regression/TestBurnPermissionRequired.t.sol +5 -5
- package/test/regression/TestCashOutBuybackFeeLeak.t.sol +8 -8
- /package/test/audit/{CodexCrossChainBuybackRouteMismatch.t.sol → CrossChainBuybackRouteMismatch.t.sol} +0 -0
- /package/test/audit/{NemesisOperatorDelegation.t.sol → OperatorDelegation.t.sol} +0 -0
- /package/test/audit/{CodexPhantomSurplusTerminal.t.sol → PhantomSurplusTerminal.t.sol} +0 -0
package/RISKS.md
CHANGED
|
@@ -27,7 +27,7 @@ This file focuses on the staged-economics, runtime-hook, hidden-supply, and loan
|
|
|
27
27
|
|
|
28
28
|
- **Stage immutability cuts both ways.** A bad stage schedule or bad cash-out tax choice is expensive to unwind.
|
|
29
29
|
- **Borrowability depends on live economics.** If surplus, supply, or cross-chain state are wrong, loan capacity becomes wrong.
|
|
30
|
-
- **Zero or degraded price feeds can undercount debt.** If a source becomes invisible to debt aggregation, later borrowing can become too permissive.
|
|
30
|
+
- **Zero or degraded price feeds can undercount debt.** If a source becomes invisible to debt aggregation, later borrowing can become too permissive. Specifically, `_debtOf` skips sources where `pricePerUnitOf` returns zero, treating them as if the borrower has no debt in that source. If a feed breaks or returns zero, existing debt in that currency is effectively invisible, inflating the borrower's apparent borrowable amount.
|
|
31
31
|
- **Hidden-token mechanics change visible supply.** That affects per-token cash-out value and can change the economics seen by other holders.
|
|
32
32
|
- **Auto-issuance dilutes holders predictably but still materially.** Timing is permissionless, even if the amounts are fixed at deployment.
|
|
33
33
|
- **Omnichain expansion can corrupt surplus aggregation.** Since borrowability aggregates surplus from all registered terminals across chains, a compromised or misconfigured terminal on a remote chain affects global surplus accounting.
|
|
@@ -95,3 +95,13 @@ A project that expands to a new chain can register additional terminals on that
|
|
|
95
95
|
### 8.7 REVLoans CEI violation in `_adjust`
|
|
96
96
|
|
|
97
97
|
In `REVLoans._adjust`, `totalCollateralOf[revnetId]` is incremented after external calls (`useAllowanceOf`, fee payment). A reentrant `borrowFrom` would see a lower `totalCollateralOf`. This is documented inline (lines 1128-1132) and requires an adversarial pay hook on the revnet's own terminal -- a trust-level configuration that is not realistic in standard deployments.
|
|
98
|
+
|
|
99
|
+
### 8.8 Remote loan corrections not reflected in local borrowability
|
|
100
|
+
|
|
101
|
+
`_borrowableAmountFrom` adds back local `totalBorrowed` and `totalCollateral` to reconstitute pre-loan economic state for the bonding curve. However, remote chain snapshots (built by `JBSuckerLib.buildSnapshotMessage`) capture raw surplus/supply WITHOUT loan corrections from the remote chain. This is accepted because:
|
|
102
|
+
|
|
103
|
+
1. Suckers are a general-purpose bridging layer and should not need knowledge of revnet-specific loan mechanics.
|
|
104
|
+
2. The `localSurplus` cap (REVLoans line 386-387) prevents extraction beyond what the local terminal actually holds.
|
|
105
|
+
3. The over-lending exposure is bounded by the difference between corrected and uncorrected remote values, which is proportional to remote outstanding loans — typically a small fraction of total surplus.
|
|
106
|
+
|
|
107
|
+
Project operators deploying cross-chain revnets with active loan markets on multiple chains should understand that local borrowability calculations do not account for remote outstanding loans.
|
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.37",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'revnet-core-v6'"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@bananapus/721-hook-v6": "^0.0.
|
|
23
|
-
"@bananapus/buyback-hook-v6": "^0.0.
|
|
24
|
-
"@bananapus/core-v6": "^0.0.
|
|
25
|
-
"@bananapus/ownable-v6": "^0.0.
|
|
26
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
27
|
-
"@bananapus/router-terminal-v6": "^0.0.
|
|
28
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
29
|
-
"@croptop/core-v6": "
|
|
22
|
+
"@bananapus/721-hook-v6": "^0.0.38",
|
|
23
|
+
"@bananapus/buyback-hook-v6": "^0.0.30",
|
|
24
|
+
"@bananapus/core-v6": "^0.0.36",
|
|
25
|
+
"@bananapus/ownable-v6": "^0.0.20",
|
|
26
|
+
"@bananapus/permission-ids-v6": "^0.0.19",
|
|
27
|
+
"@bananapus/router-terminal-v6": "^0.0.30",
|
|
28
|
+
"@bananapus/suckers-v6": "^0.0.28",
|
|
29
|
+
"@croptop/core-v6": "^0.0.36",
|
|
30
30
|
"@openzeppelin/contracts": "^5.6.1",
|
|
31
31
|
"@uniswap/v4-core": "^1.0.2",
|
|
32
32
|
"@uniswap/v4-periphery": "^1.0.3"
|
package/src/REVLoans.sol
CHANGED
|
@@ -627,93 +627,15 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
627
627
|
// Note: the operator controls `beneficiary`, so they can direct borrowed funds to any address.
|
|
628
628
|
_requirePermissionFrom({account: holder, projectId: revnetId, permissionId: JBPermissionIds.OPEN_LOAN});
|
|
629
629
|
|
|
630
|
-
|
|
631
|
-
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
|
|
632
|
-
|
|
633
|
-
// Make sure the source terminal is registered in the directory for this revnet.
|
|
634
|
-
if (!DIRECTORY.isTerminalOf({projectId: revnetId, terminal: IJBTerminal(address(source.terminal))})) {
|
|
635
|
-
revert REVLoans_InvalidTerminal(address(source.terminal), revnetId);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Make sure the prepaid fee percent is between `MIN_PREPAID_FEE_PERCENT` and `MAX_PREPAID_FEE_PERCENT`. Meaning
|
|
639
|
-
// an 16 year loan can be paid upfront with a
|
|
640
|
-
// payment of 50% of the borrowed assets, the cheapest possible rate.
|
|
641
|
-
if (prepaidFeePercent < MIN_PREPAID_FEE_PERCENT || prepaidFeePercent > MAX_PREPAID_FEE_PERCENT) {
|
|
642
|
-
revert REVLoans_InvalidPrepaidFeePercent(
|
|
643
|
-
prepaidFeePercent, MIN_PREPAID_FEE_PERCENT, MAX_PREPAID_FEE_PERCENT
|
|
644
|
-
);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Cache the current ruleset once — used by both _cashOutDelayOf and _borrowAmountFrom.
|
|
648
|
-
JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
|
|
649
|
-
|
|
650
|
-
// Enforce the cash out delay.
|
|
651
|
-
{
|
|
652
|
-
uint256 cashOutDelay = _cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset});
|
|
653
|
-
if (cashOutDelay > block.timestamp) {
|
|
654
|
-
revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
659
|
-
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
660
|
-
|
|
661
|
-
// Get a reference to the loan ID.
|
|
662
|
-
loanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
|
|
663
|
-
|
|
664
|
-
// Get a reference to the loan being created.
|
|
665
|
-
REVLoan storage loan = _loanOf[loanId];
|
|
666
|
-
|
|
667
|
-
// Set the loan's values.
|
|
668
|
-
loan.source = source;
|
|
669
|
-
loan.createdAt = uint48(block.timestamp);
|
|
670
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
671
|
-
loan.prepaidFeePercent = uint16(prepaidFeePercent);
|
|
672
|
-
loan.prepaidDuration =
|
|
673
|
-
uint32(mulDiv({x: prepaidFeePercent, y: LOAN_LIQUIDATION_DURATION, denominator: MAX_PREPAID_FEE_PERCENT}));
|
|
674
|
-
|
|
675
|
-
// Get the amount of the loan, using the cached ruleset.
|
|
676
|
-
uint256 borrowAmount = _borrowAmountFrom({
|
|
677
|
-
loan: loan, revnetId: revnetId, collateralCount: collateralCount, currentRuleset: currentRuleset
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
// Revert if the bonding curve returns zero to prevent creating zero-amount loans.
|
|
681
|
-
if (borrowAmount == 0) revert REVLoans_ZeroBorrowAmount();
|
|
682
|
-
|
|
683
|
-
// Make sure the minimum borrow amount is met.
|
|
684
|
-
if (borrowAmount < minBorrowAmount) revert REVLoans_UnderMinBorrowAmount(minBorrowAmount, borrowAmount);
|
|
685
|
-
|
|
686
|
-
// Get the amount of additional fee to take for the revnet issuing the loan.
|
|
687
|
-
// Fee rounding may leave a few wei of dust — economically insignificant relative to gas costs.
|
|
688
|
-
uint256 sourceFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: borrowAmount, feePercent: prepaidFeePercent});
|
|
689
|
-
|
|
690
|
-
// Borrow the amount.
|
|
691
|
-
_adjust({
|
|
692
|
-
loan: loan,
|
|
693
|
-
revnetId: revnetId,
|
|
694
|
-
newBorrowAmount: borrowAmount,
|
|
695
|
-
newCollateralCount: collateralCount,
|
|
696
|
-
sourceFeeAmount: sourceFeeAmount,
|
|
697
|
-
beneficiary: beneficiary,
|
|
698
|
-
holder: holder
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
// Mint the loan NFT to the holder.
|
|
702
|
-
_mint({to: holder, tokenId: loanId});
|
|
703
|
-
|
|
704
|
-
emit Borrow({
|
|
705
|
-
loanId: loanId,
|
|
630
|
+
return _borrowFrom({
|
|
706
631
|
revnetId: revnetId,
|
|
707
|
-
loan: loan,
|
|
708
632
|
source: source,
|
|
709
|
-
|
|
633
|
+
minBorrowAmount: minBorrowAmount,
|
|
710
634
|
collateralCount: collateralCount,
|
|
711
|
-
sourceFeeAmount: sourceFeeAmount,
|
|
712
635
|
beneficiary: beneficiary,
|
|
713
|
-
|
|
636
|
+
prepaidFeePercent: prepaidFeePercent,
|
|
637
|
+
holder: holder
|
|
714
638
|
});
|
|
715
|
-
|
|
716
|
-
return (loanId, loan);
|
|
717
639
|
}
|
|
718
640
|
|
|
719
641
|
/// @notice Liquidates loans that have exceeded the 10-year liquidation duration.
|
|
@@ -838,7 +760,9 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
838
760
|
|
|
839
761
|
// Make a new loan with the leftover collateral from reallocating.
|
|
840
762
|
// The loan owner is the holder for the new loan (their tokens are used as collateral).
|
|
841
|
-
|
|
763
|
+
// Uses _borrowFrom to skip the OPEN_LOAN permission check — the caller already proved REALLOCATE_LOAN
|
|
764
|
+
// permission above, and requiring OPEN_LOAN here would block operators with only REALLOCATE_LOAN.
|
|
765
|
+
(newLoanId, newLoan) = _borrowFrom({
|
|
842
766
|
revnetId: revnetId,
|
|
843
767
|
source: source,
|
|
844
768
|
minBorrowAmount: minBorrowAmount,
|
|
@@ -1234,6 +1158,119 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1234
1158
|
IERC20(token).forceApprove({spender: to, value: 0});
|
|
1235
1159
|
}
|
|
1236
1160
|
|
|
1161
|
+
/// @notice Internal implementation of loan creation, without the OPEN_LOAN permission check.
|
|
1162
|
+
/// @dev Called by `borrowFrom` (after its own permission check) and by `reallocateCollateralFromLoan`
|
|
1163
|
+
/// (which only requires REALLOCATE_LOAN permission).
|
|
1164
|
+
/// @param revnetId The ID of the revnet being borrowed from.
|
|
1165
|
+
/// @param source The source of the loan being borrowed.
|
|
1166
|
+
/// @param minBorrowAmount The minimum amount being borrowed.
|
|
1167
|
+
/// @param collateralCount The amount of tokens to use as collateral for the loan.
|
|
1168
|
+
/// @param beneficiary The address that'll receive the borrowed funds and the tokens resulting from fee payments.
|
|
1169
|
+
/// @param prepaidFeePercent The fee percent that will be charged upfront.
|
|
1170
|
+
/// @param holder The address whose tokens are used as collateral and who receives the loan NFT.
|
|
1171
|
+
/// @return loanId The ID of the loan created from borrowing.
|
|
1172
|
+
/// @return loan The loan created from borrowing.
|
|
1173
|
+
function _borrowFrom(
|
|
1174
|
+
uint256 revnetId,
|
|
1175
|
+
REVLoanSource calldata source,
|
|
1176
|
+
uint256 minBorrowAmount,
|
|
1177
|
+
uint256 collateralCount,
|
|
1178
|
+
address payable beneficiary,
|
|
1179
|
+
uint256 prepaidFeePercent,
|
|
1180
|
+
address holder
|
|
1181
|
+
)
|
|
1182
|
+
internal
|
|
1183
|
+
returns (uint256 loanId, REVLoan memory)
|
|
1184
|
+
{
|
|
1185
|
+
// A loan needs to have collateral.
|
|
1186
|
+
if (collateralCount == 0) revert REVLoans_ZeroCollateralLoanIsInvalid();
|
|
1187
|
+
|
|
1188
|
+
// Make sure the source terminal is registered in the directory for this revnet.
|
|
1189
|
+
if (!DIRECTORY.isTerminalOf({projectId: revnetId, terminal: IJBTerminal(address(source.terminal))})) {
|
|
1190
|
+
revert REVLoans_InvalidTerminal(address(source.terminal), revnetId);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// Make sure the prepaid fee percent is between `MIN_PREPAID_FEE_PERCENT` and `MAX_PREPAID_FEE_PERCENT`. Meaning
|
|
1194
|
+
// an 16 year loan can be paid upfront with a
|
|
1195
|
+
// payment of 50% of the borrowed assets, the cheapest possible rate.
|
|
1196
|
+
if (prepaidFeePercent < MIN_PREPAID_FEE_PERCENT || prepaidFeePercent > MAX_PREPAID_FEE_PERCENT) {
|
|
1197
|
+
revert REVLoans_InvalidPrepaidFeePercent(
|
|
1198
|
+
prepaidFeePercent, MIN_PREPAID_FEE_PERCENT, MAX_PREPAID_FEE_PERCENT
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Cache the current ruleset once — used by both _cashOutDelayOf and _borrowAmountFrom.
|
|
1203
|
+
JBRuleset memory currentRuleset = _currentRulesetOf(revnetId);
|
|
1204
|
+
|
|
1205
|
+
// Enforce the cash out delay.
|
|
1206
|
+
{
|
|
1207
|
+
uint256 cashOutDelay = _cashOutDelayOf({revnetId: revnetId, currentRuleset: currentRuleset});
|
|
1208
|
+
if (cashOutDelay > block.timestamp) {
|
|
1209
|
+
revert REVLoans_CashOutDelayNotFinished(cashOutDelay, block.timestamp);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Prevent the loan number from exceeding the ID namespace for this revnet.
|
|
1214
|
+
if (totalLoansBorrowedFor[revnetId] >= _ONE_TRILLION) revert REVLoans_LoanIdOverflow();
|
|
1215
|
+
|
|
1216
|
+
// Get a reference to the loan ID.
|
|
1217
|
+
loanId = _generateLoanId({revnetId: revnetId, loanNumber: ++totalLoansBorrowedFor[revnetId]});
|
|
1218
|
+
|
|
1219
|
+
// Get a reference to the loan being created.
|
|
1220
|
+
REVLoan storage loan = _loanOf[loanId];
|
|
1221
|
+
|
|
1222
|
+
// Set the loan's values.
|
|
1223
|
+
loan.source = source;
|
|
1224
|
+
loan.createdAt = uint48(block.timestamp);
|
|
1225
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
1226
|
+
loan.prepaidFeePercent = uint16(prepaidFeePercent);
|
|
1227
|
+
loan.prepaidDuration =
|
|
1228
|
+
uint32(mulDiv({x: prepaidFeePercent, y: LOAN_LIQUIDATION_DURATION, denominator: MAX_PREPAID_FEE_PERCENT}));
|
|
1229
|
+
|
|
1230
|
+
// Get the amount of the loan, using the cached ruleset.
|
|
1231
|
+
uint256 borrowAmount = _borrowAmountFrom({
|
|
1232
|
+
loan: loan, revnetId: revnetId, collateralCount: collateralCount, currentRuleset: currentRuleset
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
// Revert if the bonding curve returns zero to prevent creating zero-amount loans.
|
|
1236
|
+
if (borrowAmount == 0) revert REVLoans_ZeroBorrowAmount();
|
|
1237
|
+
|
|
1238
|
+
// Make sure the minimum borrow amount is met.
|
|
1239
|
+
if (borrowAmount < minBorrowAmount) revert REVLoans_UnderMinBorrowAmount(minBorrowAmount, borrowAmount);
|
|
1240
|
+
|
|
1241
|
+
// Get the amount of additional fee to take for the revnet issuing the loan.
|
|
1242
|
+
// Fee rounding may leave a few wei of dust — economically insignificant relative to gas costs.
|
|
1243
|
+
uint256 sourceFeeAmount = JBFees.feeAmountFrom({amountBeforeFee: borrowAmount, feePercent: prepaidFeePercent});
|
|
1244
|
+
|
|
1245
|
+
// Borrow the amount.
|
|
1246
|
+
_adjust({
|
|
1247
|
+
loan: loan,
|
|
1248
|
+
revnetId: revnetId,
|
|
1249
|
+
newBorrowAmount: borrowAmount,
|
|
1250
|
+
newCollateralCount: collateralCount,
|
|
1251
|
+
sourceFeeAmount: sourceFeeAmount,
|
|
1252
|
+
beneficiary: beneficiary,
|
|
1253
|
+
holder: holder
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
// Mint the loan NFT to the holder.
|
|
1257
|
+
_mint({to: holder, tokenId: loanId});
|
|
1258
|
+
|
|
1259
|
+
emit Borrow({
|
|
1260
|
+
loanId: loanId,
|
|
1261
|
+
revnetId: revnetId,
|
|
1262
|
+
loan: loan,
|
|
1263
|
+
source: source,
|
|
1264
|
+
borrowAmount: borrowAmount,
|
|
1265
|
+
collateralCount: collateralCount,
|
|
1266
|
+
sourceFeeAmount: sourceFeeAmount,
|
|
1267
|
+
beneficiary: beneficiary,
|
|
1268
|
+
caller: _msgSender()
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
return (loanId, loan);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1237
1274
|
/// @notice Reallocates collateral from a loan by making a new loan based on the original, with reduced collateral.
|
|
1238
1275
|
/// @param loanId The ID of the loan to reallocate collateral from.
|
|
1239
1276
|
/// @param revnetId The ID of the revnet the loan is from.
|
package/src/REVOwner.sol
CHANGED
|
@@ -178,7 +178,7 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
|
|
|
178
178
|
+ SUCKER_REGISTRY.remoteSurplusOf({
|
|
179
179
|
projectId: context.projectId,
|
|
180
180
|
decimals: context.surplus.decimals,
|
|
181
|
-
currency: uint256(
|
|
181
|
+
currency: uint256(context.surplus.currency)
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
// If there's no cash out tax (100% cash out tax rate), if there's no fee terminal, or if the beneficiary is
|
|
@@ -397,8 +397,8 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
|
|
|
397
397
|
if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
|
|
398
398
|
IERC20(context.forwardedAmount.token)
|
|
399
399
|
.safeDecreaseAllowance({
|
|
400
|
-
|
|
401
|
-
|
|
400
|
+
spender: address(feeTerminal), requestedDecrease: context.forwardedAmount.value
|
|
401
|
+
});
|
|
402
402
|
}
|
|
403
403
|
|
|
404
404
|
// If the fee can't be processed, return the funds to the project.
|
|
@@ -531,25 +531,25 @@ contract REVnet_Integrations is TestBaseWorkflow {
|
|
|
531
531
|
// The loans contract should have USE_ALLOWANCE permission for any revnet via the wildcard grant.
|
|
532
532
|
bool hasPermission = jbPermissions()
|
|
533
533
|
.hasPermission({
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
534
|
+
operator: address(REV_DEPLOYER.LOANS()),
|
|
535
|
+
account: address(REV_DEPLOYER),
|
|
536
|
+
projectId: REVNET_ID,
|
|
537
|
+
permissionId: JBPermissionIds.USE_ALLOWANCE,
|
|
538
|
+
includeRoot: false,
|
|
539
|
+
includeWildcardProjectId: true
|
|
540
|
+
});
|
|
541
541
|
assertTrue(hasPermission, "LOANS should have USE_ALLOWANCE for deployed revnet");
|
|
542
542
|
|
|
543
543
|
// Also holds for a revnet that doesn't exist yet — the wildcard covers all projects.
|
|
544
544
|
bool hasPermissionForFuture = jbPermissions()
|
|
545
545
|
.hasPermission({
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
546
|
+
operator: address(REV_DEPLOYER.LOANS()),
|
|
547
|
+
account: address(REV_DEPLOYER),
|
|
548
|
+
projectId: 999,
|
|
549
|
+
permissionId: JBPermissionIds.USE_ALLOWANCE,
|
|
550
|
+
includeRoot: false,
|
|
551
|
+
includeWildcardProjectId: true
|
|
552
|
+
});
|
|
553
553
|
assertTrue(hasPermissionForFuture, "LOANS should have USE_ALLOWANCE for any project via wildcard");
|
|
554
554
|
}
|
|
555
555
|
|
|
@@ -692,14 +692,14 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
|
|
|
692
692
|
vm.prank(USER);
|
|
693
693
|
try jbMultiTerminal()
|
|
694
694
|
.cashOutTokensOf({
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
695
|
+
holder: USER,
|
|
696
|
+
projectId: REVNET_ID,
|
|
697
|
+
cashOutCount: tokens,
|
|
698
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
699
|
+
minTokensReclaimed: 0,
|
|
700
|
+
beneficiary: payable(USER),
|
|
701
|
+
metadata: ""
|
|
702
|
+
}) returns (
|
|
703
703
|
uint256 reclaimAmount
|
|
704
704
|
) {
|
|
705
705
|
// The reclaim amount should be bounded by the bonding curve
|
|
@@ -944,14 +944,14 @@ contract REVInvincibility_PropertyTests is TestBaseWorkflow {
|
|
|
944
944
|
vm.prank(USER);
|
|
945
945
|
try jbMultiTerminal()
|
|
946
946
|
.cashOutTokensOf({
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
947
|
+
holder: USER,
|
|
948
|
+
projectId: REVNET_ID,
|
|
949
|
+
cashOutCount: tokens / 2,
|
|
950
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
951
|
+
minTokensReclaimed: 0,
|
|
952
|
+
beneficiary: payable(USER),
|
|
953
|
+
metadata: ""
|
|
954
|
+
}) returns (
|
|
955
955
|
uint256 reclaimAmount
|
|
956
956
|
) {
|
|
957
957
|
// The double fee means the fee project gets more than expected
|
package/test/REVLifecycle.t.sol
CHANGED
|
@@ -265,14 +265,14 @@ contract REVLifecycle_Local is TestBaseWorkflow {
|
|
|
265
265
|
vm.prank(USER1);
|
|
266
266
|
uint256 reclaimed = jbMultiTerminal()
|
|
267
267
|
.cashOutTokensOf({
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
268
|
+
holder: USER1,
|
|
269
|
+
projectId: REVNET_ID,
|
|
270
|
+
cashOutCount: cashOutAmount,
|
|
271
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
272
|
+
minTokensReclaimed: 0,
|
|
273
|
+
beneficiary: payable(USER1),
|
|
274
|
+
metadata: ""
|
|
275
|
+
});
|
|
276
276
|
assertGt(reclaimed, 0, "should reclaim some ETH");
|
|
277
277
|
|
|
278
278
|
// Total supply should decrease after cash out
|
|
@@ -311,14 +311,14 @@ contract REVLifecycle_Local is TestBaseWorkflow {
|
|
|
311
311
|
vm.prank(USER1);
|
|
312
312
|
uint256 reclaimedStage0 = jbMultiTerminal()
|
|
313
313
|
.cashOutTokensOf({
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
314
|
+
holder: USER1,
|
|
315
|
+
projectId: REVNET_ID,
|
|
316
|
+
cashOutCount: halfTokens,
|
|
317
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
318
|
+
minTokensReclaimed: 0,
|
|
319
|
+
beneficiary: payable(USER1),
|
|
320
|
+
metadata: ""
|
|
321
|
+
});
|
|
322
322
|
assertGt(reclaimedStage0, 0, "should reclaim in stage 0");
|
|
323
323
|
|
|
324
324
|
// Cash out tax with 50% rate means you get less than proportional share
|
|
@@ -344,14 +344,14 @@ contract REVLifecycle_Local is TestBaseWorkflow {
|
|
|
344
344
|
vm.prank(USER1);
|
|
345
345
|
uint256 reclaimed = jbMultiTerminal()
|
|
346
346
|
.cashOutTokensOf({
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
347
|
+
holder: USER1,
|
|
348
|
+
projectId: REVNET_ID,
|
|
349
|
+
cashOutCount: tokens,
|
|
350
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
351
|
+
minTokensReclaimed: 0,
|
|
352
|
+
beneficiary: payable(USER1),
|
|
353
|
+
metadata: ""
|
|
354
|
+
});
|
|
355
355
|
|
|
356
356
|
// With 50% cash out tax and single holder, reclaiming full supply
|
|
357
357
|
// should return less than full amount (due to tax)
|
|
@@ -386,14 +386,14 @@ contract REVLifecycle_Local is TestBaseWorkflow {
|
|
|
386
386
|
vm.prank(USER1);
|
|
387
387
|
uint256 reclaimed1 = jbMultiTerminal()
|
|
388
388
|
.cashOutTokensOf({
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
389
|
+
holder: USER1,
|
|
390
|
+
projectId: REVNET_ID,
|
|
391
|
+
cashOutCount: tokens1,
|
|
392
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
393
|
+
minTokensReclaimed: 0,
|
|
394
|
+
beneficiary: payable(USER1),
|
|
395
|
+
metadata: ""
|
|
396
|
+
});
|
|
397
397
|
|
|
398
398
|
// Should reclaim proportional share (minus tax)
|
|
399
399
|
assertGt(reclaimed1, 0, "user1 should reclaim some ETH");
|
|
@@ -751,11 +751,11 @@ contract REVLoansSourcedTests is TestBaseWorkflow {
|
|
|
751
751
|
|
|
752
752
|
uint256 fullReclaimableSurplus = jbMultiTerminal().STORE()
|
|
753
753
|
.currentReclaimableSurplusOf({
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
754
|
+
projectId: revnetProjectId,
|
|
755
|
+
cashOutCount: tokensToCashout,
|
|
756
|
+
totalSupply: totalSupplyExcludingAutoMint,
|
|
757
|
+
surplus: nativeSurplus
|
|
758
|
+
});
|
|
759
759
|
|
|
760
760
|
assertGe(fullReclaimableSurplus, loanable);
|
|
761
761
|
|
|
@@ -764,11 +764,11 @@ contract REVLoansSourcedTests is TestBaseWorkflow {
|
|
|
764
764
|
|
|
765
765
|
uint256 reclaimableSurplus = jbMultiTerminal().STORE()
|
|
766
766
|
.currentReclaimableSurplusOf({
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
767
|
+
projectId: revnetProjectId,
|
|
768
|
+
cashOutCount: tokensToCashout - feeTokenCount,
|
|
769
|
+
totalSupply: totalSupplyExcludingAutoMint,
|
|
770
|
+
surplus: nativeSurplus
|
|
771
|
+
});
|
|
772
772
|
|
|
773
773
|
// In the `revFee` calculation we decrease the `nativeSurplus` by the `reclaimableSurplus`
|
|
774
774
|
// but due to a `stack too deep` we can't do that there, so we decrease it here.
|
|
@@ -777,11 +777,11 @@ contract REVLoansSourcedTests is TestBaseWorkflow {
|
|
|
777
777
|
|
|
778
778
|
uint256 revFee = jbMultiTerminal().STORE()
|
|
779
779
|
.currentReclaimableSurplusOf({
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
780
|
+
projectId: revnetProjectId,
|
|
781
|
+
cashOutCount: feeTokenCount,
|
|
782
|
+
totalSupply: totalSupplyExcludingAutoMint - (tokensToCashout - feeTokenCount),
|
|
783
|
+
surplus: nativeSurplus
|
|
784
|
+
});
|
|
785
785
|
|
|
786
786
|
assertGe(fullReclaimableSurplus, mulDiv((reclaimableSurplus + revFee), 995, 1000)); // small marging for curve
|
|
787
787
|
// rounding.
|
|
@@ -315,14 +315,14 @@ contract TestCashOutCallerValidation is TestBaseWorkflow {
|
|
|
315
315
|
vm.prank(USER);
|
|
316
316
|
uint256 reclaimed = jbMultiTerminal()
|
|
317
317
|
.cashOutTokensOf({
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
318
|
+
holder: USER,
|
|
319
|
+
projectId: REVNET_ID,
|
|
320
|
+
cashOutCount: tokenCount,
|
|
321
|
+
tokenToReclaim: JBConstants.NATIVE_TOKEN,
|
|
322
|
+
minTokensReclaimed: 0,
|
|
323
|
+
beneficiary: payable(USER),
|
|
324
|
+
metadata: ""
|
|
325
|
+
});
|
|
326
326
|
|
|
327
327
|
assertGt(reclaimed, 0, "Should have reclaimed some ETH");
|
|
328
328
|
|
|
@@ -286,11 +286,8 @@ contract TestConversionDocumentation is TestBaseWorkflow {
|
|
|
286
286
|
vm.prank(USER);
|
|
287
287
|
jbController()
|
|
288
288
|
.launchRulesetsFor({
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
terminalConfigurations: termConfigs,
|
|
292
|
-
memo: ""
|
|
293
|
-
});
|
|
289
|
+
projectId: projectId, rulesetConfigurations: rulesetConfigs, terminalConfigurations: termConfigs, memo: ""
|
|
290
|
+
});
|
|
294
291
|
|
|
295
292
|
// Now try to convert this project to a revnet — should revert.
|
|
296
293
|
// Approve NFT to REV_DEPLOYER.
|