@rev-net/core-v6 0.0.34 → 0.0.36
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/ARCHITECTURE.md +14 -0
- package/RISKS.md +9 -1
- package/package.json +1 -1
- package/src/REVDeployer.sol +29 -10
- package/src/REVLoans.sol +18 -6
- package/src/REVOwner.sol +3 -1
- package/test/TestTerminalEncodingInHash.t.sol +326 -0
- package/test/audit/CodexREVOwnerRemoteSurplusCurrencyMismatch.t.sol +142 -0
- package/test/audit/SupportsInterfaceTest.t.sol +51 -0
- package/test/audit/TestFeeAllowanceLeak.t.sol +197 -0
- package/test/audit/TestLoansAndDeployerFixes.t.sol +576 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -82,6 +82,18 @@ The repo does not replace core treasury accounting. Its critical economic logic
|
|
|
82
82
|
- If you change borrowability, re-check cash-out-delay gating, omnichain surplus inputs, and local-surplus caps together.
|
|
83
83
|
- If you change hook composition, re-check 721 split handling, buyback assumptions, and mint-permission flows.
|
|
84
84
|
|
|
85
|
+
## Cross-Chain Configuration Hash
|
|
86
|
+
|
|
87
|
+
`REVDeployer` produces an `encodedConfigurationHash` for each revnet that determines sucker deployment salts. This hash commits the revnet's identity across chains. It includes:
|
|
88
|
+
|
|
89
|
+
- `baseCurrency`, `description.name`, `description.ticker`, `description.salt`
|
|
90
|
+
- Terminal addresses (order-sensitive)
|
|
91
|
+
- Stage parameters (timing, issuance, splits, tax rates, auto-issuances)
|
|
92
|
+
|
|
93
|
+
Terminal addresses are included because they are deployed deterministically at the same address across chains. Accounting contexts (token addresses) are excluded because tokens like USDC legitimately differ per chain.
|
|
94
|
+
|
|
95
|
+
This means a revnet can only expand to a new chain if it uses the exact same terminal contract it used on the host chain. Different terminal addresses produce a different hash, preventing accidental cross-chain mismatches in sucker deployments.
|
|
96
|
+
|
|
85
97
|
## Canonical Checks
|
|
86
98
|
|
|
87
99
|
- cash-out-delay interaction with loans:
|
|
@@ -90,6 +102,8 @@ The repo does not replace core treasury accounting. Its critical economic logic
|
|
|
90
102
|
`test/TestStageTransitionBorrowable.t.sol`
|
|
91
103
|
- omnichain or phantom-surplus edge cases:
|
|
92
104
|
`test/audit/CodexPhantomSurplusTerminal.t.sol`
|
|
105
|
+
- terminal encoding in configuration hash:
|
|
106
|
+
`test/TestTerminalEncodingInHash.t.sol`
|
|
93
107
|
|
|
94
108
|
## Source Map
|
|
95
109
|
|
package/RISKS.md
CHANGED
|
@@ -86,4 +86,12 @@ The model assumes that attempts to inflate surplus through donations are not pro
|
|
|
86
86
|
|
|
87
87
|
### 8.5 Omnichain terminal expansion inherits remote-chain trust
|
|
88
88
|
|
|
89
|
-
A project that expands to a new chain can register additional terminals on that chain. Because borrowability calculations aggregate surplus from all registered terminals across all chains, a compromised or misconfigured terminal on a remote chain can corrupt the project's surplus accounting globally. This is
|
|
89
|
+
A project that expands to a new chain can register additional terminals on that chain. Because borrowability calculations aggregate surplus from all registered terminals across all chains, a compromised or misconfigured terminal on a remote chain can corrupt the project's surplus accounting globally. This is mitigated by including terminal addresses in the `encodedConfigurationHash` — cross-chain expansions via suckers must use the exact same terminal address as the host chain. Terminal addresses are deterministic across chains (same CREATE2 deployment), so this prevents expansions from silently using a different terminal. Project operators should still treat each chain expansion as a trust-boundary decision since bridge integrity and network assumptions remain outside protocol control.
|
|
90
|
+
|
|
91
|
+
### 8.6 Cross-chain surplus staleness
|
|
92
|
+
|
|
93
|
+
`REVLoans._borrowableAmountFrom` and `REVOwner.beforeCashOutRecordedWith` add `remoteSurplusOf()` and `remoteTotalSupplyOf()` to local values. These remote values update only when `toRemote()` is called on the peer chain -- no heartbeat or staleness check. Stale data can inflate per-token borrowable amounts when remote supply has grown since the last bridge message. Primary safeguard: borrowable is capped at `localSurplus` (REVLoans line 386-387), preventing extraction beyond what the local terminal holds.
|
|
94
|
+
|
|
95
|
+
### 8.7 REVLoans CEI violation in `_adjust`
|
|
96
|
+
|
|
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.
|
package/package.json
CHANGED
package/src/REVDeployer.sol
CHANGED
|
@@ -798,7 +798,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
798
798
|
if (shouldDeployNewRevnet) {
|
|
799
799
|
// If we're deploying a new revnet, launch a Juicebox project for it.
|
|
800
800
|
// Sanity check that we deployed the `revnetId` that we expected to deploy.
|
|
801
|
-
// slither-disable-next-line reentrancy-benign,reentrancy-events
|
|
801
|
+
// slither-disable-next-line incorrect-equality,reentrancy-benign,reentrancy-events
|
|
802
802
|
assert(
|
|
803
803
|
CONTROLLER.launchProjectFor({
|
|
804
804
|
owner: address(this),
|
|
@@ -956,10 +956,26 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
956
956
|
configuration.description.salt
|
|
957
957
|
);
|
|
958
958
|
|
|
959
|
+
// Include terminal addresses in the hash so cross-chain expansions must use the same terminals.
|
|
960
|
+
// Terminal addresses are deterministic across chains. Accounting contexts are excluded because
|
|
961
|
+
// token addresses (e.g. USDC) legitimately differ per chain.
|
|
962
|
+
for (uint256 i; i < terminalConfigurations.length;) {
|
|
963
|
+
encodedConfiguration = abi.encode(encodedConfiguration, terminalConfigurations[i].terminal);
|
|
964
|
+
unchecked {
|
|
965
|
+
++i;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
959
969
|
// Initialize fund access limit groups for the loan contract.
|
|
960
970
|
JBFundAccessLimitGroup[] memory fundAccessLimitGroups =
|
|
961
971
|
_makeLoanFundAccessLimits({terminalConfigurations: terminalConfigurations});
|
|
962
972
|
|
|
973
|
+
// Track the previous stage's effective start time for ordering validation.
|
|
974
|
+
// When stage 0 uses `startsAtOrAfter == 0`, the effective value is `block.timestamp`.
|
|
975
|
+
// Subsequent stages must be validated against this normalized value, not the raw calldata,
|
|
976
|
+
// so that cross-chain deployments can reproduce the same encoded configuration hash.
|
|
977
|
+
uint256 previousStageStart;
|
|
978
|
+
|
|
963
979
|
// Iterate through each stage to set up its ruleset.
|
|
964
980
|
for (uint256 i; i < configuration.stageConfigurations.length;) {
|
|
965
981
|
// Set the stage being iterated on.
|
|
@@ -971,12 +987,19 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
971
987
|
revert REVDeployer_MustHaveSplits();
|
|
972
988
|
}
|
|
973
989
|
|
|
990
|
+
// Compute the effective start time for this stage.
|
|
991
|
+
uint256 effectiveStart = (i == 0 && stageConfiguration.startsAtOrAfter == 0)
|
|
992
|
+
? block.timestamp
|
|
993
|
+
: stageConfiguration.startsAtOrAfter;
|
|
994
|
+
|
|
974
995
|
// If the stage's start time is not after the previous stage's start time, revert.
|
|
975
|
-
if (i > 0 &&
|
|
976
|
-
{
|
|
996
|
+
if (i > 0 && effectiveStart <= previousStageStart) {
|
|
977
997
|
revert REVDeployer_StageTimesMustIncrease();
|
|
978
998
|
}
|
|
979
999
|
|
|
1000
|
+
// Store for the next iteration's ordering check.
|
|
1001
|
+
previousStageStart = effectiveStart;
|
|
1002
|
+
|
|
980
1003
|
// Make sure the revnet doesn't prevent cashouts all together.
|
|
981
1004
|
if (stageConfiguration.cashOutTaxRate >= JBConstants.MAX_CASH_OUT_TAX_RATE) {
|
|
982
1005
|
revert REVDeployer_CashOutsCantBeTurnedOffCompletely(
|
|
@@ -994,13 +1017,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
|
|
|
994
1017
|
// Add the stage's properties to the byte-encoded configuration.
|
|
995
1018
|
encodedConfiguration = abi.encode(
|
|
996
1019
|
encodedConfiguration,
|
|
997
|
-
//
|
|
998
|
-
//
|
|
999
|
-
|
|
1000
|
-
// same start time.
|
|
1001
|
-
(i == 0 && stageConfiguration.startsAtOrAfter == 0)
|
|
1002
|
-
? block.timestamp
|
|
1003
|
-
: stageConfiguration.startsAtOrAfter,
|
|
1020
|
+
// Use the effective start time (normalized from 0 to block.timestamp for the first stage).
|
|
1021
|
+
// Cross-chain deployments reproduce the hash by specifying the origin chain's timestamp.
|
|
1022
|
+
effectiveStart,
|
|
1004
1023
|
stageConfiguration.splitPercent,
|
|
1005
1024
|
stageConfiguration.initialIssuance,
|
|
1006
1025
|
stageConfiguration.issuanceCutFrequency,
|
package/src/REVLoans.sol
CHANGED
|
@@ -543,17 +543,18 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
543
543
|
// Get a reference to the token being iterated on.
|
|
544
544
|
REVLoanSource storage source = sources[i];
|
|
545
545
|
|
|
546
|
-
// Get a reference to the accounting context for the source.
|
|
547
|
-
// slither-disable-next-line calls-loop
|
|
548
|
-
JBAccountingContext memory accountingContext =
|
|
549
|
-
source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
|
|
550
|
-
|
|
551
546
|
// Get a reference to the amount of tokens loaned out.
|
|
552
547
|
uint256 tokensLoaned = totalBorrowedFrom[revnetId][source.terminal][source.token];
|
|
553
548
|
|
|
554
|
-
// Skip if no tokens are loaned from this source.
|
|
549
|
+
// Skip if no tokens are loaned from this source. Checked before the external call below to avoid
|
|
550
|
+
// reverting on stale sources whose terminals may no longer support this token.
|
|
555
551
|
if (tokensLoaned == 0) continue;
|
|
556
552
|
|
|
553
|
+
// Get a reference to the accounting context for the source.
|
|
554
|
+
// slither-disable-next-line calls-loop
|
|
555
|
+
JBAccountingContext memory accountingContext =
|
|
556
|
+
source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
|
|
557
|
+
|
|
557
558
|
// Normalize the token amount from the source's decimals to the target decimals.
|
|
558
559
|
uint256 normalizedTokens;
|
|
559
560
|
if (accountingContext.decimals > decimals) {
|
|
@@ -1225,6 +1226,14 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1225
1226
|
return 0;
|
|
1226
1227
|
}
|
|
1227
1228
|
|
|
1229
|
+
/// @notice Clears any token allowance granted by `_beforeTransferTo`.
|
|
1230
|
+
/// @param to The address that was granted the allowance.
|
|
1231
|
+
/// @param token The token whose allowance should be cleared.
|
|
1232
|
+
function _afterTransferTo(address to, address token) internal {
|
|
1233
|
+
if (token == JBConstants.NATIVE_TOKEN) return;
|
|
1234
|
+
IERC20(token).forceApprove({spender: to, value: 0});
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1228
1237
|
/// @notice Reallocates collateral from a loan by making a new loan based on the original, with reduced collateral.
|
|
1229
1238
|
/// @param loanId The ID of the loan to reallocate collateral from.
|
|
1230
1239
|
/// @param revnetId The ID of the revnet the loan is from.
|
|
@@ -1334,6 +1343,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1334
1343
|
memo: "",
|
|
1335
1344
|
metadata: bytes(abi.encodePacked(REV_ID))
|
|
1336
1345
|
});
|
|
1346
|
+
|
|
1347
|
+
_afterTransferTo({to: address(loan.source.terminal), token: loan.source.token});
|
|
1337
1348
|
}
|
|
1338
1349
|
|
|
1339
1350
|
/// @notice Pays down a loan.
|
|
@@ -1532,6 +1543,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
|
|
|
1532
1543
|
metadata: bytes(abi.encodePacked(metadataProjectId))
|
|
1533
1544
|
}) {
|
|
1534
1545
|
success = true;
|
|
1546
|
+
_afterTransferTo({to: address(terminal), token: token});
|
|
1535
1547
|
} catch (bytes memory) {
|
|
1536
1548
|
if (token != JBConstants.NATIVE_TOKEN) {
|
|
1537
1549
|
IERC20(token).safeDecreaseAllowance({spender: address(terminal), requestedDecrease: amount});
|
package/src/REVOwner.sol
CHANGED
|
@@ -16,6 +16,7 @@ import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashO
|
|
|
16
16
|
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
17
17
|
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
18
18
|
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
19
|
+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
19
20
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
20
21
|
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
21
22
|
import {mulDiv} from "@prb/math/src/Common.sol";
|
|
@@ -458,7 +459,8 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
|
|
|
458
459
|
/// @dev See `IERC165.supportsInterface`.
|
|
459
460
|
/// @return A flag indicating if the provided interface ID is supported.
|
|
460
461
|
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
|
461
|
-
return interfaceId == type(
|
|
462
|
+
return interfaceId == type(IERC165).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
|
|
463
|
+
|| interfaceId == type(IJBCashOutHook).interfaceId;
|
|
462
464
|
}
|
|
463
465
|
|
|
464
466
|
//*********************************************************************//
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.28;
|
|
3
|
+
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
|
+
import "forge-std/Test.sol";
|
|
6
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
7
|
+
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
8
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
9
|
+
import /* {*} from */ "./../src/REVDeployer.sol";
|
|
10
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
11
|
+
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
12
|
+
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
13
|
+
|
|
14
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
15
|
+
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
16
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
17
|
+
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
18
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
19
|
+
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
20
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
21
|
+
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
22
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
23
|
+
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
24
|
+
|
|
25
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
26
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
27
|
+
import {REVLoans} from "../src/REVLoans.sol";
|
|
28
|
+
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
29
|
+
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
30
|
+
import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
|
|
31
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
32
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
33
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
34
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
35
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
36
|
+
import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
|
|
37
|
+
import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
|
|
38
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
39
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
40
|
+
import {REVOwner} from "../src/REVOwner.sol";
|
|
41
|
+
import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
|
|
42
|
+
import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
|
|
43
|
+
import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
|
|
44
|
+
|
|
45
|
+
/// @notice Tests that terminal addresses are included in the encoded configuration hash.
|
|
46
|
+
contract TestTerminalEncodingInHash is TestBaseWorkflow {
|
|
47
|
+
using JBRulesetMetadataResolver for JBRuleset;
|
|
48
|
+
|
|
49
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
50
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
51
|
+
|
|
52
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
53
|
+
REVDeployer REV_DEPLOYER;
|
|
54
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
55
|
+
REVOwner REV_OWNER;
|
|
56
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
57
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
58
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
59
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
60
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
61
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
62
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
63
|
+
IREVLoans LOANS_CONTRACT;
|
|
64
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
65
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
66
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
67
|
+
CTPublisher PUBLISHER;
|
|
68
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
69
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
70
|
+
|
|
71
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
72
|
+
uint256 FEE_PROJECT_ID;
|
|
73
|
+
|
|
74
|
+
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
75
|
+
|
|
76
|
+
function setUp() public override {
|
|
77
|
+
super.setUp();
|
|
78
|
+
|
|
79
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
80
|
+
|
|
81
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
82
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
83
|
+
JB721TiersHook exampleHook = new JB721TiersHook(
|
|
84
|
+
jbDirectory(),
|
|
85
|
+
jbPermissions(),
|
|
86
|
+
jbPrices(),
|
|
87
|
+
jbRulesets(),
|
|
88
|
+
HOOK_STORE,
|
|
89
|
+
jbSplits(),
|
|
90
|
+
IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
|
|
91
|
+
multisig()
|
|
92
|
+
);
|
|
93
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
94
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(exampleHook, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
95
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
96
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
97
|
+
|
|
98
|
+
LOANS_CONTRACT = new REVLoans({
|
|
99
|
+
controller: jbController(),
|
|
100
|
+
suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
|
|
101
|
+
revId: FEE_PROJECT_ID,
|
|
102
|
+
owner: address(this),
|
|
103
|
+
permit2: permit2(),
|
|
104
|
+
trustedForwarder: TRUSTED_FORWARDER
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
REV_OWNER = new REVOwner(
|
|
108
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
109
|
+
jbDirectory(),
|
|
110
|
+
FEE_PROJECT_ID,
|
|
111
|
+
SUCKER_REGISTRY,
|
|
112
|
+
address(LOANS_CONTRACT),
|
|
113
|
+
address(0)
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
117
|
+
jbController(),
|
|
118
|
+
SUCKER_REGISTRY,
|
|
119
|
+
FEE_PROJECT_ID,
|
|
120
|
+
HOOK_DEPLOYER,
|
|
121
|
+
PUBLISHER,
|
|
122
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
123
|
+
address(LOANS_CONTRACT),
|
|
124
|
+
TRUSTED_FORWARDER,
|
|
125
|
+
address(REV_OWNER)
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
REV_OWNER.setDeployer(REV_DEPLOYER);
|
|
129
|
+
|
|
130
|
+
vm.prank(multisig());
|
|
131
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
132
|
+
|
|
133
|
+
// Deploy fee project.
|
|
134
|
+
_deployFeeProject();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// @notice Two revnets with identical base config but different terminals produce different hashes.
|
|
138
|
+
function test_differentTerminals_produceDifferentHashes() public {
|
|
139
|
+
// Deploy revnet A with the primary multi-terminal (same description salt for both).
|
|
140
|
+
(uint256 revnetA,) = REV_DEPLOYER.deployFor({
|
|
141
|
+
revnetId: 0,
|
|
142
|
+
configuration: _baseRevConfig("DIFF_TERM"),
|
|
143
|
+
terminalConfigurations: _terminalConfigs(jbMultiTerminal()),
|
|
144
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
145
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("A")
|
|
146
|
+
}),
|
|
147
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
148
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Deploy revnet B with the secondary multi-terminal (same config, only terminal differs).
|
|
152
|
+
(uint256 revnetB,) = REV_DEPLOYER.deployFor({
|
|
153
|
+
revnetId: 0,
|
|
154
|
+
configuration: _baseRevConfig("DIFF_TERM"),
|
|
155
|
+
terminalConfigurations: _terminalConfigs(jbMultiTerminal2()),
|
|
156
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
157
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("B")
|
|
158
|
+
}),
|
|
159
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
160
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
bytes32 hashA = REV_DEPLOYER.hashedEncodedConfigurationOf(revnetA);
|
|
164
|
+
bytes32 hashB = REV_DEPLOYER.hashedEncodedConfigurationOf(revnetB);
|
|
165
|
+
|
|
166
|
+
assertNotEq(hashA, hashB, "Different terminals must produce different configuration hashes");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// @notice The hash includes the terminal address — verify by computing it off-chain.
|
|
170
|
+
function test_hashIncludesTerminalAddress() public {
|
|
171
|
+
// Deploy a revnet.
|
|
172
|
+
(uint256 revnetId,) = REV_DEPLOYER.deployFor({
|
|
173
|
+
revnetId: 0,
|
|
174
|
+
configuration: _baseRevConfig("VERIFY"),
|
|
175
|
+
terminalConfigurations: _terminalConfigs(jbMultiTerminal()),
|
|
176
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
177
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("VERIFY")
|
|
178
|
+
}),
|
|
179
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
180
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Recompute the expected hash manually.
|
|
184
|
+
bytes memory encodedConfiguration = abi.encode(
|
|
185
|
+
uint32(uint160(JBConstants.NATIVE_TOKEN)), // baseCurrency
|
|
186
|
+
"Terminal Test", // name
|
|
187
|
+
"TERM", // ticker
|
|
188
|
+
bytes32("VERIFY") // salt
|
|
189
|
+
);
|
|
190
|
+
// Terminal address encoding.
|
|
191
|
+
encodedConfiguration = abi.encode(encodedConfiguration, jbMultiTerminal());
|
|
192
|
+
// Stage encoding.
|
|
193
|
+
encodedConfiguration = abi.encode(
|
|
194
|
+
encodedConfiguration,
|
|
195
|
+
block.timestamp, // startsAtOrAfter
|
|
196
|
+
uint256(0), // splitPercent
|
|
197
|
+
uint112(1000e18), // initialIssuance
|
|
198
|
+
uint256(0), // issuanceCutFrequency
|
|
199
|
+
uint256(0), // issuanceCutPercent
|
|
200
|
+
uint256(5000) // cashOutTaxRate
|
|
201
|
+
);
|
|
202
|
+
bytes32 expectedHash = keccak256(encodedConfiguration);
|
|
203
|
+
|
|
204
|
+
assertEq(
|
|
205
|
+
REV_DEPLOYER.hashedEncodedConfigurationOf(revnetId),
|
|
206
|
+
expectedHash,
|
|
207
|
+
"On-chain hash must match off-chain computation including terminal address"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/// @notice Terminal ordering matters — [A, B] != [B, A].
|
|
212
|
+
function test_terminalOrder_affectsHash() public {
|
|
213
|
+
// Deploy revnet with terminals in order [main, alt].
|
|
214
|
+
JBTerminalConfig[] memory tcAB = new JBTerminalConfig[](2);
|
|
215
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
216
|
+
acc[0] = JBAccountingContext({
|
|
217
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
218
|
+
});
|
|
219
|
+
tcAB[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
220
|
+
tcAB[1] = JBTerminalConfig({terminal: jbMultiTerminal2(), accountingContextsToAccept: acc});
|
|
221
|
+
|
|
222
|
+
(uint256 revnetAB,) = REV_DEPLOYER.deployFor({
|
|
223
|
+
revnetId: 0,
|
|
224
|
+
configuration: _baseRevConfig("ORDER"),
|
|
225
|
+
terminalConfigurations: tcAB,
|
|
226
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
227
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("AB")
|
|
228
|
+
}),
|
|
229
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
230
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Deploy revnet with terminals in order [alt, main].
|
|
234
|
+
JBTerminalConfig[] memory tcBA = new JBTerminalConfig[](2);
|
|
235
|
+
tcBA[0] = JBTerminalConfig({terminal: jbMultiTerminal2(), accountingContextsToAccept: acc});
|
|
236
|
+
tcBA[1] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
237
|
+
|
|
238
|
+
(uint256 revnetBA,) = REV_DEPLOYER.deployFor({
|
|
239
|
+
revnetId: 0,
|
|
240
|
+
configuration: _baseRevConfig("ORDER"),
|
|
241
|
+
terminalConfigurations: tcBA,
|
|
242
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
243
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("BA")
|
|
244
|
+
}),
|
|
245
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
246
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
bytes32 hashAB = REV_DEPLOYER.hashedEncodedConfigurationOf(revnetAB);
|
|
250
|
+
bytes32 hashBA = REV_DEPLOYER.hashedEncodedConfigurationOf(revnetBA);
|
|
251
|
+
|
|
252
|
+
assertNotEq(hashAB, hashBA, "Terminal order must affect the configuration hash");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── Helpers
|
|
256
|
+
// ───────────────────────────────────────────────────────────────
|
|
257
|
+
// //
|
|
258
|
+
|
|
259
|
+
function _baseRevConfig(bytes32 salt) internal view returns (REVConfig memory) {
|
|
260
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
261
|
+
stages[0] = REVStageConfig({
|
|
262
|
+
startsAtOrAfter: uint48(block.timestamp),
|
|
263
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
264
|
+
splitPercent: 0,
|
|
265
|
+
splits: new JBSplit[](0),
|
|
266
|
+
initialIssuance: uint112(1000e18),
|
|
267
|
+
issuanceCutFrequency: 0,
|
|
268
|
+
issuanceCutPercent: 0,
|
|
269
|
+
cashOutTaxRate: 5000,
|
|
270
|
+
extraMetadata: 0
|
|
271
|
+
});
|
|
272
|
+
return REVConfig({
|
|
273
|
+
description: REVDescription("Terminal Test", "TERM", "", salt),
|
|
274
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
275
|
+
splitOperator: multisig(),
|
|
276
|
+
stageConfigurations: stages
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function _terminalConfigs(IJBMultiTerminal terminal) internal pure returns (JBTerminalConfig[] memory tc) {
|
|
281
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
282
|
+
acc[0] = JBAccountingContext({
|
|
283
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
284
|
+
});
|
|
285
|
+
tc = new JBTerminalConfig[](1);
|
|
286
|
+
tc[0] = JBTerminalConfig({terminal: terminal, accountingContextsToAccept: acc});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function _deployFeeProject() internal {
|
|
290
|
+
JBAccountingContext[] memory acc = new JBAccountingContext[](1);
|
|
291
|
+
acc[0] = JBAccountingContext({
|
|
292
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
293
|
+
});
|
|
294
|
+
JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
|
|
295
|
+
tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
|
|
296
|
+
REVStageConfig[] memory stages = new REVStageConfig[](1);
|
|
297
|
+
stages[0] = REVStageConfig({
|
|
298
|
+
startsAtOrAfter: uint48(block.timestamp),
|
|
299
|
+
autoIssuances: new REVAutoIssuance[](0),
|
|
300
|
+
splitPercent: 0,
|
|
301
|
+
splits: new JBSplit[](0),
|
|
302
|
+
initialIssuance: uint112(1000e18),
|
|
303
|
+
issuanceCutFrequency: 0,
|
|
304
|
+
issuanceCutPercent: 0,
|
|
305
|
+
cashOutTaxRate: 0,
|
|
306
|
+
extraMetadata: 0
|
|
307
|
+
});
|
|
308
|
+
REVConfig memory feeConfig = REVConfig({
|
|
309
|
+
description: REVDescription("Fee Project", "FEE", "", bytes32("FEE")),
|
|
310
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
311
|
+
splitOperator: multisig(),
|
|
312
|
+
stageConfigurations: stages
|
|
313
|
+
});
|
|
314
|
+
vm.prank(multisig());
|
|
315
|
+
REV_DEPLOYER.deployFor({
|
|
316
|
+
revnetId: FEE_PROJECT_ID,
|
|
317
|
+
configuration: feeConfig,
|
|
318
|
+
terminalConfigurations: tc,
|
|
319
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
320
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
|
|
321
|
+
}),
|
|
322
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
323
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|