-
Notifications
You must be signed in to change notification settings - Fork 18
/
Copy pathProposals.t.sol
1757 lines (1297 loc) · 69.8 KB
/
Proposals.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: BUSL 1.1
pragma solidity =0.8.22;
import "../../dev/Deployment.sol";
import "./ExcessiveSupplyToken.sol";
contract TestProposals is Deployment
{
// User wallets for testing
address public constant alice = address(0x1111);
address public constant bob = address(0x2222);
address public constant charlie = address(0x3333);
constructor()
{
// If $COVERAGE=yes, create an instance of the contract so that coverage testing can work
// Otherwise, what is tested is the actual deployed contract on the blockchain (as specified in Deployment.sol)
if ( keccak256(bytes(vm.envString("COVERAGE" ))) == keccak256(bytes("yes" )))
initializeContracts();
grantAccessAlice();
grantAccessBob();
grantAccessCharlie();
grantAccessDeployer();
grantAccessDefault();
vm.prank(address(initialDistribution));
salt.transfer(DEPLOYER, 100000000 ether);
vm.startPrank( DEPLOYER );
salt.transfer( alice, 10000000 ether );
salt.transfer( DEPLOYER, 90000000 ether );
vm.stopPrank();
// Mint some USDS to the DEPLOYER and alice
vm.startPrank( address(collateralAndLiquidity) );
usds.mintTo( DEPLOYER, 2000000 ether );
usds.mintTo( alice, 1000000 ether );
vm.stopPrank();
// Allow time for proposals
vm.warp( block.timestamp + 45 days );
}
function setUp() public
{
vm.startPrank( DEPLOYER );
usds.approve( address(proposals), type(uint256).max );
salt.approve( address(staking), type(uint256).max );
vm.stopPrank();
vm.startPrank( alice );
usds.approve( address(proposals), type(uint256).max );
salt.approve( address(staking), type(uint256).max );
vm.stopPrank();
vm.startPrank( bob );
usds.approve( address(proposals), type(uint256).max );
salt.approve( address(staking), type(uint256).max );
vm.stopPrank();
}
// A unit test that checks the proposeParameterBallot function fails if called before the firstPossibleProposalTimestamp
function testProposeParameterBallotBeforeTimestamp() public {
vm.warp( block.timestamp - 45 days );
vm.startPrank( DEPLOYER );
staking.stakeSALT(1000 ether);
// Define a ballotName
string memory ballotName = "parameter:1";
// Ensure ballot with ballotName doesn't exist before proposing
assertEq(proposals.openBallotsByName(ballotName), 0);
// Call proposeParameterBallot
vm.expectRevert( "Cannot propose ballots within the first 45 days of deployment" );
proposals.proposeParameterBallot(1, "description" );
}
// A unit test that checks the proposeParameterBallot function with different input combinations. Verify that a new proposal gets created and that all necessary state changes occur.
function testProposeParameterBallot() public {
vm.startPrank( DEPLOYER );
staking.stakeSALT(1000 ether);
// Get initial state before proposing the ballot
uint256 initialNextBallotId = proposals.nextBallotID();
// Define a ballotName
string memory ballotName = "parameter:1";
// Ensure ballot with ballotName doesn't exist before proposing
assertEq(proposals.openBallotsByName(ballotName), 0);
// Call proposeParameterBallot
proposals.proposeParameterBallot(1, "description" );
// Check that the next ballot ID has been incremented
assertEq(proposals.nextBallotID(), initialNextBallotId + 1);
// Check that the ballot with ballotName now exists
assert(proposals.openBallotsByName(ballotName) == 1);
// Check that the proposed ballot is in the correct state
uint256 ballotID = proposals.openBallotsByName(ballotName);
Ballot memory ballot = proposals.ballotForID(ballotID);
// Check that the proposed ballot parameters are correct
assertTrue(ballot.ballotIsLive);
assertEq(uint256(ballot.ballotType), uint256(BallotType.PARAMETER));
assertEq(ballot.ballotName, ballotName);
assertEq(ballot.address1, address(0));
assertEq(ballot.number1, 1);
assertEq(ballot.string1, "");
assertEq(ballot.description, "description");
assertEq(ballot.ballotMinimumEndTime, block.timestamp + daoConfig.ballotMinimumDuration());
}
// A unit test that tries to propose the same parameter ballot multiple times. This should test the requirement check in the _possiblyCreateProposal function.
function testProposeSameBallotNameMultipleTimesAndForOpenBallot() public {
// Using address alice for initial proposal
vm.startPrank(DEPLOYER);
staking.stakeSALT( 1000000 ether ); // Default minimum quorum is 1 million
// Proposing a ParameterBallot for the first time
proposals.proposeParameterBallot(1, "description" );
vm.stopPrank();
vm.startPrank(alice);
staking.stakeSALT( 1000000 ether );
// Trying to propose the same ballot name again should fail
vm.expectRevert("Cannot create a proposal similar to a ballot that is still open" );
proposals.proposeParameterBallot(1, "description" );
// Make sure another user can't recreate the same ballot either
vm.expectRevert("Cannot create a proposal similar to a ballot that is still open" );
proposals.proposeParameterBallot(1, "description" );
uint256 ballotID = 1;
assertFalse( proposals.canFinalizeBallot(ballotID) );
// Increasing block time by ballotMinimumDuration to allow for proposal finalization
vm.warp(block.timestamp + daoConfig.ballotMinimumDuration());
assertFalse( proposals.canFinalizeBallot(ballotID) );
// Have alice cast some votes
proposals.castVote( ballotID, Vote.INCREASE );
// Ballot shoudl be able to be finalized now
assertTrue( proposals.canFinalizeBallot(ballotID) );
vm.stopPrank();
// Finalize the ballot
vm.prank( address(dao) );
proposals.markBallotAsFinalized(ballotID);
// Trying to propose for the already open (but finalized) ballot should succeed
vm.prank( address(alice) );
proposals.proposeParameterBallot(1, "description" );
}
// A unit test that verifies the proposeCountryInclusion and proposeCountryExclusion functions with different country names. Check that the appropriate country name gets stored in the proposal.
function testProposeCountryInclusionExclusion() public {
string memory inclusionBallotName = "include:us";
string memory exclusionBallotName = "exclude:ca";
string memory countryName1 = "us";
string memory countryName2 = "ca";
// Assert initial balances
assertEq(usds.balanceOf(alice), 1000000 ether);
assertEq(usds.balanceOf(bob), 0 ether);
// Propose country inclusion
vm.startPrank(alice);
staking.stakeSALT(1000 ether);
proposals.proposeCountryInclusion(countryName1, "description" );
uint256 inclusionProposalId = proposals.openBallotsByName(inclusionBallotName);
assertEq( inclusionProposalId, 1 );
vm.stopPrank();
// Check proposal details
Ballot memory inclusionProposal = proposals.ballotForID(inclusionProposalId);
assertTrue(inclusionProposal.ballotIsLive);
assertEq(uint256(inclusionProposal.ballotType), uint256(BallotType.INCLUDE_COUNTRY));
assertEq(inclusionProposal.ballotName, inclusionBallotName);
assertEq(inclusionProposal.string1, countryName1);
// Propose country exclusion
vm.startPrank(DEPLOYER);
staking.stakeSALT(1000 ether);
proposals.proposeCountryExclusion(countryName2, "description" );
uint256 exclusionProposalId = proposals.openBallotsByName(exclusionBallotName);
// Check proposal details
Ballot memory exclusionProposal = proposals.ballotForID(exclusionProposalId);
assertTrue(exclusionProposal.ballotIsLive);
assertEq(uint256(exclusionProposal.ballotType), uint256(BallotType.EXCLUDE_COUNTRY));
assertEq(exclusionProposal.ballotName, exclusionBallotName);
assertEq(exclusionProposal.string1, countryName2);
}
// A unit test that verifies the proposeSetContractAddress function. Test this function with different address values and verify that the new address gets stored in the proposal.
function testProposeSetContractAddress() public {
vm.startPrank(alice);
staking.stakeSALT(1000 ether);
// Check initial state
uint256 initialProposalCount = proposals.nextBallotID() - 1;
// Try to set an invalid address and expect a revert
address newAddress = address(0);
vm.expectRevert("Proposed address cannot be address(0)");
proposals.proposeSetContractAddress( "contractName", newAddress, "description" );
// Use a valid address
newAddress = address(0x1111111111111111111111111111111111111112);
proposals.proposeSetContractAddress("contractName", newAddress, "description" );
vm.stopPrank();
vm.startPrank(DEPLOYER);
staking.stakeSALT(1000 ether);
vm.expectRevert("Cannot create a proposal similar to a ballot that is still open");
proposals.proposeSetContractAddress("contractName", newAddress, "description" );
// Check if a new proposal is created
uint256 newProposalCount = proposals.nextBallotID() - 1;
assertEq(newProposalCount, initialProposalCount + 1, "New proposal was not created");
// Get the new proposal
Ballot memory ballot = proposals.ballotForID(newProposalCount);
// Check if the new proposal has the right new address
assertEq(ballot.address1, newAddress, "New proposal has incorrect address");
vm.stopPrank();
}
// A unit test that verifies the proposeWebsiteUpdate function. Check that the correct website URL gets stored in the proposal.
function testProposeWebsiteUpdate() public {
// Set up
vm.startPrank(alice); // Switch to Alice for the test
staking.stakeSALT(1000 ether);
// Save off the current proposals.nextBallotID() before the proposeWebsiteUpdate call
uint256 preNextBallotID = proposals.nextBallotID();
// Create a ballot with the new website URL
string memory newWebsiteURL = "https://www.newwebsite.com";
proposals.proposeWebsiteUpdate(newWebsiteURL, "description" );
// Verify the proposals.nextBallotID() has been incremented
uint256 postNextBallotID = proposals.nextBallotID();
assertEq(postNextBallotID, preNextBallotID + 1, "proposals.nextBallotID() should have incremented by 1");
string memory ballotName = "setURL:https://www.newwebsite.com";
// Verify the ballot ID associated with the ballotName
uint256 ballotID = proposals.openBallotsByName(ballotName);
assertEq(ballotID, preNextBallotID, "The ballot ID should match the preNextBallotID");
// Retrieve the new ballot
Ballot memory ballot = proposals.ballotForID(ballotID);
// Verify the ballot details
assertEq(uint256(ballot.ballotType), uint256(BallotType.SET_WEBSITE_URL), "Ballot type is incorrect");
assertEq(ballot.string1, newWebsiteURL, "Website URL is incorrect");
assertEq(ballot.ballotName, ballotName, "Ballot name is incorrect");
assertTrue(ballot.ballotIsLive, "The ballot should be live");
}
// A unit test that verifies the proposeCallContract function. Try this function with different input values and verify the correct values get stored in the proposal.
function testProposeCallContract() public {
address contractAddress = address(0xBBBB);
uint256 number = 12345;
// Simulate a call from Alice
vm.startPrank(alice);
staking.stakeSALT(1000 ether);
string memory ballotName = "callContract:0x000000000000000000000000000000000000bbbb";
// Check initial state before proposal
assertEq(proposals.openBallotsByName(ballotName), 0, "Ballot ID should be 0 before proposal");
// Make the proposal
proposals.proposeCallContract(contractAddress, number, "description" );
// Check the proposal was made
uint256 ballotID = proposals.openBallotsByName(ballotName);
assertEq(ballotID, 1);
// Check the proposal values
Ballot memory ballot = proposals.ballotForID(ballotID);
assertEq(ballot.ballotName, ballotName, "Ballot name should match");
assertEq(uint256(ballot.ballotType), uint256(BallotType.CALL_CONTRACT), "Ballot type should be CALL_CONTRACT");
assertEq(ballot.address1, contractAddress, "Contract address should match proposal");
assertEq(ballot.number1, number, "Number should match proposal");
}
// A unit test for the _markBallotAsFinalized function that confirms if a ballot's status is updated correctly after finalization.
function testMarkBallotAsFinalized() public {
string memory ballotName = "parameter:2";
vm.startPrank(DEPLOYER);
staking.stakeSALT(1000 ether);
proposals.proposeParameterBallot(2, "description" );
uint256 ballotID = proposals.openBallotsByName(ballotName);
assertEq(proposals.ballotForID(ballotID).ballotIsLive, true);
vm.stopPrank();
// Act
vm.prank( address(dao) );
proposals.markBallotAsFinalized(ballotID);
// Assert
assertEq(proposals.ballotForID(ballotID).ballotIsLive, false);
assertEq(proposals.openBallotsByName(ballotName), 0, "Ballot should have been cleared");
}
// A unit test that checks if the castVote function appropriately updates the votesCastForBallot mapping and proposals.totalVotesCastForBallot for the ballot. This should also cover situations where a user tries to vote without voting power and a situation where a user changes their vote.
function testCastVote() public {
string memory ballotName = "parameter:2";
vm.startPrank(DEPLOYER);
staking.stakeSALT( 1000 ether );
proposals.proposeParameterBallot(2, "description" );
vm.stopPrank();
uint256 ballotID = proposals.openBallotsByName(ballotName);
Vote userVote = Vote.YES;
// User has voting power
vm.startPrank(alice);
uint256 votingPower = staking.userShareForPool(alice, PoolUtils.STAKED_SALT);
assertEq( votingPower, 0, "Alice should not have any initial xSALT" );
// Vote.YES is invalid for a Parameter type ballot
vm.expectRevert( "Invalid VoteType for Parameter Ballot" );
proposals.castVote(ballotID, userVote);
// Alice has not staked SALT yet
vm.expectRevert( "Staked SALT required to vote" );
userVote = Vote.INCREASE;
proposals.castVote(ballotID, userVote);
// Stake some salt and vote again
votingPower = 1000 ether;
staking.stakeSALT( votingPower );
proposals.castVote(ballotID, userVote);
UserVote memory lastVote = proposals.lastUserVoteForBallot(ballotID, alice);
assertEq(uint256(lastVote.vote), uint256(userVote), "User vote does not match");
assertEq(lastVote.votingPower, votingPower, "Voting power is incorrect");
assertEq(proposals.totalVotesCastForBallot(ballotID), votingPower, "Total votes for ballot is incorrect");
assertEq(proposals.votesCastForBallot(ballotID, userVote), votingPower, "Votes cast for ballot is incorrect");
// User changes their vote
uint256 addedUserVotingPower = 200 ether;
staking.stakeSALT( addedUserVotingPower );
Vote newUserVote = Vote.DECREASE;
proposals.castVote(ballotID, newUserVote);
UserVote memory newLastVote = proposals.lastUserVoteForBallot(ballotID, alice);
assertEq(uint256(newLastVote.vote), uint256(newUserVote), "New user vote does not match");
assertEq(newLastVote.votingPower, votingPower + addedUserVotingPower, "New voting power is incorrect");
assertEq(proposals.totalVotesCastForBallot(ballotID), votingPower + addedUserVotingPower, "Total votes for ballot is incorrect after vote change");
assertEq(proposals.votesCastForBallot(ballotID, newUserVote), votingPower + addedUserVotingPower, "Votes cast for ballot is incorrect after vote change");
assertEq(proposals.votesCastForBallot(ballotID, userVote), 0, "The old vote should have no votes cast");
}
// A unit test that checks if canFinalizeBallot function returns the correct boolean value under various conditions, including situations where a ballot can be finalized and where it cannot due to ballot not being live, minimum end time not being reached, or not meeting the required quorum.
function testCanFinalizeBallot() public {
string memory ballotName = "parameter:2";
uint256 initialStake = 10000000 ether;
vm.startPrank(alice);
staking.stakeSALT(1110111 ether);
proposals.proposeParameterBallot(2, "description" );
staking.unstake( 1110111 ether, 2);
uint256 ballotID = proposals.openBallotsByName(ballotName);
// Early ballot, no quorum
bool canFinalizeBallotStillEarly = proposals.canFinalizeBallot(ballotID);
// Ballot reached end time, no quorum
vm.warp(block.timestamp + daoConfig.ballotMinimumDuration() + 1); // ballot end time reached
vm.expectRevert( "SALT staked cannot be zero to determine quorum" );
proposals.canFinalizeBallot(ballotID);
vm.stopPrank();
vm.prank(DEPLOYER);
staking.stakeSALT( initialStake );
bool canFinalizeBallotPastEndtime = proposals.canFinalizeBallot(ballotID);
// Almost reach quorum
vm.prank(alice);
staking.stakeSALT(1110111 ether);
// Default user has no access to the exchange, but can still vote
vm.prank(DEPLOYER);
salt.transfer(address(this), 1000 ether );
salt.approve( address(staking), type(uint256).max);
staking.stakeSALT( 1000 ether );
proposals.castVote(ballotID, Vote.INCREASE);
vm.startPrank(alice);
proposals.castVote(ballotID, Vote.INCREASE);
bool canFinalizeBallotAlmostAtQuorum = proposals.canFinalizeBallot(ballotID);
// Reach quorum
staking.stakeSALT(1 ether);
// Recast vote to include new stake
proposals.castVote(ballotID, Vote.DECREASE);
bool canFinalizeBallotAtQuorum = proposals.canFinalizeBallot(ballotID);
// Assert
assertEq(canFinalizeBallotStillEarly, false, "Should not be able to finalize live ballot");
assertEq(canFinalizeBallotPastEndtime, false, "Should not be able to finalize non-quorum ballot");
assertEq(canFinalizeBallotAlmostAtQuorum, false, "Should not be able to finalize ballot if quorum is just beyond the minimum ");
assertEq(canFinalizeBallotAtQuorum, true, "Should be able to finalize ballot if quorum is reached and past the minimum end time");
}
// A unit test for the requiredQuorumForBallotType function that confirms it returns the correct quorum requirement for each type of ballot.
function testRequiredQuorumForBallotType() public {
vm.startPrank(DEPLOYER);
// 2 million staked. Default 10% will be 200k which does not meet the 0.50% of total supply minimum quorum.
// So 500k (0.50% of the totalSupply) will be used as the quorum
staking.stakeSALT( 2000000 ether );
assertEq(proposals.requiredQuorumForBallotType(BallotType.PARAMETER), 500000 ether, "Not using the minimum 1% of totalSupply for quorum" );
// 10 million total staked. Default 10% will be 1 million which meets the 1% of total supply minimum quorum.
staking.stakeSALT( 8000000 ether );
uint256 stakedSALT = staking.totalShares(PoolUtils.STAKED_SALT);
uint256 baseBallotQuorumPercentTimes1000 = daoConfig.baseBallotQuorumPercentTimes1000();
// Check quorum for Parameter ballot type
uint256 expectedQuorum = (1 * stakedSALT * baseBallotQuorumPercentTimes1000) / (1000 * 100);
assertEq(proposals.requiredQuorumForBallotType(BallotType.PARAMETER), expectedQuorum);
// Check quorum for WhitelistToken ballot type
expectedQuorum = (2 * stakedSALT * baseBallotQuorumPercentTimes1000) / (1000 * 100);
assertEq(proposals.requiredQuorumForBallotType(BallotType.WHITELIST_TOKEN), expectedQuorum);
// Check quorum for UnwhitelistToken ballot type
expectedQuorum = (2 * stakedSALT * baseBallotQuorumPercentTimes1000) / (1000 * 100);
assertEq(proposals.requiredQuorumForBallotType(BallotType.UNWHITELIST_TOKEN), expectedQuorum);
// Check quorum for SendSalt ballot type
expectedQuorum = (3 * stakedSALT * baseBallotQuorumPercentTimes1000) / (1000 * 100);
assertEq(proposals.requiredQuorumForBallotType(BallotType.SEND_SALT), expectedQuorum);
// Check quorum for CallContract ballot type
expectedQuorum = (3 * stakedSALT * baseBallotQuorumPercentTimes1000) / (1000 * 100);
assertEq(proposals.requiredQuorumForBallotType(BallotType.CALL_CONTRACT), expectedQuorum);
// Check quorum for IncludeCountry ballot type
expectedQuorum = (3 * stakedSALT * baseBallotQuorumPercentTimes1000) / (1000 * 100);
assertEq(proposals.requiredQuorumForBallotType(BallotType.INCLUDE_COUNTRY), expectedQuorum);
// Check quorum for ExcludeCountry ballot type
expectedQuorum = (3 * stakedSALT * baseBallotQuorumPercentTimes1000) / (1000 * 100);
assertEq(proposals.requiredQuorumForBallotType(BallotType.EXCLUDE_COUNTRY), expectedQuorum);
// Check quorum for SetContract ballot type
expectedQuorum = (3 * stakedSALT * baseBallotQuorumPercentTimes1000) / (1000 * 100);
assertEq(proposals.requiredQuorumForBallotType(BallotType.SET_CONTRACT), expectedQuorum);
// Check quorum for SetWebsiteUrl ballot type
expectedQuorum = (3 * stakedSALT * baseBallotQuorumPercentTimes1000) / (1000 * 100);
assertEq(proposals.requiredQuorumForBallotType(BallotType.SET_WEBSITE_URL), expectedQuorum);
}
// A unit test for the proposals.totalVotesCastForBallot function that confirms it returns the correct total votes for a given ballot.
// A unit test that verifies if the totalVotesCastForBallot function correctly calculates the sum of all types of votes for a particular ballot.
function testTotalVotesCastForBallot() public
{
string memory ballotName = "parameter:2";
vm.prank(DEPLOYER);
salt.transfer(bob, 1000 ether);
vm.startPrank(bob);
staking.stakeSALT(1000 ether);
proposals.proposeParameterBallot(2, "description" );
vm.stopPrank();
vm.startPrank(alice);
uint256 ballotID = proposals.openBallotsByName(ballotName);
staking.stakeSALT( 1000 ether );
proposals.castVote(ballotID, Vote.INCREASE);
salt.transfer( DEPLOYER, 2000 ether );
vm.stopPrank();
vm.startPrank( DEPLOYER );
staking.stakeSALT( 2000 ether );
proposals.castVote(ballotID, Vote.INCREASE);
vm.stopPrank();
assertEq(proposals.ballotForID(ballotID).ballotIsLive, true);
assertEq(proposals.totalVotesCastForBallot(ballotID), 3000 ether);
vm.startPrank( alice );
staking.stakeSALT( 1000 ether );
proposals.castVote(ballotID, Vote.DECREASE);
vm.stopPrank();
assertEq(proposals.totalVotesCastForBallot(ballotID), 4000 ether);
}
// A unit test that verifies the numberOfOpenBallotsForTokenWhitelisting function and the tokenWhitelistingBallotWithTheMostVotes function. This should include situations where there are multiple open ballots for token whitelisting, and should correctly identify the ballot with the most votes.
function testTokenWhitelistingBallots() public {
IERC20 wbtc = exchangeConfig.wbtc();
IERC20 weth = exchangeConfig.weth();
// console.log( "tokenHasBeenWhitelisted(): ", proposals.tokenHasBeenWhitelisted(wbtc) );
vm.prank(address(dao));
poolsConfig.whitelistPool( pools, wbtc, weth );
vm.startPrank(alice);
vm.expectRevert( "The token has already been whitelisted" );
proposals.proposeTokenWhitelisting( wbtc, "https://tokenIconURL", "This is a test token");
vm.stopPrank();
vm.prank(address(dao));
poolsConfig.unwhitelistPool( pools, wbtc, weth );
// Prepare a new whitelisting ballot
uint256 initialStake = 10000000 ether;
vm.prank(DEPLOYER);
staking.stakeSALT( initialStake );
vm.startPrank(alice);
IERC20 testToken = new TestERC20("TEST", 18);
staking.stakeSALT( 2222222 ether ); // less than minimum quorum for whitelisting (which is default 10% of the amount of staked SALT)
proposals.proposeTokenWhitelisting(testToken, "https://tokenIconURL", "This is a test token");
uint256 ballotID = 1;
// Assert that the number of open ballots for token whitelisting has increased
assertEq(proposals.openBallotsForTokenWhitelisting().length, 1, "The number of open ballots for token whitelisting did not increase after a proposal");
proposals.castVote(ballotID, Vote.YES);
assertEq( proposals.totalVotesCastForBallot(ballotID), 2222222 ether, "Vote total is not what is expected" );
// console.log( "QUORUM: ", requiredQuorumForBallotType( BallotType.WHITELIST_TOKEN ) );
// console.log( "VOTES: ", proposals.totalVotesCastForBallot(ballotID) );
//
// Shouldn't have enough votes for quorum yet
assertEq(proposals.tokenWhitelistingBallotWithTheMostVotes(), 0, "The ballot shouldn't have enough votes for quorum yet");
vm.stopPrank();
// Have alice cast more votes for YES
vm.startPrank(alice);
staking.stakeSALT( 300000 ether );
proposals.castVote(ballotID, Vote.YES);
// The ballot should now be whitelistable
assertEq(proposals.tokenWhitelistingBallotWithTheMostVotes(), ballotID, "Ballot should be whitelistable");
// 10 million no votes will bring ballot to quorum, but no votes will now be more than yes votes
vm.startPrank( DEPLOYER );
proposals.castVote(ballotID, Vote.NO);
assertEq(proposals.tokenWhitelistingBallotWithTheMostVotes(), 0, "NO > YES should mean no whitelisted ballot");
// Create a second whitelisting ballot
IERC20 testToken2 = new TestERC20("TEST", 18);
proposals.proposeTokenWhitelisting(testToken2, "https://tokenIconURL", "This is a test token");
assertEq(proposals.openBallotsForTokenWhitelisting().length, 2, "The number of open ballots for token whitelisting did not increase after a second proposal");
uint256 ballotID2 = 2;
vm.startPrank( DEPLOYER );
proposals.castVote(ballotID2, Vote.NO);
proposals.castVote(ballotID2, Vote.YES);
// console.log( "max id: ", proposals.tokenWhitelistingBallotWithTheMostVotes() );
assertEq(proposals.tokenWhitelistingBallotWithTheMostVotes(), ballotID2, "The ballot with the most votes was not updated correctly after a vote");
}
// A unit test that verifies the proposeSendSALT function, checking if the proposal can be created, and ensuring that they can't send more than 5% of the existing balance.
function testProposeSendSALT() public {
uint256 daoInitialSaltBalance = 1000000 ether;
vm.startPrank( DEPLOYER );
salt.transfer( address(dao), daoInitialSaltBalance );
vm.stopPrank();
vm.startPrank( alice );
staking.stakeSALT(1000 ether);
// Test proposing to send an amount exceeding the limit
uint256 excessiveAmount = daoInitialSaltBalance / 19; // > 5% of the initial balance
vm.expectRevert("Cannot send more than 5% of the DAO SALT balance");
proposals.proposeSendSALT(bob, excessiveAmount, "description" );
vm.stopPrank();
vm.startPrank( DEPLOYER);
staking.stakeSALT(1000 ether);
// Test proposing to send an amount within the limit (less than 5% of the balance)
uint256 validAmount = daoInitialSaltBalance / 21; // <5% of the initial balance
proposals.proposeSendSALT( bob, validAmount, "description" );
uint256 validBallotId = 1;
Ballot memory validBallot = proposals.ballotForID(validBallotId);
assertEq(validBallot.ballotIsLive, true, "The valid ballot should be live");
assertEq(validBallot.number1, validAmount, "The proposed amount should be the same as the input amount");
vm.stopPrank();
vm.startPrank( alice );
// Test only one sendSALT proposal being able to be pending at a time
vm.expectRevert( "Cannot create a proposal similar to a ballot that is still open" );
proposals.proposeSendSALT( DEPLOYER, validAmount, "description" );
}
// A unit test for the proposeTokenWhitelisting function that includes the situation where the maximum number of token whitelisting proposals are already pending.
function testProposeTokenWhitelistingMaxPending() public {
// Reduce maxPendingTokensForWhitelisting to 3
vm.startPrank(address(dao));
daoConfig.changeMaxPendingTokensForWhitelisting(false);
daoConfig.changeMaxPendingTokensForWhitelisting(false);
vm.stopPrank();
string memory tokenIconURL = "http://test.com/token.png";
string memory tokenDescription = "Test Token for Whitelisting";
vm.startPrank(DEPLOYER);
staking.stakeSALT(1000 ether);
IERC20 token = new TestERC20("TEST", 18);
salt.transfer( bob, 1000 ether );
salt.transfer( charlie, 1000 ether );
proposals.proposeTokenWhitelisting(token, tokenIconURL, tokenDescription);
vm.stopPrank();
vm.startPrank(bob);
salt.approve(address(staking), 1000 ether);
staking.stakeSALT(1000 ether);
token = new TestERC20("TEST", 18);
proposals.proposeTokenWhitelisting(token, tokenIconURL, tokenDescription);
vm.stopPrank();
vm.startPrank(charlie);
salt.approve(address(staking), 1000 ether);
staking.stakeSALT(1000 ether);
token = new TestERC20("TEST", 18);
proposals.proposeTokenWhitelisting(token, tokenIconURL, tokenDescription);
vm.stopPrank();
// Attempt to create another token whitelisting proposal beyond the maximum limit
vm.startPrank(alice);
salt.approve(address(staking), 1000 ether);
staking.stakeSALT(1000 ether);
token = new TestERC20("TEST", 18);
vm.expectRevert("The maximum number of token whitelisting proposals are already pending");
proposals.proposeTokenWhitelisting(token, tokenIconURL, tokenDescription);
}
// A unit test that makes sures that none of the initial core tokens can be unwhitelisted
function testUnwhitelistingCoreTokens() public {
IERC20 wbtc = exchangeConfig.wbtc();
IERC20 weth = exchangeConfig.weth();
IERC20 dai = exchangeConfig.dai();
vm.startPrank(address(dao));
poolsConfig.whitelistPool( pools, wbtc, weth );
poolsConfig.whitelistPool( pools, wbtc, dai );
poolsConfig.whitelistPool( pools, wbtc, salt );
poolsConfig.whitelistPool( pools, wbtc, usds );
vm.stopPrank();
vm.startPrank(DEPLOYER);
vm.expectRevert("Cannot unwhitelist WBTC");
proposals.proposeTokenUnwhitelisting(wbtc, "test", "test");
vm.expectRevert("Cannot unwhitelist WETH");
proposals.proposeTokenUnwhitelisting(weth, "test", "test");
vm.expectRevert("Cannot unwhitelist DAI");
proposals.proposeTokenUnwhitelisting(dai, "test", "test");
vm.expectRevert("Cannot unwhitelist SALT");
proposals.proposeTokenUnwhitelisting(salt, "test", "test");
vm.expectRevert("Cannot unwhitelist USDS");
proposals.proposeTokenUnwhitelisting(usds, "test", "test");
}
// A unit test that verifies the proposeTokenUnwhitelisting function. This should include situations where the token is not whitelisted and a situation where the token is whitelisted.
function testProposeTokenUnwhitelisting() public {
vm.startPrank( DEPLOYER );
staking.stakeSALT(1000 ether);
// Trying to unwhitelist an unwhitelisted token should fail.
IERC20 newToken = new TestERC20("TEST", 18);
vm.expectRevert("Can only unwhitelist a whitelisted token");
proposals.proposeTokenUnwhitelisting( newToken, "test", "test");
IERC20 wbtc = exchangeConfig.wbtc();
IERC20 weth = exchangeConfig.weth();
vm.stopPrank();
// Whitelist the token (which will be paired with WBTC and WETH)
vm.prank(address(dao));
poolsConfig.whitelistPool( pools, newToken, wbtc );
vm.prank(address(dao));
poolsConfig.whitelistPool( pools, newToken, weth );
// Unwhitelist the token and expect no revert
vm.prank( DEPLOYER );
proposals.proposeTokenUnwhitelisting(newToken, "test", "test");
vm.startPrank( alice );
staking.stakeSALT(1000 ether);
vm.expectRevert("Cannot create a proposal similar to a ballot that is still open");
proposals.proposeTokenUnwhitelisting( newToken, "test", "test");
// Get the ballot id
uint256 ballotID = 1;
assertEq(uint256(proposals.ballotForID(ballotID).ballotType), uint256(BallotType.UNWHITELIST_TOKEN));
assertEq(proposals.ballotForID(ballotID).address1, address(newToken));
}
// A unit test that changes votes after unstaking SALT
function testChangeVotesAfterUnstakingSALT() public {
vm.startPrank(alice);
address randomAddress = address(0x543210);
// Staking SALT
staking.stakeSALT(100000 ether);
// Create a proposal
proposals.proposeCallContract(randomAddress, 1000, "description" );
uint256 ballotID = 1;
assertEq(proposals.ballotForID(ballotID).ballotIsLive, true, "Ballot should be live after proposal");
// Vote YES
proposals.castVote(ballotID, Vote.YES);
// Assert vote has been cast
assertEq(uint256(proposals.lastUserVoteForBallot(ballotID, alice).vote), uint256(Vote.YES), "Vote should have been casted");
assertEq(proposals.lastUserVoteForBallot(ballotID, alice).votingPower, 100000 ether, "Vote should have been casted with 100000 ether voting power");
// Unstake SALT
staking.unstake(50000 ether, 2 );
// Vote NO
proposals.castVote(ballotID, Vote.NO);
// Assert vote has been changed and voting power decreased
assertEq(uint256(proposals.lastUserVoteForBallot(ballotID, alice).vote), uint256(Vote.NO), "Vote should have been changed to NO");
assertEq(proposals.lastUserVoteForBallot(ballotID, alice).votingPower, 50000 ether, "Vote should have been casted with 50000 ether voting power after unstaking");
// Unstake all remaining SALT
staking.unstake(50000 ether, 2 );
// Expect voting to fail due to lack of voting power
vm.expectRevert("Staked SALT required to vote");
proposals.castVote(ballotID, Vote.YES);
}
// A unit test with multiple users voting on a parameter ballot and verifying the vote totals
function testParameterBallotVoting() public {
// Test proposeParameterBallot function
vm.startPrank(DEPLOYER);
staking.stakeSALT( 2000000 ether );
proposals.proposeParameterBallot(14, "description" );
vm.stopPrank();
uint256 ballotID = 1;
Ballot memory ballot = proposals.ballotForID(ballotID);
assertEq(ballot.ballotIsLive, true, "The ballot should be live.");
// Test multiple users voting on the ballot
// Voting by DEPLOYER
vm.startPrank(DEPLOYER);
vm.expectRevert( "Invalid VoteType for Parameter Ballot" );
proposals.castVote(ballotID, Vote.YES);
vm.expectRevert( "Invalid VoteType for Parameter Ballot" );
proposals.castVote(ballotID, Vote.NO);
proposals.castVote(ballotID, Vote.INCREASE);
salt.transfer(bob, 1000000 ether);
vm.stopPrank();
// Voting by alice
vm.startPrank(alice);
staking.stakeSALT( 1000000 ether );
proposals.castVote(ballotID, Vote.NO_CHANGE);
vm.stopPrank();
// Coting by bob
vm.startPrank(bob);
salt.approve( address(staking), type(uint256).max );
salt.approve( address(proposals), type(uint256).max );
staking.stakeSALT( 500000 ether );
proposals.castVote(ballotID, Vote.NO_CHANGE);
vm.stopPrank();
// Verify vote totals
uint256 increaseVotes = proposals.votesCastForBallot(ballotID, Vote.INCREASE);
uint256 noChangeVotes = proposals.votesCastForBallot(ballotID, Vote.NO_CHANGE);
uint256 totalVotes = proposals.totalVotesCastForBallot(ballotID);
assertEq(increaseVotes, 2000000 ether, "INCREASE votes do not match the sum of votes.");
assertEq(noChangeVotes, 1500000 ether, "NO_CHANGE votes do not match the sum of votes.");
assertEq(totalVotes, increaseVotes + noChangeVotes, "Total votes do not match the sum of votes.");
}
// A unit test with multiple users voting on an approval ballot and verifying the vote totals
function testApprovalBallotVoting() public {
// Test proposeParameterBallot function
vm.startPrank(DEPLOYER);
staking.stakeSALT( 2500000 ether );
proposals.proposeCountryInclusion( "US", "description" );
vm.stopPrank();
uint256 ballotID = 1;
Ballot memory ballot = proposals.ballotForID(ballotID);
assertEq(ballot.ballotIsLive, true, "The ballot should be live.");
// Test multiple users voting on the ballot
// Voting by DEPLOYER
vm.startPrank(DEPLOYER);
vm.expectRevert( "Invalid VoteType for Approval Ballot" );
proposals.castVote(ballotID, Vote.INCREASE);
vm.expectRevert( "Invalid VoteType for Approval Ballot" );
proposals.castVote(ballotID, Vote.NO_CHANGE);
vm.expectRevert( "Invalid VoteType for Approval Ballot" );
proposals.castVote(ballotID, Vote.DECREASE);
proposals.castVote(ballotID, Vote.YES);
staking.unstake(500000 ether, 12 );
proposals.castVote(ballotID, Vote.YES);
salt.transfer(bob, 1000000 ether);
vm.stopPrank();
// Voting by alice
vm.startPrank(alice);
staking.stakeSALT( 500000 ether );
proposals.castVote(ballotID, Vote.NO);
staking.stakeSALT( 500000 ether );
proposals.castVote(ballotID, Vote.NO);
vm.stopPrank();
// Coting by bob
vm.startPrank(bob);
salt.approve( address(staking), type(uint256).max );
salt.approve( address(proposals), type(uint256).max );
staking.stakeSALT( 500000 ether );
proposals.castVote(ballotID, Vote.NO);
vm.stopPrank();
// Verify vote totals
uint256 yesVotes = proposals.votesCastForBallot(ballotID, Vote.YES);
uint256 noVotes = proposals.votesCastForBallot(ballotID, Vote.NO);
uint256 totalVotes = proposals.totalVotesCastForBallot(ballotID);
assertEq(yesVotes, 2000000 ether, "YES votes do not match the sum of votes.");
assertEq(noVotes, 1500000 ether, "NO votes do not match the sum of votes.");
assertEq(totalVotes, yesVotes + noVotes, "Total votes do not match the sum of votes.");
}
// A unit test to verify that a user cannot cast a vote on a ballot that is not open for voting.
function testUserCannotVoteOnClosedBallot() public {
vm.startPrank( alice );
staking.stakeSALT( 1000000 ether );
// Alice proposes a parameter ballot
proposals.proposeParameterBallot(20, "description" );
uint256 ballotID = 1;
// Alice casts a vote on the newly created ballot
proposals.castVote(ballotID, Vote.INCREASE);
// Close the ballot
vm.expectRevert( "Only the DAO can mark a ballot as finalized" );
proposals.markBallotAsFinalized( ballotID );
vm.stopPrank();
vm.prank( address(dao) );
proposals.markBallotAsFinalized( ballotID );
// Alice attempts to cast a vote on the closed ballot
vm.prank( alice );
vm.expectRevert("The specified ballot is not open for voting");
proposals.castVote(ballotID, Vote.DECREASE);
}
// A unit test to verify that a user cannot cast an incorrect votetype on a Parameter Ballot
function testIncorrectParameterVote() public {
// Test proposeParameterBallot function
vm.startPrank(DEPLOYER);
staking.stakeSALT( 2000000 ether );
proposals.proposeParameterBallot(16, "description" );
vm.stopPrank();
uint256 ballotID = 1;
// Voting by DEPLOYER
vm.startPrank(DEPLOYER);
vm.expectRevert( "Invalid VoteType for Parameter Ballot" );
proposals.castVote(ballotID, Vote.YES);
vm.expectRevert( "Invalid VoteType for Parameter Ballot" );
proposals.castVote(ballotID, Vote.NO);
vm.stopPrank();
}
// A unit test to verify that a user cannot cast an incorrect votetype on an Approval Ballot
function testIncorrectApprovalVote() public {
// Test proposeParameterBallot function
vm.startPrank(DEPLOYER);
staking.stakeSALT( 2000000 ether );
proposals.proposeCountryInclusion("US", "description" );
vm.stopPrank();
uint256 ballotID = 1;
// Voting by DEPLOYER
vm.startPrank(DEPLOYER);
vm.expectRevert( "Invalid VoteType for Approval Ballot" );
proposals.castVote(ballotID, Vote.INCREASE);
vm.expectRevert( "Invalid VoteType for Approval Ballot" );
proposals.castVote(ballotID, Vote.NO_CHANGE);
vm.expectRevert( "Invalid VoteType for Approval Ballot" );
proposals.castVote(ballotID, Vote.DECREASE);
vm.stopPrank();
}