forked from InverterNetwork/contracts
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathFM_PC_ExternalPrice_Redeeming_v1.t.sol
1723 lines (1505 loc) · 70.5 KB
/
FM_PC_ExternalPrice_Redeeming_v1.t.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import {Test, console} from "forge-std/Test.sol"; // @todo remove console imports
import {IERC20Metadata} from
"@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import {IERC20Errors} from
"@openzeppelin/contracts/interfaces/draft-IERC6093.sol";
import {IOraclePrice_v1} from "@lm/interfaces/IOraclePrice_v1.sol";
import {IFM_PC_ExternalPrice_Redeeming_v1} from
"@fm/oracle/interfaces/IFM_PC_ExternalPrice_Redeeming_v1.sol";
import {IModule_v1} from "src/modules/base/IModule_v1.sol";
import {ModuleTest} from "test/modules/ModuleTest.sol";
import {Clones} from "@oz/proxy/Clones.sol";
import {ERC1967Proxy} from
"@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {OZErrors} from "test/utils/errors/OZErrors.sol";
import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
import {BondingCurveBase_v1} from
"@fm/bondingCurve/abstracts/BondingCurveBase_v1.sol";
import {RedeemingBondingCurveBase_v1} from
"@fm/bondingCurve/abstracts/RedeemingBondingCurveBase_v1.sol";
import {ERC20Issuance_v1} from "@ex/token/ERC20Issuance_v1.sol";
import {LM_ManualExternalPriceSetter_v1} from
"src/modules/logicModule/LM_ManualExternalPriceSetter_v1.sol";
import {OraclePrice_Mock} from
"test/utils/mocks/modules/logicModules/OraclePrice_Mock.sol";
import {FM_PC_ExternalPrice_Redeeming_v1} from
"src/modules/fundingManager/oracle/FM_PC_ExternalPrice_Redeeming_v1.sol";
import {
IERC20PaymentClientBase_v1,
ERC20PaymentClientBaseV1Mock,
ERC20Mock
} from "test/utils/mocks/modules/paymentClient/ERC20PaymentClientBaseV1Mock.sol";
import {PP_Streaming_v1AccessMock} from
"test/utils/mocks/modules/paymentProcessor/PP_Streaming_v1AccessMock.sol";
import {IERC20} from "@oz/token/ERC20/IERC20.sol";
import {RedeemingBondingCurveBaseV1Mock} from
"test/modules/fundingManager/bondingCurve/utils/mocks/RedeemingBondingCurveBaseV1Mock.sol";
/**
* @title FM_PC_ExternalPrice_Redeeming_v1_Test
* @notice Test contract for FM_PC_ExternalPrice_Redeeming_v1
*/
contract FM_PC_ExternalPrice_Redeeming_v1_Test is ModuleTest {
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
// Storage
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
FM_PC_ExternalPrice_Redeeming_v1 fundingManager;
// RedeemingBondingCurveBaseV1Mock bondingCurveFundingManager;
// Test addresses
address admin;
address user;
address whitelisted;
address queueManager;
address projectTreasury;
// Mock tokens
ERC20Issuance_v1 issuanceToken; // The token to be issued
// Mock oracle
OraclePrice_Mock oracle;
// Payment processor
PP_Streaming_v1AccessMock paymentProcessor;
ERC20PaymentClientBaseV1Mock paymentClient;
// Constants
string internal constant NAME = "Issuance Token";
string internal constant SYMBOL = "IST";
uint8 internal constant DECIMALS = 18;
uint internal constant MAX_SUPPLY = type(uint).max;
bytes32 constant WHITELIST_ROLE = "WHITELIST_ROLE";
bytes32 constant ORACLE_ROLE = "ORACLE_ROLE";
bytes32 constant QUEUE_MANAGER_ROLE = "QUEUE_MANAGER_ROLE";
uint8 constant INTERNAL_DECIMALS = 18;
uint constant BPS = 10_000; // Basis points (100%)
// Fee settings
uint constant DEFAULT_BUY_FEE = 100; // 1%
uint constant DEFAULT_SELL_FEE = 100; // 1%
uint constant MAX_BUY_FEE = 500; // 5%
uint constant MAX_SELL_FEE = 500; // 5%
bool constant DIRECT_OPERATIONS_ONLY = false;
// Module Constants
uint constant MAJOR_VERSION = 1;
uint constant MINOR_VERSION = 0;
uint constant PATCH_VERSION = 0;
string constant URL = "https://github.com/organization/module";
string constant TITLE = "Module";
bytes32 internal roleId;
// bytes32 internal roleIdOracle;
bytes32 internal queueManagerRoleId;
uint private constant BUY_FEE = 0;
uint private constant SELL_FEE = 0;
bool private constant BUY_IS_OPEN = true;
bool private constant SELL_IS_OPEN = true;
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
// Setup
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
function setUp() public {
// Setup addresses
admin = makeAddr("admin");
user = makeAddr("user");
whitelisted = makeAddr("whitelisted");
queueManager = makeAddr("queueManager");
projectTreasury = makeAddr("projectTreasury");
admin = address(this);
// Create issuance token
issuanceToken = new ERC20Issuance_v1(
NAME, SYMBOL, DECIMALS, MAX_SUPPLY, address(this)
);
// Setup mockoracle
address impl = address(new OraclePrice_Mock());
oracle = OraclePrice_Mock(Clones.clone(impl));
_setUpOrchestrator(oracle);
// Init mock oracle. No role authorization required as it is a mock
oracle.init(_orchestrator, _METADATA, "");
// Prepare config data
bytes memory configData = abi.encode(
projectTreasury, // oracle address
address(issuanceToken), // issuance token
address(_token), // accepted token
DEFAULT_BUY_FEE, // buy fee
DEFAULT_SELL_FEE, // sell fee
MAX_SELL_FEE, // max sell fee
MAX_BUY_FEE, // max buy fee
DIRECT_OPERATIONS_ONLY // direct operations only flag
);
// Setup funding manager
impl = address(new FM_PC_ExternalPrice_Redeeming_v1());
fundingManager = FM_PC_ExternalPrice_Redeeming_v1(Clones.clone(impl));
_setUpOrchestrator(fundingManager);
// Initialize the funding manager
fundingManager.init(_orchestrator, _METADATA, configData);
// Grant minting rights to the funding manager
issuanceToken.setMinter(address(fundingManager), true);
// set oracle address
fundingManager.setOracleAddress(address(oracle));
// Deploy up PaymentClient for later testing
impl = address(new ERC20PaymentClientBaseV1Mock());
paymentClient = ERC20PaymentClientBaseV1Mock(Clones.clone(impl));
paymentClient.init(_orchestrator, _METADATA, bytes(""));
paymentClient.setIsAuthorized(address(paymentProcessor), true);
paymentClient.setToken(_token);
// Grant admin roles
_authorizer.grantRole(_authorizer.getAdminRole(), admin);
_authorizer.grantRole(_authorizer.getAdminRole(), address(this));
// Set max fee of feeManager to 100% for testing purposes
// @todo why is the feeManager referenced?
// vm.prank(address(governor));
// feeManager.setMaxFee(feeManager.BPS());
// Grant whitelist role
fundingManager.grantModuleRole(WHITELIST_ROLE, whitelisted);
// // Grant queue manager role to queueManager address
fundingManager.grantModuleRole(QUEUE_MANAGER_ROLE, queueManager);
}
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
// Initialization
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
/* testInit()
└── Given a newly deployed contract
├── When initializing with valid parameters
│ ├── Then the oracle should be set correctly
│ ├── Then the tokens should be set correctly
│ ├── Then the fees should be set correctly
│ └── Then the orchestrator should be set correctly
└── When checking initialization state
└── Then it should be initialized correctly
*/
function testInit() public override(ModuleTest) {
assertEq(
address(fundingManager.orchestrator()),
address(_orchestrator),
"Orchestrator not set correctly"
);
}
/* testReinitFails()
└── Given an initialized contract
└── When trying to initialize again
└── Then it should revert with InvalidInitialization
*/
function testReinitFails() public override(ModuleTest) {
bytes memory configData = abi.encode(
address(oracle), // oracle address
address(issuanceToken), // issuance token
address(_token), // accepted token
DEFAULT_BUY_FEE, // buy fee
DEFAULT_SELL_FEE, // sell fee
MAX_SELL_FEE, // max sell fee
MAX_BUY_FEE, // max buy fee
DIRECT_OPERATIONS_ONLY // direct operations only flag
);
vm.expectRevert(OZErrors.Initializable__InvalidInitialization);
fundingManager.init(_orchestrator, _METADATA, configData);
}
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
// Configuration
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
/* Test testFees_initializesToDefaultConfiguration() function
├── Given a newly deployed funding manager
│ └── Then it should initialize with:
│ ├── Buy fee = DEFAULT_BUY_FEE
│ ├── Sell fee = DEFAULT_SELL_FEE
│ ├── Maximum buy fee = MAX_BUY_FEE
│ └── Maximum sell fee = MAX_SELL_FEE
*/
function testFees_initializesToDefaultConfiguration() public {
// Then - Verify buy fee configuration
assertEq(
fundingManager.buyFee(),
DEFAULT_BUY_FEE,
"Initial buy fee must match default value"
);
// Then - Verify sell fee configuration
assertEq(
fundingManager.sellFee(),
DEFAULT_SELL_FEE,
"Initial sell fee must match default value"
);
// Then - Verify maximum fee constraints
assertEq(
fundingManager.getMaxBuyFee(),
MAX_BUY_FEE,
"Maximum buy fee must match configured cap"
);
assertEq(
fundingManager.getMaxProjectSellFee(),
MAX_SELL_FEE,
"Maximum sell fee must match configured cap"
);
}
/* Test testToken_initializesIssuanceTokenCorrectly() function
├── Given a newly deployed funding manager
│ └── Then it should:
│ ├── Store correct issuance token address
│ └── Maintain token reference accessibility
*/
function testToken_initializesIssuanceTokenCorrectly() public {
assertEq(
fundingManager.getIssuanceToken(),
address(issuanceToken),
"Contract must reference configured issuance token"
);
}
/* Test testToken_initializesCollateralTokenCorrectly() function
├── Given a newly deployed funding manager
│ └── Then it should:
│ ├── Store correct collateral token address
│ └── Maintain token reference accessibility
*/
function testToken_initializesCollateralTokenCorrectly() public {
assertEq(
address(fundingManager.token()),
address(_token),
"Contract must reference configured collateral token"
);
}
/* Test testInit_rejectsExcessiveBuyFee() function
├── Given contract deployment parameters
│ └── When initializing with buy fee > MAX_BUY_FEE
│ └── Then it should:
│ ├── Revert with FeeExceedsMaximum error
│ └── Include exceeded and maximum values
*/
function testInit_rejectsExcessiveBuyFee() public {
bytes memory invalidConfigData = abi.encode(
address(oracle), // oracle address
address(issuanceToken), // issuance token
address(_token), // accepted token
MAX_BUY_FEE + 1, // buy fee exceeds max
DEFAULT_SELL_FEE, // sell fee
MAX_SELL_FEE, // max sell fee
MAX_BUY_FEE, // max buy fee
DIRECT_OPERATIONS_ONLY // direct operations only flag
);
address impl = address(new FM_PC_ExternalPrice_Redeeming_v1());
address newFundingManager = address(new ERC1967Proxy(impl, ""));
vm.expectRevert(
abi.encodeWithSelector(
IFM_PC_ExternalPrice_Redeeming_v1
.Module__FM_PC_ExternalPrice_Redeeming_FeeExceedsMaximum
.selector,
MAX_BUY_FEE + 1,
MAX_BUY_FEE
)
);
FM_PC_ExternalPrice_Redeeming_v1(newFundingManager).init(
_orchestrator, _METADATA, invalidConfigData
);
}
/* Test initialization with invalid fees
├── Given a new contract deployment
│ └── And sell fee exceeds MAX_SELL_FEE
│ └── When the init() function is called
│ └── Then should revert with FeeExceedsMaximum error
*/
function testExternalInit_revertsGivenSellFeeExceedsMaximum() public {
bytes memory invalidConfigData = abi.encode(
address(oracle), // oracle address
address(issuanceToken), // issuance token
address(_token), // accepted token
DEFAULT_BUY_FEE, // buy fee
MAX_SELL_FEE + 1, // sell fee exceeds max
MAX_SELL_FEE, // max sell fee
MAX_BUY_FEE, // max buy fee
DIRECT_OPERATIONS_ONLY // direct operations only flag
);
address impl = address(new FM_PC_ExternalPrice_Redeeming_v1());
address newFundingManager = address(new ERC1967Proxy(impl, ""));
vm.expectRevert(
abi.encodeWithSelector(
IFM_PC_ExternalPrice_Redeeming_v1
.Module__FM_PC_ExternalPrice_Redeeming_FeeExceedsMaximum
.selector,
MAX_SELL_FEE + 1,
MAX_SELL_FEE
)
);
FM_PC_ExternalPrice_Redeeming_v1(newFundingManager).init(
_orchestrator, _METADATA, invalidConfigData
);
}
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
// Oracle
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
/* Test oracle configuration and validation
├── Given a valid oracle implementation
│ └── When checking oracle interface and prices
│ ├── Then should support IOraclePrice_v1 interface
│ └── Then should return correct issuance and redemption prices
├── Given an invalid oracle address
│ └── When initializing contract
│ └── Then initialization should revert
*/
function testExternalInit_succeedsGivenValidOracleAndRevertsGivenInvalidOracle(
) public {
// Verify valid oracle interface
assertTrue(
ERC165(address(oracle)).supportsInterface(
type(IOraclePrice_v1).interfaceId
),
"Mock oracle should support IOraclePrice_v1 interface"
);
// Verify oracle price reporting
oracle.setIssuancePrice(2e18); // 2:1 ratio
oracle.setRedemptionPrice(1.9e18); // 1.9:1 ratio
assertEq(
oracle.getPriceForIssuance(),
2e18,
"Oracle issuance price not set correctly"
);
assertEq(
oracle.getPriceForRedemption(),
1.9e18,
"Oracle redemption price not set correctly"
);
// Test initialization with invalid oracle
bytes memory invalidConfigData = abi.encode(
address(_token), // invalid oracle address
address(issuanceToken), // issuance token
address(_token), // accepted token
DEFAULT_BUY_FEE, // buy fee
DEFAULT_SELL_FEE, // sell fee
MAX_SELL_FEE, // max sell fee
MAX_BUY_FEE, // max buy fee
DIRECT_OPERATIONS_ONLY // direct operations only flag
);
vm.expectRevert();
fundingManager.init(_orchestrator, _METADATA, invalidConfigData);
}
/* Test initialization with invalid oracle interface
├── Given a mock contract that doesn't implement IOraclePrice_v1
│ └── And a new funding manager instance
│ └── When initializing with invalid oracle
│ └── Then should revert with InvalidInitialization error
*/
function testExternalInit_revertsGivenOracleWithoutRequiredInterface()
public
{
// Create mock without IOraclePrice_v1 interface
address invalidOracle = makeAddr("InvalidOracleMock");
// Deploy new funding manager
address impl = address(new FM_PC_ExternalPrice_Redeeming_v1());
FM_PC_ExternalPrice_Redeeming_v1 invalidOracleFM =
FM_PC_ExternalPrice_Redeeming_v1(Clones.clone(impl));
// Prepare config with invalid oracle
bytes memory configData = abi.encode(
address(invalidOracle), // invalid oracle address
address(issuanceToken), // issuance token
address(_token), // accepted token
DEFAULT_BUY_FEE, // buy fee
DEFAULT_SELL_FEE, // sell fee
MAX_SELL_FEE, // max sell fee
MAX_BUY_FEE, // max buy fee
DIRECT_OPERATIONS_ONLY // direct operations only flag
);
// Setup orchestrator
_setUpOrchestrator(invalidOracleFM);
// Verify revert on initialization
vm.expectRevert(OZErrors.Initializable__InvalidInitialization);
fundingManager.init(_orchestrator, _METADATA, configData);
}
/* Test token decimals validation
├── Given an initialized contract with ERC20 tokens
│ └── When checking token decimals
│ ├── Then issuance token should have exactly 18 decimals
│ └── Then collateral token should have exactly 18 decimals
*/
function testExternalInit_succeedsGivenTokensWithCorrectDecimals() public {
assertEq(
IERC20Metadata(address(issuanceToken)).decimals(),
18,
"Issuance token should have 18 decimals"
);
assertEq(
IERC20Metadata(address(_token)).decimals(),
18,
"Collateral token should have 18 decimals"
);
}
/* Test fee configuration validation
├── Given an initialized funding manager contract
│ └── When reading fee configuration values
│ ├── Then buyFee should equal DEFAULT_BUY_FEE (1% = 100 basis points)
│ ├── Then sellFee should equal DEFAULT_SELL_FEE (1% = 100 basis points)
│ ├── Then maxBuyFee should equal MAX_BUY_FEE (5% = 500 basis points)
│ └── Then maxSellFee should equal MAX_SELL_FEE (5% = 500 basis points)
*/
function testExternalInit_succeedsGivenDefaultFeeConfiguration() public {
// Verify buy fee
assertEq(
BondingCurveBase_v1(address(fundingManager)).buyFee(), // @todo I think can be done through normal initiated funding manager, no casting
DEFAULT_BUY_FEE,
"Buy fee not set correctly"
);
// Verify sell fee
assertEq(
RedeemingBondingCurveBase_v1(address(fundingManager)).sellFee(),
DEFAULT_SELL_FEE,
"Sell fee not set correctly"
);
// Verify max buy fee
assertEq(
fundingManager.getMaxBuyFee(),
MAX_BUY_FEE,
"Max buy fee not set correctly"
);
// Verify max sell fee
assertEq(
fundingManager.getMaxProjectSellFee(),
MAX_SELL_FEE,
"Max sell fee not set correctly"
);
}
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
// Fee Management
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
/* Test fee limits enforcement
├── Given a funding manager initialized with default fees
│ ├── When setBuyFee is called with fee > MAX_BUY_FEE
│ │ └── Then should revert with FeeExceedsMaximum(invalidFee, MAX_BUY_FEE)
│ └── When setSellFee is called with fee > MAX_SELL_FEE
│ └── Then should revert with FeeExceedsMaximum(invalidFee, MAX_SELL_FEE)
*/
function testExternalSetFees_revertGivenFeesExceedingMaximum() public {
// Verify buy fee limit
uint invalidBuyFee = MAX_BUY_FEE + 1;
vm.expectRevert(
abi.encodeWithSelector(
IFM_PC_ExternalPrice_Redeeming_v1
.Module__FM_PC_ExternalPrice_Redeeming_FeeExceedsMaximum
.selector,
MAX_BUY_FEE + 1,
MAX_BUY_FEE
)
);
fundingManager.setBuyFee(invalidBuyFee);
// Verify sell fee limit
uint invalidSellFee = MAX_SELL_FEE + 1;
vm.expectRevert(
abi.encodeWithSelector(
IFM_PC_ExternalPrice_Redeeming_v1
.Module__FM_PC_ExternalPrice_Redeeming_FeeExceedsMaximum
.selector,
MAX_SELL_FEE + 1,
MAX_SELL_FEE
)
);
fundingManager.setSellFee(invalidSellFee);
}
/* Test buy fee update authorization and validation
├── Given an initialized funding manager and valid fee value
│ ├── When non-admin calls setBuyFee
│ │ └── Then reverts with unauthorized error
│ └── When admin calls setBuyFee
│ └── Then buyFee state and getter return new value
*/
function testExternalSetBuyFee_succeedsGivenAdminAndValidFee(uint newBuyFee)
public
{
// Bound fee to valid range
newBuyFee = bound(newBuyFee, 0, MAX_BUY_FEE);
// Verify non-admin cannot set fee
vm.prank(user);
vm.expectRevert();
fundingManager.setBuyFee(newBuyFee);
// Update fee as admin
vm.prank(admin);
fundingManager.setBuyFee(newBuyFee);
// Verify fee updated in state
assertEq(
BondingCurveBase_v1(address(fundingManager)).buyFee(),
newBuyFee,
"Buy fee state variable not updated correctly"
);
// Verify fee getter
assertEq(
fundingManager.getBuyFee(),
newBuyFee,
"Buy fee getter not returning correct value"
);
}
/* Test sell fee update authorization and validation
├── Given an initialized funding manager and valid fee value
│ ├── When non-admin calls setSellFee
│ │ └── Then reverts with unauthorized error
│ └── When admin calls setSellFee
│ └── Then sellFee state and getter return new value
*/
function testExternalSetSellFee_succeedsGivenAdminAndValidFee(
uint newSellFee
) public {
// Bound fee to valid range
newSellFee = bound(newSellFee, 0, MAX_SELL_FEE);
// Verify non-admin cannot set fee
vm.prank(user);
vm.expectRevert();
fundingManager.setSellFee(newSellFee);
// Update fee as admin
vm.prank(admin);
fundingManager.setSellFee(newSellFee);
// Verify fee updated in state
assertEq(
RedeemingBondingCurveBase_v1(address(fundingManager)).sellFee(),
newSellFee,
"Sell fee state variable not updated correctly"
);
// Verify fee getter
assertEq(
fundingManager.getSellFee(),
newSellFee,
"Sell fee getter not returning correct value"
);
}
/* Test fee update permissions for different roles
├── Given initialized funding manager and valid fee values
│ ├── When whitelisted user calls setBuyFee and setSellFee
│ │ └── Then both calls revert with unauthorized error
│ ├── When regular user calls setBuyFee and setSellFee
│ │ └── Then both calls revert with unauthorized error
│ └── When admin calls setBuyFee and setSellFee
│ └── Then fees are updated successfully
*/
function testExternalSetFees_succeedsOnlyForAdmin(
uint newBuyFee,
uint newSellFee
) public {
// Bound fees to valid ranges
newBuyFee = bound(newBuyFee, 0, MAX_BUY_FEE);
newSellFee = bound(newSellFee, 0, MAX_SELL_FEE);
// Verify whitelisted user cannot set fees
vm.startPrank(whitelisted);
vm.expectRevert();
fundingManager.setBuyFee(newBuyFee);
vm.expectRevert();
fundingManager.setSellFee(newSellFee);
vm.stopPrank();
// Verify regular user cannot set fees
vm.startPrank(user);
vm.expectRevert();
fundingManager.setBuyFee(newBuyFee);
vm.expectRevert();
fundingManager.setSellFee(newSellFee);
vm.stopPrank();
// Verify admin can set fees
vm.startPrank(admin);
fundingManager.setBuyFee(newBuyFee);
assertEq(
fundingManager.getBuyFee(),
newBuyFee,
"Admin should be able to update buy fee"
);
fundingManager.setSellFee(newSellFee);
assertEq(
fundingManager.getSellFee(),
newSellFee,
"Admin should be able to update sell fee"
);
vm.stopPrank();
}
/* Test sequential fee updates validation
├── Given initialized funding manager and admin role
│ ├── When admin performs three sequential buy fee updates
│ │ └── Then getBuyFee returns the latest set value after each update
│ └── When admin performs three sequential sell fee updates
│ └── Then getSellFee returns the latest set value after each update
*/
function testExternalSetFees_succeedsWithSequentialUpdates(
uint fee1,
uint fee2,
uint fee3
) public {
vm.startPrank(admin);
// Sequential buy fee updates
fee1 = bound(fee1, 0, MAX_BUY_FEE);
fee2 = bound(fee2, 0, MAX_BUY_FEE);
fee3 = bound(fee3, 0, MAX_BUY_FEE);
fundingManager.setBuyFee(fee1);
assertEq(
fundingManager.getBuyFee(),
fee1,
"Buy fee not updated correctly in first update"
);
fundingManager.setBuyFee(fee2);
assertEq(
fundingManager.getBuyFee(),
fee2,
"Buy fee not updated correctly in second update"
);
fundingManager.setBuyFee(fee3);
assertEq(
fundingManager.getBuyFee(),
fee3,
"Buy fee not updated correctly in third update"
);
// Sequential sell fee updates
fee1 = bound(fee1, 0, MAX_SELL_FEE);
fee2 = bound(fee2, 0, MAX_SELL_FEE);
fee3 = bound(fee3, 0, MAX_SELL_FEE);
fundingManager.setSellFee(fee1);
assertEq(
fundingManager.getSellFee(),
fee1,
"Sell fee not updated correctly in first update"
);
fundingManager.setSellFee(fee2);
assertEq(
fundingManager.getSellFee(),
fee2,
"Sell fee not updated correctly in second update"
);
fundingManager.setSellFee(fee3);
assertEq(
fundingManager.getSellFee(),
fee3,
"Sell fee not updated correctly in third update"
);
vm.stopPrank();
}
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
// Buy Operations
// ═══════════════════════════════════════════════════════════════════════════════════════════════════════
/* Test testExternalBuy_succeedsGivenWhitelistedUserAndValidAmount() function
├── Given an initialized funding manager contract with sufficient collateral
│ └── And a whitelisted user
│ ├── When the user buys tokens with a valid amount
│ │ ├── Then the buy fee should be calculated correctly
│ │ ├── Then the collateral tokens should be transferred to project treasury
│ │ └── Then the issued tokens should be minted to user
│ └── When checking final balances
│ ├── Then user should have correct issued token balance
│ ├── Then user should have correct collateral token balance
│ └── Then project treasury should have correct collateral token balance
*/
function testExternalBuy_succeedsGivenWhitelistedUserAndValidAmount(
uint buyAmount
) public {
// Given - Bound the buy amount to reasonable values
uint minAmount = 1 * 10 ** _token.decimals();
uint maxAmount = 1_000_000 * 10 ** _token.decimals();
buyAmount = bound(buyAmount, minAmount, maxAmount);
// Calculate expected issuance tokens using helper
uint expectedIssuedTokens = _calculateExpectedIssuance(buyAmount);
// Setup buying conditions using helper
_prepareBuyConditions(whitelisted, buyAmount);
// Record initial balances
uint initialUserCollateral = _token.balanceOf(whitelisted);
uint initialProjectTreasuryCollateral =
_token.balanceOf(fundingManager.getProjectTreasury());
uint initialUserIssuedTokens = issuanceToken.balanceOf(whitelisted);
// Execute buy operation
vm.startPrank(whitelisted);
_token.approve(address(fundingManager), buyAmount);
fundingManager.buy(buyAmount, expectedIssuedTokens);
vm.stopPrank();
// Verify user's collateral token balance decreased correctly
assertEq(
_token.balanceOf(whitelisted),
initialUserCollateral - buyAmount,
"User collateral balance incorrect"
);
// Verify project treasury received the collateral
assertEq(
_token.balanceOf(fundingManager.getProjectTreasury()),
initialProjectTreasuryCollateral + buyAmount,
"Project treasury collateral balance incorrect"
);
// Verify user received the correct amount of issuance tokens
assertEq(
issuanceToken.balanceOf(whitelisted),
initialUserIssuedTokens + expectedIssuedTokens,
"User issued token balance incorrect"
);
// Verify the oracle price used matches what we expect
uint oraclePrice = oracle.getPriceForIssuance();
assertGt(oraclePrice, 0, "Oracle price should be greater than 0");
// Verify the funding manager's buy functionality is still open
assertTrue(
fundingManager.buyIsOpen(), "Buy functionality should remain open"
);
}
/* Test testExternalBuy_revertsGivenInvalidInputs() function revert conditions
├── Given a whitelisted user and initialized funding manager
│ ├── When attempting to buy with zero amount
│ │ └── Then it should revert with InvalidDepositAmount
│ │
│ ├── When attempting to buy with zero expected tokens
│ │ └── Then it should revert with InvalidMinAmountOut
│ │
│ ├── When buying functionality is closed
│ │ └── Then buying attempt should revert with BuyingFunctionalitiesClosed
│ │
│ └── When attempting to buy with excessive slippage
│ ├── Given buying is reopened
│ └── Then buying with multiplied expected amount should revert with InsufficientOutputAmount
*/
function testExternalBuy_revertsGivenInvalidInputs(
uint buyAmount,
uint slippageMultiplier
) public {
// Bound the buy amount to reasonable values
uint minAmount = 1 * 10 ** _token.decimals();
uint maxAmount = 1_000_000 * 10 ** _token.decimals();
buyAmount = bound(buyAmount, minAmount, maxAmount);
// Bound slippage multiplier (between 2x and 10x)
slippageMultiplier = bound(slippageMultiplier, 2, 10);
// Setup
_prepareBuyConditions(whitelisted, buyAmount);
// Test zero amount
vm.startPrank(whitelisted);
vm.expectRevert(
abi.encodeWithSignature(
"Module__BondingCurveBase__InvalidDepositAmount()"
)
);
fundingManager.buy(0, 0);
// Test zero expected tokens
vm.expectRevert(
abi.encodeWithSignature(
"Module__BondingCurveBase__InvalidMinAmountOut()"
)
);
fundingManager.buy(1 ether, 0);
vm.stopPrank();
// Test closed buy
vm.prank(admin);
fundingManager.closeBuy();
vm.expectRevert(
abi.encodeWithSignature(
"Module__BondingCurveBase__BuyingFunctionaltiesClosed()"
)
);
vm.prank(whitelisted);
fundingManager.buy(buyAmount, buyAmount);
// Test slippage
vm.prank(admin);
fundingManager.openBuy();
uint expectedTokens = _calculateExpectedIssuance(buyAmount);
vm.startPrank(whitelisted);
vm.expectRevert(
abi.encodeWithSignature(
"Module__BondingCurveBase__InsufficientOutputAmount()"
)
);
// Try to buy with higher expected tokens than possible (multiplied by fuzzed value)
fundingManager.buy(buyAmount, expectedTokens * slippageMultiplier);
vm.stopPrank();
}
/* Test testExternalBuy_revertsGivenExcessiveSlippage() function slippage protection
├── Given a whitelisted user and initialized funding manager
│ └── And buying functionality is open
│ └── When attempting to buy with excessive slippage (2x-10x expected amount)
│ └── Then it should revert with InsufficientOutputAmount
*/
function testExternalBuy_revertsGivenExcessiveSlippage(
uint buyAmount,
uint slippageMultiplier
) public {
// Bound the buy amount to reasonable values
uint minAmount = 1 * 10 ** _token.decimals();
uint maxAmount = 1_000_000 * 10 ** _token.decimals();
buyAmount = bound(buyAmount, minAmount, maxAmount);
// Bound slippage multiplier (between 2x and 10x)
slippageMultiplier = bound(slippageMultiplier, 2, 10);
// Setup
_prepareBuyConditions(whitelisted, buyAmount);
// Test slippage
vm.prank(admin);
fundingManager.openBuy();
uint expectedTokens = _calculateExpectedIssuance(buyAmount);
vm.startPrank(whitelisted);
vm.expectRevert(
abi.encodeWithSignature(
"Module__BondingCurveBase__InsufficientOutputAmount()"
)
);
// Try to buy with higher expected tokens than possible (multiplied by fuzzed value)
fundingManager.buy(buyAmount, expectedTokens * slippageMultiplier);
vm.stopPrank();
}
/* Test testExternalBuy_revertsGivenNonWhitelistedUser() function
├── Given an initialized funding manager contract
│ └── And buying is open
│ └── When a non-whitelisted address attempts to buy tokens
│ ├── And the address has enough payment tokens
│ ├── And the address is not zero address
│ ├── And the address is not an admin
│ ├── And the address is not whitelisted
│ └── Then it should revert with CallerNotAuthorized error
*/
function testExternalBuy_revertsGivenNonWhitelistedUser(
address nonWhitelisted,
uint buyAmount
) public {
vm.assume(nonWhitelisted != address(0));
vm.assume(nonWhitelisted != whitelisted);
vm.assume(nonWhitelisted != admin);
vm.assume(nonWhitelisted != address(this));
roleId =
_authorizer.generateRoleId(address(fundingManager), WHITELIST_ROLE);
// Prepare buy conditions with a fixed amount
uint minAmount = 1 * 10 ** _token.decimals();
uint maxAmount = 1_000_000 * 10 ** _token.decimals();
buyAmount = bound(buyAmount, minAmount, maxAmount);
vm.prank(admin);
_prepareBuyConditions(nonWhitelisted, buyAmount);
vm.prank(admin);
fundingManager.openBuy();
// Calculate expected tokens
uint expectedTokens = _calculateExpectedIssuance(buyAmount);
// Attempt to buy tokens with any non-whitelisted address
vm.startPrank(nonWhitelisted);
vm.expectRevert(
abi.encodeWithSelector(
IModule_v1.Module__CallerNotAuthorized.selector,
roleId,
nonWhitelisted
)
);
fundingManager.buy(buyAmount, expectedTokens);
vm.stopPrank();
}
/* Test testExternalBuy_revertsGivenBuyingClosed() function
├── Given an initialized funding manager contract
│ └── And buying is closed
│ └── When a whitelisted user attempts to buy tokens
│ ├── And the user is not zero address
│ ├── And the user has sufficient payment tokens
│ ├── And the amount is within valid bounds
│ └── Then it should revert with BuyingFunctionalitiesClosed error
*/
function testExternalBuy_revertsGivenBuyingClosed(
address buyer,
uint buyAmount
) public {
// Given - Valid user assumptions
vm.assume(buyer != address(0));
vm.assume(buyer != address(this));
vm.assume(buyer != admin);
// Given - Valid amount bounds
uint minAmount = 1 * 10 ** _token.decimals();
uint maxAmount = 1_000_000 * 10 ** _token.decimals();
buyAmount = bound(buyAmount, minAmount, maxAmount);