harb/onchain/src/Kraiken.sol
openhands 85350caf52 feat: OptimizerV3 with direct 2D staking-to-LP parameter mapping
Core protocol changes for launch readiness:

- OptimizerV3: binary bear/bull mapping from (staking%, avgTax) — avoids
  exploitable AW 30-90 kill zone. Bear: AS=30%, AW=100, CI=0, DD=0.3e18.
  Bull: AS=100%, AW=20, CI=0, DD=1e18. UUPS upgradeable with __gap[48].
- Directional VWAP: only records prices on ETH inflow (buys), preventing
  sell-side dilution of price memory
- Floor formula: unified max(scarcity, mirror, clamp) — VWAP mirror uses
  distance from adjusted VWAP as floor distance, no branching
- PriceOracle (M-1 fix): correct fallback TWAP divisor (60000s, not 300s)
- Access control (M-2 fix): deployer-only guard on one-time setters
- Recenter rate limit (M-3 fix): 60-second cooldown for open recenters
- Safe fallback params: recenter() optimizer-failure defaults changed from
  exploitable CI=50%/AW=50 to safe bear-mode CI=0/AW=100
- Recentered event for monitoring and indexing
- VERSION bump to 2, kraiken-lib COMPATIBLE_CONTRACT_VERSIONS updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:21:18 +00:00

158 lines
6.6 KiB
Solidity

// SPDX-License-Identifier: GPL-3.0-or-later
pragma solidity ^0.8.19;
import { ERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
import { ERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
import { Math } from "@openzeppelin/utils/math/Math.sol";
/**
* @title stakeable ERC20 Token
* @notice This contract implements an ERC20 token with mechanisms for minting and burning in which a single account (staking Pool) is proportionally receiving a share. Only the liquidity manager has permission to manage token supply.
* @dev Key features:
* - Controlled minting exclusively by LiquidityManager
* - Tax collection and redistribution mechanism through staking pool
* - 20% supply cap for staking (20,000 positions max)
* - Staking pool receives proportional share of all mints/burns
*/
contract Kraiken is ERC20, ERC20Permit {
using Math for uint256;
/**
* @notice Protocol version for data structure compatibility.
* Increment when making breaking changes to TAX_RATES, events, or core data structures.
* Indexers and frontends validate against this to ensure sync.
*
* Version History:
* - v1: Initial deployment with 30-tier TAX_RATES
* - v2: OptimizerV3, VWAP mirror floor, directional VWAP recording
*/
uint256 public constant VERSION = 2;
// Minimum fraction of the total supply required for staking to prevent fragmentation of staking positions
uint256 private constant MIN_STAKE_FRACTION = 3000;
// Address authorized to call one-time setters (prevents frontrunning)
address private immutable deployer;
// Address of the liquidity manager
address private liquidityManager;
// Address of the staking pool
address private stakingPool;
// Previous total supply for staking calculations
uint256 public previousTotalSupply;
// Custom errors
error ZeroAddressInSetter();
error AddressAlreadySet();
// Modifier to restrict access to the liquidity manager
modifier onlyLiquidityManager() {
require(msg.sender == address(liquidityManager), "only liquidity manager");
_;
}
/**
* @notice Constructor for the Kraiken token
* @param name_ The name of the token
* @param symbol_ The symbol of the token
*/
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) ERC20Permit(name_) {
deployer = msg.sender;
}
/**
* @notice Sets the address for the liquidityManager. Used once post-deployment to initialize the contract.
* @dev Should be called only once right after the contract deployment to set the liquidity manager address.
* Throws AddressAlreadySet if called more than once.
* @param liquidityManager_ The address of the liquidity manager.
*/
function setLiquidityManager(address liquidityManager_) external {
require(msg.sender == deployer, "only deployer");
if (address(0) == liquidityManager_) revert ZeroAddressInSetter();
if (liquidityManager != address(0)) revert AddressAlreadySet();
liquidityManager = liquidityManager_;
}
/**
* @notice Sets the address for the stakingPool. Used once post-deployment to initialize the contract.
* @dev Should be called only once right after the contract deployment to set the staking pool address.
* Throws AddressAlreadySet if called more than once.
* @param stakingPool_ The address of the staking pool.
*/
function setStakingPool(address stakingPool_) external {
require(msg.sender == deployer, "only deployer");
if (address(0) == stakingPool_) revert ZeroAddressInSetter();
if (stakingPool != address(0)) revert AddressAlreadySet();
stakingPool = stakingPool_;
}
/**
* @notice Returns the addresses of the periphery contracts
* @return The addresses of the TWAB controller, liquidity manager, staking pool, and liquidity pool
*/
function peripheryContracts() external view returns (address, address) {
return (liquidityManager, stakingPool);
}
/**
* @notice Calculates the minimum stake based on the previous total supply
* @return The minimum stake amount
*/
function minStake() external view returns (uint256) {
return previousTotalSupply / MIN_STAKE_FRACTION;
}
/**
* @notice Allows the liquidity manager to mint tokens for itself.
* @dev Tokens minted are managed as community liquidity in the Uniswap pool to stabilize KRAIKEN prices.
* Only callable by the Liquidity Manager. Minting rules and limits are defined externally.
* @param _amount The number of tokens to mint.
*/
function mint(uint256 _amount) external onlyLiquidityManager {
if (_amount > 0) {
// make sure staking pool grows proportional to economy
uint256 stakingPoolBalance = balanceOf(stakingPool);
if (stakingPoolBalance > 0) {
uint256 newStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance);
_mint(stakingPool, newStake);
}
_mint(address(liquidityManager), _amount);
}
if (previousTotalSupply == 0) {
previousTotalSupply = totalSupply();
}
}
/**
* @notice Allows the liquidity manager to burn tokens from its account, adjusting the staking pool accordingly.
* @dev When tokens are burned, the total supply shrinks, making excess tokens in the staking pool unnecessary.
* These excess tokens are burned to maintain the guaranteed fixed percentage of the total supply for stakers.
* @param _amount The number of tokens to burn.
*/
function burn(uint256 _amount) external onlyLiquidityManager {
if (_amount > 0) {
// shrink staking pool proportional to economy
uint256 stakingPoolBalance = balanceOf(stakingPool);
if (stakingPoolBalance > 0) {
uint256 excessStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance);
_burn(stakingPool, excessStake);
}
_burn(address(liquidityManager), _amount);
}
}
/**
* @notice Sets the previous total supply
* @param _ts The previous total supply value
*/
function setPreviousTotalSupply(uint256 _ts) external onlyLiquidityManager {
previousTotalSupply = _ts;
}
/**
* @notice Returns the outstanding supply, excluding the balances of the liquidity pool and liquidity manager
* @return The outstanding supply
*/
function outstandingSupply() public view returns (uint256) {
return totalSupply() - balanceOf(liquidityManager);
}
}