fix(onchain): resolve KRK token supply corruption during recenter (#98)

PROBLEM:
Recenter operations were burning ~137,866 KRK tokens instead of minting
them, causing severe deflation when inflation should occur. This was due
to the liquidity manager burning ALL collected tokens from old positions
and then minting tokens for new positions separately, causing asymmetric
supply adjustments to the staking pool.

ROOT CAUSE:
During recenter():
1. _scrapePositions() collected tokens from old positions and immediately
   burned them ALL (+ proportional staking pool adjustment)
2. _setPositions() minted tokens for new positions (+ proportional
   staking pool adjustment)
3. The burn and mint operations used DIFFERENT totalSupply values in
   their proportion calculations, causing imbalanced adjustments
4. When old positions had more tokens than new positions needed, the net
   result was deflation

WHY THIS HAPPENED:
When KRK price increases (users buying), the same liquidity depth
requires fewer KRK tokens. The old code would:
- Burn 120k KRK from old positions (+ 30k from staking pool)
- Mint 10k KRK for new positions (+ 2.5k to staking pool)
- Net: -137.5k KRK total supply (WRONG!)

FIX:
1. Modified uniswapV3MintCallback() to use existing KRK balance first
   before minting new tokens
2. Removed burn() from _scrapePositions() - keep collected tokens
3. Removed burn() from end of recenter() - don't burn "excess"
4. Tokens held by LiquidityManager are already excluded from
   outstandingSupply(), so they don't affect staking calculations

RESULT:
Now during recenter, only the NET difference is minted or used:
- Collect old positions into LiquidityManager balance
- Use that balance for new positions
- Only mint additional tokens if more are needed
- Keep any unused balance for future recenters
- No more asymmetric burn/mint causing supply corruption

VERIFICATION:
- All 107 existing tests pass
- Added 2 new regression tests in test/SupplyCorruption.t.sol
- testRecenterDoesNotCorruptSupply: verifies single recenter preserves supply
- testMultipleRecentersPreserveSupply: verifies no accumulation over time

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
openhands 2026-02-04 16:20:57 +00:00
parent e5e1308e72
commit d581b8394b
3 changed files with 154 additions and 8 deletions

View file

@ -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");
}
}
}