diff --git a/test/KSXVault.t.sol b/test/KSXVault.t.sol index abe3896..f57942f 100644 --- a/test/KSXVault.t.sol +++ b/test/KSXVault.t.sol @@ -14,6 +14,11 @@ contract KSXVaultTest is Bootstrap { MockERC20 depositToken; MockStakingRewards stakingRewards; + // Custom error from ERC4626 + error ERC4626ExceededMaxRedeem( + address owner, uint256 shares, uint256 maxShares + ); + function setUp() public { depositToken = new MockERC20("Deposit Token", "DT"); stakingRewards = new MockStakingRewards(address(depositToken)); @@ -87,7 +92,7 @@ contract KSXVaultTest is Bootstrap { } // Asserts the 1:1000 ratio is maintained when no rewards have accrued - function test_consecutive_deposits() public { + function test_vault_deposits() public { // First deposit: 1 KWENTA uint256 firstDeposit = 1 ether; @@ -98,14 +103,8 @@ contract KSXVaultTest is Bootstrap { uint256 firstShares = ksxVault.deposit(firstDeposit, alice); // Verify first deposit results - assertEq( - firstShares, 1000 ether, "First deposit should mint at 1:1000 ratio" - ); - assertEq( - stakingRewards.balanceOf(address(ksxVault)), - firstDeposit, - "Should have staked first deposit" - ); + assertEq(firstShares, 1000 ether); + assertEq(stakingRewards.balanceOf(address(ksxVault)), firstDeposit); // Second deposit: 2 KWENTA uint256 secondDeposit = 2 ether; @@ -113,135 +112,258 @@ contract KSXVaultTest is Bootstrap { uint256 secondShares = ksxVault.deposit(secondDeposit, alice); // Verify second deposit results - assertEq( - secondShares, - 2000 ether, - "Second deposit should mint at 1:1000 ratio" - ); + assertEq(secondShares, 2000 ether); assertEq( stakingRewards.balanceOf(address(ksxVault)), - firstDeposit + secondDeposit, - "Should have staked both deposits" + firstDeposit + secondDeposit ); // Verify final state - assertEq( - ksxVault.balanceOf(alice), - 3000 ether, - "Should have total of 3000 KSX" - ); - assertEq( - stakingRewards.balanceOf(address(ksxVault)), - 3 ether, - "Should have 3 KWENTA staked total" - ); + assertEq(ksxVault.balanceOf(alice), 3000 ether); + assertEq(stakingRewards.balanceOf(address(ksxVault)), 3 ether); vm.stopPrank(); } - // Asserts correct mint at 1000 shares ratio - // Mints a specified number of shares and requires the equivalent asset - // value to be deposited - function test_vault_mint() public { - uint256 amount = 1 ether; + function test_vault_deposits_with_rewards_between() public { + // Initial setup + uint256 initialDeposit = 1 ether; + uint256 rewardsAmount = 0.5 ether; + + // First deposit from Alice vm.startPrank(alice); - depositToken.approve(address(ksxVault), amount); - ksxVault.mint(1 ether, alice); - assertEq(ksxVault.balanceOf(alice), amount); + depositToken.approve(address(ksxVault), initialDeposit); + uint256 aliceShares = ksxVault.deposit(initialDeposit, alice); + vm.stopPrank(); + + // Verify Alice's initial position (1:1000 ratio) + assertEq(aliceShares, 1000 ether); + assertEq(stakingRewards.balanceOf(address(ksxVault)), initialDeposit); + + // Simulate rewards accrual to the vault's staked balance + depositToken.mint(address(stakingRewards), rewardsAmount); + stakingRewards.addRewardToStaker(address(ksxVault), rewardsAmount); + + // Verify rewards were properly added assertEq( stakingRewards.balanceOf(address(ksxVault)), - amount / (10 ** ksxVault.offset()) + initialDeposit + rewardsAmount ); + + // Bob's deposit + uint256 bobDeposit = 1 ether; + + // Calculate expected shares for Bob + // At this point, ratio is no longer 1:1000 due to rewards: + // - Total assets = 1.5 KWENTA (1 initial + 0.5 rewards) + // - Total shares = 1000 KSX (Alice's shares) + // - Bob deposits 1 KWENTA, should receive: (1 * 1000) / 1.5 = + // 666.666... KSX + uint256 expectedBobShares = + (bobDeposit * 1000 ether) / (initialDeposit + rewardsAmount); + + vm.startPrank(bob); + depositToken.approve(address(ksxVault), bobDeposit); + uint256 bobShares = ksxVault.deposit(bobDeposit, bob); vm.stopPrank(); + + // Verify Bob's position + assertApproxEqRel( + bobShares, + expectedBobShares, + 0.001e18 // 0.1% tolerance for rounding + ); + + // Verify final states + assertEq( + stakingRewards.balanceOf(address(ksxVault)), + initialDeposit + rewardsAmount + bobDeposit + ); + + // Verify share distribution + assertEq(ksxVault.balanceOf(alice), 1000 ether); + assertEq(ksxVault.balanceOf(bob), bobShares); + + // Verify total shares + assertEq(ksxVault.totalSupply(), 1000 ether + bobShares); } - // Asserts correct mint at 1000 shares ratio - function test_vault_mint_1000_ratio() public { - uint256 sharesToMint = 1000 ether; // minting 1000 KSX shares requires 1 - // KWENTA - uint256 depositTokenNeeded = 1 ether; + function test_vault_deposit_more_than_balance() public { + uint256 aliceBalance = depositToken.balanceOf(alice); - uint256 initialDepositTokenBalance = depositToken.balanceOf(alice); + vm.startPrank(alice); + depositToken.approve(address(ksxVault), aliceBalance + 1 ether); + vm.expectRevert(); // ERC20: transfer amount exceeds balance + ksxVault.deposit(aliceBalance + 1 ether, alice); + vm.stopPrank(); + } + // Test that mint is disabled + function test_vault_mint_disabled() public { vm.startPrank(alice); - depositToken.approve(address(ksxVault), depositTokenNeeded); - ksxVault.mint(sharesToMint, alice); + vm.expectRevert("Disabled"); + ksxVault.mint(1000 ether, alice); + vm.stopPrank(); + } - // Verify we got the correct number of shares - assertEq(ksxVault.balanceOf(alice), sharesToMint); - assertEq( - depositToken.balanceOf(alice), - initialDepositTokenBalance - depositTokenNeeded - ); + // Test that withdraw is disabled + function test_withdraw_disabled() public { + vm.startPrank(alice); + vm.expectRevert("Disabled"); + ksxVault.withdraw(1 ether, alice, alice); vm.stopPrank(); } - // Asserts correct mint at 1000 shares ratio Fuzzing sharesToMint - function test_vault_mint_1000_ratio_Fuzz(uint256 sharesToMint) public { - sharesToMint = bound(sharesToMint, 1000 ether, 100_000_000 ether); + function test_vault_redeem() public { + uint256 depositAmount = 1 ether; + vm.startPrank(alice); - uint256 depositTokenNeeded = (sharesToMint + 999) / 1000; + uint256 initialBalance = depositToken.balanceOf(alice); - // Ensure alice has enough tokens - depositToken.mint(alice, depositTokenNeeded); + // Deposit + depositToken.approve(address(ksxVault), depositAmount); + uint256 sharesMinted = ksxVault.deposit(depositAmount, alice); - uint256 initialDepositTokenBalance = depositToken.balanceOf(alice); + // Verify initial state + assertEq(sharesMinted, depositAmount * (10 ** ksxVault.offset())); + assertEq(stakingRewards.balanceOf(address(ksxVault)), depositAmount); + assertEq(depositToken.balanceOf(alice), initialBalance - depositAmount); - vm.startPrank(alice); - depositToken.approve(address(ksxVault), depositTokenNeeded); - ksxVault.mint(sharesToMint, alice); + // Redeem all shares + ksxVault.redeem(sharesMinted, alice, alice); - // Verify we got the correct number of shares - assertEq(ksxVault.balanceOf(alice), sharesToMint); + // Verify final state + assertEq(ksxVault.balanceOf(alice), 0); + assertEq(stakingRewards.balanceOf(address(ksxVault)), 0); + assertEq(depositToken.balanceOf(alice), initialBalance); - assertEq( - depositToken.balanceOf(alice), - initialDepositTokenBalance - depositTokenNeeded - ); vm.stopPrank(); } - // Withdraws a specified amount of assets from the vault by burning the - // equivalent shares - function test_withdraw() public { - uint256 amount = 1 ether; + function test_vault_redeem_1000_ratio() public { + // Setup: Alice deposits 1 KWENTA first + uint256 depositAmount = 1 ether; + uint256 expectedShares = 1000 ether; // 1000 KSX for 1 KWENTA vm.startPrank(alice); - // Approve and deposit - depositToken.approve(address(ksxVault), amount); - ksxVault.deposit(amount, alice); + // Initial deposit + depositToken.approve(address(ksxVault), depositAmount); + ksxVault.deposit(depositAmount, alice); // Verify initial state - assertEq(ksxVault.balanceOf(alice), amount * (10 ** ksxVault.offset())); - assertEq(stakingRewards.balanceOf(address(ksxVault)), amount); + assertEq(ksxVault.balanceOf(alice), expectedShares); + assertEq(stakingRewards.balanceOf(address(ksxVault)), depositAmount); + + // Record balance before redeem + uint256 preBalance = depositToken.balanceOf(alice); - // Withdraw - ksxVault.withdraw(amount, alice, alice); + // Redeem all shares + uint256 assetsReceived = ksxVault.redeem(expectedShares, alice, alice); // Verify final state + assertEq(assetsReceived, depositAmount); assertEq(ksxVault.balanceOf(alice), 0); assertEq(stakingRewards.balanceOf(address(ksxVault)), 0); - assertEq(depositToken.balanceOf(alice), 10 ether); + assertEq(depositToken.balanceOf(alice), preBalance + depositAmount); vm.stopPrank(); } - function test_redeem() public { - uint256 amount = 1 ether; + function test_vault_redeem_with_rewards() public { + uint256 depositAmount = 1 ether; + uint256 rewardsAmount = 0.5 ether; + + // Initial Deposit vm.startPrank(alice); - depositToken.approve(address(ksxVault), amount); - ksxVault.mint(1 ether, alice); - assertEq(stakingRewards.balanceOf(address(ksxVault)), amount / 1000); - assertEq( - stakingRewards.balanceOf(address(ksxVault)), - amount / (10 ** ksxVault.offset()) - ); + uint256 initialBalance = depositToken.balanceOf(alice); + depositToken.approve(address(ksxVault), depositAmount); + uint256 aliceShares = ksxVault.deposit(depositAmount, alice); + + // Verify initial state - Initial shares should be 1000 KSX + assertEq(aliceShares, 1000 ether); + assertEq(ksxVault.totalAssets(), depositAmount); + assertEq(stakingRewards.balanceOf(address(ksxVault)), depositAmount); + + // Add Rewards + vm.stopPrank(); + depositToken.mint(address(stakingRewards), rewardsAmount); + stakingRewards.addRewardToStaker(address(ksxVault), rewardsAmount); + + // Verify rewards were added + assertEq(ksxVault.totalAssets(), depositAmount + rewardsAmount); + + vm.startPrank(alice); + + // First redemption (redeem 2/3 of shares ~ 1 KWENTA initial value) + uint256 sharesToRedeem = (aliceShares * 2) / 3; // 666.666... KSX + uint256 firstRedeemAssets = + ksxVault.redeem(sharesToRedeem, alice, alice); + + // ~ 1.5 KWENTA in the vault so 2/3 of shares should be ~ 1 KWENTA + assertApproxEqAbs(firstRedeemAssets, 1 ether, 1); + + // Alice Should have 1/3 of shares remaining + assertEq(ksxVault.balanceOf(alice), aliceShares - sharesToRedeem); + + // Should have ~0.5 KWENTA remaining in the vault + assertApproxEqAbs(ksxVault.totalAssets(), 0.5 ether, 1); + + // Redeem remaining shares + uint256 remainingShares = ksxVault.balanceOf(alice); + uint256 secondRedeemAssets = + ksxVault.redeem(remainingShares, alice, alice); - ksxVault.redeem(amount, alice, alice); assertEq(ksxVault.balanceOf(alice), 0); - // assertEq(stakingRewards.stakedBalanceOf(address(ksxVault)), 0); - // assertEq(depositToken.balanceOf(alice), 10 ether); + // "Should have at most 1 wei remaining due to rounding" + assertLe(stakingRewards.balanceOf(address(ksxVault)), 1); + + // Verify total assets received + uint256 totalReceived = firstRedeemAssets + secondRedeemAssets; + assertApproxEqAbs( + totalReceived, + depositAmount + rewardsAmount, + 2 // Allow 2 wei tolerance for cumulative rounding + ); + + // Verify final balance + assertApproxEqAbs( + depositToken.balanceOf(alice), + initialBalance - depositAmount + totalReceived, + 2 + ); + + vm.stopPrank(); + } + + function test_vault_redeem_no_shares() public { + vm.startPrank(alice); + vm.expectRevert( + abi.encodeWithSelector( + ERC4626ExceededMaxRedeem.selector, alice, 1 ether, 0 + ) + ); + ksxVault.redeem(1 ether, alice, alice); + vm.stopPrank(); + } + + function test_vault_redeem_more_than_balance() public { + // Setup: Alice deposits 1 KWENTA + uint256 depositAmount = 1 ether; + + vm.startPrank(alice); + depositToken.approve(address(ksxVault), depositAmount); + ksxVault.deposit(depositAmount, alice); + + // Try to redeem more shares than owned + // Alice has 1000 KSX (1000 ether) but tries to redeem 2000 KSX (2000 + // ether) + vm.expectRevert( + abi.encodeWithSelector( + ERC4626ExceededMaxRedeem.selector, alice, 2000 ether, 1000 ether + ) + ); + ksxVault.redeem(2000 ether, alice, alice); vm.stopPrank(); }