Skip to content

Commit

Permalink
✅ add comprehensive vault operation test suite
Browse files Browse the repository at this point in the history
  • Loading branch information
Flocqst committed Oct 29, 2024
1 parent c7e5aea commit c79d016
Showing 1 changed file with 211 additions and 89 deletions.
300 changes: 211 additions & 89 deletions test/KSXVault.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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;

Expand All @@ -98,150 +103,267 @@ 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;
depositToken.approve(address(ksxVault), secondDeposit);
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();
}

Expand Down

0 comments on commit c79d016

Please sign in to comment.