@rev-net/core-v6 0.0.11 → 0.0.13
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/ADMINISTRATION.md +7 -7
- package/ARCHITECTURE.md +11 -11
- package/AUDIT_INSTRUCTIONS.md +295 -0
- package/CHANGE_LOG.md +316 -0
- package/README.md +9 -6
- package/RISKS.md +180 -35
- package/SKILLS.md +9 -11
- package/STYLE_GUIDE.md +14 -1
- package/USER_JOURNEYS.md +489 -0
- package/package.json +9 -9
- package/script/Deploy.s.sol +124 -40
- package/script/helpers/RevnetCoreDeploymentLib.sol +19 -6
- package/src/REVDeployer.sol +183 -175
- package/src/REVLoans.sol +65 -28
- package/src/interfaces/IREVDeployer.sol +25 -23
- package/src/structs/REV721TiersHookFlags.sol +1 -0
- package/src/structs/REVAutoIssuance.sol +1 -0
- package/src/structs/REVBaseline721HookConfig.sol +1 -0
- package/src/structs/REVConfig.sol +1 -0
- package/src/structs/REVCroptopAllowedPost.sol +1 -0
- package/src/structs/REVDeploy721TiersHookConfig.sol +13 -14
- package/src/structs/REVDescription.sol +1 -0
- package/src/structs/REVLoan.sol +1 -0
- package/src/structs/REVLoanSource.sol +1 -0
- package/src/structs/REVStageConfig.sol +1 -0
- package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
- package/test/REV.integrations.t.sol +148 -19
- package/test/REVAutoIssuanceFuzz.t.sol +31 -6
- package/test/REVDeployerRegressions.t.sol +47 -9
- package/test/REVInvincibility.t.sol +83 -19
- package/test/REVInvincibilityHandler.sol +29 -0
- package/test/REVLifecycle.t.sol +36 -6
- package/test/REVLoans.invariants.t.sol +64 -10
- package/test/REVLoansAttacks.t.sol +54 -9
- package/test/REVLoansFeeRecovery.t.sol +61 -15
- package/test/REVLoansFindings.t.sol +42 -9
- package/test/REVLoansRegressions.t.sol +33 -6
- package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
- package/test/REVLoansSourced.t.sol +79 -17
- package/test/REVLoansUnSourced.t.sol +61 -10
- package/test/TestBurnHeldTokens.t.sol +47 -11
- package/test/TestCEIPattern.t.sol +37 -6
- package/test/TestCashOutCallerValidation.t.sol +41 -8
- package/test/TestConversionDocumentation.t.sol +50 -13
- package/test/TestCrossCurrencyReclaim.t.sol +584 -0
- package/test/TestCrossSourceReallocation.t.sol +37 -6
- package/test/TestERC2771MetaTx.t.sol +557 -0
- package/test/TestEmptyBuybackSpecs.t.sol +45 -10
- package/test/TestFlashLoanSurplus.t.sol +39 -7
- package/test/TestHookArrayOOB.t.sol +42 -13
- package/test/TestLiquidationBehavior.t.sol +37 -7
- package/test/TestLoanSourceRotation.t.sol +525 -0
- package/test/TestLongTailEconomics.t.sol +651 -0
- package/test/TestLowFindings.t.sol +80 -8
- package/test/TestMixedFixes.t.sol +43 -9
- package/test/TestPermit2Signatures.t.sol +657 -0
- package/test/TestReallocationSandwich.t.sol +384 -0
- package/test/TestRevnetRegressions.t.sol +324 -0
- package/test/TestSplitWeightAdjustment.t.sol +52 -13
- package/test/TestSplitWeightE2E.t.sol +53 -18
- package/test/TestSplitWeightFork.t.sol +66 -21
- package/test/TestStageTransitionBorrowable.t.sol +38 -6
- package/test/TestSwapTerminalPermission.t.sol +37 -7
- package/test/TestUint112Overflow.t.sol +39 -6
- package/test/TestZeroRepayment.t.sol +37 -6
- package/test/fork/ForkTestBase.sol +66 -17
- package/test/fork/TestCashOutFork.t.sol +9 -3
- package/test/fork/TestLoanBorrowFork.t.sol +1 -0
- package/test/fork/TestLoanCrossRulesetFork.t.sol +11 -3
- package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
- package/test/fork/TestLoanReallocateFork.t.sol +1 -0
- package/test/fork/TestLoanRepayFork.t.sol +1 -0
- package/test/fork/TestLoanTransferFork.t.sol +133 -0
- package/test/fork/TestSplitWeightFork.t.sol +3 -0
- package/test/helpers/REVEmpty721Config.sol +46 -0
- package/test/mock/MockBuybackDataHook.sol +1 -0
- package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
- package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
- package/test/regression/TestCumulativeLoanCounter.t.sol +38 -8
- package/test/regression/TestLiquidateGapHandling.t.sol +40 -8
- package/test/regression/TestZeroPriceFeed.t.sol +396 -0
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
4
5
|
import "forge-std/Test.sol";
|
|
6
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
7
|
import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
|
|
6
|
-
import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
8
|
+
// import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
9
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
7
10
|
import /* {*} from */ "./../src/REVDeployer.sol";
|
|
11
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
8
12
|
import "@croptop/core-v6/src/CTPublisher.sol";
|
|
9
13
|
import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
|
|
14
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
10
15
|
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
16
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
11
17
|
import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
|
|
18
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
12
19
|
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
20
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
13
21
|
import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
|
|
22
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
14
23
|
import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
|
|
15
24
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
16
25
|
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
@@ -29,25 +38,40 @@ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
|
29
38
|
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
30
39
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
31
40
|
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
41
|
+
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
32
42
|
|
|
33
43
|
/// @notice Tests for PR #13: cross-source reallocation prevention.
|
|
34
44
|
contract TestCrossSourceReallocation is TestBaseWorkflow {
|
|
45
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
35
46
|
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
36
47
|
|
|
48
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
37
49
|
REVDeployer REV_DEPLOYER;
|
|
50
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
38
51
|
JB721TiersHook EXAMPLE_HOOK;
|
|
52
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
39
53
|
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
54
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
40
55
|
IJB721TiersHookStore HOOK_STORE;
|
|
56
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
41
57
|
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
58
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
42
59
|
IREVLoans LOANS_CONTRACT;
|
|
60
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
43
61
|
MockERC20 TOKEN;
|
|
62
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
44
63
|
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
64
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
45
65
|
CTPublisher PUBLISHER;
|
|
66
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
46
67
|
MockBuybackDataHook MOCK_BUYBACK;
|
|
47
68
|
|
|
69
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
48
70
|
uint256 FEE_PROJECT_ID;
|
|
71
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
49
72
|
uint256 REVNET_ID;
|
|
50
73
|
|
|
74
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
51
75
|
address USER = makeAddr("user");
|
|
52
76
|
|
|
53
77
|
address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
|
|
@@ -57,8 +81,9 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
|
|
|
57
81
|
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
58
82
|
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
59
83
|
HOOK_STORE = new JB721TiersHookStore();
|
|
60
|
-
EXAMPLE_HOOK =
|
|
61
|
-
|
|
84
|
+
EXAMPLE_HOOK = new JB721TiersHook(
|
|
85
|
+
jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
|
|
86
|
+
);
|
|
62
87
|
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
63
88
|
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
64
89
|
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
@@ -119,6 +144,7 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
|
|
|
119
144
|
extraMetadata: 0
|
|
120
145
|
});
|
|
121
146
|
REVConfig memory cfg = REVConfig({
|
|
147
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
122
148
|
description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
|
|
123
149
|
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
124
150
|
splitOperator: multisig(),
|
|
@@ -131,7 +157,9 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
|
|
|
131
157
|
terminalConfigurations: tc,
|
|
132
158
|
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
133
159
|
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
|
|
134
|
-
})
|
|
160
|
+
}),
|
|
161
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
162
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
135
163
|
});
|
|
136
164
|
}
|
|
137
165
|
|
|
@@ -163,18 +191,21 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
|
|
|
163
191
|
REVLoanSource[] memory ls = new REVLoanSource[](1);
|
|
164
192
|
ls[0] = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
165
193
|
REVConfig memory cfg = REVConfig({
|
|
194
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
166
195
|
description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
|
|
167
196
|
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
168
197
|
splitOperator: multisig(),
|
|
169
198
|
stageConfigurations: stages
|
|
170
199
|
});
|
|
171
|
-
REVNET_ID = REV_DEPLOYER.deployFor({
|
|
200
|
+
(REVNET_ID,) = REV_DEPLOYER.deployFor({
|
|
172
201
|
revnetId: 0,
|
|
173
202
|
configuration: cfg,
|
|
174
203
|
terminalConfigurations: tc,
|
|
175
204
|
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
176
205
|
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
|
|
177
|
-
})
|
|
206
|
+
}),
|
|
207
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
208
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
178
209
|
});
|
|
179
210
|
}
|
|
180
211
|
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
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 {MockBuybackDataHook} from "./mock/MockBuybackDataHook.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 {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
28
|
+
import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
|
|
29
|
+
import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
|
|
30
|
+
import {REVLoans} from "../src/REVLoans.sol";
|
|
31
|
+
import {REVLoan} from "../src/structs/REVLoan.sol";
|
|
32
|
+
import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
|
|
33
|
+
import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
|
|
34
|
+
import {REVDescription} from "../src/structs/REVDescription.sol";
|
|
35
|
+
import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
|
|
36
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
37
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
38
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
39
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
40
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
41
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
42
|
+
import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
|
|
43
|
+
import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
|
|
44
|
+
import {ERC2771Forwarder} from "@openzeppelin/contracts/metatx/ERC2771Forwarder.sol";
|
|
45
|
+
import {ERC2771ForwarderMock, ForwardRequest} from "@bananapus/core-v6/test/mock/ERC2771ForwarderMock.sol";
|
|
46
|
+
|
|
47
|
+
struct MetaTxProjectConfig {
|
|
48
|
+
REVConfig configuration;
|
|
49
|
+
JBTerminalConfig[] terminalConfigurations;
|
|
50
|
+
REVSuckerDeploymentConfig suckerDeploymentConfiguration;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// @title TestERC2771MetaTx
|
|
54
|
+
/// @notice Tests that REVLoans and REVDeployer correctly use ERC2771Context,
|
|
55
|
+
/// ensuring _msgSender() returns the actual user when called through a trusted forwarder,
|
|
56
|
+
/// and falls back to msg.sender when called through an untrusted one.
|
|
57
|
+
contract TestERC2771MetaTx is TestBaseWorkflow {
|
|
58
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
59
|
+
bytes32 REV_DEPLOYER_SALT = "REVDeployer";
|
|
60
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
61
|
+
bytes32 ERC20_SALT = "REV_TOKEN";
|
|
62
|
+
|
|
63
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
64
|
+
REVDeployer REV_DEPLOYER;
|
|
65
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
66
|
+
JB721TiersHook EXAMPLE_HOOK;
|
|
67
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
68
|
+
IJB721TiersHookDeployer HOOK_DEPLOYER;
|
|
69
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
70
|
+
IJB721TiersHookStore HOOK_STORE;
|
|
71
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
72
|
+
IJBAddressRegistry ADDRESS_REGISTRY;
|
|
73
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
74
|
+
REVLoans LOANS_CONTRACT;
|
|
75
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
76
|
+
MockERC20 TOKEN;
|
|
77
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
78
|
+
IJBSuckerRegistry SUCKER_REGISTRY;
|
|
79
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
80
|
+
CTPublisher PUBLISHER;
|
|
81
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
82
|
+
MockBuybackDataHook MOCK_BUYBACK;
|
|
83
|
+
|
|
84
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
85
|
+
uint256 FEE_PROJECT_ID;
|
|
86
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
87
|
+
uint256 REVNET_ID;
|
|
88
|
+
|
|
89
|
+
// The trusted forwarder mock deployed at a specific address.
|
|
90
|
+
ERC2771ForwarderMock internal erc2771Forwarder;
|
|
91
|
+
address internal constant FORWARDER_ADDRESS = address(123_456);
|
|
92
|
+
|
|
93
|
+
// Meta-tx signer and relayer.
|
|
94
|
+
uint256 internal signerPrivateKey;
|
|
95
|
+
uint256 internal relayerPrivateKey;
|
|
96
|
+
address internal signerAddr;
|
|
97
|
+
address internal relayerAddr;
|
|
98
|
+
|
|
99
|
+
function _getFeeProjectConfig() internal view returns (MetaTxProjectConfig memory) {
|
|
100
|
+
string memory name = "Revnet";
|
|
101
|
+
string memory symbol = "$REV";
|
|
102
|
+
string memory projectUri = "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNcosvuhX21wkF3tx";
|
|
103
|
+
uint8 decimals = 18;
|
|
104
|
+
uint256 decimalMultiplier = 10 ** decimals;
|
|
105
|
+
|
|
106
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
|
|
107
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
108
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
109
|
+
});
|
|
110
|
+
accountingContextsToAccept[1] =
|
|
111
|
+
JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
112
|
+
|
|
113
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
114
|
+
terminalConfigurations[0] =
|
|
115
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
116
|
+
|
|
117
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
|
|
118
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
119
|
+
splits[0].beneficiary = payable(multisig());
|
|
120
|
+
splits[0].percent = 10_000;
|
|
121
|
+
|
|
122
|
+
REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
|
|
123
|
+
issuanceConfs[0] = REVAutoIssuance({
|
|
124
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
125
|
+
chainId: uint32(block.chainid),
|
|
126
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
127
|
+
count: uint104(70_000 * decimalMultiplier),
|
|
128
|
+
beneficiary: multisig()
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
stageConfigurations[0] = REVStageConfig({
|
|
132
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
133
|
+
autoIssuances: issuanceConfs,
|
|
134
|
+
splitPercent: 2000,
|
|
135
|
+
splits: splits,
|
|
136
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
137
|
+
initialIssuance: uint112(1000 * decimalMultiplier),
|
|
138
|
+
issuanceCutFrequency: 90 days,
|
|
139
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
140
|
+
cashOutTaxRate: 6000,
|
|
141
|
+
extraMetadata: 0
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
REVConfig memory revnetConfiguration = REVConfig({
|
|
145
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
146
|
+
description: REVDescription(name, symbol, projectUri, ERC20_SALT),
|
|
147
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
148
|
+
splitOperator: multisig(),
|
|
149
|
+
stageConfigurations: stageConfigurations
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return MetaTxProjectConfig({
|
|
153
|
+
configuration: revnetConfiguration,
|
|
154
|
+
terminalConfigurations: terminalConfigurations,
|
|
155
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
156
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _getRevnetConfig() internal view returns (MetaTxProjectConfig memory) {
|
|
162
|
+
string memory name = "NANA";
|
|
163
|
+
string memory symbol = "$NANA";
|
|
164
|
+
string memory projectUri = "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNxosvuhX21wkF3tx";
|
|
165
|
+
uint8 decimals = 18;
|
|
166
|
+
uint256 decimalMultiplier = 10 ** decimals;
|
|
167
|
+
|
|
168
|
+
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
|
|
169
|
+
accountingContextsToAccept[0] = JBAccountingContext({
|
|
170
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
171
|
+
});
|
|
172
|
+
accountingContextsToAccept[1] =
|
|
173
|
+
JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
|
|
174
|
+
|
|
175
|
+
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
176
|
+
terminalConfigurations[0] =
|
|
177
|
+
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
178
|
+
|
|
179
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
180
|
+
splits[0].beneficiary = payable(multisig());
|
|
181
|
+
splits[0].percent = 10_000;
|
|
182
|
+
|
|
183
|
+
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
|
|
184
|
+
REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
|
|
185
|
+
issuanceConfs[0] = REVAutoIssuance({
|
|
186
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
187
|
+
chainId: uint32(block.chainid),
|
|
188
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
189
|
+
count: uint104(70_000 * decimalMultiplier),
|
|
190
|
+
beneficiary: multisig()
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
stageConfigurations[0] = REVStageConfig({
|
|
194
|
+
startsAtOrAfter: uint40(block.timestamp),
|
|
195
|
+
autoIssuances: issuanceConfs,
|
|
196
|
+
splitPercent: 2000,
|
|
197
|
+
splits: splits,
|
|
198
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
199
|
+
initialIssuance: uint112(1000 * decimalMultiplier),
|
|
200
|
+
issuanceCutFrequency: 90 days,
|
|
201
|
+
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
202
|
+
cashOutTaxRate: 6000,
|
|
203
|
+
extraMetadata: 0
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
REVConfig memory revnetConfiguration = REVConfig({
|
|
207
|
+
// forge-lint: disable-next-line(named-struct-fields)
|
|
208
|
+
description: REVDescription(name, symbol, projectUri, "NANA_TOKEN"),
|
|
209
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
210
|
+
splitOperator: multisig(),
|
|
211
|
+
stageConfigurations: stageConfigurations
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return MetaTxProjectConfig({
|
|
215
|
+
configuration: revnetConfiguration,
|
|
216
|
+
terminalConfigurations: terminalConfigurations,
|
|
217
|
+
suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
|
|
218
|
+
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("NANA"))
|
|
219
|
+
})
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// =========================================================================
|
|
224
|
+
// Helper: construct a ForwardRequestData from a ForwardRequest
|
|
225
|
+
// =========================================================================
|
|
226
|
+
|
|
227
|
+
function _forgeRequestData(
|
|
228
|
+
uint256 value,
|
|
229
|
+
uint256 nonce,
|
|
230
|
+
uint48 deadline,
|
|
231
|
+
bytes memory data,
|
|
232
|
+
address target
|
|
233
|
+
)
|
|
234
|
+
internal
|
|
235
|
+
view
|
|
236
|
+
returns (ERC2771Forwarder.ForwardRequestData memory)
|
|
237
|
+
{
|
|
238
|
+
ForwardRequest memory request = ForwardRequest({
|
|
239
|
+
from: signerAddr, to: target, value: value, gas: 3_000_000, nonce: nonce, deadline: deadline, data: data
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
bytes32 digest = erc2771Forwarder.structHash(request);
|
|
243
|
+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, digest);
|
|
244
|
+
bytes memory signature = abi.encodePacked(r, s, v);
|
|
245
|
+
|
|
246
|
+
return ERC2771Forwarder.ForwardRequestData({
|
|
247
|
+
from: request.from,
|
|
248
|
+
to: request.to,
|
|
249
|
+
value: request.value,
|
|
250
|
+
gas: request.gas,
|
|
251
|
+
deadline: request.deadline,
|
|
252
|
+
data: request.data,
|
|
253
|
+
signature: signature
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function setUp() public override {
|
|
258
|
+
super.setUp();
|
|
259
|
+
|
|
260
|
+
signerPrivateKey = 0xA11CE;
|
|
261
|
+
relayerPrivateKey = 0xB0B;
|
|
262
|
+
signerAddr = vm.addr(signerPrivateKey);
|
|
263
|
+
relayerAddr = vm.addr(relayerPrivateKey);
|
|
264
|
+
|
|
265
|
+
// Deploy ERC2771ForwarderMock at the FORWARDER_ADDRESS using deployCodeTo.
|
|
266
|
+
deployCodeTo("ERC2771ForwarderMock.sol", abi.encode("ERC2771Forwarder"), FORWARDER_ADDRESS);
|
|
267
|
+
erc2771Forwarder = ERC2771ForwarderMock(FORWARDER_ADDRESS);
|
|
268
|
+
|
|
269
|
+
FEE_PROJECT_ID = jbProjects().createFor(multisig());
|
|
270
|
+
|
|
271
|
+
SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
|
|
272
|
+
HOOK_STORE = new JB721TiersHookStore();
|
|
273
|
+
EXAMPLE_HOOK = new JB721TiersHook(
|
|
274
|
+
jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
|
|
275
|
+
);
|
|
276
|
+
ADDRESS_REGISTRY = new JBAddressRegistry();
|
|
277
|
+
HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
|
|
278
|
+
PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
|
|
279
|
+
MOCK_BUYBACK = new MockBuybackDataHook();
|
|
280
|
+
TOKEN = new MockERC20("1/2 ETH", "1/2");
|
|
281
|
+
|
|
282
|
+
MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
|
|
283
|
+
vm.prank(multisig());
|
|
284
|
+
jbPrices()
|
|
285
|
+
.addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
|
|
286
|
+
|
|
287
|
+
// Deploy LOANS_CONTRACT with the forwarder as trusted forwarder.
|
|
288
|
+
LOANS_CONTRACT = new REVLoans({
|
|
289
|
+
controller: jbController(),
|
|
290
|
+
projects: jbProjects(),
|
|
291
|
+
revId: FEE_PROJECT_ID,
|
|
292
|
+
owner: address(this),
|
|
293
|
+
permit2: permit2(),
|
|
294
|
+
trustedForwarder: FORWARDER_ADDRESS
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
|
|
298
|
+
jbController(),
|
|
299
|
+
SUCKER_REGISTRY,
|
|
300
|
+
FEE_PROJECT_ID,
|
|
301
|
+
HOOK_DEPLOYER,
|
|
302
|
+
PUBLISHER,
|
|
303
|
+
IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
|
|
304
|
+
address(LOANS_CONTRACT),
|
|
305
|
+
FORWARDER_ADDRESS
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Approve the deployer to configure the project.
|
|
309
|
+
vm.prank(address(multisig()));
|
|
310
|
+
jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
|
|
311
|
+
|
|
312
|
+
// Deploy fee project.
|
|
313
|
+
MetaTxProjectConfig memory feeProjectConfig = _getFeeProjectConfig();
|
|
314
|
+
vm.prank(address(multisig()));
|
|
315
|
+
REV_DEPLOYER.deployFor({
|
|
316
|
+
revnetId: FEE_PROJECT_ID,
|
|
317
|
+
configuration: feeProjectConfig.configuration,
|
|
318
|
+
terminalConfigurations: feeProjectConfig.terminalConfigurations,
|
|
319
|
+
suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
|
|
320
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
321
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Deploy second revnet.
|
|
325
|
+
MetaTxProjectConfig memory revnetConfig = _getRevnetConfig();
|
|
326
|
+
(REVNET_ID,) = REV_DEPLOYER.deployFor({
|
|
327
|
+
revnetId: 0,
|
|
328
|
+
configuration: revnetConfig.configuration,
|
|
329
|
+
terminalConfigurations: revnetConfig.terminalConfigurations,
|
|
330
|
+
suckerDeploymentConfiguration: revnetConfig.suckerDeploymentConfiguration,
|
|
331
|
+
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
332
|
+
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Fund the signer and relayer.
|
|
336
|
+
vm.deal(signerAddr, 1000e18);
|
|
337
|
+
vm.deal(relayerAddr, 1000e18);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// =========================================================================
|
|
341
|
+
// Test: ERC2771 trusted forwarder is correctly configured
|
|
342
|
+
// =========================================================================
|
|
343
|
+
|
|
344
|
+
/// @notice Verifies that the trusted forwarder is set correctly on REVLoans.
|
|
345
|
+
function test_erc2771_trustedForwarderIsSet() public view {
|
|
346
|
+
assertTrue(LOANS_CONTRACT.isTrustedForwarder(FORWARDER_ADDRESS), "Forwarder should be trusted");
|
|
347
|
+
assertFalse(LOANS_CONTRACT.isTrustedForwarder(address(0x999)), "Random address should not be trusted");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// =========================================================================
|
|
351
|
+
// Test: borrow via trusted forwarder — loan owned by signer, not relayer
|
|
352
|
+
// =========================================================================
|
|
353
|
+
|
|
354
|
+
/// @notice When borrowFrom() is called through the trusted forwarder, the loan NFT
|
|
355
|
+
/// should be minted to the actual signer (from the appended calldata),
|
|
356
|
+
/// not the relayer who submitted the transaction.
|
|
357
|
+
function test_erc2771_borrowViaForwarder() public {
|
|
358
|
+
// First, signer pays into the revnet to get tokens.
|
|
359
|
+
vm.prank(signerAddr);
|
|
360
|
+
uint256 tokenCount =
|
|
361
|
+
jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, signerAddr, 0, "", "");
|
|
362
|
+
assertTrue(tokenCount > 0, "Should receive tokens from payment");
|
|
363
|
+
|
|
364
|
+
// Check borrowable amount.
|
|
365
|
+
uint256 borrowable =
|
|
366
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
367
|
+
vm.assume(borrowable > 0);
|
|
368
|
+
|
|
369
|
+
// Mock permission for loans contract to burn the signer's tokens.
|
|
370
|
+
mockExpect(
|
|
371
|
+
address(jbPermissions()),
|
|
372
|
+
abi.encodeCall(
|
|
373
|
+
IJBPermissions.hasPermission, (address(LOANS_CONTRACT), signerAddr, REVNET_ID, 11, true, true)
|
|
374
|
+
),
|
|
375
|
+
abi.encode(true)
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// Encode the borrowFrom call.
|
|
379
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
380
|
+
|
|
381
|
+
bytes memory borrowData = abi.encodeWithSelector(
|
|
382
|
+
IREVLoans.borrowFrom.selector,
|
|
383
|
+
REVNET_ID,
|
|
384
|
+
source,
|
|
385
|
+
0, // minBorrowAmount
|
|
386
|
+
tokenCount,
|
|
387
|
+
payable(signerAddr),
|
|
388
|
+
uint256(25) // MIN_PREPAID_FEE_PERCENT
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Build the forwarded request signed by the signer.
|
|
392
|
+
ERC2771Forwarder.ForwardRequestData memory requestData = _forgeRequestData({
|
|
393
|
+
value: 0, nonce: 0, deadline: uint48(block.timestamp + 1), data: borrowData, target: address(LOANS_CONTRACT)
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Relayer submits the meta-tx.
|
|
397
|
+
vm.prank(relayerAddr);
|
|
398
|
+
erc2771Forwarder.execute{value: 0}(requestData);
|
|
399
|
+
|
|
400
|
+
// Verify the loan was created and is owned by the signer (not the relayer).
|
|
401
|
+
uint256 loansBalance = LOANS_CONTRACT.balanceOf(signerAddr);
|
|
402
|
+
assertTrue(loansBalance > 0, "Signer should own the loan NFT");
|
|
403
|
+
|
|
404
|
+
uint256 relayerLoans = LOANS_CONTRACT.balanceOf(relayerAddr);
|
|
405
|
+
assertEq(relayerLoans, 0, "Relayer should not own any loan NFTs");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// =========================================================================
|
|
409
|
+
// Test: repay via trusted forwarder
|
|
410
|
+
// =========================================================================
|
|
411
|
+
|
|
412
|
+
/// @notice When repayLoan() is called through the trusted forwarder, the loan owner
|
|
413
|
+
/// check should use _msgSender() (the signer), not msg.sender (the forwarder).
|
|
414
|
+
function test_erc2771_repayViaForwarder() public {
|
|
415
|
+
// Signer pays to get tokens and creates a loan.
|
|
416
|
+
vm.prank(signerAddr);
|
|
417
|
+
uint256 tokenCount =
|
|
418
|
+
jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, signerAddr, 0, "", "");
|
|
419
|
+
|
|
420
|
+
uint256 borrowable =
|
|
421
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
422
|
+
vm.assume(borrowable > 0);
|
|
423
|
+
|
|
424
|
+
// Mock permission.
|
|
425
|
+
mockExpect(
|
|
426
|
+
address(jbPermissions()),
|
|
427
|
+
abi.encodeCall(
|
|
428
|
+
IJBPermissions.hasPermission, (address(LOANS_CONTRACT), signerAddr, REVNET_ID, 11, true, true)
|
|
429
|
+
),
|
|
430
|
+
abi.encode(true)
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
434
|
+
|
|
435
|
+
vm.prank(signerAddr);
|
|
436
|
+
(uint256 loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(signerAddr), 25);
|
|
437
|
+
|
|
438
|
+
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
439
|
+
assertTrue(loan.amount > 0, "Loan should exist");
|
|
440
|
+
|
|
441
|
+
// Calculate repay amount.
|
|
442
|
+
uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
|
|
443
|
+
uint256 totalRepay = loan.amount + sourceFee;
|
|
444
|
+
|
|
445
|
+
// Encode the repayLoan call.
|
|
446
|
+
bytes memory repayData = abi.encodeWithSelector(
|
|
447
|
+
IREVLoans.repayLoan.selector,
|
|
448
|
+
loanId,
|
|
449
|
+
totalRepay * 2, // maxRepayBorrowAmount
|
|
450
|
+
loan.collateral,
|
|
451
|
+
payable(signerAddr),
|
|
452
|
+
JBSingleAllowance({sigDeadline: 0, amount: 0, expiration: 0, nonce: 0, signature: ""})
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
// Build the forwarded request signed by the signer.
|
|
456
|
+
ERC2771Forwarder.ForwardRequestData memory requestData = _forgeRequestData({
|
|
457
|
+
value: totalRepay * 2,
|
|
458
|
+
nonce: 0,
|
|
459
|
+
deadline: uint48(block.timestamp + 1),
|
|
460
|
+
data: repayData,
|
|
461
|
+
target: address(LOANS_CONTRACT)
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Relayer submits the meta-tx with ETH.
|
|
465
|
+
vm.prank(relayerAddr);
|
|
466
|
+
erc2771Forwarder.execute{value: totalRepay * 2}(requestData);
|
|
467
|
+
|
|
468
|
+
// Verify loan was repaid: collateral should be zero.
|
|
469
|
+
uint256 totalCollateral = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
|
|
470
|
+
assertEq(totalCollateral, 0, "All collateral should be returned after repay via forwarder");
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// =========================================================================
|
|
474
|
+
// Test: untrusted forwarder uses msg.sender, not appended address
|
|
475
|
+
// =========================================================================
|
|
476
|
+
|
|
477
|
+
/// @notice When a call is forwarded through a forwarder that is NOT the trusted one,
|
|
478
|
+
/// OpenZeppelin's ERC2771Forwarder checks `isTrustedForwarder` on the target
|
|
479
|
+
/// and reverts with `ERC2771UntrustfulTarget`. This prevents identity spoofing
|
|
480
|
+
/// at the forwarder level itself.
|
|
481
|
+
function test_erc2771_untrustedForwarder_usesMsgSender() public {
|
|
482
|
+
// Deploy a different forwarder at a different address (NOT the trusted one).
|
|
483
|
+
address untrustedForwarderAddr = address(789_012);
|
|
484
|
+
deployCodeTo("ERC2771ForwarderMock.sol", abi.encode("UntrustedForwarder"), untrustedForwarderAddr);
|
|
485
|
+
ERC2771ForwarderMock untrustedForwarder = ERC2771ForwarderMock(untrustedForwarderAddr);
|
|
486
|
+
|
|
487
|
+
// Verify the untrusted forwarder is not trusted by the LOANS_CONTRACT.
|
|
488
|
+
assertFalse(
|
|
489
|
+
LOANS_CONTRACT.isTrustedForwarder(untrustedForwarderAddr), "Untrusted forwarder should not be trusted"
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
// Signer pays to get tokens.
|
|
493
|
+
vm.prank(signerAddr);
|
|
494
|
+
uint256 tokenCount =
|
|
495
|
+
jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, signerAddr, 0, "", "");
|
|
496
|
+
|
|
497
|
+
uint256 borrowable =
|
|
498
|
+
LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
|
|
499
|
+
vm.assume(borrowable > 0);
|
|
500
|
+
|
|
501
|
+
// Encode the borrowFrom call.
|
|
502
|
+
REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
503
|
+
|
|
504
|
+
bytes memory borrowData = abi.encodeWithSelector(
|
|
505
|
+
IREVLoans.borrowFrom.selector, REVNET_ID, source, 0, tokenCount, payable(signerAddr), uint256(25)
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
// Build a forwarded request using the signer's key via the UNTRUSTED forwarder.
|
|
509
|
+
ForwardRequest memory request = ForwardRequest({
|
|
510
|
+
from: signerAddr,
|
|
511
|
+
to: address(LOANS_CONTRACT),
|
|
512
|
+
value: 0,
|
|
513
|
+
gas: 3_000_000,
|
|
514
|
+
nonce: 0,
|
|
515
|
+
deadline: uint48(block.timestamp + 1),
|
|
516
|
+
data: borrowData
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
bytes32 digest = untrustedForwarder.structHash(request);
|
|
520
|
+
(uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, digest);
|
|
521
|
+
bytes memory signature = abi.encodePacked(r, s, v);
|
|
522
|
+
|
|
523
|
+
ERC2771Forwarder.ForwardRequestData memory requestData = ERC2771Forwarder.ForwardRequestData({
|
|
524
|
+
from: request.from,
|
|
525
|
+
to: request.to,
|
|
526
|
+
value: request.value,
|
|
527
|
+
gas: request.gas,
|
|
528
|
+
deadline: request.deadline,
|
|
529
|
+
data: request.data,
|
|
530
|
+
signature: signature
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// OpenZeppelin's ERC2771Forwarder.execute() checks isTrustedForwarder on the
|
|
534
|
+
// target contract. Since the untrusted forwarder is not the trusted one,
|
|
535
|
+
// it reverts with ERC2771UntrustfulTarget(target, forwarder).
|
|
536
|
+
vm.prank(relayerAddr);
|
|
537
|
+
vm.expectRevert(
|
|
538
|
+
abi.encodeWithSelector(
|
|
539
|
+
ERC2771Forwarder.ERC2771UntrustfulTarget.selector, address(LOANS_CONTRACT), untrustedForwarderAddr
|
|
540
|
+
)
|
|
541
|
+
);
|
|
542
|
+
untrustedForwarder.execute{value: 0}(requestData);
|
|
543
|
+
|
|
544
|
+
// Verify no loan was created for the signer.
|
|
545
|
+
uint256 signerLoansBalance = LOANS_CONTRACT.balanceOf(signerAddr);
|
|
546
|
+
assertEq(signerLoansBalance, 0, "No loan should exist since untrusted forwarder was rejected");
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// =========================================================================
|
|
550
|
+
// Test: forwarder is correctly deployed and functional
|
|
551
|
+
// =========================================================================
|
|
552
|
+
|
|
553
|
+
/// @notice Sanity check that the forwarder mock deployed correctly.
|
|
554
|
+
function test_erc2771_forwarderDeployed() public view {
|
|
555
|
+
assertTrue(erc2771Forwarder.deployed(), "Forwarder should report as deployed");
|
|
556
|
+
}
|
|
557
|
+
}
|