// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.19; import "../src/Kraiken.sol"; import { ExceededAvailableStake, Stake, TooMuchSnatch } from "../src/Stake.sol"; import "./helpers/TestBase.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; contract StakeTest is TestConstants { Kraiken kraiken; Stake stakingPool; address liquidityPool; address liquidityManager; event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 kraikenDeposit, uint256 share, uint32 taxRate); event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 kraikenPayout); function setUp() public { kraiken = new Kraiken("KRAIKEN", "KRK"); stakingPool = new Stake(address(kraiken), makeAddr("taxRecipient")); kraiken.setStakingPool(address(stakingPool)); liquidityManager = makeAddr("liquidityManager"); kraiken.setLiquidityManager(liquidityManager); } function assertPosition(uint256 positionId, uint256 expectedShares, uint32 expectedTaxRate) private view { (uint256 shares,,,, uint32 taxRate) = stakingPool.positions(positionId); assertEq(shares, expectedShares, "Incorrect share amount for new position"); assertEq(taxRate, expectedTaxRate, "Incorrect tax rate for new position"); } function verifyPositionShrunkOrRemoved(uint256 positionId, uint256 initialStake) private view { (uint256 remainingShare,,,,) = stakingPool.positions(positionId); uint256 expectedInitialShares = stakingPool.assetsToShares(initialStake); bool positionRemoved = remainingShare == 0; bool positionShrunk = remainingShare < expectedInitialShares; assertTrue(positionRemoved || positionShrunk, "Position was not correctly shrunk or removed"); } function testBasicStaking() public { // Setup uint256 stakeAmount = 1 ether; address staker = makeAddr("staker"); vm.startPrank(liquidityManager); kraiken.mint(stakeAmount * 5); kraiken.transfer(staker, stakeAmount); vm.stopPrank(); vm.startPrank(staker); // Approve and stake kraiken.approve(address(stakingPool), stakeAmount); uint256[] memory empty; uint256 sharesExpected = stakingPool.assetsToShares(stakeAmount); vm.expectEmit(address(stakingPool)); emit PositionCreated(654_321, staker, stakeAmount, sharesExpected, 1); uint256 positionId = stakingPool.snatch(stakeAmount, staker, 1, empty); // Check results assertEq(stakingPool.outstandingStake(), stakingPool.assetsToShares(stakeAmount), "Outstanding stake did not update correctly"); (uint256 share, address owner, uint32 creationTime,, uint32 taxRate) = stakingPool.positions(positionId); assertEq(stakingPool.sharesToAssets(share), stakeAmount, "Stake amount in position is incorrect"); assertEq(owner, staker, "Stake owner is incorrect"); assertEq(creationTime, uint32(block.timestamp), "Creation time is incorrect"); assertEq(taxRate, 1, "Tax rate should be initialized to 1"); vm.stopPrank(); } function testUnstaking() public { // Setup: Create a staking position first uint256 stakeAmount = 1 ether; address staker = makeAddr("staker"); vm.startPrank(liquidityManager); kraiken.mint(stakeAmount * 5); // Ensuring the staker has enough balance kraiken.transfer(staker, stakeAmount); vm.stopPrank(); // Staker stakes tokens vm.startPrank(staker); kraiken.approve(address(stakingPool), stakeAmount); uint256[] memory empty; uint256 positionId = stakingPool.snatch(stakeAmount, staker, 1, empty); // Simulate time passage to accumulate tax liability vm.warp(block.timestamp + 3 days); // Calculate expected tax due at the moment of unstaking uint256 taxAmount = stakingPool.taxDue(positionId, 0); uint256 assetsAfterTax = stakeAmount - taxAmount; // Expect the PositionRemoved event with the expected parameters vm.expectEmit(true, true, true, true); emit PositionRemoved(positionId, staker, assetsAfterTax); // Perform unstaking stakingPool.exitPosition(positionId); // Check results after unstaking assertEq(kraiken.balanceOf(staker), assetsAfterTax, "Assets after tax not returned correctly"); assertEq(stakingPool.outstandingStake(), 0, "Outstanding stake not updated correctly"); // Ensure the position is cleared (, address owner, uint32 time,,) = stakingPool.positions(positionId); assertEq(time, 0, "Position time not cleared"); assertEq(owner, address(0), "Position owner not cleared"); vm.stopPrank(); } function testSnatchFunction() public { uint256 initialStake1 = 1 ether; uint256 initialStake2 = 2 ether; address firstStaker = makeAddr("firstStaker"); address secondStaker = makeAddr("secondStaker"); address newStaker = makeAddr("newStaker"); uint256 snatchAmount = 1.5 ether; // Mint and distribute tokens vm.startPrank(liquidityManager); kraiken.mint((initialStake1 + initialStake2) * 5); kraiken.transfer(firstStaker, initialStake1); kraiken.transfer(secondStaker, initialStake2); kraiken.transfer(newStaker, snatchAmount); vm.stopPrank(); // Setup initial stakers uint256 positionId1 = doSnatch(firstStaker, initialStake1, 1); uint256 positionId2 = doSnatch(secondStaker, initialStake2, 5); // Snatch setup vm.startPrank(newStaker); kraiken.approve(address(stakingPool), snatchAmount); uint256 snatchShares = stakingPool.assetsToShares(snatchAmount); uint256[] memory targetPositions = new uint256[](2); targetPositions[0] = positionId1; targetPositions[1] = positionId2; // Perform snatch uint256 newPositionId = stakingPool.snatch(snatchAmount, newStaker, 10, targetPositions); // Verify new position assertPosition(newPositionId, snatchShares, 10); // Check old positions verifyPositionShrunkOrRemoved(1, initialStake1); verifyPositionShrunkOrRemoved(2, initialStake2); vm.stopPrank(); } function doSnatch(address staker, uint256 amount, uint32 taxRate) private returns (uint256 positionId) { vm.startPrank(staker); kraiken.approve(address(stakingPool), amount); uint256[] memory empty; positionId = stakingPool.snatch(amount, staker, taxRate, empty); vm.stopPrank(); } // Using bp() and denormTR() from TestBase function testAvgTaxRateAndPercentageStaked() public { uint256 smallstake = 0.3e17; uint256 stakeOneThird = 1 ether; uint256 stakeTwoThird = 2 ether; address staker = makeAddr("staker"); // Mint and distribute tokens vm.startPrank(liquidityManager); // mint all the tokens we will need in the test kraiken.mint((smallstake + stakeOneThird + stakeTwoThird) * 5); // send 20% of that to staker kraiken.transfer(staker, (smallstake + stakeOneThird + stakeTwoThird) * 2); vm.stopPrank(); // Setup initial stakers uint256 positionId1 = doSnatch(staker, smallstake, 0); uint256 avgTaxRate; uint256 percentageStaked; avgTaxRate = stakingPool.getAverageTaxRate(); percentageStaked = stakingPool.getPercentageStaked(); // let this be about 10 basis points of tax rate assertApproxEqRel(bp(denormTR(avgTaxRate)), 10, 1e17); assertApproxEqRel(bp(percentageStaked), 10, 1e17); vm.prank(staker); stakingPool.exitPosition(positionId1); uint256 positionId2 = doSnatch(staker, stakeOneThird, 2); avgTaxRate = stakingPool.getAverageTaxRate(); percentageStaked = stakingPool.getPercentageStaked(); assertApproxEqRel(bp(denormTR(avgTaxRate)), 50, 1e17); assertApproxEqRel(bp(percentageStaked), 300, 1e17); vm.prank(staker); stakingPool.exitPosition(positionId2); positionId1 = doSnatch(staker, stakeOneThird, 10); positionId2 = doSnatch(staker, stakeTwoThird, 11); avgTaxRate = stakingPool.getAverageTaxRate(); percentageStaked = stakingPool.getPercentageStaked(); assertApproxEqRel(bp(denormTR(avgTaxRate)), 730, 1e17); assertApproxEqRel(bp(percentageStaked), 1000, 1e17); vm.startPrank(staker); stakingPool.exitPosition(positionId1); stakingPool.exitPosition(positionId2); vm.stopPrank(); positionId1 = doSnatch(staker, stakeOneThird, 29); positionId2 = doSnatch(staker, stakeTwoThird, 29); avgTaxRate = stakingPool.getAverageTaxRate(); assertApproxEqRel(bp(denormTR(avgTaxRate)), 97_000, 1e17); vm.startPrank(staker); stakingPool.exitPosition(positionId1); stakingPool.exitPosition(positionId2); vm.stopPrank(); positionId2 = doSnatch(staker, stakeTwoThird, 15); avgTaxRate = stakingPool.getAverageTaxRate(); percentageStaked = stakingPool.getPercentageStaked(); assertApproxEqRel(bp(denormTR(avgTaxRate)), 2500, 1e17); assertApproxEqRel(bp(percentageStaked), 660, 1e17); vm.startPrank(staker); stakingPool.exitPosition(positionId2); vm.stopPrank(); positionId1 = doSnatch(staker, stakeOneThird, 15); avgTaxRate = stakingPool.getAverageTaxRate(); percentageStaked = stakingPool.getPercentageStaked(); assertApproxEqRel(bp(denormTR(avgTaxRate)), 2500, 1e17); assertApproxEqRel(bp(percentageStaked), 330, 1e17); } function testRevert_SharesTooLow() public { address staker = makeAddr("staker"); vm.startPrank(liquidityManager); kraiken.mint(10 ether); uint256 tooSmallStake = kraiken.previousTotalSupply() / 4000; // Less than minStake calculation kraiken.transfer(staker, tooSmallStake); vm.stopPrank(); vm.startPrank(staker); kraiken.approve(address(stakingPool), tooSmallStake); uint256[] memory empty; vm.expectRevert(abi.encodeWithSelector(Stake.StakeTooLow.selector, staker, tooSmallStake, kraiken.previousTotalSupply() / 3000)); stakingPool.snatch(tooSmallStake, staker, 1, empty); vm.stopPrank(); } function testRevert_TaxTooLow() public { address existingStaker = makeAddr("existingStaker"); address newStaker = makeAddr("newStaker"); vm.startPrank(liquidityManager); kraiken.mint(10 ether); kraiken.transfer(existingStaker, 1 ether); kraiken.transfer(newStaker, 1 ether); vm.stopPrank(); uint256 positionId = doSnatch(existingStaker, 1 ether, 5); // Existing staker with tax rate 5 vm.startPrank(newStaker); kraiken.transfer(newStaker, 1 ether); kraiken.approve(address(stakingPool), 1 ether); uint256[] memory positions = new uint256[](1); positions[0] = positionId; // Assuming position ID 1 has tax rate 5 vm.expectRevert(abi.encodeWithSelector(Stake.TaxTooLow.selector, newStaker, 5, 5, positionId)); stakingPool.snatch(1 ether, newStaker, 5, positions); // Same tax rate should fail vm.stopPrank(); } function testRevert_TooMuchSnatch() public { address staker = makeAddr("staker"); address ambitiousStaker = makeAddr("ambitiousStaker"); vm.startPrank(liquidityManager); kraiken.mint(20 ether); kraiken.transfer(staker, 2 ether); kraiken.transfer(ambitiousStaker, 1 ether); vm.stopPrank(); uint256 positionId = doSnatch(staker, 2 ether, 10); vm.startPrank(ambitiousStaker); kraiken.approve(address(stakingPool), 1 ether); uint256[] memory positions = new uint256[](1); positions[0] = positionId; vm.expectRevert(abi.encodeWithSelector(TooMuchSnatch.selector, ambitiousStaker, 500_000 ether, 1_000_000 ether, 1_000_000 ether)); stakingPool.snatch(1 ether, ambitiousStaker, 20, positions); vm.stopPrank(); } function testRevert_PositionNotFound() public { address staker = makeAddr("staker"); vm.startPrank(liquidityManager); kraiken.mint(10 ether); kraiken.transfer(staker, 1 ether); vm.stopPrank(); vm.startPrank(staker); kraiken.approve(address(stakingPool), 1 ether); uint256[] memory nonExistentPositions = new uint256[](1); nonExistentPositions[0] = 999; // Assumed non-existent position ID vm.expectRevert(abi.encodeWithSelector(Stake.PositionNotFound.selector, 999, staker)); stakingPool.snatch(1 ether, staker, 15, nonExistentPositions); vm.stopPrank(); } function testSuccessfulChangeTaxAndReverts() public { uint32 newTaxRate = 3; // New valid tax rate address staker = makeAddr("staker"); vm.startPrank(liquidityManager); kraiken.mint(10 ether); kraiken.transfer(staker, 1 ether); vm.stopPrank(); vm.startPrank(staker); kraiken.approve(address(stakingPool), 1 ether); uint256[] memory empty; uint256 positionId = stakingPool.snatch(1 ether, staker, 1, empty); uint32 invalidTaxRate = 9999; // Assuming this is out of bounds vm.expectRevert(bytes("tax rate out of bounds")); stakingPool.changeTax(positionId, invalidTaxRate); stakingPool.changeTax(positionId, newTaxRate); vm.stopPrank(); // Verify the change (,,,, uint32 taxRate) = stakingPool.positions(positionId); assertEq(taxRate, newTaxRate, "Tax rate did not update correctly"); // notOwner tries to change tax rate address notOwner = makeAddr("notOwner"); vm.prank(notOwner); vm.expectRevert(abi.encodeWithSelector(Stake.NoPermission.selector, notOwner, staker)); stakingPool.changeTax(positionId, newTaxRate * 2); } function testNormalTaxPayment() public { address staker = makeAddr("staker"); vm.startPrank(liquidityManager); kraiken.mint(10 ether); kraiken.transfer(staker, 1 ether); vm.stopPrank(); vm.startPrank(staker); kraiken.approve(address(stakingPool), 1 ether); uint256[] memory empty; uint256 positionId = stakingPool.snatch(1 ether, staker, 5, empty); // Using tax rate index 5, which is 18% per year (uint256 shareBefore,,,,) = stakingPool.positions(positionId); // Immediately after staking, no tax due stakingPool.payTax(positionId); // Verify no change in position (uint256 share,,,,) = stakingPool.positions(positionId); assertEq(share, shareBefore, "Share should not change when no tax is due"); // Move time forward 30 days vm.warp(block.timestamp + 30 days); stakingPool.payTax(positionId); vm.stopPrank(); // Check that the tax was paid and position updated (share,,,,) = stakingPool.positions(positionId); uint256 daysElapsed = 30; uint256 taxRate = 18; // Corresponding to 18% annually uint256 daysInYear = 365; uint256 taxBase = 100; uint256 taxFractionForTime = taxRate * daysElapsed * 1 ether / daysInYear / taxBase; uint256 expectedShareAfterTax = (1 ether - taxFractionForTime) * 1_000_000; assertTrue(share < shareBefore, "Share should decrease correctly after tax payment 1"); assertEq(share, expectedShareAfterTax, "Share should decrease correctly after tax payment 2"); } function testLiquidation() public { address staker = makeAddr("staker"); vm.startPrank(liquidityManager); kraiken.mint(10 ether); kraiken.transfer(staker, 1 ether); vm.stopPrank(); vm.startPrank(staker); kraiken.approve(address(stakingPool), 1 ether); uint256[] memory empty; uint256 positionId = stakingPool.snatch(1 ether, staker, 12, empty); // Using tax rate index 5, which is 100% per year vm.warp(block.timestamp + 365 days); // Move time forward to ensure maximum tax due stakingPool.payTax(positionId); vm.stopPrank(); // Verify position is liquidated (uint256 share,,,,) = stakingPool.positions(positionId); assertEq(share, 0, "Share should be zero after liquidation"); } // ── New branch-coverage tests ────────────────────────────────────────────── /// @dev Covers: require(taxRate < TAX_RATES.length) revert in snatch() function testRevert_TaxRateOutOfBounds_InSnatch() public { address staker = makeAddr("staker"); vm.startPrank(liquidityManager); kraiken.mint(5 ether); kraiken.transfer(staker, 1 ether); vm.stopPrank(); vm.startPrank(staker); kraiken.approve(address(stakingPool), 1 ether); uint256[] memory empty; vm.expectRevert(bytes("tax rate out of bounds")); stakingPool.snatch(1 ether, staker, 9999, empty); vm.stopPrank(); } /// @dev Covers: PositionNotFound revert inside the non-last-position loop in snatch() function testRevert_PositionNotFound_NonLastInLoop() public { address staker = makeAddr("staker"); address newStaker = makeAddr("newStaker"); vm.startPrank(liquidityManager); kraiken.mint(10 ether); kraiken.transfer(staker, 1 ether); kraiken.transfer(newStaker, 1 ether); vm.stopPrank(); uint256 realPositionId = doSnatch(staker, 1 ether, 5); // positions[0] is the non-last (loop) position and does not exist vm.startPrank(newStaker); kraiken.approve(address(stakingPool), 1 ether); uint256[] memory positions = new uint256[](2); positions[0] = 9999; // non-existent → triggers PositionNotFound in loop positions[1] = realPositionId; vm.expectRevert(abi.encodeWithSelector(Stake.PositionNotFound.selector, 9999, newStaker)); stakingPool.snatch(1 ether, newStaker, 10, positions); vm.stopPrank(); } /// @dev Covers: TaxTooLow revert inside the non-last-position loop in snatch() function testRevert_TaxTooLow_NonLastInLoop() public { address staker1 = makeAddr("staker1"); address staker2 = makeAddr("staker2"); address newStaker = makeAddr("newStaker"); vm.startPrank(liquidityManager); kraiken.mint(15 ether); kraiken.transfer(staker1, 1 ether); kraiken.transfer(staker2, 1 ether); kraiken.transfer(newStaker, 1 ether); vm.stopPrank(); // staker1 has taxRate 10, staker2 has taxRate 15 uint256 positionId1 = doSnatch(staker1, 1 ether, 10); uint256 positionId2 = doSnatch(staker2, 1 ether, 15); // newStaker uses taxRate 8 < positionId1.taxRate(10) → TaxTooLow in the loop vm.startPrank(newStaker); kraiken.approve(address(stakingPool), 1 ether); uint256[] memory positions = new uint256[](2); positions[0] = positionId1; // non-last: taxRate 10 checked in loop positions[1] = positionId2; // last vm.expectRevert(abi.encodeWithSelector(Stake.TaxTooLow.selector, newStaker, uint64(8), uint64(10), positionId1)); stakingPool.snatch(1 ether, newStaker, 8, positions); vm.stopPrank(); } /// @dev Covers: _exitPosition() branch for the last snatched position /// Triggered when lastSharesNeeded > lastPos.share * 80 / 100 function testSnatch_ExitLastPosition() public { address staker = makeAddr("staker"); address newStaker = makeAddr("newStaker"); // Mint 20 ether so authorizedStake ≈ 4 ether in share-asset terms. // Staker stakes 3.8 ether, leaving 0.2 ether of authorized stake free. // NewStaker wants 3.9 ether → lastSharesNeeded (3.7 ether) > 80% of lastPos (3.04 ether) // → _exitPosition is called on the last (only) position. vm.startPrank(liquidityManager); kraiken.mint(20 ether); kraiken.transfer(staker, 3.8 ether); kraiken.transfer(newStaker, 3.9 ether); vm.stopPrank(); uint256 positionId = doSnatch(staker, 3.8 ether, 3); vm.startPrank(newStaker); kraiken.approve(address(stakingPool), 3.9 ether); uint256[] memory positions = new uint256[](1); positions[0] = positionId; uint256 newPositionId = stakingPool.snatch(3.9 ether, newStaker, 10, positions); vm.stopPrank(); // New position must exist with the right owner (, address owner,,,) = stakingPool.positions(newPositionId); assertEq(owner, newStaker, "New position owner should be newStaker"); // Old position must be fully cleared (uint256 remainingShare,,,,) = stakingPool.positions(positionId); assertEq(remainingShare, 0, "Old position should be fully exited"); } /// @dev Covers: ExceededAvailableStake revert in snatch() when no positions are provided function testRevert_ExceededAvailableStake() public { address staker = makeAddr("staker"); address staker2 = makeAddr("staker2"); // Mint 5 ether → authorizedStake = 1 ether in share terms. // After staking 1 ether the pool is full. vm.startPrank(liquidityManager); kraiken.mint(5 ether); kraiken.transfer(staker, 1 ether); kraiken.transfer(staker2, 0.5 ether); vm.stopPrank(); doSnatch(staker, 1 ether, 1); // fills authorized stake completely vm.startPrank(staker2); kraiken.approve(address(stakingPool), 0.5 ether); uint256[] memory empty; uint256 sharesWanted = stakingPool.assetsToShares(0.5 ether); vm.expectRevert(abi.encodeWithSelector(ExceededAvailableStake.selector, staker2, sharesWanted, 0)); stakingPool.snatch(0.5 ether, staker2, 1, empty); vm.stopPrank(); } /// @dev Covers: TooMuchSnatch revert at the post-snatch check (line 250) /// Two positions are provided; after dissolving the non-last position and /// exiting the last one, the freed stake exceeds the smallest snatched position. function testRevert_TooMuchSnatch_AvailableExceedsNeed() public { address staker1 = makeAddr("staker1"); address staker2 = makeAddr("staker2"); address newStaker = makeAddr("newStaker"); // Mint 20 ether → authorizedStake = 4 ether. // staker1 stakes 0.3 ether (small, becomes non-last), staker2 stakes 3.5 ether (last). // outstanding = 3.8 ether, available = 0.2 ether. // newStaker wants 3.5 ether; provides [staker1pos, staker2pos]. // After dissolving staker1pos: available = 0.5 ether < 3.5 ether → no early TooMuchSnatch. // lastSharesNeeded = 3.5 - 0.5 = 3.0 ether > 80% of 3.5 ether → exit staker2pos. // Post-exit available = 4 ether; 4 - 3.5 = 0.5 > smallestShare(0.3) → TooMuchSnatch. vm.startPrank(liquidityManager); kraiken.mint(20 ether); kraiken.transfer(staker1, 0.3 ether); kraiken.transfer(staker2, 3.5 ether); kraiken.transfer(newStaker, 3.5 ether); vm.stopPrank(); uint256 posId1 = doSnatch(staker1, 0.3 ether, 3); uint256 posId2 = doSnatch(staker2, 3.5 ether, 3); // Compute expected revert values: // After all exits outstandingStake = 0, so availableStake = authorizedStake = totalSupply*20/100 uint256 sharesWanted = stakingPool.assetsToShares(3.5 ether); uint256 authorizedStakeVal = stakingPool.totalSupply() * 20 / 100; uint256 smallestShare = stakingPool.assetsToShares(0.3 ether); // staker1 position (non-last, no tax) vm.startPrank(newStaker); kraiken.approve(address(stakingPool), 3.5 ether); uint256[] memory positions = new uint256[](2); positions[0] = posId1; // non-last: dissolved in loop positions[1] = posId2; // last: exits fully vm.expectRevert(abi.encodeWithSelector(TooMuchSnatch.selector, newStaker, sharesWanted, authorizedStakeVal, smallestShare)); stakingPool.snatch(3.5 ether, newStaker, 10, positions); vm.stopPrank(); } /// @dev Covers: PositionNotFound revert in changeTax() function testRevert_PositionNotFound_InChangeTax() public { address staker = makeAddr("staker"); vm.prank(staker); vm.expectRevert(abi.encodeWithSelector(Stake.PositionNotFound.selector, 9999, staker)); stakingPool.changeTax(9999, 5); } /// @dev Covers: require(taxRate > pos.taxRate, "tax too low to snatch") in changeTax() function testRevert_TaxTooLow_InChangeTax() public { address staker = makeAddr("staker"); vm.startPrank(liquidityManager); kraiken.mint(5 ether); kraiken.transfer(staker, 1 ether); vm.stopPrank(); vm.startPrank(staker); kraiken.approve(address(stakingPool), 1 ether); uint256[] memory empty; uint256 positionId = stakingPool.snatch(1 ether, staker, 5, empty); // Attempting to change to same or lower tax rate must revert vm.expectRevert(bytes("tax too low to snatch")); stakingPool.changeTax(positionId, 5); // same rate vm.expectRevert(bytes("tax too low to snatch")); stakingPool.changeTax(positionId, 3); // lower rate vm.stopPrank(); } /// @dev Covers: NoPermission revert in exitPosition() when called by non-owner function testRevert_NoPermission_InExitPosition() public { address staker = makeAddr("staker"); address notOwner = makeAddr("notOwner"); vm.startPrank(liquidityManager); kraiken.mint(5 ether); kraiken.transfer(staker, 1 ether); vm.stopPrank(); uint256 positionId = doSnatch(staker, 1 ether, 1); vm.prank(notOwner); vm.expectRevert(abi.encodeWithSelector(Stake.NoPermission.selector, notOwner, staker)); stakingPool.exitPosition(positionId); } /// @dev Covers: PositionNotFound revert in payTax() for a non-existent position function testRevert_PositionNotFound_InPayTax() public { address caller = makeAddr("caller"); vm.prank(caller); vm.expectRevert(abi.encodeWithSelector(Stake.PositionNotFound.selector, 9999, caller)); stakingPool.payTax(9999); } /// @dev Covers: permitAndSnatch() — the only uncovered function function testPermitAndSnatch() public { uint256 stakeAmount = 1 ether; (address staker, uint256 stakerKey) = makeAddrAndKey("staker"); vm.startPrank(liquidityManager); kraiken.mint(stakeAmount * 5); kraiken.transfer(staker, stakeAmount); vm.stopPrank(); uint256 deadline = block.timestamp + 1 hours; uint256 nonce = kraiken.nonces(staker); bytes32 permitTypehash = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); bytes32 structHash = keccak256(abi.encode(permitTypehash, staker, address(stakingPool), stakeAmount, nonce, deadline)); bytes32 digest = keccak256(abi.encodePacked("\x19\x01", kraiken.DOMAIN_SEPARATOR(), structHash)); (uint8 v, bytes32 r, bytes32 s) = vm.sign(stakerKey, digest); uint256[] memory empty; vm.prank(staker); uint256 positionId = stakingPool.permitAndSnatch(stakeAmount, staker, 1, empty, deadline, v, r, s); (, address owner,,, uint32 taxRate) = stakingPool.positions(positionId); assertEq(owner, staker, "Position owner should be staker"); assertEq(taxRate, 1, "Tax rate should be 1"); } }