diff --git a/onchain/deployments-local.json b/onchain/deployments-local.json index c9e40aa..6fc4bfd 100644 --- a/onchain/deployments-local.json +++ b/onchain/deployments-local.json @@ -1,7 +1,7 @@ { "contracts": { - "Kraiken": "0xe527ddac2592faa45884a0b78e4d377a5d3df8cc", - "Stake": "0x935b78d1862de1ff6504f338752a32e1c0211920", - "LiquidityManager": "0xa887973a2ec1a3b4c7d50b84306ebcbc21bf2d5a" + "Kraiken": "0xff196f1e3a895404d073b8611252cf97388773a7", + "Stake": "0xc36e784e1dff616bdae4eac7b310f0934faf04a4", + "LiquidityManager": "0x33d10f2449ffede92b43d4fba562f132ba6a766a" } } diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index 9ab8434..32b6a13 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -76,9 +76,12 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { CallbackValidation.verifyCallback(factory, poolKey); - // Handle KRAIKEN minting + // Handle KRAIKEN minting - use existing balance first, then mint only the difference uint256 kraikenPulled = token0isWeth ? amount1Owed : amount0Owed; - kraiken.mint(kraikenPulled); + uint256 kraikenBalance = kraiken.balanceOf(address(this)); + if (kraikenBalance < kraikenPulled) { + kraiken.mint(kraikenPulled - kraikenBalance); + } // Handle WETH conversion uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed; @@ -211,9 +214,6 @@ contract LiquidityManager is ThreePositionStrategy, PriceOracle { _recordVolumeAndPrice(currentPrice, fee1); } } - - // Burn any remaining KRAIKEN tokens - kraiken.burn(kraiken.balanceOf(address(this))); } /// @notice Allow contract to receive ETH diff --git a/onchain/test/SupplyCorruption.t.sol b/onchain/test/SupplyCorruption.t.sol new file mode 100644 index 0000000..113ebb2 --- /dev/null +++ b/onchain/test/SupplyCorruption.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +/** + * @title Supply Corruption Test + * @notice Regression test for issue #98: KRK Token Supply Corruption During Recenter Operations + * @dev Verifies that recenter() correctly mints tokens when liquidity increases, preventing deflation + */ +import { Kraiken } from "../src/Kraiken.sol"; +import "../src/interfaces/IWETH9.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; +import "forge-std/Test.sol"; + +import { LiquidityManager } from "../src/LiquidityManager.sol"; +import { Optimizer } from "../src/Optimizer.sol"; +import { Stake } from "../src/Stake.sol"; +import { TestEnvironment } from "./helpers/TestBase.sol"; +import { UniSwapHelper } from "./helpers/UniswapTestBase.sol"; + +contract SupplyCorruptionTest is UniSwapHelper { + address constant RECENTER_CALLER = address(0x7777); + address feeDestination = makeAddr("fees"); + + IUniswapV3Factory factory; + Stake stake; + LiquidityManager lm; + Optimizer optimizer; + TestEnvironment testEnv; + + function setUp() public { + testEnv = new TestEnvironment(feeDestination); + + ( + IUniswapV3Factory _factory, + IUniswapV3Pool _pool, + IWETH9 _weth, + Kraiken _harberg, + Stake _stake, + LiquidityManager _lm, + Optimizer _optimizer, + bool _token0isWeth + ) = testEnv.setupEnvironment(false, RECENTER_CALLER); + + factory = _factory; + pool = _pool; + weth = _weth; + harberg = _harberg; + stake = _stake; + lm = _lm; + optimizer = _optimizer; + token0isWeth = _token0isWeth; + } + + /** + * @notice Test that recenter does not cause supply corruption + * @dev Reproduces the bug scenario: after buying tokens and calling recenter, + * totalSupply should increase or stay the same, never decrease + */ + function testRecenterDoesNotCorruptSupply() public { + // Fund liquidity manager with ETH + vm.deal(address(lm), 10 ether); + + // Initial recenter to set up positions + vm.prank(RECENTER_CALLER); + lm.recenter(); + + // Record initial state + uint256 initialTotalSupply = harberg.totalSupply(); + (address liquidityManagerAddr, address stakingPool) = harberg.peripheryContracts(); + uint256 initialStakingBalance = harberg.balanceOf(stakingPool); + + assertGt(initialTotalSupply, 0, "Initial total supply should be positive"); + console.log("Initial totalSupply:", initialTotalSupply); + console.log("Initial staking balance:", initialStakingBalance); + + // Simulate user buying tokens (price movement) + vm.deal(account, 5 ether); + vm.prank(account); + weth.deposit{ value: 5 ether }(); + + // Perform swap to move price + performSwap(5 ether, true); + + console.log("Performed 5 ETH swap to move price"); + + // Call recenter + vm.prank(RECENTER_CALLER); + lm.recenter(); + + // Check final state + uint256 finalTotalSupply = harberg.totalSupply(); + uint256 finalStakingBalance = harberg.balanceOf(stakingPool); + + console.log("Final totalSupply:", finalTotalSupply); + console.log("Final staking balance:", finalStakingBalance); + + // CRITICAL ASSERTION: Total supply should not decrease + assertGe(finalTotalSupply, initialTotalSupply, "BUG #98: Total supply should not decrease during recenter"); + + // Staking pool ratio should be maintained (within 1% tolerance for rounding) + if (initialTotalSupply > 0) { + uint256 initialRatio = (initialStakingBalance * 10_000) / initialTotalSupply; + uint256 finalRatio = (finalStakingBalance * 10_000) / finalTotalSupply; + uint256 ratioDiff = finalRatio > initialRatio ? finalRatio - initialRatio : initialRatio - finalRatio; + + assertLt( + ratioDiff, + 100, // 1% tolerance (100 out of 10000) + "Staking pool ratio should be maintained" + ); + } + } + + /** + * @notice Test multiple recenter operations don't accumulate supply corruption + */ + function testMultipleRecentersPreserveSupply() public { + vm.deal(address(lm), 20 ether); + + // Initial recenter + vm.prank(RECENTER_CALLER); + lm.recenter(); + + uint256 initialTotalSupply = harberg.totalSupply(); + console.log("Initial supply:", initialTotalSupply); + + // Perform multiple recenter cycles + for (uint256 i = 0; i < 3; i++) { + // Swap to move price + vm.deal(account, 2 ether); + vm.prank(account); + weth.deposit{ value: 2 ether }(); + + performSwap(2 ether, true); + + vm.prank(RECENTER_CALLER); + lm.recenter(); + + uint256 currentSupply = harberg.totalSupply(); + console.log("Supply after recenter", i + 1, ":", currentSupply); + + assertGe(currentSupply, initialTotalSupply, "Supply should not decrease across multiple recenters"); + } + } +}