Update the embedded _scrapePositions definition to accept (bool recordVWAP, int24 currentTick), compute currentPrice directly from the passed tick instead of sampling the ANCHOR position's centre tick, remove the ANCHOR-specific price-sampling branch from the loop, and replace the old split fee+VWAP transfer logic with the current contract's structure: feeDestination != address(this) guard before transfers, single ethFee branch for VWAP recording. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
852 lines
35 KiB
Vue
852 lines
35 KiB
Vue
<template>
|
|
<div>
|
|
<h1 id="first">Source Code</h1>
|
|
<p>
|
|
The Kraiken protocol is built on four core Solidity contracts deployed on Base.
|
|
Every contract is immutable (except the upgradeable Optimizer) — what you see here is what runs on-chain.
|
|
</p>
|
|
<p class="warning">Full source and verification will be published at mainnet launch.</p>
|
|
|
|
<div class="code-addresses" v-if="krkAddress || stakeAddress">
|
|
<h2 id="contracts">Contract Addresses</h2>
|
|
<div class="code-address-list">
|
|
<div v-if="krkAddress" class="code-address-item">
|
|
<span class="code-address-label">KRK Token</span>
|
|
<a :href="`https://basescan.org/address/${krkAddress}`" target="_blank" rel="noopener noreferrer">{{ krkAddress }}</a>
|
|
</div>
|
|
<div v-if="stakeAddress" class="code-address-item">
|
|
<span class="code-address-label">Stake</span>
|
|
<a :href="`https://basescan.org/address/${stakeAddress}`" target="_blank" rel="noopener noreferrer">{{ stakeAddress }}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h2 id="kraiken">Kraiken.sol</h2>
|
|
<p>
|
|
ERC20 token with controlled minting/burning exclusively by the LiquidityManager.
|
|
The staking pool grows proportionally with every mint — 20% of supply is reserved for staking positions.
|
|
</p>
|
|
<div class="code-block">
|
|
<pre><code v-html="highlightSolidity(kraikenSol)"></code></pre>
|
|
</div>
|
|
|
|
<h2 id="liquidity-manager">LiquidityManager.sol</h2>
|
|
<p>
|
|
Manages the three-position anti-arbitrage strategy on Uniswap V3 (1% fee tier).
|
|
Handles FLOOR, ANCHOR, and DISCOVERY positions with dynamic parameter adjustment via the Optimizer.
|
|
Uses 5-minute TWAP with 50-tick tolerance to prevent oracle manipulation.
|
|
</p>
|
|
<div class="code-block">
|
|
<pre><code v-html="highlightSolidity(liquidityManagerSol)"></code></pre>
|
|
</div>
|
|
|
|
<h2 id="stake">Stake.sol</h2>
|
|
<p>
|
|
Harberger tax-based staking with self-assessed valuations. 30-tier discrete tax rates prevent micro-increment snatching.
|
|
Stakers set their own tax rate — higher rates protect against buyouts but cost more. Tax revenue flows back as UBI.
|
|
</p>
|
|
<div class="code-block">
|
|
<pre><code v-html="highlightSolidity(stakeSol)"></code></pre>
|
|
</div>
|
|
|
|
<h2 id="optimizer">OptimizerV3.sol</h2>
|
|
<p>
|
|
Binary bear/bull liquidity optimizer using a direct 2D mapping from (staking%, avgTax) to configuration.
|
|
Bull requires >91% staked with low tax — any decline snaps instantly to bear mode.
|
|
UUPS upgradeable proxy pattern allows parameter tuning without redeploying the core contracts.
|
|
</p>
|
|
<div class="code-block">
|
|
<pre><code v-html="highlightSolidity(optimizerSol)"></code></pre>
|
|
</div>
|
|
|
|
<p>
|
|
View the full repository on <a href="https://codeberg.org/johba/harb" target="_blank" rel="noopener noreferrer">Codeberg</a>.
|
|
</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted } from 'vue';
|
|
|
|
interface CodeDocsEmits {
|
|
(event: 'onmounted'): void;
|
|
}
|
|
|
|
const emit = defineEmits<CodeDocsEmits>();
|
|
|
|
onMounted(() => {
|
|
emit('onmounted');
|
|
});
|
|
|
|
const krkAddress = import.meta.env.VITE_KRAIKEN_ADDRESS || '';
|
|
const stakeAddress = import.meta.env.VITE_STAKE_ADDRESS || '';
|
|
|
|
function highlightSolidity(code: string): string {
|
|
// Escape HTML first
|
|
let html = code
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
|
|
// Comments (single-line and multi-line)
|
|
html = html.replace(/(\/\/[^\n]*)/g, '<span class="sol-comment">$1</span>');
|
|
html = html.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="sol-comment">$1</span>');
|
|
|
|
// Strings
|
|
html = html.replace(/("[^&]*?"|"[^"]*?")/g, '<span class="sol-string">$1</span>');
|
|
|
|
// Numbers
|
|
html = html.replace(/\b(\d[\d_]*(?:e\d+)?)\b/g, '<span class="sol-number">$1</span>');
|
|
|
|
// Keywords
|
|
const keywords = [
|
|
'pragma', 'solidity', 'import', 'contract', 'interface', 'library', 'abstract',
|
|
'function', 'modifier', 'event', 'error', 'struct', 'enum', 'mapping',
|
|
'public', 'private', 'internal', 'external', 'view', 'pure', 'payable',
|
|
'returns', 'return', 'if', 'else', 'for', 'while', 'do', 'break', 'continue',
|
|
'require', 'revert', 'emit', 'new', 'delete', 'try', 'catch',
|
|
'memory', 'storage', 'calldata', 'immutable', 'constant', 'override', 'virtual',
|
|
'is', 'using', 'constructor', 'receive', 'fallback',
|
|
];
|
|
const kwPattern = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g');
|
|
html = html.replace(kwPattern, '<span class="sol-keyword">$1</span>');
|
|
|
|
// Types
|
|
const types = [
|
|
'address', 'bool', 'string', 'bytes', 'bytes32',
|
|
'uint256', 'uint128', 'uint64', 'uint32', 'uint24', 'uint8',
|
|
'int256', 'int24',
|
|
];
|
|
const typePattern = new RegExp(`\\b(${types.join('|')})\\b`, 'g');
|
|
html = html.replace(typePattern, '<span class="sol-type">$1</span>');
|
|
|
|
return html;
|
|
}
|
|
|
|
const kraikenSol = `// 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;
|
|
|
|
uint256 public constant VERSION = 2;
|
|
|
|
uint256 private constant MIN_STAKE_FRACTION = 3000;
|
|
address private immutable deployer;
|
|
address private liquidityManager;
|
|
address private stakingPool;
|
|
|
|
uint256 public previousTotalSupply;
|
|
|
|
error ZeroAddressInSetter();
|
|
error AddressAlreadySet();
|
|
|
|
modifier onlyLiquidityManager() {
|
|
require(msg.sender == address(liquidityManager), "only liquidity manager");
|
|
_;
|
|
}
|
|
|
|
constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_) ERC20Permit(name_) {
|
|
deployer = msg.sender;
|
|
}
|
|
|
|
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_;
|
|
}
|
|
|
|
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_;
|
|
}
|
|
|
|
function peripheryContracts() external view returns (address, address) {
|
|
return (liquidityManager, stakingPool);
|
|
}
|
|
|
|
function minStake() external view returns (uint256) {
|
|
return previousTotalSupply / MIN_STAKE_FRACTION;
|
|
}
|
|
|
|
function mint(uint256 _amount) external onlyLiquidityManager {
|
|
if (_amount > 0) {
|
|
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();
|
|
}
|
|
}
|
|
|
|
function burn(uint256 _amount) external onlyLiquidityManager {
|
|
if (_amount > 0) {
|
|
uint256 stakingPoolBalance = balanceOf(stakingPool);
|
|
if (stakingPoolBalance > 0) {
|
|
uint256 excessStake = stakingPoolBalance * _amount / (totalSupply() - stakingPoolBalance);
|
|
_burn(stakingPool, excessStake);
|
|
}
|
|
_burn(address(liquidityManager), _amount);
|
|
}
|
|
}
|
|
|
|
function setPreviousTotalSupply(uint256 _ts) external onlyLiquidityManager {
|
|
previousTotalSupply = _ts;
|
|
}
|
|
|
|
function outstandingSupply() public view returns (uint256) {
|
|
return totalSupply() - balanceOf(liquidityManager);
|
|
}
|
|
}`;
|
|
|
|
const liquidityManagerSol = `// SPDX-License-Identifier: GPL-3.0-or-later
|
|
pragma solidity ^0.8.19;
|
|
|
|
import { Kraiken } from "./Kraiken.sol";
|
|
import { Optimizer } from "./Optimizer.sol";
|
|
import { PriceOracle } from "./abstracts/PriceOracle.sol";
|
|
import { ThreePositionStrategy } from "./abstracts/ThreePositionStrategy.sol";
|
|
import { IWETH9 } from "./interfaces/IWETH9.sol";
|
|
import { CallbackValidation } from "@aperture/uni-v3-lib/CallbackValidation.sol";
|
|
import { PoolAddress, PoolKey } from "@aperture/uni-v3-lib/PoolAddress.sol";
|
|
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";
|
|
import { SafeERC20 } from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
|
|
import { IUniswapV3Pool } from "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
|
import { PositionKey } from "@uniswap-v3-periphery/libraries/PositionKey.sol";
|
|
|
|
/**
|
|
* @title LiquidityManager
|
|
* @notice Manages liquidity provisioning on Uniswap V3 using the three-position anti-arbitrage strategy
|
|
* @dev Inherits from modular contracts for better separation of concerns and testability
|
|
*
|
|
* Key features:
|
|
* - Three-position anti-arbitrage strategy (ANCHOR, DISCOVERY, FLOOR)
|
|
* - Dynamic parameter adjustment via Optimizer contract
|
|
* - Asymmetric slippage profile prevents profitable arbitrage
|
|
* - Exclusive minting rights for KRAIKEN token
|
|
*
|
|
* Price Validation:
|
|
* - 5-minute TWAP with 50-tick tolerance
|
|
* - Prevents oracle manipulation attacks
|
|
*/
|
|
contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|
using SafeERC20 for IERC20;
|
|
|
|
uint24 internal constant FEE = uint24(10_000);
|
|
|
|
address private immutable factory;
|
|
IWETH9 private immutable weth;
|
|
Kraiken private immutable kraiken;
|
|
Optimizer private immutable optimizer;
|
|
IUniswapV3Pool private immutable pool;
|
|
bool private immutable token0isWeth;
|
|
PoolKey private poolKey;
|
|
|
|
address private immutable deployer;
|
|
address public feeDestination;
|
|
|
|
int24 public lastRecenterTick;
|
|
uint256 public lastRecenterTime;
|
|
uint256 internal constant MIN_RECENTER_INTERVAL = 60;
|
|
|
|
event Recentered(int24 indexed currentTick, bool indexed isUp);
|
|
|
|
error ZeroAddressInSetter();
|
|
error AddressAlreadySet();
|
|
|
|
constructor(address _factory, address _WETH9, address _kraiken, address _optimizer) {
|
|
deployer = msg.sender;
|
|
factory = _factory;
|
|
weth = IWETH9(_WETH9);
|
|
poolKey = PoolAddress.getPoolKey(_WETH9, _kraiken, FEE);
|
|
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
|
|
kraiken = Kraiken(_kraiken);
|
|
token0isWeth = _WETH9 < _kraiken;
|
|
optimizer = Optimizer(_optimizer);
|
|
}
|
|
|
|
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
|
|
CallbackValidation.verifyCallback(factory, poolKey);
|
|
uint256 kraikenPulled = token0isWeth ? amount1Owed : amount0Owed;
|
|
uint256 kraikenBalance = kraiken.balanceOf(address(this));
|
|
if (kraikenBalance < kraikenPulled) {
|
|
kraiken.mint(kraikenPulled - kraikenBalance);
|
|
}
|
|
uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed;
|
|
if (weth.balanceOf(address(this)) < ethOwed) {
|
|
weth.deposit{ value: address(this).balance }();
|
|
}
|
|
if (amount0Owed > 0) IERC20(poolKey.token0).safeTransfer(msg.sender, amount0Owed);
|
|
if (amount1Owed > 0) IERC20(poolKey.token1).safeTransfer(msg.sender, amount1Owed);
|
|
}
|
|
|
|
function setFeeDestination(address feeDestination_) external {
|
|
require(msg.sender == deployer, "only deployer");
|
|
if (address(0) == feeDestination_) revert ZeroAddressInSetter();
|
|
if (feeDestination != address(0)) revert AddressAlreadySet();
|
|
feeDestination = feeDestination_;
|
|
}
|
|
|
|
function recenter() external returns (bool isUp) {
|
|
(, int24 currentTick,,,,,) = pool.slot0();
|
|
|
|
// Always enforce cooldown and TWAP price stability — no bypass path
|
|
require(block.timestamp >= lastRecenterTime + MIN_RECENTER_INTERVAL, "recenter cooldown");
|
|
require(_isPriceStable(currentTick), "price deviated from oracle");
|
|
lastRecenterTime = block.timestamp;
|
|
|
|
isUp = false;
|
|
if (positions[Stage.ANCHOR].liquidity > 0) {
|
|
int24 anchorTickLower = positions[Stage.ANCHOR].tickLower;
|
|
int24 anchorTickUpper = positions[Stage.ANCHOR].tickUpper;
|
|
int24 centerTick = anchorTickLower + (anchorTickUpper - anchorTickLower) / 2;
|
|
bool isEnough;
|
|
(isUp, isEnough) = _validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
|
|
require(isEnough, "amplitude not reached.");
|
|
}
|
|
|
|
bool shouldRecordVWAP;
|
|
if (cumulativeVolume == 0) {
|
|
shouldRecordVWAP = true;
|
|
} else {
|
|
shouldRecordVWAP = token0isWeth ? (currentTick > lastRecenterTick) : (currentTick < lastRecenterTick);
|
|
}
|
|
lastRecenterTick = currentTick;
|
|
|
|
_scrapePositions(shouldRecordVWAP, currentTick);
|
|
|
|
if (isUp) {
|
|
kraiken.setPreviousTotalSupply(kraiken.totalSupply());
|
|
}
|
|
|
|
try optimizer.getLiquidityParams() returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
|
|
PositionParams memory params = PositionParams({
|
|
capitalInefficiency: (capitalInefficiency > 10 ** 18) ? 10 ** 18 : capitalInefficiency,
|
|
anchorShare: (anchorShare > 10 ** 18) ? 10 ** 18 : anchorShare,
|
|
anchorWidth: (anchorWidth > 100) ? 100 : anchorWidth,
|
|
discoveryDepth: (discoveryDepth > 10 ** 18) ? 10 ** 18 : discoveryDepth
|
|
});
|
|
_setPositions(currentTick, params);
|
|
} catch {
|
|
PositionParams memory defaultParams = PositionParams({
|
|
capitalInefficiency: 0,
|
|
anchorShare: 3e17,
|
|
anchorWidth: 100,
|
|
discoveryDepth: 3e17
|
|
});
|
|
_setPositions(currentTick, defaultParams);
|
|
}
|
|
|
|
emit Recentered(currentTick, isUp);
|
|
}
|
|
|
|
function _scrapePositions(bool recordVWAP, int24 currentTick) internal {
|
|
uint256 fee0 = 0;
|
|
uint256 fee1 = 0;
|
|
uint256 currentPrice = _priceAtTick(token0isWeth ? -1 * currentTick : currentTick);
|
|
|
|
for (uint256 i = uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) {
|
|
TokenPosition storage position = positions[Stage(i)];
|
|
if (position.liquidity > 0) {
|
|
(uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity);
|
|
(uint256 collected0, uint256 collected1) =
|
|
pool.collect(address(this), position.tickLower, position.tickUpper, type(uint128).max, type(uint128).max);
|
|
fee0 += collected0 - amount0;
|
|
fee1 += collected1 - amount1;
|
|
}
|
|
}
|
|
|
|
if (feeDestination != address(this)) {
|
|
if (fee0 > 0) {
|
|
if (token0isWeth) {
|
|
IERC20(address(weth)).safeTransfer(feeDestination, fee0);
|
|
} else {
|
|
IERC20(address(kraiken)).safeTransfer(feeDestination, fee0);
|
|
}
|
|
}
|
|
if (fee1 > 0) {
|
|
if (token0isWeth) {
|
|
IERC20(address(kraiken)).safeTransfer(feeDestination, fee1);
|
|
} else {
|
|
IERC20(address(weth)).safeTransfer(feeDestination, fee1);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (recordVWAP) {
|
|
uint256 ethFee = token0isWeth ? fee0 : fee1;
|
|
if (ethFee > 0) _recordVolumeAndPrice(currentPrice, ethFee);
|
|
}
|
|
}
|
|
|
|
receive() external payable { }
|
|
|
|
function _getPool() internal view override returns (IUniswapV3Pool) { return pool; }
|
|
function _getKraikenToken() internal view override returns (address) { return address(kraiken); }
|
|
function _getWethToken() internal view override returns (address) { return address(weth); }
|
|
function _isToken0Weth() internal view override returns (bool) { return token0isWeth; }
|
|
|
|
function _mintPosition(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal override {
|
|
pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey));
|
|
positions[stage] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper });
|
|
}
|
|
|
|
function _getEthBalance() internal view override returns (uint256) {
|
|
return address(this).balance + weth.balanceOf(address(this));
|
|
}
|
|
|
|
function _getOutstandingSupply() internal view override returns (uint256) {
|
|
uint256 supply = kraiken.outstandingSupply();
|
|
if (feeDestination != address(0)) {
|
|
supply -= kraiken.balanceOf(feeDestination);
|
|
}
|
|
(, address stakingPoolAddr) = kraiken.peripheryContracts();
|
|
if (stakingPoolAddr != address(0)) {
|
|
supply -= kraiken.balanceOf(stakingPoolAddr);
|
|
}
|
|
return supply;
|
|
}
|
|
}`;
|
|
|
|
const stakeSol = `// SPDX-License-Identifier: GPL-3.0-or-later
|
|
pragma solidity ^0.8.19;
|
|
|
|
import { Kraiken } from "./Kraiken.sol";
|
|
import { IERC20 } from "@openzeppelin/token/ERC20/ERC20.sol";
|
|
import { ERC20Permit } from "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol";
|
|
import { IERC20Metadata } from "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol";
|
|
import { SafeERC20 } from "@openzeppelin/token/ERC20/utils/SafeERC20.sol";
|
|
import { Math } from "@openzeppelin/utils/math/Math.sol";
|
|
|
|
error ExceededAvailableStake(address receiver, uint256 stakeWanted, uint256 availableStake);
|
|
error TooMuchSnatch(address receiver, uint256 stakeWanted, uint256 availableStake, uint256 smallestShare);
|
|
|
|
/**
|
|
* @title Stake Contract for Kraiken Token
|
|
* @notice Manages staking positions with Harberger tax. Stakers set and adjust tax rates,
|
|
* which affect UBI paid from the tax pool. Handles position creation, snatching,
|
|
* tax calculation, rate adjustment, and exit.
|
|
*/
|
|
contract Stake {
|
|
using Math for uint256;
|
|
|
|
uint256 internal DECIMAL_OFFSET = 5 + 2;
|
|
uint256 internal constant MAX_STAKE = 20;
|
|
uint256 internal constant TAX_FLOOR_DURATION = 60 * 60 * 24 * 3;
|
|
uint256[] public TAX_RATES =
|
|
[1, 3, 5, 8, 12, 18, 24, 30, 40, 50, 60, 80, 100, 130, 180, 250, 320, 420, 540, 700, 920, 1200, 1600, 2000, 2600, 3400, 4400, 5700, 7500, 9700];
|
|
uint256 internal constant TAX_RATE_BASE = 100;
|
|
|
|
error TaxTooLow(address receiver, uint64 taxRateWanted, uint64 taxRateMet, uint256 positionId);
|
|
error StakeTooLow(address receiver, uint256 assets, uint256 minStake);
|
|
error NoPermission(address requester, address owner);
|
|
error PositionNotFound(uint256 positionId, address requester);
|
|
|
|
event PositionCreated(uint256 indexed positionId, address indexed owner, uint256 kraikenDeposit, uint256 share, uint32 taxRate);
|
|
event PositionTaxPaid(uint256 indexed positionId, address indexed owner, uint256 taxPaid, uint256 newShares, uint256 taxRate);
|
|
event PositionRateHiked(uint256 indexed positionId, address indexed owner, uint256 newTaxRate);
|
|
event PositionShrunk(uint256 indexed positionId, address indexed owner, uint256 newShares, uint256 kraikenPayout);
|
|
event PositionRemoved(uint256 indexed positionId, address indexed owner, uint256 kraikenPayout);
|
|
|
|
struct StakingPosition {
|
|
uint256 share;
|
|
address owner;
|
|
uint32 creationTime;
|
|
uint32 lastTaxTime;
|
|
uint32 taxRate;
|
|
}
|
|
|
|
Kraiken private immutable kraiken;
|
|
address private immutable taxReceiver;
|
|
|
|
uint256 public immutable totalSupply;
|
|
uint256 public outstandingStake;
|
|
uint256 public nextPositionId;
|
|
|
|
mapping(uint256 => StakingPosition) public positions;
|
|
uint256[] public totalSharesAtTaxRate;
|
|
|
|
constructor(address _kraiken, address _taxReceiver) {
|
|
kraiken = Kraiken(_kraiken);
|
|
taxReceiver = _taxReceiver;
|
|
totalSupply = 10 ** (kraiken.decimals() + DECIMAL_OFFSET);
|
|
nextPositionId = 654_321;
|
|
totalSharesAtTaxRate = new uint256[](TAX_RATES.length);
|
|
}
|
|
|
|
function authorizedStake() private view returns (uint256) {
|
|
return totalSupply * MAX_STAKE / 100;
|
|
}
|
|
|
|
function _payTax(uint256 positionId, StakingPosition storage pos, uint256 taxFloorDuration) private {
|
|
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp;
|
|
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
|
uint256 assetsBefore = sharesToAssets(pos.share);
|
|
uint256 taxAmountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
|
if (taxAmountDue >= assetsBefore) {
|
|
taxAmountDue = assetsBefore;
|
|
}
|
|
if (assetsBefore - taxAmountDue > 0) {
|
|
uint256 shareAfterTax = assetsToShares(assetsBefore - taxAmountDue);
|
|
uint256 deltaShare = pos.share - shareAfterTax;
|
|
totalSharesAtTaxRate[pos.taxRate] -= deltaShare;
|
|
outstandingStake -= deltaShare;
|
|
pos.share = shareAfterTax;
|
|
pos.lastTaxTime = uint32(block.timestamp);
|
|
emit PositionTaxPaid(positionId, pos.owner, taxAmountDue, shareAfterTax, pos.taxRate);
|
|
} else {
|
|
totalSharesAtTaxRate[pos.taxRate] -= pos.share;
|
|
outstandingStake -= pos.share;
|
|
emit PositionTaxPaid(positionId, pos.owner, taxAmountDue, 0, pos.taxRate);
|
|
emit PositionRemoved(positionId, pos.owner, 0);
|
|
delete pos.owner;
|
|
delete pos.creationTime;
|
|
delete pos.share;
|
|
}
|
|
SafeERC20.safeTransfer(kraiken, taxReceiver, taxAmountDue);
|
|
}
|
|
|
|
function _exitPosition(uint256 positionId, StakingPosition storage pos) private {
|
|
totalSharesAtTaxRate[pos.taxRate] -= pos.share;
|
|
outstandingStake -= pos.share;
|
|
address owner = pos.owner;
|
|
uint256 assets = sharesToAssets(pos.share);
|
|
emit PositionRemoved(positionId, owner, assets);
|
|
delete pos.owner;
|
|
delete pos.creationTime;
|
|
delete pos.share;
|
|
SafeERC20.safeTransfer(kraiken, owner, assets);
|
|
}
|
|
|
|
function _shrinkPosition(uint256 positionId, StakingPosition storage pos, uint256 sharesToTake) private {
|
|
require(sharesToTake < pos.share, "position too small");
|
|
uint256 assets = sharesToAssets(sharesToTake);
|
|
pos.share -= sharesToTake;
|
|
totalSharesAtTaxRate[pos.taxRate] -= sharesToTake;
|
|
outstandingStake -= sharesToTake;
|
|
emit PositionShrunk(positionId, pos.owner, pos.share, assets);
|
|
SafeERC20.safeTransfer(kraiken, pos.owner, assets);
|
|
}
|
|
|
|
function assetsToShares(uint256 assets) public view returns (uint256) {
|
|
return assets.mulDiv(totalSupply, kraiken.totalSupply(), Math.Rounding.Down);
|
|
}
|
|
|
|
function sharesToAssets(uint256 shares) public view returns (uint256) {
|
|
return shares.mulDiv(kraiken.totalSupply(), totalSupply, Math.Rounding.Down);
|
|
}
|
|
|
|
function snatch(uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch) public returns (uint256 positionId) {
|
|
uint256 sharesWanted = assetsToShares(assets);
|
|
{
|
|
uint256 minStake = kraiken.minStake();
|
|
if (assets < minStake) {
|
|
revert StakeTooLow(receiver, assets, minStake);
|
|
}
|
|
}
|
|
require(taxRate < TAX_RATES.length, "tax rate out of bounds");
|
|
|
|
uint256 smallestPositionShare = totalSupply;
|
|
uint256 availableStake = authorizedStake() - outstandingStake;
|
|
|
|
if (positionsToSnatch.length >= 2) {
|
|
for (uint256 i = 0; i < positionsToSnatch.length - 1; i++) {
|
|
StakingPosition storage pos = positions[positionsToSnatch[i]];
|
|
if (pos.creationTime == 0) { revert PositionNotFound(positionsToSnatch[i], receiver); }
|
|
if (taxRate <= pos.taxRate) { revert TaxTooLow(receiver, taxRate, pos.taxRate, positionsToSnatch[i]); }
|
|
if (pos.share < smallestPositionShare) { smallestPositionShare = pos.share; }
|
|
_payTax(positionsToSnatch[i], pos, 0);
|
|
_exitPosition(positionsToSnatch[i], pos);
|
|
}
|
|
}
|
|
availableStake = authorizedStake() - outstandingStake;
|
|
|
|
if (positionsToSnatch.length > 0) {
|
|
uint256 index = positionsToSnatch.length - 1;
|
|
StakingPosition storage lastPos = positions[positionsToSnatch[index]];
|
|
if (lastPos.creationTime == 0) { revert PositionNotFound(positionsToSnatch[index], receiver); }
|
|
if (taxRate <= lastPos.taxRate) { revert TaxTooLow(receiver, taxRate, lastPos.taxRate, positionsToSnatch[index]); }
|
|
if (lastPos.share < smallestPositionShare) { smallestPositionShare = lastPos.share; }
|
|
_payTax(positionsToSnatch[index], lastPos, TAX_FLOOR_DURATION);
|
|
if (availableStake > sharesWanted) { revert TooMuchSnatch(receiver, sharesWanted, availableStake, smallestPositionShare); }
|
|
uint256 lastSharesNeeded = sharesWanted - availableStake;
|
|
if (lastSharesNeeded > lastPos.share * 80 / 100) {
|
|
_exitPosition(positionsToSnatch[index], lastPos);
|
|
} else {
|
|
_shrinkPosition(positionsToSnatch[index], lastPos, lastSharesNeeded);
|
|
}
|
|
}
|
|
availableStake = authorizedStake() - outstandingStake;
|
|
|
|
if (sharesWanted > availableStake) { revert ExceededAvailableStake(receiver, sharesWanted, availableStake); }
|
|
if (availableStake - sharesWanted > smallestPositionShare) { revert TooMuchSnatch(receiver, sharesWanted, availableStake, smallestPositionShare); }
|
|
|
|
SafeERC20.safeTransferFrom(kraiken, msg.sender, address(this), assets);
|
|
|
|
positionId = nextPositionId++;
|
|
StakingPosition storage sp = positions[positionId];
|
|
sp.share = sharesWanted;
|
|
sp.owner = receiver;
|
|
sp.lastTaxTime = uint32(block.timestamp);
|
|
sp.creationTime = uint32(block.timestamp);
|
|
sp.taxRate = taxRate;
|
|
|
|
totalSharesAtTaxRate[taxRate] += sharesWanted;
|
|
outstandingStake += sharesWanted;
|
|
emit PositionCreated(positionId, sp.owner, assets, sp.share, sp.taxRate);
|
|
}
|
|
|
|
function permitAndSnatch(
|
|
uint256 assets, address receiver, uint32 taxRate, uint256[] calldata positionsToSnatch,
|
|
uint256 deadline, uint8 v, bytes32 r, bytes32 s
|
|
) external returns (uint256 positionId) {
|
|
ERC20Permit(address(kraiken)).permit(receiver, address(this), assets, deadline, v, r, s);
|
|
return snatch(assets, receiver, taxRate, positionsToSnatch);
|
|
}
|
|
|
|
function changeTax(uint256 positionId, uint32 taxRate) external {
|
|
require(taxRate < TAX_RATES.length, "tax rate out of bounds");
|
|
StakingPosition storage pos = positions[positionId];
|
|
if (pos.creationTime == 0) { revert PositionNotFound(positionId, msg.sender); }
|
|
if (pos.owner != msg.sender) { revert NoPermission(msg.sender, pos.owner); }
|
|
require(taxRate > pos.taxRate, "tax too low to snatch");
|
|
_payTax(positionId, pos, 0);
|
|
totalSharesAtTaxRate[pos.taxRate] -= pos.share;
|
|
totalSharesAtTaxRate[taxRate] += pos.share;
|
|
pos.taxRate = taxRate;
|
|
emit PositionRateHiked(positionId, pos.owner, taxRate);
|
|
}
|
|
|
|
function exitPosition(uint256 positionId) external {
|
|
StakingPosition storage pos = positions[positionId];
|
|
if (pos.owner != msg.sender) { revert NoPermission(msg.sender, pos.owner); }
|
|
if (pos.creationTime == 0) { revert PositionNotFound(positionId, msg.sender); }
|
|
_payTax(positionId, pos, TAX_FLOOR_DURATION);
|
|
_exitPosition(positionId, pos);
|
|
}
|
|
|
|
function payTax(uint256 positionId) external {
|
|
StakingPosition storage pos = positions[positionId];
|
|
if (pos.creationTime == 0) { revert PositionNotFound(positionId, msg.sender); }
|
|
_payTax(positionId, pos, 0);
|
|
}
|
|
|
|
function taxDue(uint256 positionId, uint256 taxFloorDuration) public view returns (uint256 amountDue) {
|
|
StakingPosition storage pos = positions[positionId];
|
|
uint256 ihet = (block.timestamp - pos.creationTime < taxFloorDuration) ? pos.creationTime + taxFloorDuration : block.timestamp;
|
|
uint256 elapsedTime = ihet - pos.lastTaxTime;
|
|
uint256 assetsBefore = sharesToAssets(pos.share);
|
|
amountDue = assetsBefore * TAX_RATES[pos.taxRate] * elapsedTime / (365 * 24 * 60 * 60) / TAX_RATE_BASE;
|
|
}
|
|
|
|
function getAverageTaxRate() external view returns (uint256 averageTaxRate) {
|
|
averageTaxRate = 0;
|
|
if (outstandingStake > 0) {
|
|
for (uint256 i = 0; i < TAX_RATES.length; i++) {
|
|
averageTaxRate += TAX_RATES[i] * totalSharesAtTaxRate[i];
|
|
}
|
|
averageTaxRate = averageTaxRate / outstandingStake;
|
|
averageTaxRate = averageTaxRate * 1e18 / TAX_RATES[TAX_RATES.length - 1];
|
|
}
|
|
}
|
|
|
|
function getPercentageStaked() external view returns (uint256 percentageStaked) {
|
|
percentageStaked = (outstandingStake * 1e18) / authorizedStake();
|
|
}
|
|
}`;
|
|
|
|
const optimizerSol = `// SPDX-License-Identifier: GPL-3.0-or-later
|
|
pragma solidity ^0.8.19;
|
|
|
|
import { Kraiken } from "./Kraiken.sol";
|
|
import { Stake } from "./Stake.sol";
|
|
|
|
import { Initializable } from "@openzeppelin/proxy/utils/Initializable.sol";
|
|
import { UUPSUpgradeable } from "@openzeppelin/proxy/utils/UUPSUpgradeable.sol";
|
|
|
|
/**
|
|
* @title OptimizerV3
|
|
* @notice Direct 2D (staking%, avgTax) to binary bear/bull liquidity optimizer.
|
|
* @dev Replaces the three-zone score-based model with a direct mapping:
|
|
*
|
|
* staked <= 91% -> BEAR always (no euphoria signal)
|
|
* staked > 91% -> BULL if deltaS^3 * effIdx / 20 < 50, else BEAR
|
|
*
|
|
* Bear: AS=30%, AW=100, CI=0, DD=0.3e18
|
|
* Bull: AS=100%, AW=20, CI=0, DD=1e18
|
|
*/
|
|
contract OptimizerV3 is Initializable, UUPSUpgradeable {
|
|
Kraiken private kraiken;
|
|
Stake private stake;
|
|
|
|
uint256[48] private __gap;
|
|
|
|
error UnauthorizedAccount(address account);
|
|
|
|
function initialize(address _kraiken, address _stake) public initializer {
|
|
_changeAdmin(msg.sender);
|
|
kraiken = Kraiken(_kraiken);
|
|
stake = Stake(_stake);
|
|
}
|
|
|
|
modifier onlyAdmin() {
|
|
_checkAdmin();
|
|
_;
|
|
}
|
|
|
|
function _checkAdmin() internal view virtual {
|
|
if (_getAdmin() != msg.sender) {
|
|
revert UnauthorizedAccount(msg.sender);
|
|
}
|
|
}
|
|
|
|
function _authorizeUpgrade(address newImplementation) internal override onlyAdmin { }
|
|
|
|
function _taxRateToEffectiveIndex(uint256 averageTaxRate) internal pure returns (uint256) {
|
|
uint256 idx;
|
|
if (averageTaxRate <= 206_185_567_010_309) idx = 0;
|
|
else if (averageTaxRate <= 412_371_134_020_618) idx = 1;
|
|
else if (averageTaxRate <= 618_556_701_030_927) idx = 2;
|
|
else if (averageTaxRate <= 1_030_927_835_051_546) idx = 3;
|
|
else if (averageTaxRate <= 1_546_391_752_577_319) idx = 4;
|
|
else if (averageTaxRate <= 2_164_948_453_608_247) idx = 5;
|
|
else if (averageTaxRate <= 2_783_505_154_639_175) idx = 6;
|
|
else if (averageTaxRate <= 3_608_247_422_680_412) idx = 7;
|
|
else if (averageTaxRate <= 4_639_175_257_731_958) idx = 8;
|
|
else if (averageTaxRate <= 5_670_103_092_783_505) idx = 9;
|
|
else if (averageTaxRate <= 7_216_494_845_360_824) idx = 10;
|
|
else if (averageTaxRate <= 9_278_350_515_463_917) idx = 11;
|
|
else if (averageTaxRate <= 11_855_670_103_092_783) idx = 12;
|
|
else if (averageTaxRate <= 15_979_381_443_298_969) idx = 13;
|
|
else if (averageTaxRate <= 22_164_948_453_608_247) idx = 14;
|
|
else if (averageTaxRate <= 29_381_443_298_969_072) idx = 15;
|
|
else if (averageTaxRate <= 38_144_329_896_907_216) idx = 16;
|
|
else if (averageTaxRate <= 49_484_536_082_474_226) idx = 17;
|
|
else if (averageTaxRate <= 63_917_525_773_195_876) idx = 18;
|
|
else if (averageTaxRate <= 83_505_154_639_175_257) idx = 19;
|
|
else if (averageTaxRate <= 109_278_350_515_463_917) idx = 20;
|
|
else if (averageTaxRate <= 144_329_896_907_216_494) idx = 21;
|
|
else if (averageTaxRate <= 185_567_010_309_278_350) idx = 22;
|
|
else if (averageTaxRate <= 237_113_402_061_855_670) idx = 23;
|
|
else if (averageTaxRate <= 309_278_350_515_463_917) idx = 24;
|
|
else if (averageTaxRate <= 402_061_855_670_103_092) idx = 25;
|
|
else if (averageTaxRate <= 520_618_556_701_030_927) idx = 26;
|
|
else if (averageTaxRate <= 680_412_371_134_020_618) idx = 27;
|
|
else if (averageTaxRate <= 886_597_938_144_329_896) idx = 28;
|
|
else idx = 29;
|
|
|
|
if (idx >= 14) {
|
|
idx = idx + 1;
|
|
if (idx > 29) idx = 29;
|
|
}
|
|
|
|
return idx;
|
|
}
|
|
|
|
function isBullMarket(uint256 percentageStaked, uint256 averageTaxRate) public pure returns (bool bull) {
|
|
require(percentageStaked <= 1e18, "Invalid percentage staked");
|
|
uint256 stakedPct = percentageStaked * 100 / 1e18;
|
|
if (stakedPct <= 91) return false;
|
|
|
|
uint256 deltaS = 100 - stakedPct;
|
|
uint256 effIdx = _taxRateToEffectiveIndex(averageTaxRate);
|
|
uint256 penalty = deltaS * deltaS * deltaS * effIdx / 20;
|
|
return penalty < 50;
|
|
}
|
|
|
|
function getLiquidityParams() external view returns (uint256 capitalInefficiency, uint256 anchorShare, uint24 anchorWidth, uint256 discoveryDepth) {
|
|
uint256 percentageStaked = stake.getPercentageStaked();
|
|
uint256 averageTaxRate = stake.getAverageTaxRate();
|
|
|
|
capitalInefficiency = 0;
|
|
|
|
if (isBullMarket(percentageStaked, averageTaxRate)) {
|
|
anchorShare = 1e18;
|
|
anchorWidth = 20;
|
|
discoveryDepth = 1e18;
|
|
} else {
|
|
anchorShare = 3e17;
|
|
anchorWidth = 100;
|
|
discoveryDepth = 3e17;
|
|
}
|
|
}
|
|
}`;
|
|
</script>
|
|
|
|
<style scoped lang="sass">
|
|
.code-addresses
|
|
margin: 32px 0
|
|
padding: 20px
|
|
background: rgba(255, 255, 255, 0.05)
|
|
border-radius: 8px
|
|
|
|
.code-address-list
|
|
display: flex
|
|
flex-direction: column
|
|
gap: 12px
|
|
margin-top: 12px
|
|
|
|
.code-address-item
|
|
display: flex
|
|
align-items: center
|
|
gap: 12px
|
|
flex-wrap: wrap
|
|
.code-address-label
|
|
font-weight: 600
|
|
font-size: 14px
|
|
color: #9A9898
|
|
min-width: 80px
|
|
a
|
|
font-family: monospace
|
|
font-size: 13px
|
|
color: #7550AE
|
|
word-break: break-all
|
|
|
|
.code-block
|
|
margin: 16px 0 32px
|
|
border-radius: 8px
|
|
overflow: hidden
|
|
pre
|
|
background: #0d1117
|
|
padding: 20px
|
|
overflow-x: auto
|
|
margin: 0
|
|
border: 1px solid rgba(255, 255, 255, 0.1)
|
|
border-radius: 8px
|
|
code
|
|
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace
|
|
font-size: 13px
|
|
line-height: 1.5
|
|
color: #e6edf3
|
|
white-space: pre
|
|
:deep(.sol-keyword)
|
|
color: #c084fc
|
|
:deep(.sol-type)
|
|
color: #7dd3fc
|
|
:deep(.sol-comment)
|
|
color: #6b7280
|
|
:deep(.sol-string)
|
|
color: #a5d6ff
|
|
:deep(.sol-number)
|
|
color: #79c0ff
|
|
</style>
|