Skip to content

Commit

Permalink
Merge pull request #4 from ronin-chain/feature/fee-tiers
Browse files Browse the repository at this point in the history
feat: fee tiers
  • Loading branch information
thaixuandang authored Aug 26, 2024
2 parents ff771e7 + ff226c1 commit 3747c28
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 84 deletions.
46 changes: 35 additions & 11 deletions src/core/KatanaV3Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,37 @@ contract KatanaV3Factory is IKatanaV3Factory, KatanaV3PoolDeployer {

/// @inheritdoc IKatanaV3Factory
address public override owner;
/// @inheritdoc IKatanaV3Factory
address public override treasury;

/// @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() {
constructor(address _owner, address _treasury) {
address poolImplementation = address(new KatanaV3Pool());
beacon = address(new UpgradeableBeacon(poolImplementation));

owner = msg.sender;
emit OwnerChanged(address(0), msg.sender);
owner = _owner;
emit OwnerChanged(address(0), _owner);

treasury = _treasury;
emit TreasuryChanged(address(0), _treasury);

// swap fee 0.01% = 0.005% for LP + 0.005% for protocol
// tick spacing of 1, equivalent to 0.01% between initializable ticks
_enableFeeAmount(100, 1, 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));

feeAmountTickSpacing[500] = 10;
emit FeeAmountEnabled(500, 10);
feeAmountTickSpacing[3000] = 60;
emit FeeAmountEnabled(3000, 60);
feeAmountTickSpacing[10000] = 200;
emit FeeAmountEnabled(10000, 200);
// 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 @@ -65,17 +77,29 @@ contract KatanaV3Factory is IKatanaV3Factory, KatanaV3PoolDeployer {
owner = _owner;
}

function setTreasury(address _treasury) external override {
require(msg.sender == owner);
emit TreasuryChanged(treasury, _treasury);
treasury = _treasury;
}

/// @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);
}
}
133 changes: 76 additions & 57 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 @@ -349,7 +348,8 @@ contract KatanaV3Pool is IKatanaV3Pool {

uint256 _feeGrowthGlobal0X128 = feeGrowthGlobal0X128; // SLOAD for gas optimization
uint256 _feeGrowthGlobal1X128 = feeGrowthGlobal1X128; // SLOAD for gas optimization

uint128 _maxLiquidityPerTick = maxLiquidityPerTick; // SLOAD for gas optimization

// if we need to update the ticks, do it
bool flippedLower;
bool flippedUpper;
Expand All @@ -368,7 +368,7 @@ contract KatanaV3Pool is IKatanaV3Pool {
tickCumulative,
time,
false,
maxLiquidityPerTick
_maxLiquidityPerTick
);
flippedUpper = ticks.update(
tickUpper,
Expand All @@ -380,14 +380,15 @@ contract KatanaV3Pool is IKatanaV3Pool {
tickCumulative,
time,
true,
maxLiquidityPerTick
_maxLiquidityPerTick
);

int24 _tickSpacing = tickSpacing; // SLOAD for gas optimization
if (flippedLower) {
tickBitmap.flipTick(tickLower, tickSpacing);
tickBitmap.flipTick(tickLower, _tickSpacing);
}
if (flippedUpper) {
tickBitmap.flipTick(tickUpper, tickSpacing);
tickBitmap.flipTick(tickUpper, _tickSpacing);
}
}

Expand Down Expand Up @@ -492,8 +493,12 @@ contract KatanaV3Pool is IKatanaV3Pool {
}

struct SwapCache {
// the swap fee in hundredths of a bip
uint24 fee;
// the protocol fee for the input token
uint8 feeProtocol;
uint16 feeProtocol;
// the spacing between usable ticks
int24 tickSpacing;
// liquidity at the beginning of the swap
uint128 liquidityStart;
// the timestamp of the current block
Expand Down Expand Up @@ -566,7 +571,9 @@ contract KatanaV3Pool is IKatanaV3Pool {
SwapCache memory cache = SwapCache({
liquidityStart: liquidity,
blockTimestamp: _blockTimestamp(),
feeProtocol: zeroForOne ? (slot0Start.feeProtocol % 16) : (slot0Start.feeProtocol >> 4),
fee: fee,
feeProtocol: slot0Start.feeProtocol,
tickSpacing: tickSpacing,
secondsPerLiquidityCumulativeX128: 0,
tickCumulative: 0,
computedLatestObservation: false
Expand All @@ -591,7 +598,7 @@ contract KatanaV3Pool is IKatanaV3Pool {
step.sqrtPriceStartX96 = state.sqrtPriceX96;

(step.tickNext, step.initialized) =
tickBitmap.nextInitializedTickWithinOneWord(state.tick, tickSpacing, zeroForOne);
tickBitmap.nextInitializedTickWithinOneWord(state.tick, cache.tickSpacing, zeroForOne);

// ensure that we do not overshoot the min/max tick, as the tick bitmap is not aware of these bounds
if (step.tickNext < TickMath.MIN_TICK) {
Expand All @@ -611,7 +618,7 @@ contract KatanaV3Pool is IKatanaV3Pool {
: step.sqrtPriceNextX96,
state.liquidity,
state.amountSpecifiedRemaining,
fee
cache.fee
);

if (exactInput) {
Expand All @@ -624,7 +631,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 @@ -693,14 +700,15 @@ contract KatanaV3Pool is IKatanaV3Pool {
// update liquidity if it changed
if (cache.liquidityStart != state.liquidity) liquidity = state.liquidity;

// update fee growth global and, if necessary, protocol fees
// overflow is acceptable, protocol has to withdraw before it hits type(uint128).max fees
if (zeroForOne) {
feeGrowthGlobal0X128 = state.feeGrowthGlobalX128;
if (state.protocolFee > 0) protocolFees.token0 += state.protocolFee;
} else {
feeGrowthGlobal1X128 = state.feeGrowthGlobalX128;
if (state.protocolFee > 0) protocolFees.token1 += state.protocolFee;
address tokenIn = zeroForOne ? token0 : token1;
address tokenOut = zeroForOne ? token1 : token0;

// update fee growth global
if (zeroForOne) feeGrowthGlobal0X128 = state.feeGrowthGlobalX128;
else feeGrowthGlobal1X128 = state.feeGrowthGlobalX128;
// transfer protocol fees to the treasury
if (state.protocolFee > 0) {
TransferHelper.safeTransfer(tokenIn, IKatanaV3Factory(factory).treasury(), state.protocolFee);
}

(amount0, amount1) = zeroForOne == exactInput
Expand All @@ -709,13 +717,13 @@ contract KatanaV3Pool is IKatanaV3Pool {

// do the transfers and collect payment
if (zeroForOne) {
if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));
if (amount1 < 0) TransferHelper.safeTransfer(tokenOut, recipient, uint256(-amount1));

uint256 balance0Before = balance0();
IKatanaV3SwapCallback(msg.sender).katanaV3SwapCallback(amount0, amount1, data);
require(balance0Before.add(uint256(amount0)) <= balance0(), "IIA");
} else {
if (amount0 < 0) TransferHelper.safeTransfer(token0, recipient, uint256(-amount0));
if (amount0 < 0) TransferHelper.safeTransfer(tokenOut, recipient, uint256(-amount0));

uint256 balance1Before = balance1();
IKatanaV3SwapCallback(msg.sender).katanaV3SwapCallback(amount0, amount1, data);
Expand All @@ -731,51 +739,62 @@ contract KatanaV3Pool is IKatanaV3Pool {
uint128 _liquidity = liquidity;
require(_liquidity > 0, "L");

uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6);
uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6);
uint256 balance0Before = balance0();
uint256 balance1Before = balance1();
uint256 paid0;
uint256 paid1;

if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0);
if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1);
// scope to avoid stack too deep error
{
address _token0 = token0;
address _token1 = token1;
uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6);
uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6);
uint256 balance0Before = balance0();
uint256 balance1Before = balance1();

IKatanaV3FlashCallback(msg.sender).katanaV3FlashCallback(fee0, fee1, data);
if (amount0 > 0) TransferHelper.safeTransfer(_token0, recipient, amount0);
if (amount1 > 0) TransferHelper.safeTransfer(_token1, recipient, amount1);

uint256 balance0After = balance0();
uint256 balance1After = balance1();
IKatanaV3FlashCallback(msg.sender).katanaV3FlashCallback(fee0, fee1, data);

require(balance0Before.add(fee0) <= balance0After, "F0");
require(balance1Before.add(fee1) <= balance1After, "F1");
uint256 balance0After = balance0();
uint256 balance1After = balance1();
address treasury = IKatanaV3Factory(factory).treasury();

// sub is safe because we know balanceAfter is gt balanceBefore by at least fee
uint256 paid0 = balance0After - balance0Before;
uint256 paid1 = balance1After - balance1Before;
require(balance0Before.add(fee0) <= balance0After, "F0");
require(balance1Before.add(fee1) <= balance1After, "F1");

if (paid0 > 0) {
uint8 feeProtocol0 = slot0.feeProtocol % 16;
uint256 fees0 = feeProtocol0 == 0 ? 0 : paid0 / feeProtocol0;
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;
if (uint128(fees1) > 0) protocolFees.token1 += uint128(fees1);
feeGrowthGlobal1X128 += FullMath.mulDiv(paid1 - fees1, FixedPoint128.Q128, _liquidity);
// sub is safe because we know balanceAfter is gt balanceBefore by at least fee
paid0 = balance0After - balance0Before;
paid1 = balance1After - balance1Before;

if (paid0 > 0) {
uint256 fees0 = FullMath.mulDiv(paid0, slot0.feeProtocol & 255, slot0.feeProtocol >> 8);
if (fees0 > 0) TransferHelper.safeTransfer(_token0, treasury, fees0);
feeGrowthGlobal0X128 += FullMath.mulDiv(paid0 - fees0, FixedPoint128.Q128, _liquidity);
}
if (paid1 > 0) {
uint256 fees1 = FullMath.mulDiv(paid1, slot0.feeProtocol & 255, slot0.feeProtocol >> 8);
if (fees1 > 0) TransferHelper.safeTransfer(_token1, treasury, fees1);
feeGrowthGlobal1X128 += FullMath.mulDiv(paid1 - fees1, FixedPoint128.Q128, _liquidity);
}
}

emit Flash(msg.sender, recipient, amount0, amount1, paid0, paid1);
}

/// @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
27 changes: 25 additions & 2 deletions src/core/interfaces/IKatanaV3Factory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ interface IKatanaV3Factory {
/// @param newOwner The owner after the owner was changed
event OwnerChanged(address indexed oldOwner, address indexed newOwner);

/// @notice Emitted when the treasury address is changed
/// @param oldTreasury The treasury address before the treasury was changed
/// @param newTreasury The treasury address after the treasury was changed
event TreasuryChanged(address indexed oldTreasury, address indexed newTreasury);

/// @notice Emitted when a pool is created
/// @param token0 The first token of the pool by address sort order
/// @param token1 The second token of the pool by address sort order
Expand All @@ -22,7 +27,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 @@ -33,12 +39,23 @@ interface IKatanaV3Factory {
/// @return The address of the factory owner
function owner() external view returns (address);

/// @notice Returns the treasury address that receives protocol fees
/// @dev Can be changed by the current owner via setTreasury
/// @return The address of the treasury
function treasury() external view returns (address);

/// @notice Returns the tick spacing for a given fee amount, if enabled, or 0 if not enabled
/// @dev A fee amount can never be removed, so this value should be hard coded or cached in the calling context
/// @param fee The enabled fee, denominated in hundredths of a bip. Returns 0 in case of unenabled fee
/// @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 All @@ -62,9 +79,15 @@ interface IKatanaV3Factory {
/// @param _owner The new owner of the factory
function setOwner(address _owner) external;

/// @notice Updates the treasury address
/// @dev Must be called by the current owner
/// @param _treasury The new treasury address
function setTreasury(address _treasury) external;

/// @notice Enables a fee amount with the given tickSpacing
/// @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;
}
Loading

0 comments on commit 3747c28

Please sign in to comment.