Skip to content

Commit

Permalink
feat: implement fee tiers
Browse files Browse the repository at this point in the history
  • Loading branch information
thaixuandang committed Jul 10, 2024
1 parent fc11546 commit a793b97
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 42 deletions.
29 changes: 21 additions & 8 deletions src/core/KatanaV3Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ contract KatanaV3Factory is IKatanaV3Factory, KatanaV3PoolDeployer {
/// @inheritdoc IKatanaV3Factory
mapping(uint24 => int24) public override feeAmountTickSpacing;
/// @inheritdoc IKatanaV3Factory
mapping(uint24 => uint16) public override feeAmountProtocol;
/// @inheritdoc IKatanaV3Factory
mapping(address => mapping(address => mapping(uint24 => address))) public override getPool;

constructor() {
Expand All @@ -30,12 +32,17 @@ contract KatanaV3Factory is IKatanaV3Factory, KatanaV3PoolDeployer {
owner = msg.sender;
emit OwnerChanged(address(0), msg.sender);

feeAmountTickSpacing[500] = 10;
emit FeeAmountEnabled(500, 10);
feeAmountTickSpacing[3000] = 60;
emit FeeAmountEnabled(3000, 60);
feeAmountTickSpacing[10000] = 200;
emit FeeAmountEnabled(10000, 200);
// swap fee 0.01% = 0.005% for LP + 0.005% for protocol
// tick spacing of 2, approximately 0.02% between initializable ticks
_enableFeeAmount(100, 2, 5 | (10 << 8));

// swap fee 0.3% = 0.25% for LP + 0.05% for protocol
// tick spacing of 60, approximately 0.60% between initializable ticks
_enableFeeAmount(3000, 60, 5 | (30 << 8));

// swap fee 1% = 0.85% for LP + 0.15% for protocol
// tick spacing of 200, approximately 2.02% between initializable ticks
_enableFeeAmount(10000, 200, 15 | (100 << 8));
}

function upgradeBeacon(address newImplementation) external {
Expand Down Expand Up @@ -66,16 +73,22 @@ contract KatanaV3Factory is IKatanaV3Factory, KatanaV3PoolDeployer {
}

/// @inheritdoc IKatanaV3Factory
function enableFeeAmount(uint24 fee, int24 tickSpacing) public override {
function enableFeeAmount(uint24 fee, int24 tickSpacing, uint16 feeProtocol) public override {
require(msg.sender == owner);
require(fee < 1000000);
// tick spacing is capped at 16384 to prevent the situation where tickSpacing is so large that
// TickBitmap#nextInitializedTickWithinOneWord overflows int24 container from a valid tick
// 16384 ticks represents a >5x price change with ticks of 1 bips
require(tickSpacing > 0 && tickSpacing < 16384);
require((feeProtocol & 255) < (feeProtocol >> 8));
require(feeAmountTickSpacing[fee] == 0);

_enableFeeAmount(fee, tickSpacing, feeProtocol);
}

function _enableFeeAmount(uint24 fee, int24 tickSpacing, uint16 feeProtocol) private {
feeAmountTickSpacing[fee] = tickSpacing;
emit FeeAmountEnabled(fee, tickSpacing);
feeAmountProtocol[fee] = feeProtocol;
emit FeeAmountEnabled(fee, tickSpacing, feeProtocol);
}
}
39 changes: 20 additions & 19 deletions src/core/KatanaV3Pool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,13 @@ contract KatanaV3Pool is IKatanaV3Pool {
uint16 observationCardinality;
// the next maximum number of observations to store, triggered in observations.write
uint16 observationCardinalityNext;
// the current protocol fee as a percentage of the swap fee taken on withdrawal
// represented as an integer denominator (1/x)%
uint8 feeProtocol;
// the current protocol fee as a ratio of the swap fee: first byte is numerator, second byte is denominator
uint16 feeProtocol;
// whether the pool is locked
bool unlocked;
}
/// @inheritdoc IKatanaV3PoolState

/// @inheritdoc IKatanaV3PoolState
Slot0 public override slot0;

/// @inheritdoc IKatanaV3PoolState
Expand Down Expand Up @@ -261,7 +260,7 @@ contract KatanaV3Pool is IKatanaV3Pool {
observationIndex: 0,
observationCardinality: cardinality,
observationCardinalityNext: cardinalityNext,
feeProtocol: 0,
feeProtocol: IKatanaV3Factory(factory).feeAmountProtocol(fee),
unlocked: true
});

Expand Down Expand Up @@ -493,7 +492,7 @@ contract KatanaV3Pool is IKatanaV3Pool {

struct SwapCache {
// the protocol fee for the input token
uint8 feeProtocol;
uint16 feeProtocol;
// liquidity at the beginning of the swap
uint128 liquidityStart;
// the timestamp of the current block
Expand Down Expand Up @@ -566,7 +565,7 @@ contract KatanaV3Pool is IKatanaV3Pool {
SwapCache memory cache = SwapCache({
liquidityStart: liquidity,
blockTimestamp: _blockTimestamp(),
feeProtocol: zeroForOne ? (slot0Start.feeProtocol % 16) : (slot0Start.feeProtocol >> 4),
feeProtocol: slot0Start.feeProtocol,
secondsPerLiquidityCumulativeX128: 0,
tickCumulative: 0,
computedLatestObservation: false
Expand Down Expand Up @@ -624,7 +623,7 @@ contract KatanaV3Pool is IKatanaV3Pool {

// if the protocol fee is on, calculate how much is owed, decrement feeAmount, and increment protocolFee
if (cache.feeProtocol > 0) {
uint256 delta = step.feeAmount / cache.feeProtocol;
uint256 delta = FullMath.mulDiv(step.feeAmount, cache.feeProtocol & 255, cache.feeProtocol >> 8);
step.feeAmount -= delta;
state.protocolFee += uint128(delta);
}
Expand Down Expand Up @@ -752,14 +751,12 @@ contract KatanaV3Pool is IKatanaV3Pool {
uint256 paid1 = balance1After - balance1Before;

if (paid0 > 0) {
uint8 feeProtocol0 = slot0.feeProtocol % 16;
uint256 fees0 = feeProtocol0 == 0 ? 0 : paid0 / feeProtocol0;
uint256 fees0 = FullMath.mulDiv(paid0, slot0.feeProtocol & 255, slot0.feeProtocol >> 8);
if (uint128(fees0) > 0) protocolFees.token0 += uint128(fees0);
feeGrowthGlobal0X128 += FullMath.mulDiv(paid0 - fees0, FixedPoint128.Q128, _liquidity);
}
if (paid1 > 0) {
uint8 feeProtocol1 = slot0.feeProtocol >> 4;
uint256 fees1 = feeProtocol1 == 0 ? 0 : paid1 / feeProtocol1;
uint256 fees1 = FullMath.mulDiv(paid1, slot0.feeProtocol & 255, slot0.feeProtocol >> 8);
if (uint128(fees1) > 0) protocolFees.token1 += uint128(fees1);
feeGrowthGlobal1X128 += FullMath.mulDiv(paid1 - fees1, FixedPoint128.Q128, _liquidity);
}
Expand All @@ -768,14 +765,18 @@ contract KatanaV3Pool is IKatanaV3Pool {
}

/// @inheritdoc IKatanaV3PoolOwnerActions
function setFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external override lock onlyFactoryOwner {
require(
(feeProtocol0 == 0 || (feeProtocol0 >= 4 && feeProtocol0 <= 10))
&& (feeProtocol1 == 0 || (feeProtocol1 >= 4 && feeProtocol1 <= 10))
function setFeeProtocol(uint8 feeProtocolNumerator, uint8 feeProtocolDenominator)
external
override
lock
onlyFactoryOwner
{
require(feeProtocolNumerator < feeProtocolDenominator);
uint16 feeProtocolOld = slot0.feeProtocol;
slot0.feeProtocol = uint16(feeProtocolNumerator) | (uint16(feeProtocolDenominator) << 8);
emit SetFeeProtocol(
uint8(feeProtocolOld & 255), uint8(feeProtocolOld >> 8), feeProtocolNumerator, feeProtocolDenominator
);
uint8 feeProtocolOld = slot0.feeProtocol;
slot0.feeProtocol = feeProtocol0 + (feeProtocol1 << 4);
emit SetFeeProtocol(feeProtocolOld % 16, feeProtocolOld >> 4, feeProtocol0, feeProtocol1);
}

/// @inheritdoc IKatanaV3PoolOwnerActions
Expand Down
12 changes: 10 additions & 2 deletions src/core/interfaces/IKatanaV3Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ interface IKatanaV3Factory {
/// @notice Emitted when a new fee amount is enabled for pool creation via the factory
/// @param fee The enabled fee, denominated in hundredths of a bip
/// @param tickSpacing The minimum number of ticks between initialized ticks for pools created with the given fee
event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing);
/// @param protocolFee The ratio of the fee amount to be sent to the Ronin treasury.
event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing, uint16 indexed protocolFee);

/// @notice Returns the beacon used for creating new pools
/// @return The beacon contract address
Expand All @@ -39,6 +40,12 @@ interface IKatanaV3Factory {
/// @return The tick spacing
function feeAmountTickSpacing(uint24 fee) external view returns (int24);

/// @notice Returns the default protocol fee ratio for a given fee amount, if enabled, or 0 if not enabled
/// @dev This protocol fee can be changed by the factory owner in each pool later
/// @param fee The enabled fee, denominated in hundredths of a bip. Returns 0 in case of unenabled fee
/// @return The protocol fee as a ratio of the fee amount: first byte is numerator, second byte is denominator
function feeAmountProtocol(uint24 fee) external view returns (uint16);

/// @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist
/// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order
/// @param tokenA The contract address of either token0 or token1
Expand Down Expand Up @@ -66,5 +73,6 @@ interface IKatanaV3Factory {
/// @dev Fee amounts may never be removed once enabled
/// @param fee The fee amount to enable, denominated in hundredths of a bip (i.e. 1e-6)
/// @param tickSpacing The spacing between ticks to be enforced for all pools created with the given fee amount
function enableFeeAmount(uint24 fee, int24 tickSpacing) external;
/// @param feeProtocol The protocol fee as a ratio of the fee amount: first byte is numerator, second byte is denominator
function enableFeeAmount(uint24 fee, int24 tickSpacing, uint16 feeProtocol) external;
}
10 changes: 5 additions & 5 deletions src/core/interfaces/pool/IKatanaV3PoolEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ interface IKatanaV3PoolEvents {
event IncreaseObservationCardinalityNext(uint16 observationCardinalityNextOld, uint16 observationCardinalityNextNew);

/// @notice Emitted when the protocol fee is changed by the pool
/// @param feeProtocol0Old The previous value of the token0 protocol fee
/// @param feeProtocol1Old The previous value of the token1 protocol fee
/// @param feeProtocol0New The updated value of the token0 protocol fee
/// @param feeProtocol1New The updated value of the token1 protocol fee
event SetFeeProtocol(uint8 feeProtocol0Old, uint8 feeProtocol1Old, uint8 feeProtocol0New, uint8 feeProtocol1New);
/// @param feeProtocolNumeratorOld The numerator of the previous value of protocol fee
/// @param feeProtocolDenominatorOld The denominator of the previous value of protocol fee
/// @param feeProtocolNumeratorNew The numerator of the updated value of protocol fee
/// @param feeProtocolDenominatorNew The denominator of the oupdated value of protocol fee
event SetFeeProtocol(uint8 feeProtocolNumeratorOld, uint8 feeProtocolDenominatorOld, uint8 feeProtocolNumeratorNew, uint8 feeProtocolDenominatorNew);

/// @notice Emitted when the collected protocol fees are withdrawn by the factory owner
/// @param sender The address that collects the protocol fees
Expand Down
8 changes: 4 additions & 4 deletions src/core/interfaces/pool/IKatanaV3PoolOwnerActions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ pragma solidity >=0.5.0;
/// @title Permissioned pool actions
/// @notice Contains pool methods that may only be called by the factory owner
interface IKatanaV3PoolOwnerActions {
/// @notice Set the denominator of the protocol's % share of the fees
/// @param feeProtocol0 new protocol fee for token0 of the pool
/// @param feeProtocol1 new protocol fee for token1 of the pool
function setFeeProtocol(uint8 feeProtocol0, uint8 feeProtocol1) external;
/// @notice Set the protocol fee as a ratio of the swap fees
/// @param feeProtocolNumerator new protocol fee numerator
/// @param feeProtocolDenominator new protocol fee denominator
function setFeeProtocol(uint8 feeProtocolNumerator, uint8 feeProtocolDenominator) external;

/// @notice Collect the protocol fee accrued to the pool
/// @param recipient The address to which collected protocol fees should be sent
Expand Down
7 changes: 3 additions & 4 deletions src/core/interfaces/pool/IKatanaV3PoolState.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ interface IKatanaV3PoolState {
/// observationIndex The index of the last oracle observation that was written,
/// observationCardinality The current maximum number of observations stored in the pool,
/// observationCardinalityNext The next maximum number of observations, to be updated when the observation.
/// feeProtocol The protocol fee for both tokens of the pool.
/// Encoded as two 4 bit values, where the protocol fee of token1 is shifted 4 bits and the protocol fee of token0
/// is the lower 4 bits. Used as the denominator of a fraction of the swap fee, e.g. 4 means 1/4th of the swap fee.
/// feeProtocol The protocol fee as a ratio of the swap fee.
/// Encoded as a fraction, where first byte (8 bit value) is the numerator and the second byte is the denominator.
/// unlocked Whether the pool is currently locked to reentrancy
function slot0()
external
Expand All @@ -27,7 +26,7 @@ interface IKatanaV3PoolState {
uint16 observationIndex,
uint16 observationCardinality,
uint16 observationCardinalityNext,
uint8 feeProtocol,
uint16 feeProtocol,
bool unlocked
);

Expand Down
72 changes: 72 additions & 0 deletions test/core/KatanaV3Pool.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.6;
pragma abicoder v2;

import { Test, console } from "forge-std/Test.sol";

import { ERC20Mock } from "@openzeppelin/contracts/mocks/ERC20Mock.sol";

import { TickMath } from "@katana/v3-contracts/core/libraries/TickMath.sol";

import { KatanaV3Factory } from "@katana/v3-contracts/core/KatanaV3Factory.sol";
import { KatanaV3Pool } from "@katana/v3-contracts/core/KatanaV3Pool.sol";
import { IKatanaV3Pool } from "@katana/v3-contracts/core/interfaces/IKatanaV3Pool.sol";

contract KatanaV3PoolTest is Test {
KatanaV3Factory factory;
uint24[] fees = [100, 3000, 10000];
int24[] tickSpacings = [2, 60, 200];
KatanaV3Pool[] pools;

// Deploy token1 first to make token0 < token1
address token1 = address(new ERC20Mock("Token1", "TK1", address(this), 10 ** 9 * 10 ** 9));
address token0 = address(new ERC20Mock("Token0", "TK0", address(this), 10 ** 9 * 10 ** 9));

function setUp() public {
vm.label(token0, "token0");
vm.label(token1, "token1");

factory = new KatanaV3Factory();

pools.push(KatanaV3Pool(factory.createPool(token0, token1, 100)));
pools.push(KatanaV3Pool(factory.createPool(token0, token1, 3000)));
pools.push(KatanaV3Pool(factory.createPool(token0, token1, 10000)));

vm.label(address(pools[0]), "pool[0.01%]");
vm.label(address(pools[1]), "pool[0.3%]");
vm.label(address(pools[2]), "pool[1%]");

for (uint256 i = 0; i < pools.length; ++i) {
KatanaV3Pool pool = pools[i];
int24 tickSpacing = tickSpacings[i];
// price: token0 = 10 token1
pool.initialize(250541448375047931186413801569); // sqrt(10) * 2**96
pool.mint(
address(this),
(23027 - 10000) / tickSpacing * tickSpacing,
(23027 + 10000) / tickSpacing * tickSpacing,
100_000_000,
""
);
}
}

function katanaV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
ERC20Mock(token0).transfer(msg.sender, amount0Owed);
ERC20Mock(token1).transfer(msg.sender, amount1Owed);
}

function katanaV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {
if (amount0Delta > 0) ERC20Mock(token0).transfer(msg.sender, uint256(amount0Delta));
if (amount1Delta > 0) ERC20Mock(token1).transfer(msg.sender, uint256(amount1Delta));
}

function test_swap() public {
for (uint256 i = 0; i < 3; ++i) {
KatanaV3Pool pool = pools[i];
pool.swap(address(this), true, 10_000_000, TickMath.MIN_SQRT_RATIO + 1, "");
(uint128 protocolFee0, uint128 protocolFee1) = pool.protocolFees();
console.log("protocolFees", protocolFee0, protocolFee1);
}
}
}

0 comments on commit a793b97

Please sign in to comment.