@@ -6,12 +6,38 @@ import "./DeploymentSetup.t.sol";
66import "../src/CustomBaal.sol " ;
77import "@openzeppelin/contracts/token/ERC20/IERC20.sol " ;
88import { console2 } from "forge-std/console2.sol " ;
9+ import "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol " ;
910
1011/**
1112 * @dev RagequitTest contract to test the ragequit functionality of the Baal DAO
1213 */
1314contract RagequitTest is DeploymentSetup {
1415
16+ // For config-agnostic tests
17+ CustomBaal public customBaalDirect;
18+ address public mockCommunityVault;
19+ address public mockLootToken;
20+ address public mockSharesToken;
21+ address public testUser1;
22+ address public testUser2;
23+ address public testUser3;
24+ ERC20PresetMinterPauser public testToken1;
25+ ERC20PresetMinterPauser public testToken2;
26+
27+ // Override setup to initialize config-agnostic test variables
28+ function setUp () public override {
29+ super .setUp ();
30+
31+ // Create test users
32+ testUser1 = makeAddr ("testUser1 " );
33+ testUser2 = makeAddr ("testUser2 " );
34+ testUser3 = makeAddr ("testUser3 " );
35+
36+ // Deploy test tokens for ragequit
37+ testToken1 = new ERC20PresetMinterPauser ("Test Token 1 " , "TT1 " );
38+ testToken2 = new ERC20PresetMinterPauser ("Test Token 2 " , "TT2 " );
39+ }
40+
1541 // Test ragequit with ETH
1642 function testRagequitWithEth () public {
1743 // Get the Baal contract instance
@@ -324,4 +350,317 @@ contract RagequitTest is DeploymentSetup {
324350 console2.log ("Baal avatar new ETH balance: " , newAvatarEthBalance);
325351 assertEq (newAvatarEthBalance, avatarEthBalance - expectedEthReturn, "Avatar ETH balance not reduced correctly " );
326352 }
353+
354+ // Direct test of ragequit with multiple tokens
355+ function testRagequitWithMultipleTokens () public {
356+ CustomBaal baalContract = CustomBaal (baal);
357+
358+ // Get user's initial loot balance
359+ uint256 initialLootBalance = IERC20 (lootToken).balanceOf (user);
360+ uint256 lootToBurn = initialLootBalance / 3 ; // Burn a third of the loot
361+
362+ // Setup test tokens in treasury
363+ uint256 treasuryToken1Amount = 100 ether ;
364+ uint256 treasuryToken2Amount = 200 ether ;
365+
366+ // Mint tokens and send to avatar (treasury)
367+ testToken1.mint (address (this ), treasuryToken1Amount);
368+ testToken2.mint (address (this ), treasuryToken2Amount);
369+ testToken1.transfer (baalContract.avatar (), treasuryToken1Amount);
370+ testToken2.transfer (baalContract.avatar (), treasuryToken2Amount);
371+
372+ // Send ETH to avatar
373+ vm.deal (address (this ), 5 ether);
374+ (bool success , ) = payable (baalContract.avatar ()).call {value: 5 ether }("" );
375+ require (success, "ETH transfer failed " );
376+
377+ // Get initial balances
378+ uint256 initialTotalSupply = baalContract.totalSupply ();
379+ uint256 vaultLootBalance = IERC20 (lootToken).balanceOf (communityVault);
380+ uint256 vaultSharesBalance = IERC20 (address (baalContract.sharesToken ())).balanceOf (communityVault);
381+
382+ // Calculate adjusted total supply
383+ uint256 adjustedTotalSupply = initialTotalSupply - vaultLootBalance - vaultSharesBalance;
384+
385+ // Calculate expected returns for each token
386+ uint256 expectedEthReturn = (lootToBurn * 5 ether) / adjustedTotalSupply;
387+ uint256 expectedToken1Return = (lootToBurn * treasuryToken1Amount) / adjustedTotalSupply;
388+ uint256 expectedToken2Return = (lootToBurn * treasuryToken2Amount) / adjustedTotalSupply;
389+
390+ // Record initial balances
391+ uint256 initialUserEthBalance = user.balance;
392+ uint256 initialUserToken1Balance = testToken1.balanceOf (user);
393+ uint256 initialUserToken2Balance = testToken2.balanceOf (user);
394+
395+ // Sort token addresses in ascending order for ragequit
396+ address ethAddress = address (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE );
397+ address token1Address = address (testToken1);
398+ address token2Address = address (testToken2);
399+
400+ // Create the tokens array with sorted addresses
401+ address [] memory tokens = new address [](3 );
402+
403+ // Simple sorting based on address values
404+ if (token1Address < token2Address && token1Address < ethAddress) {
405+ tokens[0 ] = token1Address;
406+ if (token2Address < ethAddress) {
407+ tokens[1 ] = token2Address;
408+ tokens[2 ] = ethAddress;
409+ } else {
410+ tokens[1 ] = ethAddress;
411+ tokens[2 ] = token2Address;
412+ }
413+ } else if (token2Address < token1Address && token2Address < ethAddress) {
414+ tokens[0 ] = token2Address;
415+ if (token1Address < ethAddress) {
416+ tokens[1 ] = token1Address;
417+ tokens[2 ] = ethAddress;
418+ } else {
419+ tokens[1 ] = ethAddress;
420+ tokens[2 ] = token1Address;
421+ }
422+ } else {
423+ tokens[0 ] = ethAddress;
424+ if (token1Address < token2Address) {
425+ tokens[1 ] = token1Address;
426+ tokens[2 ] = token2Address;
427+ } else {
428+ tokens[1 ] = token2Address;
429+ tokens[2 ] = token1Address;
430+ }
431+ }
432+
433+ // Execute ragequit
434+ vm.startPrank (user);
435+ baalContract.ragequit (user, 0 , lootToBurn, tokens);
436+ vm.stopPrank ();
437+
438+ // Check loot was burned
439+ assertEq (IERC20 (lootToken).balanceOf (user), initialLootBalance - lootToBurn);
440+
441+ // Check user received tokens and treasury balances were reduced
442+ // We need to verify each token based on its position in the sorted array
443+ for (uint i = 0 ; i < tokens.length ; i++ ) {
444+ if (tokens[i] == ethAddress) {
445+ assertEq (user.balance, initialUserEthBalance + expectedEthReturn, "ETH balance incorrect " );
446+ assertEq (address (baalContract.avatar ()).balance, 5 ether - expectedEthReturn, "Avatar ETH balance incorrect " );
447+ } else if (tokens[i] == token1Address) {
448+ assertEq (testToken1.balanceOf (user), initialUserToken1Balance + expectedToken1Return, "Token1 balance incorrect " );
449+ assertEq (testToken1.balanceOf (baalContract.avatar ()), treasuryToken1Amount - expectedToken1Return, "Avatar Token1 balance incorrect " );
450+ } else if (tokens[i] == token2Address) {
451+ assertEq (testToken2.balanceOf (user), initialUserToken2Balance + expectedToken2Return, "Token2 balance incorrect " );
452+ assertEq (testToken2.balanceOf (baalContract.avatar ()), treasuryToken2Amount - expectedToken2Return, "Avatar Token2 balance incorrect " );
453+ }
454+ }
455+ }
456+
457+ // Test multiple users doing ragequit sequentially
458+ function testSequentialRagequit () public {
459+ CustomBaal baalContract = CustomBaal (baal);
460+
461+ // Setup initial state - share loot among test users
462+ vm.startPrank (user);
463+ uint256 initialLootBalance = IERC20 (lootToken).balanceOf (user);
464+
465+ // Keep half, give 1/4 to each test user
466+ uint256 transferAmount = initialLootBalance / 4 ;
467+ IERC20 (lootToken).transfer (testUser1, transferAmount);
468+ IERC20 (lootToken).transfer (testUser2, transferAmount);
469+ vm.stopPrank ();
470+
471+ // Send ETH to avatar
472+ vm.deal (address (this ), 12 ether);
473+ (bool success , ) = payable (baalContract.avatar ()).call {value: 12 ether }("" );
474+ require (success, "ETH transfer failed " );
475+
476+ // Get adjusted total supply
477+ uint256 totalSupply = baalContract.totalSupply ();
478+ uint256 vaultLootBalance = IERC20 (lootToken).balanceOf (communityVault);
479+ uint256 vaultSharesBalance = IERC20 (address (baalContract.sharesToken ())).balanceOf (communityVault);
480+ uint256 adjustedTotalSupply = totalSupply - vaultLootBalance - vaultSharesBalance;
481+
482+ // User 1 ragequits with all their loot
483+ address [] memory tokens = new address [](1 );
484+ tokens[0 ] = address (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE ); // ETH
485+
486+ uint256 user1InitialBalance = testUser1.balance;
487+ uint256 avatarInitialBalance = address (baalContract.avatar ()).balance;
488+ uint256 expectedReturn1 = (transferAmount * avatarInitialBalance) / adjustedTotalSupply;
489+
490+ vm.startPrank (testUser1);
491+ baalContract.ragequit (testUser1, 0 , transferAmount, tokens);
492+ vm.stopPrank ();
493+
494+ // Check user1 received ETH
495+ assertEq (testUser1.balance, user1InitialBalance + expectedReturn1);
496+
497+ // User 2 ragequits with all their loot
498+ uint256 user2InitialBalance = testUser2.balance;
499+ uint256 avatarUpdatedBalance = address (baalContract.avatar ()).balance;
500+
501+ // Recalculate adjusted total supply after user1's ragequit
502+ adjustedTotalSupply -= transferAmount;
503+ uint256 expectedReturn2 = (transferAmount * avatarUpdatedBalance) / adjustedTotalSupply;
504+
505+ vm.startPrank (testUser2);
506+ baalContract.ragequit (testUser2, 0 , transferAmount, tokens);
507+ vm.stopPrank ();
508+
509+ // Check user2 received ETH
510+ assertEq (testUser2.balance, user2InitialBalance + expectedReturn2);
511+
512+ // Verify original user's loot remains intact
513+ assertEq (IERC20 (lootToken).balanceOf (user), initialLootBalance / 2 );
514+
515+ // Verify both test users have 0 loot
516+ assertEq (IERC20 (lootToken).balanceOf (testUser1), 0 );
517+ assertEq (IERC20 (lootToken).balanceOf (testUser2), 0 );
518+ }
519+
520+ // Test edge case: total supply equals vault balance
521+ function testRagequitWithTotalSupplyEqualsVaultBalance () public {
522+ CustomBaal baalContract = CustomBaal (baal);
523+
524+ // Get user's current loot
525+ uint256 userLootBalance = IERC20 (lootToken).balanceOf (user);
526+
527+ // Mint more loot to the community vault to make adjustedTotalSupply very small
528+ vm.startPrank (baalContract.avatar ());
529+ baalContract.mintLoot (
530+ _singleAddressArray (communityVault),
531+ _singleUint256Array (baalContract.totalSupply () * 100 ) // Make vault balance far exceed total supply
532+ );
533+ vm.stopPrank ();
534+
535+ // Send ETH to avatar
536+ vm.deal (address (this ), 5 ether);
537+ (bool success , ) = payable (baalContract.avatar ()).call {value: 5 ether }("" );
538+ require (success, "ETH transfer failed " );
539+
540+ // Calculate expected return
541+ uint256 lootToBurn = userLootBalance / 2 ;
542+ address [] memory tokens = new address [](1 );
543+ tokens[0 ] = address (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE ); // ETH
544+
545+ uint256 initialUserEthBalance = user.balance;
546+ uint256 avatarEthBalance = address (baalContract.avatar ()).balance;
547+
548+ vm.startPrank (user);
549+ baalContract.ragequit (user, 0 , lootToBurn, tokens);
550+ vm.stopPrank ();
551+
552+ // Even with extreme vault balance, user should still get proportional ETH
553+ assertGt (user.balance, initialUserEthBalance, "User should have received some ETH " );
554+ assertLt (address (baalContract.avatar ()).balance, avatarEthBalance, "Avatar balance should have decreased " );
555+ }
556+
557+ // Test ragequit with zero community vault
558+ function testRagequitWithZeroCommunityVault () public {
559+ CustomBaal baalContract = CustomBaal (baal);
560+
561+ // Get initial balances and information
562+ uint256 initialUserLootBalance = IERC20 (lootToken).balanceOf (user);
563+ uint256 vaultLootBalance = IERC20 (lootToken).balanceOf (communityVault);
564+ console2.log ("Initial community vault LOOT balance: " , vaultLootBalance);
565+
566+ vm.startPrank (baalContract.avatar ());
567+
568+ // Burn all loot tokens from the community vault
569+ baalContract.burnLoot (
570+ _singleAddressArray (communityVault),
571+ _singleUint256Array (vaultLootBalance)
572+ );
573+ vm.stopPrank ();
574+
575+ // Verify the community vault now has zero loot tokens
576+ uint256 newVaultBalance = IERC20 (lootToken).balanceOf (communityVault);
577+ console2.log ("Community vault LOOT balance after burning: " , newVaultBalance);
578+ assertEq (newVaultBalance, 0 , "Vault should have 0 loot " );
579+
580+ // Send ETH to the avatar for ragequit
581+ vm.deal (address (this ), 5 ether);
582+ (bool success , ) = payable (baalContract.avatar ()).call {value: 5 ether }("" );
583+ require (success, "ETH transfer failed " );
584+
585+ // Record initial balances before ragequit
586+ uint256 initialTotalSupply = baalContract.totalSupply ();
587+ uint256 initialUserEthBalance = user.balance;
588+ uint256 avatarEthBalance = address (baalContract.avatar ()).balance;
589+
590+ // Calculate loot to burn (half of user's balance)
591+ uint256 lootToBurn = initialUserLootBalance / 2 ;
592+
593+ // Prepare token array for ragequit
594+ address [] memory tokens = new address [](1 );
595+ tokens[0 ] = address (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE ); // ETH
596+
597+ // Calculate expected return with zero vault balance
598+ uint256 adjustedTotalSupply = initialTotalSupply;
599+ uint256 expectedEthReturn = (lootToBurn * avatarEthBalance) / adjustedTotalSupply;
600+
601+ // Execute ragequit with zero vault balance
602+ vm.startPrank (user);
603+ baalContract.ragequit (user, 0 , lootToBurn, tokens);
604+ vm.stopPrank ();
605+
606+ // Verify user received the expected ETH amount
607+ assertEq (user.balance, initialUserEthBalance + expectedEthReturn, "User didn't receive correct ETH amount " );
608+
609+ // Verify user's loot was burned
610+ assertEq (IERC20 (lootToken).balanceOf (user), initialUserLootBalance - lootToBurn, "Incorrect loot balance after ragequit " );
611+
612+ // Verify avatar ETH balance decreased correctly
613+ assertEq (address (baalContract.avatar ()).balance, avatarEthBalance - expectedEthReturn, "Avatar ETH balance didn't decrease correctly " );
614+ }
615+
616+ // Test ragequit with precise amounts and custom loot distribution
617+ function testRagequitWithPreciseAmounts () public {
618+ CustomBaal baalContract = CustomBaal (baal);
619+
620+ // User wants to burn exactly 35% of their loot
621+ uint256 initialLootBalance = IERC20 (lootToken).balanceOf (user);
622+ uint256 lootToBurn = (initialLootBalance * 35 ) / 100 ; // 35%
623+
624+ // Send a precise amount of ETH to avatar
625+ uint256 ethAmount = 7.77777 ether ;
626+ vm.deal (address (this ), ethAmount);
627+ (bool success , ) = payable (baalContract.avatar ()).call {value: ethAmount}("" );
628+ require (success, "ETH transfer failed " );
629+
630+ // Calculate expected return
631+ uint256 totalSupply = baalContract.totalSupply ();
632+ uint256 vaultLootBalance = IERC20 (lootToken).balanceOf (communityVault);
633+ uint256 vaultSharesBalance = IERC20 (address (baalContract.sharesToken ())).balanceOf (communityVault);
634+ uint256 adjustedTotalSupply = totalSupply - vaultLootBalance - vaultSharesBalance;
635+
636+ uint256 expectedEthReturn = (lootToBurn * ethAmount) / adjustedTotalSupply;
637+
638+ // Record initial balances
639+ uint256 initialUserEthBalance = user.balance;
640+
641+ // Ragequit with ETH
642+ address [] memory tokens = new address [](1 );
643+ tokens[0 ] = address (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE );
644+
645+ vm.startPrank (user);
646+ baalContract.ragequit (user, 0 , lootToBurn, tokens);
647+ vm.stopPrank ();
648+
649+ // Verify results
650+ assertEq (IERC20 (lootToken).balanceOf (user), initialLootBalance - lootToBurn);
651+ assertEq (user.balance, initialUserEthBalance + expectedEthReturn);
652+ }
653+
654+ // Helper functions
655+ function _singleAddressArray (address _addr ) private pure returns (address [] memory ) {
656+ address [] memory arr = new address [](1 );
657+ arr[0 ] = _addr;
658+ return arr;
659+ }
660+
661+ function _singleUint256Array (uint256 _val ) private pure returns (uint256 [] memory ) {
662+ uint256 [] memory arr = new uint256 [](1 );
663+ arr[0 ] = _val;
664+ return arr;
665+ }
327666}
0 commit comments