diff --git a/.gitignore b/.gitignore index e387d66a1..bd1cdfebe 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ fabric.properties build .DS_Store .vscode +*.code-workspace .keystores .deployment gradle.properties diff --git a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/testtokens/DexIntTestToken.jar b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/testtokens/DexIntTestToken.jar deleted file mode 100644 index de679b5c1..000000000 Binary files a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/testtokens/DexIntTestToken.jar and /dev/null differ diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPool.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPool.java new file mode 100644 index 000000000..5d03a8a38 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPool.java @@ -0,0 +1,1498 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex; + +import static java.math.BigInteger.ZERO; +import static network.balanced.score.core.dex.utils.IntUtils.uint256; +import java.math.BigInteger; +import network.balanced.score.core.dex.interfaces.factory.IBalancedFactory; +import network.balanced.score.core.dex.interfaces.irc2.IIRC2ICX; +import network.balanced.score.core.dex.interfaces.pool.IConcentratedLiquidityPoolCallee; +import network.balanced.score.core.dex.libs.FixedPoint128; +import network.balanced.score.core.dex.libs.FullMath; +import network.balanced.score.core.dex.libs.LiquidityMath; +import network.balanced.score.core.dex.libs.PositionLib; +import network.balanced.score.core.dex.libs.SqrtPriceMath; +import network.balanced.score.core.dex.libs.SwapMath; +import network.balanced.score.core.dex.libs.TickLib; +import network.balanced.score.core.dex.libs.TickMath; +import network.balanced.score.core.dex.models.Observations; +import network.balanced.score.core.dex.models.Positions; +import network.balanced.score.core.dex.models.TickBitmap; +import network.balanced.score.core.dex.models.Ticks; +import network.balanced.score.core.dex.structs.factory.Parameters; +import network.balanced.score.core.dex.structs.pool.ModifyPositionParams; +import network.balanced.score.core.dex.structs.pool.ModifyPositionResult; +import network.balanced.score.core.dex.structs.pool.NextInitializedTickWithinOneWordResult; +import network.balanced.score.core.dex.structs.pool.ObserveResult; +import network.balanced.score.core.dex.structs.pool.Oracle; +import network.balanced.score.core.dex.structs.pool.PairAmounts; +import network.balanced.score.core.dex.structs.pool.PoolSettings; +import network.balanced.score.core.dex.structs.pool.Position; +import network.balanced.score.core.dex.structs.pool.PositionStorage; +import network.balanced.score.core.dex.structs.pool.ProtocolFees; +import network.balanced.score.core.dex.structs.pool.Slot0; +import network.balanced.score.core.dex.structs.pool.SnapshotCumulativesInsideResult; +import network.balanced.score.core.dex.structs.pool.StepComputations; +import network.balanced.score.core.dex.structs.pool.SwapCache; +import network.balanced.score.core.dex.structs.pool.SwapState; +import network.balanced.score.core.dex.structs.pool.Tick; +import network.balanced.score.core.dex.utils.TimeUtils; +import network.balanced.score.lib.utils.Names; +import score.Address; +import score.Context; +import score.VarDB; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; + +public class ConcentratedLiquidityPool { + // ================================================ + // Consts + // ================================================ + // Contract class name + public static final String NAME = Names.CONCENTRATED_LIQUIDITY_POOL; + + // Observations default cardinality + public static final int DEFAULT_OBSERVATIONS_CARDINALITY = 1024; + + // Pool settings + private final PoolSettings settings; + + // ================================================ + // DB Variables + // ================================================ + // The 0th storage slot in the pool stores many values, and is exposed as a single method to save steps when accessed externally. + protected final VarDB slot0 = Context.newVarDB(NAME + "_slot0", Slot0.class); + + // The fee growth as a Q128.128 fees of token0 collected per unit of liquidity for the entire life of the pool + protected final VarDB feeGrowthGlobal0X128 = Context.newVarDB(NAME + "_feeGrowthGlobal0X128", BigInteger.class); + + // The fee growth as a Q128.128 fees of token1 collected per unit of liquidity for the entire life of the pool + protected final VarDB feeGrowthGlobal1X128 = Context.newVarDB(NAME + "_feeGrowthGlobal1X128", BigInteger.class); + + // The amounts of token0 and token1 that are owed to the protocol + protected final VarDB protocolFees = Context.newVarDB(NAME + "_protocolFees", ProtocolFees.class); + + // The amounts of token0 and token1 that are owed to the protocol + protected final VarDB liquidity = Context.newVarDB(NAME + "_liquidity", BigInteger.class); + + // Implements IObservations + // Returns data about a specific observation index + protected final Observations observations = new Observations(); + + // Implements IPositions + // Returns the information about a position by the position's key + protected final Positions positions = new Positions(); + + // Implements ITickBitmap + // Returns 256 packed tick initialized boolean values. See TickBitmap for more information + protected final TickBitmap tickBitmap = new TickBitmap(); + + // Implements ITicks + // Look up information about a specific tick in the pool + protected final Ticks ticks = new Ticks(); + + // ================================================ + // Event Logs + // ================================================ + /** + * @notice Emitted by the pool for increases to the number of observations that can be stored + * @dev observationCardinalityNext is not the observation cardinality until an observation is written at the index + * just before a mint/swap/burn. + * @param observationCardinalityNextOld The previous value of the next observation cardinality + * @param observationCardinalityNextNew The updated value of the next observation cardinality + */ + @EventLog + public void IncreaseObservationCardinalityNext ( + int observationCardinalityNextOld, + int observationCardinalityNextNew + ) {} + + /** + * @notice Emitted exactly once by a pool when #initialize is first called on the pool + * @dev Mint/Burn/Swap cannot be emitted by the pool before Initialize + * @param sqrtPriceX96 The initial sqrt price of the pool, as a Q64.96 + * @param tick The initial tick of the pool, i.e. log base 1.0001 of the starting price of the pool + */ + @EventLog + public void Initialized ( + BigInteger sqrtPriceX96, + int tick + ) {} + + + /** + * @notice Emitted whenever pool intrinsics are updated + * @param sqrtPriceX96 The sqrt price of the pool, as a Q64.96 + * @param tick The current tick of the pool, i.e. log base 1.0001 of the starting price of the pool + * @param liquidity The current liquidity of the pool + */ + @EventLog + public void PoolIntrinsicsUpdate ( + BigInteger sqrtPriceX96, + int tick, + BigInteger liquidity + ) {} + + /** + * @notice Emitted when liquidity is minted for a given position + * @param sender The address that minted the liquidity + * @param owner The owner of the position and recipient of any minted liquidity + * @param tickLower The lower tick of the position + * @param tickUpper The upper tick of the position + * @param amount The amount of liquidity minted to the position range + * @param amount0 How much token0 was required for the minted liquidity + * @param amount1 How much token1 was required for the minted liquidity + */ + @EventLog(indexed = 3) + public void Mint ( + Address recipient, + int tickLower, + int tickUpper, + Address sender, + BigInteger amount, + BigInteger amount0, + BigInteger amount1 + ) {} + + /** + * @notice Emitted when fees are collected by the owner of a position + * @dev Collect events may be emitted with zero amount0 and amount1 when the caller chooses not to collect fees + * @param owner The owner of the position for which fees are collected + * @param tickLower The lower tick of the position + * @param tickUpper The upper tick of the position + * @param amount0 The amount of token0 fees collected + * @param amount1 The amount of token1 fees collected + */ + @EventLog(indexed = 3) + public void Collect ( + Address caller, + int tickLower, + int tickUpper, + Address recipient, + BigInteger amount0, + BigInteger amount1 + ) {} + + /** + * @notice Emitted when a position's liquidity is removed + * @dev Does not withdraw any fees earned by the liquidity position, which must be withdrawn via #collect + * @param owner The owner of the position for which liquidity is removed + * @param tickLower The lower tick of the position + * @param tickUpper The upper tick of the position + * @param amount The amount of liquidity to remove + * @param amount0 The amount of token0 withdrawn + * @param amount1 The amount of token1 withdrawn + */ + @EventLog(indexed = 3) + public void Burn ( + Address caller, + int tickLower, + int tickUpper, + BigInteger amount, + BigInteger amount0, + BigInteger amount1 + ) {} + + /** + * @notice Emitted by the pool for any swaps between token0 and token1 + * @param sender The address that initiated the swap call, and that received the callback + * @param recipient The address that received the output of the swap + * @param amount0 The delta of the token0 balance of the pool + * @param amount1 The delta of the token1 balance of the pool + * @param sqrtPriceX96 The sqrt(price) of the pool after the swap, as a Q64.96 + * @param liquidity The liquidity of the pool after the swap + * @param tick The log base 1.0001 of price of the pool after the swap + */ + @EventLog(indexed = 2) + public void Swap ( + Address sender, + Address recipient, + BigInteger amount0, + BigInteger amount1, + BigInteger sqrtPriceX96, + BigInteger liquidity, + int tick + ) {} + + /** + * @notice Emitted by the pool for any flashes of token0/token1 + * @param sender The address that initiated the swap call, and that received the callback + * @param recipient The address that received the tokens from flash + * @param amount0 The amount of token0 that was flashed + * @param amount1 The amount of token1 that was flashed + * @param paid0 The amount of token0 paid for the flash, which can exceed the amount0 plus the fee + * @param paid1 The amount of token1 paid for the flash, which can exceed the amount1 plus the fee + */ + @EventLog(indexed = 2) + public void Flash ( + Address sender, + Address recipient, + BigInteger amount0, + BigInteger amount1, + BigInteger paid0, + BigInteger paid1 + ) {} + + /** + * @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 + */ + @EventLog + public void SetFeeProtocol ( + int feeProtocol0Old, + int feeProtocol1Old, + int feeProtocol0New, + int feeProtocol1New + ) {} + + /** + * @notice Emitted when the collected protocol fees are withdrawn by the factory owner + * @param sender The address that collects the protocol fees + * @param recipient The address that receives the collected protocol fees + * @param amount0 The amount of token0 protocol fees that is withdrawn + * @param amount0 The amount of token1 protocol fees that is withdrawn + */ + @EventLog(indexed = 2) + public void CollectProtocol ( + Address sender, + Address recipient, + BigInteger amount0, + BigInteger amount1 + ) {} + + /** + * @notice Emitted whenever a tick is modified in the Ticks DB + * @param index See {@code Tick.Info} for information about the parameters + */ + @EventLog(indexed = 1) + public void TickUpdate ( + int index, + BigInteger liquidityGross, + BigInteger liquidityNet, + BigInteger feeGrowthOutside0X128, + BigInteger feeGrowthOutside1X128, + BigInteger tickCumulativeOutside, + BigInteger secondsPerLiquidityOutsideX128, + BigInteger secondsOutside, + boolean initialized + ) {} + + // ================================================ + // Methods + // ================================================ + /** + * @notice Contract constructor + * See {@code ConcentratedLiquidityPoolFactored} constructor for the actual pool deployed on the network + */ + public ConcentratedLiquidityPool (Parameters parameters) { + // Initialize settings + this.settings = new PoolSettings ( + parameters.factory, + parameters.token0, + parameters.token1, + parameters.fee, + parameters.tickSpacing, + TickLib.tickSpacingToMaxLiquidityPerTick(parameters.tickSpacing), + "Balanced Concentrated Liquidity Pool (" + IIRC2ICX.symbol(parameters.token0) + " / " + IIRC2ICX.symbol(parameters.token1) + " " + ((float) parameters.fee / 10000) + "%)" + ); + + // Default values + if (this.liquidity.get() == null) { + this.liquidity.set(ZERO); + } + if (this.feeGrowthGlobal0X128.get() == null) { + this.feeGrowthGlobal0X128.set(ZERO); + } + if (this.feeGrowthGlobal1X128.get() == null) { + this.feeGrowthGlobal1X128.set(ZERO); + } + if (this.protocolFees.get() == null) { + this.protocolFees.set(new ProtocolFees(ZERO, ZERO)); + } + } + + /** + * @notice Increase the maximum number of price and liquidity observations that this pool will store + * + * Access: Everyone + * + * @dev This method is no-op if the pool already has an observationCardinalityNext greater than or equal to + * the input observationCardinalityNext. + * @param observationCardinalityNext The desired minimum number of observations for the pool to store + */ + @External + public void increaseObservationCardinalityNext (int observationCardinalityNext) { + this.unlock(false); + + Slot0 _slot0 = this.slot0.get(); + int observationCardinalityNextOld = _slot0.observationCardinalityNext; + int observationCardinalityNextNew = this.observations.grow(observationCardinalityNextOld, observationCardinalityNext); + + _slot0.observationCardinalityNext = observationCardinalityNextNew; + this.slot0.set(_slot0); + + if (observationCardinalityNextOld != observationCardinalityNextNew) { + this.IncreaseObservationCardinalityNext(observationCardinalityNextOld, observationCardinalityNextNew); + } + + this.unlock(true); + } + + /** + * Enable or disable the lock + * @param state Lock state + */ + public void unlock (boolean state) { + // Check current unlock state + var slot0 = this.slot0.get(); + Context.require(slot0 != null, + "unlock: pool isn't initialized yet"); + boolean unlock_state = slot0.unlocked; + Context.require(state != unlock_state, + NAME + "::unlock: wrong lock state: " + unlock_state); + + // OK + slot0.unlocked = state; + this.slot0.set(slot0); + } + + /** + * Sets the initial price for the pool + * + * Access: Everyone + * + * @dev Price is represented as a sqrt(amountToken1/amountToken0) Q64.96 value + * @param sqrtPriceX96 the initial sqrt price of the pool as a Q64.96 + * @dev not locked because it initializes unlocked + */ + @External + public void initialize (BigInteger sqrtPriceX96) { + Context.require(this.slot0.get() == null, + "initialize: this pool is already initialized"); + + int tick = TickMath.getTickAtSqrtRatio(sqrtPriceX96); + + var result = this.observations.initialize(TimeUtils.now()); + + this.slot0.set(new Slot0( + sqrtPriceX96, + tick, + 0, + result.cardinality, + result.cardinalityNext, + 0, + // Unlock the pool + true + )); + + // Set observations at 1024 by default + this.increaseObservationCardinalityNext(DEFAULT_OBSERVATIONS_CARDINALITY); + + this.Initialized(sqrtPriceX96, tick); + this.PoolIntrinsicsUpdate(sqrtPriceX96, tick, ZERO); + } + + /** + * @notice Adds liquidity for the given recipient/tickLower/tickUpper position + * + * Access: Everyone + * + * @dev The caller of this method receives a callback in the form of balancedMintCallback + * in which they must pay any token0 or token1 owed for the liquidity. The amount of token0/token1 due depends + * on tickLower, tickUpper, the amount of liquidity, and the current price. + * @param recipient The address for which the liquidity will be created + * @param tickLower The lower tick of the position in which to add liquidity + * @param tickUpper The upper tick of the position in which to add liquidity + * @param amount The amount of liquidity to mint + * @param data Any data that should be passed through to the callback + * @return amount0 The amount of token0 that was paid to mint the given amount of liquidity. Matches the value in the callback + * @return amount1 The amount of token1 that was paid to mint the given amount of liquidity. Matches the value in the callback + */ + @External + public PairAmounts mint ( + Address recipient, + int tickLower, + int tickUpper, + BigInteger amount, + byte[] data + ) { + this.unlock(false); + + final Address caller = Context.getCaller(); + + Context.require(amount.compareTo(ZERO) > 0, + "mint: amount must be superior to 0"); + + BigInteger amount0; + BigInteger amount1; + + var result = _modifyPosition(new ModifyPositionParams( + recipient, + tickLower, + tickUpper, + amount + )); + + amount0 = result.amount0; + amount1 = result.amount1; + + BigInteger balance0Before = ZERO; + BigInteger balance1Before = ZERO; + + if (amount0.compareTo(ZERO) > 0) { + balance0Before = balance0(); + } + if (amount1.compareTo(ZERO) > 0) { + balance1Before = balance1(); + } + + IConcentratedLiquidityPoolCallee.balancedMintCallback(caller, amount0, amount1, data); + + if (amount0.compareTo(ZERO) > 0) { + BigInteger expected = balance0Before.add(amount0); + BigInteger balance0 = balance0(); + Context.require(expected.compareTo(balance0) <= 0, + "mint: callback didn't send enough of token0, expected " + expected + ", got " + balance0); + } + if (amount1.compareTo(ZERO) > 0) { + BigInteger expected = balance1Before.add(amount1); + BigInteger balance1 = balance1(); + Context.require(expected.compareTo(balance1()) <= 0, + "mint: callback didn't send enough of token1, expected " + expected + ", got " + balance1); + } + + this.Mint(recipient, tickLower, tickUpper, caller, amount, amount0, amount1); + + this.unlock(true); + return new PairAmounts(amount0, amount1); + } + + /** + * @notice Collects tokens owed to a position + * + * Access: Everyone + * + * @dev Does not recompute fees earned, which must be done either via mint or burn of any amount of liquidity. + * Collect must be called by the position owner. To withdraw only token0 or only token1, amount0Requested or + * amount1Requested may be set to zero. To withdraw all tokens owed, caller may pass any value greater than the + * actual tokens owed, e.g. type(uint128).max. Tokens owed may be from accumulated swap fees or burned liquidity. + * @param recipient The address which should receive the fees collected + * @param tickLower The lower tick of the position for which to collect fees + * @param tickUpper The upper tick of the position for which to collect fees + * @param amount0Requested How much token0 should be withdrawn from the fees owed + * @param amount1Requested How much token1 should be withdrawn from the fees owed + * @return amount0 The amount of fees collected in token0 + * @return amount1 The amount of fees collected in token1 + */ + @External + public PairAmounts collect ( + Address recipient, + int tickLower, + int tickUpper, + BigInteger amount0Requested, + BigInteger amount1Requested + ) { + this.unlock(false); + + final Address caller = Context.getCaller(); + + // we don't need to checkTicks here, because invalid positions will never have non-zero tokensOwed{0,1} + byte[] key = Positions.getKey(caller, tickLower, tickUpper); + Position.Info position = this.positions.get(key); + + BigInteger amount0 = amount0Requested.compareTo(position.tokensOwed0) > 0 ? position.tokensOwed0 : amount0Requested; + BigInteger amount1 = amount1Requested.compareTo(position.tokensOwed1) > 0 ? position.tokensOwed1 : amount1Requested; + + if (amount0.compareTo(ZERO) > 0) { + position.tokensOwed0 = position.tokensOwed0.subtract(amount0); + this.positions.set(key, position); + pay(this.settings.token0, recipient, amount0); + } + if (amount1.compareTo(ZERO) > 0) { + position.tokensOwed1 = position.tokensOwed1.subtract(amount1); + this.positions.set(key, position); + pay(this.settings.token1, recipient, amount1); + } + + this.Collect(caller, tickLower, tickUpper, recipient, amount0, amount1); + + this.unlock(true); + return new PairAmounts(amount0, amount1); + } + + /** + * @notice Burn liquidity from the sender and account tokens owed for the liquidity to the position + * + * Access: Everyone + * + * @dev Can be used to trigger a recalculation of fees owed to a position by calling with an amount of 0 + * @dev Fees must be collected separately via a call to #collect + * @param tickLower The lower tick of the position for which to burn liquidity + * @param tickUpper The upper tick of the position for which to burn liquidity + * @param amount How much liquidity to burn + * @return amount0 The amount of token0 sent to the recipient + * @return amount1 The amount of token1 sent to the recipient + */ + @External + public PairAmounts burn ( + int tickLower, + int tickUpper, + BigInteger amount + ) { + this.unlock(false); + final Address caller = Context.getCaller(); + + var result = _modifyPosition(new ModifyPositionParams( + caller, + tickLower, + tickUpper, + amount.negate() + )); + + BigInteger amount0 = result.amount0.negate(); + BigInteger amount1 = result.amount1.negate(); + Position.Info position = result.positionStorage.position; + byte[] positionKey = result.positionStorage.key; + + if (amount0.compareTo(ZERO) > 0 || amount1.compareTo(ZERO) > 0) { + position.tokensOwed0 = position.tokensOwed0.add(amount0); + position.tokensOwed1 = position.tokensOwed1.add(amount1); + this.positions.set(positionKey, position); + } + + this.Burn(caller, tickLower, tickUpper, amount, amount0, amount1); + + this.unlock(true); + return new PairAmounts(amount0, amount1); + } + + /** + * @notice Swap token0 for token1, or token1 for token0 + * + * Access: Everyone + * + * @dev The caller of this method receives a callback in the form of balancedSwapCallback + * @param recipient The address to receive the output of the swap + * @param zeroForOne The direction of the swap, true for token0 to token1, false for token1 to token0 + * @param amountSpecified The amount of the swap, which implicitly configures the swap as exact input (positive), or exact output (negative) + * @param sqrtPriceLimitX96 The Q64.96 sqrt price limit. If zero for one, the price cannot be less than this value after the swap. If one for zero, the price cannot be greater than this value after the swap. + * @param data Any data to be passed through to the callback + * @return amount0 The delta of the balance of token0 of the pool, exact when negative, minimum when positive + * @return amount1 The delta of the balance of token1 of the pool, exact when negative, minimum when positive + */ + @External + public PairAmounts swap ( + Address recipient, + boolean zeroForOne, + BigInteger amountSpecified, + BigInteger sqrtPriceLimitX96, + byte[] data + ) { + this.unlock(false); + final Address caller = Context.getCaller(); + + Context.require(!amountSpecified.equals(ZERO), + "swap: amountSpecified must be different from zero"); + + Slot0 slot0Start = this.slot0.get(); + + Context.require ( + zeroForOne + ? sqrtPriceLimitX96.compareTo(slot0Start.sqrtPriceX96) < 0 && sqrtPriceLimitX96.compareTo(TickMath.MIN_SQRT_RATIO) > 0 + : sqrtPriceLimitX96.compareTo(slot0Start.sqrtPriceX96) > 0 && sqrtPriceLimitX96.compareTo(TickMath.MAX_SQRT_RATIO) < 0, + "swap: Wrong sqrtPriceLimitX96" + ); + + SwapCache cache = new SwapCache( + this.liquidity.get(), + TimeUtils.now(), + zeroForOne ? (slot0Start.feeProtocol % 16) : (slot0Start.feeProtocol >> 4), + ZERO, + ZERO, + false + ); + + boolean exactInput = amountSpecified.compareTo(ZERO) > 0; + + SwapState state = new SwapState( + amountSpecified, + ZERO, + slot0Start.sqrtPriceX96, + slot0Start.tick, + zeroForOne ? feeGrowthGlobal0X128.get() : feeGrowthGlobal1X128.get(), + ZERO, + cache.liquidityStart + ); + + // continue swapping as long as we haven't used the entire input/output and haven't reached the price limit + while ( + !state.amountSpecifiedRemaining.equals(ZERO) + && !state.sqrtPriceX96.equals(sqrtPriceLimitX96) + ) { + + StepComputations step = new StepComputations(); + step.sqrtPriceStartX96 = state.sqrtPriceX96; + + var next = tickBitmap.nextInitializedTickWithinOneWord( + state.tick, + this.settings.tickSpacing, + zeroForOne + ); + + step.tickNext = next.tickNext; + step.initialized = next.initialized; + + // 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) { + step.tickNext = TickMath.MIN_TICK; + } else if (step.tickNext > TickMath.MAX_TICK) { + step.tickNext = TickMath.MAX_TICK; + } + + // get the price for the next tick + step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext); + + // compute values to swap to the target tick, price limit, or point where input/output amount is exhausted + var swapStep = SwapMath.computeSwapStep( + state.sqrtPriceX96, + (zeroForOne ? step.sqrtPriceNextX96.compareTo(sqrtPriceLimitX96) < 0 : step.sqrtPriceNextX96.compareTo(sqrtPriceLimitX96) > 0) + ? sqrtPriceLimitX96 + : step.sqrtPriceNextX96, + state.liquidity, + state.amountSpecifiedRemaining, + this.settings.fee + ); + + state.sqrtPriceX96 = swapStep.sqrtRatioNextX96; + step.amountIn = swapStep.amountIn; + step.amountOut = swapStep.amountOut; + step.feeAmount = swapStep.feeAmount; + + if (exactInput) { + state.amountSpecifiedRemaining = state.amountSpecifiedRemaining.subtract(step.amountIn.add(step.feeAmount)); + state.amountCalculated = state.amountCalculated.subtract(step.amountOut); + } else { + state.amountSpecifiedRemaining = state.amountSpecifiedRemaining.add(step.amountOut); + state.amountCalculated = state.amountCalculated.add((step.amountIn.add(step.feeAmount))); + } + + // if the protocol fee is on, calculate how much is owed, decrement feeAmount, and increment protocolFee + if (cache.feeProtocol > 0) { + BigInteger delta = step.feeAmount.divide(BigInteger.valueOf(cache.feeProtocol)); + step.feeAmount = step.feeAmount.subtract(delta); + state.protocolFee = state.protocolFee.add(delta); + } + + // update global fee tracker + if (state.liquidity.compareTo(ZERO) > 0) { + state.feeGrowthGlobalX128 = state.feeGrowthGlobalX128.add(FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity)); + } + + // shift tick if we reached the next price + if (state.sqrtPriceX96.equals(step.sqrtPriceNextX96)) { + // if the tick is initialized, run the tick transition + if (step.initialized) { + // check for the placeholder value, which we replace with the actual value the first time the swap + // crosses an initialized tick + if (!cache.computedLatestObservation) { + var result = observations.observeSingle( + cache.blockTimestamp, + ZERO, + slot0Start.tick, + slot0Start.observationIndex, + cache.liquidityStart, + slot0Start.observationCardinality + ); + cache.tickCumulative = result.tickCumulative; + cache.secondsPerLiquidityCumulativeX128 = result.secondsPerLiquidityCumulativeX128; + cache.computedLatestObservation = true; + } + Tick.Info info = ticks.cross( + step.tickNext, + (zeroForOne ? state.feeGrowthGlobalX128 : this.feeGrowthGlobal0X128.get()), + (zeroForOne ? this.feeGrowthGlobal1X128.get() : state.feeGrowthGlobalX128), + cache.secondsPerLiquidityCumulativeX128, + cache.tickCumulative, + cache.blockTimestamp + ); + BigInteger liquidityNet = info.liquidityNet; + this.onTickUpdate(step.tickNext, info); + + // if we're moving leftward, we interpret liquidityNet as the opposite sign + // safe because liquidityNet cannot be type(int128).min + if (zeroForOne) liquidityNet = liquidityNet.negate(); + + state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet); + } + + state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext; + } else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) { + // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved + state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96); + } + } + + // update tick and write an oracle entry if the tick change + Slot0 _slot0 = this.slot0.get(); + if (state.tick != slot0Start.tick) { + var result = + this.observations.write( + slot0Start.observationIndex, + cache.blockTimestamp, + slot0Start.tick, + cache.liquidityStart, + slot0Start.observationCardinality, + slot0Start.observationCardinalityNext + ); + _slot0.sqrtPriceX96 = state.sqrtPriceX96; + _slot0.tick = state.tick; + _slot0.observationIndex = result.observationIndex; + _slot0.observationCardinality = result.observationCardinality; + } else { + // otherwise just update the price + _slot0.sqrtPriceX96 = state.sqrtPriceX96; + } + this.slot0.set(_slot0); + + // update liquidity if it changed + if (cache.liquidityStart != state.liquidity) { + this.liquidity.set(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) { + this.feeGrowthGlobal0X128.set(state.feeGrowthGlobalX128); + if (state.protocolFee.compareTo(ZERO) > 0) { + var _protocolFees = this.protocolFees.get(); + _protocolFees.token0 = _protocolFees.token0.add(state.protocolFee); + this.protocolFees.set(_protocolFees); + } + } else { + this.feeGrowthGlobal1X128.set(state.feeGrowthGlobalX128); + if (state.protocolFee.compareTo(ZERO) > 0) { + var _protocolFees = this.protocolFees.get(); + _protocolFees.token1 = _protocolFees.token1.add(state.protocolFee); + this.protocolFees.set(_protocolFees); + } + } + + BigInteger amount0; + BigInteger amount1; + + if (zeroForOne == exactInput) { + amount0 = amountSpecified.subtract(state.amountSpecifiedRemaining); + amount1 = state.amountCalculated; + } else { + amount0 = state.amountCalculated; + amount1 = amountSpecified.subtract(state.amountSpecifiedRemaining); + } + + // do the transfers and collect payment + if (zeroForOne) { + if (amount1.compareTo(ZERO) < 0) { + pay(this.settings.token1, recipient, amount1.negate()); + } + + BigInteger balance0Before = balance0(); + IConcentratedLiquidityPoolCallee.balancedSwapCallback(caller, amount0, amount1, data); + + Context.require(balance0Before.add(amount0).compareTo(balance0()) <= 0, + "swap: the callback didn't charge the payment (1)"); + } else { + if (amount0.compareTo(ZERO) < 0) { + pay(this.settings.token0, recipient, amount0.negate()); + } + + BigInteger balance1Before = balance1(); + IConcentratedLiquidityPoolCallee.balancedSwapCallback(caller, amount0, amount1, data); + + Context.require(balance1Before.add(amount1).compareTo(balance1()) <= 0, + "swap: the callback didn't charge the payment (2)"); + } + + this.PoolIntrinsicsUpdate(state.sqrtPriceX96, state.tick, state.liquidity); + this.Swap(caller, recipient, amount0, amount1, state.sqrtPriceX96, state.liquidity, state.tick); + this.unlock(true); + + return new PairAmounts(amount0, amount1); + } + + /** + * @notice Receive token0 and/or token1 and pay it back, plus a fee, in the callback + * + * Access: Everyone + * + * @dev The caller of this method receives a callback in the form of balancedFlashCallback + * @dev Can be used to donate underlying tokens pro-rata to currently in-range liquidity providers by calling + * with 0 amount{0,1} and sending the donation amount(s) from the callback + * @param recipient The address which will receive the token0 and token1 amounts + * @param amount0 The amount of token0 to send + * @param amount1 The amount of token1 to send + * @param data Any data to be passed through to the callback + */ + @External + public void flash ( + Address recipient, + BigInteger amount0, + BigInteger amount1, + byte[] data + ) { + this.unlock(false); + final Address caller = Context.getCaller(); + + BigInteger _liquidity = this.liquidity.get(); + Context.require(_liquidity.compareTo(ZERO) > 0, + "flash: no liquidity"); + + final BigInteger TEN_E6 = BigInteger.valueOf(1000000); + + BigInteger fee0 = FullMath.mulDivRoundingUp(amount0, BigInteger.valueOf(this.settings.fee), TEN_E6); + BigInteger fee1 = FullMath.mulDivRoundingUp(amount1, BigInteger.valueOf(this.settings.fee), TEN_E6); + BigInteger balance0Before = balance0(); + BigInteger balance1Before = balance1(); + + if (amount0.compareTo(ZERO) > 0) { + pay(this.settings.token0, recipient, amount0); + } + if (amount1.compareTo(ZERO) > 0) { + pay(this.settings.token1, recipient, amount1); + } + + IConcentratedLiquidityPoolCallee.balancedFlashCallback(caller, fee0, fee1, data); + + BigInteger balance0After = balance0(); + BigInteger balance1After = balance1(); + + Context.require(balance0Before.add(fee0).compareTo(balance0After) <= 0, + "flash: not enough token0 returned"); + + Context.require(balance1Before.add(fee1).compareTo(balance1After) <= 0, + "flash: not enough token1 returned"); + + // sub is safe because we know balanceAfter is gt balanceBefore by at least fee + BigInteger paid0 = balance0After.subtract(balance0Before); + BigInteger paid1 = balance1After.subtract(balance1Before); + + Slot0 _slot0 = this.slot0.get(); + + if (paid0.compareTo(ZERO) > 0) { + int feeProtocol0 = _slot0.feeProtocol % 16; + BigInteger fees0 = feeProtocol0 == 0 ? ZERO : paid0.divide(BigInteger.valueOf(feeProtocol0)); + if (fees0.compareTo(ZERO) > 0) { + var _protocolFees = protocolFees.get(); + _protocolFees.token0 = _protocolFees.token0.add(fees0); + protocolFees.set(_protocolFees); + } + this.feeGrowthGlobal0X128.set(uint256(this.feeGrowthGlobal0X128.get().add(FullMath.mulDiv(paid0.subtract(fees0), FixedPoint128.Q128, _liquidity)))); + } + if (paid1.compareTo(ZERO) > 0) { + int feeProtocol1 = _slot0.feeProtocol >> 4; + BigInteger fees1 = feeProtocol1 == 0 ? ZERO : paid1.divide(BigInteger.valueOf(feeProtocol1)); + if (fees1.compareTo(ZERO) > 0) { + var _protocolFees = protocolFees.get(); + _protocolFees.token1 = _protocolFees.token1.add(fees1); + protocolFees.set(_protocolFees); + } + this.feeGrowthGlobal1X128.set(uint256(this.feeGrowthGlobal1X128.get().add(FullMath.mulDiv(paid1.subtract(fees1), FixedPoint128.Q128, _liquidity)))); + } + + this.Flash(caller, recipient, amount0, amount1, paid0, paid1); + + this.unlock(true); + } + + /** + * @notice Set the denominator of the protocol's % share of the fees + * + * Access: Factory Owner + * + * @param feeProtocol0 new protocol fee for token0 of the pool + * @param feeProtocol1 new protocol fee for token1 of the pool + */ + @External + public void setFeeProtocol ( + int feeProtocol0, + int feeProtocol1 + ) { + this.unlock(false); + + // Access control + this.checkCallerIsFactoryOwner(); + + // Check user input for protocol fees + Context.require( + (feeProtocol0 == 0 || (feeProtocol0 >= 4 && feeProtocol0 <= 10)) && + (feeProtocol1 == 0 || (feeProtocol1 >= 4 && feeProtocol1 <= 10)), + "setFeeProtocol: Bad fees amount" + ); + + // OK + Slot0 _slot0 = this.slot0.get(); + int feeProtocolOld = _slot0.feeProtocol; + _slot0.feeProtocol = feeProtocol0 + (feeProtocol1 << 4); + this.slot0.set(_slot0); + + this.SetFeeProtocol(feeProtocolOld % 16, feeProtocolOld >> 4, feeProtocol0, feeProtocol1); + + this.unlock(true); + } + + /** + * @notice Collect the protocol fee accrued to the pool + * + * Access: Factory Owner + * + * @param recipient The address to which collected protocol fees should be sent + * @param amount0Requested The maximum amount of token0 to send, can be 0 to collect fees in only token1 + * @param amount1Requested The maximum amount of token1 to send, can be 0 to collect fees in only token0 + * @return amount0 The protocol fee collected in token0 + * @return amount1 The protocol fee collected in token1 + */ + @External + public PairAmounts collectProtocol ( + Address recipient, + BigInteger amount0Requested, + BigInteger amount1Requested + ) { + this.unlock(false); + + // Access control + this.checkCallerIsFactoryOwner(); + + // OK + var _protocolFees = protocolFees.get(); + final Address caller = Context.getCaller(); + + BigInteger amount0 = amount0Requested.compareTo(_protocolFees.token0) > 0 ? _protocolFees.token0 : amount0Requested; + BigInteger amount1 = amount1Requested.compareTo(_protocolFees.token1) > 0 ? _protocolFees.token1 : amount1Requested; + + if (amount0.compareTo(ZERO) > 0) { + if (amount0.equals(_protocolFees.token0)) { + // ensure that the slot is not cleared, for steps savings + amount0 = amount0.subtract(BigInteger.ONE); + } + _protocolFees.token0 = _protocolFees.token0.subtract(amount0); + this.protocolFees.set(_protocolFees); + pay(this.settings.token0, recipient, amount0); + } + if (amount1.compareTo(ZERO) > 0) { + if (amount1.equals(_protocolFees.token1)) { + // ensure that the slot is not cleared, for steps savings + amount1 = amount1.subtract(BigInteger.ONE); + } + _protocolFees.token1 = _protocolFees.token1.subtract(amount1); + this.protocolFees.set(_protocolFees); + pay(this.settings.token1, recipient, amount1); + } + + this.CollectProtocol(caller, recipient, amount0, amount1); + + this.unlock(true); + return new PairAmounts(amount0, amount1); + } + + @External + @Payable + public void depositIcx () { + Context.require(Context.getCaller().isContract(), + "depositIcx: Pool shouldn't need to receive ICX from EOA"); + } + + @External + public void tokenFallback (Address _from, BigInteger _value, @Optional byte[] _data) throws Exception { + Context.require(_from.isContract(), + "tokenFallback: Pool shouldn't need to receive tokens from EOA"); + } + + // ================================================ + // ReadOnly methods + // ================================================ + /** + * @notice Returns a snapshot of the tick cumulative, seconds per liquidity and seconds inside a tick range + * + * Access: Everyone + * + * @dev Snapshots must only be compared to other snapshots, taken over a period for which a position existed. + * I.e., snapshots cannot be compared if a position is not held for the entire period between when the first + * snapshot is taken and the second snapshot is taken. + * @param tickLower The lower tick of the range + * @param tickUpper The upper tick of the range + * @return tickCumulativeInside The snapshot of the tick accumulator for the range + * @return secondsPerLiquidityInsideX128 The snapshot of seconds per liquidity for the range + * @return secondsInside The snapshot of seconds per liquidity for the range + */ + @External(readonly = true) + public SnapshotCumulativesInsideResult snapshotCumulativesInside (int tickLower, int tickUpper) { + checkTicks(tickLower, tickUpper); + + Tick.Info lower = this.ticks.get(tickLower); + Tick.Info upper = this.ticks.get(tickUpper); + + BigInteger tickCumulativeLower = lower.tickCumulativeOutside; + BigInteger tickCumulativeUpper = upper.tickCumulativeOutside; + BigInteger secondsPerLiquidityOutsideLowerX128 = lower.secondsPerLiquidityOutsideX128; + BigInteger secondsPerLiquidityOutsideUpperX128 = upper.secondsPerLiquidityOutsideX128; + BigInteger secondsOutsideLower = lower.secondsOutside; + BigInteger secondsOutsideUpper = upper.secondsOutside; + + Context.require(lower.initialized, + "snapshotCumulativesInside: lower not initialized"); + Context.require(upper.initialized, + "snapshotCumulativesInside: upper not initialized"); + + Slot0 _slot0 = this.slot0.get(); + + if (_slot0.tick < tickLower) { + return new SnapshotCumulativesInsideResult( + tickCumulativeLower.subtract(tickCumulativeUpper), + secondsPerLiquidityOutsideLowerX128.subtract(secondsPerLiquidityOutsideUpperX128), + secondsOutsideLower.subtract(secondsOutsideUpper) + ); + } else if (_slot0.tick < tickUpper) { + BigInteger time = TimeUtils.now(); + Observations.ObserveSingleResult result = this.observations.observeSingle( + time, + ZERO, + _slot0.tick, + _slot0.observationIndex, + this.liquidity.get(), + _slot0.observationCardinality + ); + BigInteger tickCumulative = result.tickCumulative; + BigInteger secondsPerLiquidityCumulativeX128 = result.secondsPerLiquidityCumulativeX128; + + return new SnapshotCumulativesInsideResult( + tickCumulative.subtract(tickCumulativeLower).subtract(tickCumulativeUpper), + secondsPerLiquidityCumulativeX128.subtract(secondsPerLiquidityOutsideLowerX128).subtract(secondsPerLiquidityOutsideUpperX128), + time.subtract(secondsOutsideLower).subtract(secondsOutsideUpper) + ); + } else { + return new SnapshotCumulativesInsideResult ( + tickCumulativeUpper.subtract(tickCumulativeLower), + secondsPerLiquidityOutsideUpperX128.subtract(secondsPerLiquidityOutsideLowerX128), + secondsOutsideUpper.subtract(secondsOutsideLower) + ); + } + } + + /** + * @notice Returns the cumulative tick and liquidity as of each timestamp `secondsAgo` from the current block timestamp + * + * Access: Everyone + * + * @dev To get a time weighted average tick or liquidity-in-range, you must call this with two values, one representing + * the beginning of the period and another for the end of the period. E.g., to get the last hour time-weighted average tick, + * you must call it with secondsAgos = [3600, 0]. + * @dev The time weighted average tick represents the geometric time weighted average price of the pool, in + * log base sqrt(1.0001) of token1 / token0. The TickMath library can be used to go from a tick value to a ratio. + * @param secondsAgos From how long ago each cumulative tick and liquidity value should be returned + * @return tickCumulatives Cumulative tick values as of each `secondsAgos` from the current block timestamp + * @return secondsPerLiquidityCumulativeX128s Cumulative seconds per liquidity-in-range value as of each `secondsAgos` from the current block + * timestamp + */ + @External(readonly = true) + public ObserveResult observe (BigInteger[] secondsAgos) { + Slot0 _slot0 = this.slot0.get(); + return this.observations.observe( + TimeUtils.now(), + secondsAgos, + _slot0.tick, + _slot0.observationIndex, + this.liquidity.get(), + _slot0.observationCardinality + ); + } + + // ================================================ + // Private methods + // ================================================ + private void pay (Address token, Address recipient, BigInteger amount) { + IIRC2ICX.transfer(token, recipient, amount, "deposit"); + } + + /** + * @dev Gets and updates a position with the given liquidity delta + * @param owner the owner of the position + * @param tickLower the lower tick of the position's tick range + * @param tickUpper the upper tick of the position's tick range + * @param tick the current tick, passed to avoid sloads + */ + private PositionStorage _updatePosition ( + Address owner, + int tickLower, + int tickUpper, + BigInteger liquidityDelta, + int tick + ) { + byte[] positionKey = Positions.getKey(owner, tickLower, tickUpper); + Position.Info position = this.positions.get(positionKey); + + BigInteger _feeGrowthGlobal0X128 = this.feeGrowthGlobal0X128.get(); + BigInteger _feeGrowthGlobal1X128 = this.feeGrowthGlobal1X128.get(); + Slot0 _slot0 = this.slot0.get(); + + // if we need to update the ticks, do it + boolean flippedLower = false; + boolean flippedUpper = false; + if (!liquidityDelta.equals(ZERO)) { + BigInteger time = TimeUtils.now(); + var result = this.observations.observeSingle( + time, + ZERO, + _slot0.tick, + _slot0.observationIndex, + this.liquidity.get(), + _slot0.observationCardinality + ); + + BigInteger tickCumulative = result.tickCumulative; + BigInteger secondsPerLiquidityCumulativeX128 = result.secondsPerLiquidityCumulativeX128; + + Ticks.UpdateResult resultLower = this.ticks.update( + tickLower, + tick, + liquidityDelta, + _feeGrowthGlobal0X128, + _feeGrowthGlobal1X128, + secondsPerLiquidityCumulativeX128, + tickCumulative, + time, + false, + this.settings.maxLiquidityPerTick + ); + flippedLower = resultLower.flipped; + this.onTickUpdate(tickLower, resultLower.info); + + Ticks.UpdateResult resultUpper = this.ticks.update( + tickUpper, + tick, + liquidityDelta, + _feeGrowthGlobal0X128, + _feeGrowthGlobal1X128, + secondsPerLiquidityCumulativeX128, + tickCumulative, + time, + true, + this.settings.maxLiquidityPerTick + ); + flippedUpper = resultUpper.flipped; + this.onTickUpdate(tickUpper, resultUpper.info); + + if (flippedLower) { + this.tickBitmap.flipTick(tickLower, this.settings.tickSpacing); + } + if (flippedUpper) { + this.tickBitmap.flipTick(tickUpper, this.settings.tickSpacing); + } + } + + var result = this.ticks.getFeeGrowthInside(tickLower, tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128); + BigInteger feeGrowthInside0X128 = result.feeGrowthInside0X128; + BigInteger feeGrowthInside1X128 = result.feeGrowthInside1X128; + + PositionLib.update(position, liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128); + + // clear any tick data that is no longer needed + if (liquidityDelta.compareTo(ZERO) < 0) { + if (flippedLower) { + this.ticks.clear(tickLower); + this.onTickUpdate(tickLower, null); + } + if (flippedUpper) { + this.ticks.clear(tickUpper); + this.onTickUpdate(tickUpper, null); + } + } + + this.positions.set(positionKey, position); + return new PositionStorage(position, positionKey); + } + + private void onTickUpdate (int index, Tick.Info info) { + BigInteger liquidityGross = info != null ? info.liquidityGross : ZERO; + BigInteger liquidityNet = info != null ? info.liquidityNet : ZERO; + BigInteger feeGrowthOutside0X128 = info != null ? info.feeGrowthOutside0X128 : ZERO; + BigInteger feeGrowthOutside1X128 = info != null ? info.feeGrowthOutside1X128 : ZERO; + BigInteger tickCumulativeOutside = info != null ? info.tickCumulativeOutside : ZERO; + BigInteger secondsPerLiquidityOutsideX128 = info != null ? info.secondsPerLiquidityOutsideX128 : ZERO; + BigInteger secondsOutside = info != null ? info.secondsOutside : ZERO; + boolean initialized = info != null ? info.initialized : false; + + this.TickUpdate (index, + liquidityGross, + liquidityNet, + feeGrowthOutside0X128, + feeGrowthOutside1X128, + tickCumulativeOutside, + secondsPerLiquidityOutsideX128, + secondsOutside, + initialized + ); + } + + /** + * @dev Effect some changes to a position + * @param params the position details and the change to the position's liquidity to effect + * @return position a storage pointer referencing the position with the given owner and tick range + * @return amount0 the amount of token0 owed to the pool, negative if the pool should pay the recipient + * @return amount1 the amount of token1 owed to the pool, negative if the pool should pay the recipient + */ + private ModifyPositionResult _modifyPosition (ModifyPositionParams params) { + checkTicks(params.tickLower, params.tickUpper); + + Slot0 _slot0 = this.slot0.get(); + + var positionStorage = _updatePosition( + params.owner, + params.tickLower, + params.tickUpper, + params.liquidityDelta, + _slot0.tick + ); + + BigInteger amount0 = ZERO; + BigInteger amount1 = ZERO; + + if (!params.liquidityDelta.equals(ZERO)) { + if (_slot0.tick < params.tickLower) { + // current tick is below the passed range; liquidity can only become in range by crossing from left to + // right, when we'll need _more_ token0 (it's becoming more valuable) so user must provide it + amount0 = SqrtPriceMath.getAmount0Delta( + TickMath.getSqrtRatioAtTick(params.tickLower), + TickMath.getSqrtRatioAtTick(params.tickUpper), + params.liquidityDelta + ); + } else if (_slot0.tick < params.tickUpper) { + // current tick is inside the passed range + BigInteger liquidityBefore = this.liquidity.get(); + + // write an oracle entry + var writeResult = this.observations.write( + _slot0.observationIndex, + TimeUtils.now(), + _slot0.tick, + liquidityBefore, + _slot0.observationCardinality, + _slot0.observationCardinalityNext + ); + + _slot0.observationIndex = writeResult.observationIndex; + _slot0.observationCardinality = writeResult.observationCardinality; + this.slot0.set(_slot0); + + amount0 = SqrtPriceMath.getAmount0Delta( + _slot0.sqrtPriceX96, + TickMath.getSqrtRatioAtTick(params.tickUpper), + params.liquidityDelta + ); + amount1 = SqrtPriceMath.getAmount1Delta( + TickMath.getSqrtRatioAtTick(params.tickLower), + _slot0.sqrtPriceX96, + params.liquidityDelta + ); + + BigInteger newLiquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta); + this.liquidity.set(newLiquidity); + this.PoolIntrinsicsUpdate(_slot0.sqrtPriceX96, _slot0.tick, newLiquidity); + } else { + // current tick is above the passed range; liquidity can only become in range by crossing from right to + // left, when we'll need _more_ token1 (it's becoming more valuable) so user must provide it + amount1 = SqrtPriceMath.getAmount1Delta( + TickMath.getSqrtRatioAtTick(params.tickLower), + TickMath.getSqrtRatioAtTick(params.tickUpper), + params.liquidityDelta + ); + } + } + + return new ModifyPositionResult(positionStorage, amount0, amount1); + } + + /** + * @notice Get the pool's balance of token0 + */ + private BigInteger balance0 () { + return IIRC2ICX.balanceOf(this.settings.token0, Context.getAddress()); + } + + /** + * @notice Get the pool's balance of token1 + */ + private BigInteger balance1 () { + return IIRC2ICX.balanceOf(this.settings.token1, Context.getAddress()); + } + + // ================================================ + // Checks + // ================================================ + private void checkTicks (int tickLower, int tickUpper) { + Context.require(tickLower < tickUpper, + "checkTicks: tickLower must be lower than tickUpper"); + Context.require(tickLower >= TickMath.MIN_TICK, + "checkTicks: tickLower lower than expected"); + Context.require(tickUpper <= TickMath.MAX_TICK, + "checkTicks: tickUpper greater than expected"); + } + + private void checkCallerIsFactoryOwner() { + final Address factoryOwner = IBalancedFactory.owner(this.settings.factory); + final Address caller = Context.getCaller(); + + Context.require(caller.equals(factoryOwner), + "checkCallerIsFactoryOwner: Only owner can call this method"); + } + + // ================================================ + // Public variable getters + // ================================================ + @External(readonly = true) + public String name() { + return this.settings.name; + } + + @External(readonly = true) + public Address factory() { + return this.settings.factory; + } + + @External(readonly = true) + public Address token0() { + return this.settings.token0; + } + + @External(readonly = true) + public Address token1() { + return this.settings.token1; + } + + @External(readonly = true) + public PoolSettings settings() { + return this.settings; + } + + /** + * The 0th storage slot in the pool stores many values, and is exposed as a single method to save steps when accessed externally. + */ + @External(readonly = true) + public Slot0 slot0 () { + return this.slot0.get(); + } + + @External(readonly = true) + public ProtocolFees protocolFees () { + return this.protocolFees.get(); + } + + @External(readonly = true) + public BigInteger maxLiquidityPerTick () { + return this.settings.maxLiquidityPerTick; + } + + @External(readonly = true) + public BigInteger liquidity () { + return this.liquidity.get(); + } + + @External(readonly = true) + public BigInteger fee () { + return BigInteger.valueOf(this.settings.fee); + } + + @External(readonly = true) + public BigInteger tickSpacing () { + return BigInteger.valueOf(this.settings.tickSpacing); + } + + @External(readonly = true) + public BigInteger feeGrowthGlobal0X128 () { + return feeGrowthGlobal0X128.get(); + } + + @External(readonly = true) + public BigInteger feeGrowthGlobal1X128 () { + return feeGrowthGlobal1X128.get(); + } + + // Implements Interfaces + // --- Ticks --- + @External(readonly = true) + public Tick.Info ticks (int tick) { + return this.ticks.get(tick); + } + + @External(readonly = true) + public BigInteger ticksInitializedSize () { + return BigInteger.valueOf(this.ticks.initializedSize()); + } + + @External(readonly = true) + public BigInteger ticksInitialized (int index) { + return BigInteger.valueOf(this.ticks.initialized(index)); + } + + @External(readonly = true) + public Tick.Info[] ticksInitializedRange (int start, int end) { + Tick.Info[] result = new Tick.Info[end-start]; + for (int i = start, j = 0; i < end; i++, j++) { + result[j] = this.ticks.get(this.ticks.initialized(i)); + } + return result; + } + + // --- Position --- + @External(readonly = true) + public Position.Info positions (byte[] key) { + return this.positions.get(key); + } + + // --- Observations --- + @External(readonly = true) + public Oracle.Observation observations (int index) { + return this.observations.get(index); + } + + @External(readonly = true) + public Oracle.Observation oldestObservation () { + return this.observations.getOldest(); + } + + // --- TickBitmap --- + @External(readonly = true) + public BigInteger tickBitmap (int index) { + return this.tickBitmap.get(index); + } + + @External(readonly = true) + public NextInitializedTickWithinOneWordResult nextInitializedTickWithinOneWord ( + int tick, + int tickSpacing, + boolean zeroForOne + ) { + return tickBitmap.nextInitializedTickWithinOneWord( + tick, + tickSpacing, + zeroForOne + ); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPoolFactory.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPoolFactory.java new file mode 100644 index 000000000..e652a6585 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPoolFactory.java @@ -0,0 +1,395 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex; + +import java.math.BigInteger; +import network.balanced.score.core.dex.models.ConcentratedLiquidityPoolDeployer; +import network.balanced.score.core.dex.interfaces.pooldeployer.IConcentratedLiquidityPoolDeployer; +import network.balanced.score.core.dex.structs.factory.Parameters; +import network.balanced.score.core.dex.interfaces.pool.IConcentratedLiquidityPool; +import network.balanced.score.core.dex.utils.AddressUtils; +import network.balanced.score.core.dex.utils.EnumerableSet; +import score.Address; +import score.BranchDB; +import score.Context; +import score.DictDB; +import score.VarDB; +import score.annotation.EventLog; +import score.annotation.External; + +/** + * @title Canonical Balanced factory + * @notice Deploys Balanced pools and manages ownership and control over pool protocol fees + */ +public class ConcentratedLiquidityPoolFactory implements IConcentratedLiquidityPoolDeployer { + + // ================================================ + // Consts + // ================================================ + + // Contract class name + private static final String NAME = "ConcentratedLiquidityPoolFactory"; + + // Contract name + private final String name; + + // ================================================ + // DB Variables + // ================================================ + protected final VarDB
owner = Context.newVarDB(NAME + "_owner", Address.class); + protected final DictDB feeAmountTickSpacing = Context.newDictDB(NAME + "_feeAmountTickSpacing", Integer.class); + protected final BranchDB>> getPool = Context.newBranchDB(NAME + "_getPool", Address.class); + protected final VarDB poolContract = Context.newVarDB(NAME + "_poolContract", byte[].class); + protected final EnumerableSet
poolsSet = new EnumerableSet
(NAME + "_poolsSet", Address.class); + + // Implements IConcentratedLiquidityPoolDeployer + private final ConcentratedLiquidityPoolDeployer poolDeployer; + + // ================================================ + // Event Logs + // ================================================ + /** + * @notice Emitted when the owner of the factory is changed + * @param oldOwner The owner before the owner was changed + * @param newOwner The owner after the owner was changed + */ + @EventLog(indexed = 2) + public void OwnerChanged( + Address zeroAddress, + Address caller + ) {} + + /** + * @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 + */ + @EventLog(indexed = 2) + public void FeeAmountEnabled( + int fee, + int tickSpacing + ) {} + + @EventLog(indexed = 3) + public void PoolCreated( + Address token0, + Address token1, + int fee, + int tickSpacing, + Address pool + ) {} + + @EventLog(indexed = 3) + public void PoolUpdated( + Address token0, + Address token1, + int fee, + int tickSpacing, + Address pool + ) {} + + // ================================================ + // Methods + // ================================================ + /** + * Contract constructor + */ + public ConcentratedLiquidityPoolFactory() { + this.poolDeployer = new ConcentratedLiquidityPoolDeployer(); + + final Address caller = Context.getCaller(); + this.name = "Balanced Factory"; + + // Default values during deployment + Context.println(this.owner.toString()); + if (this.owner.get() == null) { + this.owner.set(caller); + this.OwnerChanged(AddressUtils.ZERO_ADDRESS, caller); + } + + if (this.feeAmountTickSpacing.get(500) == null) { + this.feeAmountTickSpacing.set(500, 10); + this.FeeAmountEnabled(500, 10); + } + + if (this.feeAmountTickSpacing.get(3000) == null) { + this.feeAmountTickSpacing.set(3000, 60); + this.FeeAmountEnabled(3000, 60); + } + + if (this.feeAmountTickSpacing.get(10000) == null) { + this.feeAmountTickSpacing.set(10000, 200); + this.FeeAmountEnabled(10000, 200); + } + } + + /** + * @notice Creates a pool for the given two tokens and fee + * + * Access: Everyone + * + * @param tokenA One of the two tokens in the desired pool + * @param tokenB The other of the two tokens in the desired pool + * @param fee The desired fee for the pool + * @dev tokenA and tokenB may be passed in either order: token0/token1 or token1/token0. tickSpacing is retrieved + * from the fee. The call will revert if the pool already exists, the fee is invalid, or the token arguments + * are invalid. + * @return pool The address of the newly created pool + */ + @External + public Address createPool ( + Address tokenA, + Address tokenB, + int fee + ) { + // Checks + Context.require(!tokenA.equals(tokenB), + "createPool: tokenA must be different from tokenB"); + + Address token0 = tokenA; + Address token1 = tokenB; + + // Make sure tokens addresses are ordered + if (AddressUtils.compareTo(tokenA, tokenB) >= 0) { + token0 = tokenB; + token1 = tokenA; + } + + Context.require(!token0.equals(AddressUtils.ZERO_ADDRESS), + "createPool: token0 cannot be ZERO_ADDRESS"); + + int tickSpacing = this.feeAmountTickSpacing.getOrDefault(fee, 0); + Context.require(tickSpacing != 0, + "createPool: tickSpacing cannot be 0"); + + Context.require(getPool.at(token0).at(token1).get(fee) == null, + "createPool: pool already exists"); + + // OK + Address pool = this.poolDeployer.deploy ( + this.poolContract.get(), + Context.getAddress(), + token0, token1, fee, tickSpacing + ); + + this.getPool.at(token0).at(token1).set(fee, pool); + // populate mapping in the reverse direction, deliberate choice to avoid the cost of comparing addresses + this.getPool.at(token1).at(token0).set(fee, pool); + // Add to the global pool list + this.poolsSet.add(pool); + + this.PoolCreated(token0, token1, fee, tickSpacing, pool); + + return pool; + } + + /** + * @notice Update an existing pool contract given a pool address + * + * Access: Owner + * + * @param pool An existing pool address + */ + @External + public void updatePool ( + Address pool + ) { + // Access control + checkOwner(); + + // OK + Address token0 = IConcentratedLiquidityPool.token0(pool); + Address token1 = IConcentratedLiquidityPool.token1(pool); + int fee = IConcentratedLiquidityPool.fee(pool); + int tickSpacing = IConcentratedLiquidityPool.tickSpacing(pool); + + this.poolDeployer.update ( + pool, + this.poolContract.get(), + Context.getAddress(), + token0, token1, fee, tickSpacing + ); + + this.PoolUpdated(token0, token1, fee, tickSpacing, pool); + } + + /** + * @notice Updates the owner of the factory + * + * Access: Owner + * + * @dev Must be called by the current owner + * @param _owner The new owner of the factory + */ + @External + public void setOwner ( + Address _owner + ) { + // Access control + checkOwner(); + + // OK + Address currentOwner = this.owner.get(); + this.OwnerChanged(currentOwner, _owner); + this.owner.set(_owner); + } + + /** + * @notice Enables a fee amount with the given tickSpacing + * + * Access: Owner + * + * @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 + */ + @External + public void enableFeeAmount ( + int fee, + int tickSpacing + ) { + // Access control + checkOwner(); + + // Checks + Context.require(fee < 1_000_000, + "enableFeeAmount: fee needs to be lower than 1,000,000"); + + // 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 + Context.require(tickSpacing > 0 && tickSpacing < 16384, + "enableFeeAmount: tickSpacing > 0 && tickSpacing < 16384"); + + Context.require(this.feeAmountTickSpacing.get(fee) == 0, + "enableFeeAmount: fee amount is already enabled"); + + // OK + this.feeAmountTickSpacing.set(fee, tickSpacing); + this.FeeAmountEnabled(fee, tickSpacing); + } + + /** + * Set the pool contract bytes to be newly deployed with `createPool` + * + * Access: Owner + * + * @param contractBytes + */ + @External + public void setPoolContract ( + byte[] contractBytes + ) { + // Access control + checkOwner(); + + // OK + this.poolContract.set(contractBytes); + } + + // ================================================ + // Checks + // ================================================ + private void checkOwner () { + Address currentOwner = this.owner.get(); + Context.require(Context.getCaller().equals(currentOwner), + "checkOwner: caller must be owner"); + } + + // ================================================ + // Public variable getters + // ================================================ + /** + * Get the contract name + */ + @External(readonly = true) + public String name() { + return this.name; + } + + /** + * Get the current owner of the Factory + */ + @External(readonly = true) + public Address owner() { + return this.owner.get(); + } + + /** + * Get the current pool contract bytes of the Factory + */ + @External(readonly = true) + public byte[] poolContract () { + return this.poolContract.get(); + } + + /** + * Get the deployed pools list size + */ + @External(readonly = true) + public BigInteger poolsSize() { + return BigInteger.valueOf(this.poolsSet.length()); + } + + /** + * Get a deployed pools list item + * @param index the index of the item to read from the deployed pools list + * @return The pool address + */ + @External(readonly = true) + public Address pools ( + int index + ) { + return this.poolsSet.get(index); + } + + /** + * Check if the pool exists in the Factory + * @param pool A pool address + * @return True if exists, false otherwise + */ + @External(readonly = true) + public boolean poolExists ( + Address pool + ) { + return this.poolsSet.contains(pool); + } + + /** + * Get a deployed pool address from its parameters + * The `token0` and `token1` parameters can be inverted, it will return the same pool address + * + * @param token0 One of the two tokens in the desired pool + * @param token1 The other of the two tokens in the desired pool + * @param fee The desired fee for the pool ; divide this value by 10000 to get the percent value + * @return The pool address if it exists + */ + @External(readonly = true) + public Address getPool ( + Address token0, + Address token1, + int fee + ) { + return this.getPool.at(token0).at(token1).get(fee); + } + + // --- Implement IConcentratedLiquidityPoolDeployer --- + @External(readonly = true) + public Parameters parameters() { + return this.poolDeployer.parameters(); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/factory/IBalancedFactory.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/factory/IBalancedFactory.java new file mode 100644 index 000000000..9b67fb819 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/factory/IBalancedFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Balanced Protocol + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.interfaces.factory; + +import score.Address; +import score.Context; + +public class IBalancedFactory { + + // ReadOnly methods + public static Address owner(Address factory) { + return (Address) Context.call(factory, "owner"); + } + + public static Address getPool ( + Address factory, + Address token0, + Address token1, + int fee + ) { + return (Address) Context.call(factory, "getPool", token0, token1, fee); + } + + public static Address createPool ( + Address factory, + Address token0, + Address token1, + int fee + ) { + return (Address) Context.call(factory, "createPool", token0, token1, fee); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IIRC2.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IIRC2.java new file mode 100644 index 000000000..11cd200c9 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IIRC2.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.interfaces.irc2; + +import java.math.BigInteger; + +import score.Address; +import score.Context; + +public class IIRC2 { + + public static void transfer ( + Address irc2, + Address to, + BigInteger amount, + byte[] data + ) { + Context.call(irc2, "transfer", to, amount, data); + } + + public static int decimals (Address irc2) { + return ((BigInteger) Context.call(irc2, "decimals")).intValue(); + } + + public static String symbol (Address irc2) { + return ((String) Context.call(irc2, "symbol")); + } + + public static BigInteger totalSupply (Address irc2) { + return (BigInteger) Context.call(irc2, "totalSupply"); + } + + public static BigInteger balanceOf(Address irc2, Address address) { + return (BigInteger) Context.call(irc2, "balanceOf", address); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IIRC2ICX.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IIRC2ICX.java new file mode 100644 index 000000000..7b2b45f21 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IIRC2ICX.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.interfaces.irc2; + +import java.math.BigInteger; +import network.balanced.score.core.dex.utils.JSONUtils; +import network.balanced.score.core.dex.utils.ICX; +import score.Address; + +public class IIRC2ICX { + + public static void transfer (Address token, Address destination, BigInteger value) { + if (ICX.isICX(token)) { + ICX.transfer(destination, value); + } else { + IIRC2.transfer(token, destination, value, "".getBytes()); + } + } + + public static void transfer (Address token, Address destination, BigInteger value, String method) { + if (ICX.isICX(token)) { + ICX.transfer(destination, value, method + "Icx"); + } else { + IIRC2.transfer(token, destination, value, JSONUtils.method(method)); + } + } + + public static void transfer (Address token, Address destination, BigInteger value, String method, IRC2ICXParam params) { + if (ICX.isICX(token)) { + ICX.transfer(destination, value, method + "Icx", params.toRaw()); + } else { + IIRC2.transfer(token, destination, value, JSONUtils.method(method, params.toJson())); + } + } + + public static String symbol (Address token) { + if (ICX.isICX(token)) { + return ICX.symbol(); + } else { + return IIRC2.symbol(token); + } + } + + public static int decimals (Address token) { + if (ICX.isICX(token)) { + return ICX.decimals(); + } else { + return IIRC2.decimals(token); + } + } + + public static BigInteger balanceOf(Address token, Address address) { + if (ICX.isICX(token)) { + return ICX.balanceOf(address); + } else { + return IIRC2.balanceOf(token, address); + } + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IRC2ICXParam.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IRC2ICXParam.java new file mode 100644 index 000000000..490c7cb81 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IRC2ICXParam.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.interfaces.irc2; + +import com.eclipsesource.json.JsonObject; + +public interface IRC2ICXParam { + public JsonObject toJson (); + public Object[] toRaw (); +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedFlashCallback.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedFlashCallback.java new file mode 100644 index 000000000..1955eaabc --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedFlashCallback.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.interfaces.pool; + +import java.math.BigInteger; + +public interface IBalancedFlashCallback { + /** + * @param fee0 The fee from calling flash for token0 + * @param fee1 The fee from calling flash for token1 + * @param data The data needed in the callback passed as FlashCallbackData from `initFlash` + * @notice implements the callback called from flash + * @dev fails if the flash is not profitable, meaning the amountOut from the flash is less than the amount borrowed + */ + public void balancedFlashCallback ( + BigInteger fee0, + BigInteger fee1, + byte[] data + ); +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedMintCallback.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedMintCallback.java new file mode 100644 index 000000000..99f71580a --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedMintCallback.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.interfaces.pool; + +import java.math.BigInteger; + +public interface IBalancedMintCallback { + /** + * @notice Called to `Context.getCaller()` after minting liquidity to a position from ConcentratedLiquidityPool#mint. + * @dev In the implementation you must pay the pool tokens owed for the minted liquidity. + * The caller of this method must be checked to be a ConcentratedLiquidityPool deployed by the canonical ConcentratedLiquidityPoolFactory. + * @param amount0Owed The amount of token0 due to the pool for the minted liquidity + * @param amount1Owed The amount of token1 due to the pool for the minted liquidity + * @param data Any data passed through by the caller via the mint call + */ + public void balancedMintCallback ( + BigInteger amount0Owed, + BigInteger amount1Owed, + byte[] data + ); +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedSwapCallback.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedSwapCallback.java new file mode 100644 index 000000000..4e60b16eb --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedSwapCallback.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.interfaces.pool; + +import java.math.BigInteger; + +public interface IBalancedSwapCallback { + /** + * Called to `msg.sender` after executing a swap via `ConcentratedLiquidityPool::swap`. + * @dev In the implementation you must pay the pool tokens owed for the swap. The caller of this method must be checked to be a ConcentratedLiquidityPool deployed by the canonical ConcentratedLiquidityPoolFactory. amount0Delta and amount1Delta can both be 0 if no tokens were swapped. + * @param amount0Delta The amount of token0 that was sent (negative) or must be received (positive) by the pool by the end of the swap. If positive, the callback must send that amount of token0 to the pool. + * @param amount1Delta The amount of token1 that was sent (negative) or must be received (positive) by the pool by the end of the swap. If positive, the callback must send that amount of token1 to the pool. + * @param data Any data passed through by the caller via the swap call + */ + public void balancedSwapCallback ( + BigInteger amount0Delta, + BigInteger amount1Delta, + byte[] data + ); +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPool.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPool.java new file mode 100644 index 000000000..6fd9f392e --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPool.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.interfaces.pool; + +import java.math.BigInteger; +import network.balanced.score.core.dex.structs.factory.Parameters; +import network.balanced.score.core.dex.structs.pool.PairAmounts; +import network.balanced.score.core.dex.structs.pool.PoolSettings; +import network.balanced.score.core.dex.structs.pool.Position; +import network.balanced.score.core.dex.structs.pool.Slot0; +import network.balanced.score.core.dex.structs.pool.SnapshotCumulativesInsideResult; +import network.balanced.score.core.dex.structs.pool.Tick; +import network.balanced.score.core.dex.structs.pool.Oracle.Observation; +import score.Address; +import score.Context; + +public class IConcentratedLiquidityPool { + + // Write methods + public static PairAmounts mint ( + Address pool, + Address recipient, + int tickLower, + int tickUpper, + BigInteger amount, + byte[] data + ) { + return PairAmounts.fromMap ( + Context.call(pool, "mint", recipient, tickLower, tickUpper, amount, data) + ); + } + + public static void flash ( + Address pool, + Address recipient, + BigInteger amount0, + BigInteger amount1, + byte[] data + ) { + Context.call(pool, "flash", recipient, amount0, amount1, data); + } + + public static PairAmounts swap ( + Address pool, + Address recipient, + boolean zeroForOne, + BigInteger amountSpecified, + BigInteger sqrtPriceLimitX96, + byte[] data + ) { + return PairAmounts.fromMap ( + Context.call(pool, "swap", recipient, zeroForOne, amountSpecified, sqrtPriceLimitX96, data) + ); + } + + public static PairAmounts swapReadOnly ( + Address pool, + Address recipient, + boolean zeroForOne, + BigInteger amountSpecified, + BigInteger sqrtPriceLimitX96, + byte[] data + ) { + return PairAmounts.fromMap ( + Context.call(pool, "swapReadOnly", recipient, zeroForOne, amountSpecified, sqrtPriceLimitX96, data) + ); + } + + public static PairAmounts collect ( + Address pool, + Address recipient, + int tickLower, + int tickUpper, + BigInteger amount0Requested, + BigInteger amount1Requested + ) { + return PairAmounts.fromMap ( + Context.call(pool, "collect", recipient, tickLower, tickUpper, amount0Requested, amount1Requested) + ); + } + + public static void collectProtocol ( + Address pool, + Address recipient, + BigInteger amount0Requested, + BigInteger amount1Requested + ) { + Context.call(pool, "collectProtocol", recipient, amount0Requested, amount1Requested); + } + + public static PairAmounts burn ( + Address pool, + int tickLower, + int tickUpper, + BigInteger amount + ) { + return PairAmounts.fromMap ( + Context.call(pool, "burn", tickLower, tickUpper, amount) + ); + } + + public static void initialize ( + Address pool, + BigInteger sqrtPriceX96 + ) { + Context.call(pool, "initialize", sqrtPriceX96); + } + + // ReadOnly methods + public static Address token0 (Address pool) { + return (Address) Context.call(pool, "token0"); + } + + public static Address token1 (Address pool) { + return (Address) Context.call(pool, "token1"); + } + + public static Parameters parameters(Address pool) { + return Parameters.fromMap(Context.call(pool, "parameters")); + } + + public static Slot0 slot0(Address pool) { + return Slot0.fromMap(Context.call(pool, "slot0")); + } + + public static Position.Info positions(Address pool, byte[] positionKey) { + return Position.Info.fromMap(Context.call(pool, "positions", positionKey)); + } + + public static int tickSpacing (Address pool) { + return ((BigInteger) Context.call(pool, "tickSpacing")).intValue(); + } + + public static BigInteger tickBitmap (Address pool, int pos) { + return (BigInteger) Context.call(pool, "tickBitmap", pos); + } + + public static SnapshotCumulativesInsideResult snapshotCumulativesInside ( + Address pool, + int tickLower, + int tickUpper + ) { + return SnapshotCumulativesInsideResult.fromMap( + Context.call(pool, "snapshotCumulativesInside", tickLower, tickUpper) + ); + } + + public static Tick.Info ticks (Address pool, int populatedTick) { + return Tick.Info.fromMap( + Context.call(pool, "ticks", populatedTick) + ); + } + + public static BigInteger liquidity (Address pool) { + return (BigInteger) Context.call(pool, "liquidity"); + } + + public static int fee (Address pool) { + return ((BigInteger) Context.call(pool, "fee")).intValue(); + } + + public static BigInteger feeGrowthGlobal0X128 (Address pool) { + return (BigInteger) Context.call(pool, "feeGrowthGlobal0X128"); + } + + public static BigInteger feeGrowthGlobal1X128 (Address pool) { + return (BigInteger) Context.call(pool, "feeGrowthGlobal1X128"); + } + + public static PoolSettings settings (Address pool) { + return PoolSettings.fromMap(Context.call(pool, "settings")); + } + + public static Observation observations (Address pool, int index) { + return Observation.fromMap(Context.call(pool, "observations", index)); + } + + public static BigInteger maxLiquidityPerTick (Address pool) { + return (BigInteger) Context.call(pool, "maxLiquidityPerTick"); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPoolCallee.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPoolCallee.java new file mode 100644 index 000000000..e88476f48 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPoolCallee.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.interfaces.pool; + +import java.math.BigInteger; +import score.Address; +import score.Context; + +public class IConcentratedLiquidityPoolCallee { + + // Write methods + public static void balancedMintCallback ( + Address callee, + BigInteger amount0Owed, + BigInteger amount1Owed, + byte[] data + ) { + Context.call(callee, "balancedMintCallback", amount0Owed, amount1Owed, data); + } + + public static void balancedSwapCallback ( + Address callee, + BigInteger amount0Delta, + BigInteger amount1Delta, + byte[] data + ) { + Context.call(callee, "balancedSwapCallback", amount0Delta, amount1Delta, data); + } + + public static void balancedFlashCallback ( + Address callee, + BigInteger fee0, + BigInteger fee1, + byte[] data + ) { + Context.call(callee, "balancedFlashCallback", fee0, fee1, data); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IConcentratedLiquidityPoolDeployer.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IConcentratedLiquidityPoolDeployer.java new file mode 100644 index 000000000..090fcba3a --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IConcentratedLiquidityPoolDeployer.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.interfaces.pooldeployer; + +import network.balanced.score.core.dex.structs.factory.Parameters; +import score.annotation.External; + +public interface IConcentratedLiquidityPoolDeployer { + + // ================================================ + // Methods + // ================================================ + /** + * @notice Get the parameters to be used in constructing the pool, set transiently during pool creation. + * @dev Called by the pool constructor to fetch the parameters of the pool + * + * Returns factory The factory address + * Returns token0 The first token of the pool by address sort order + * Returns token1 The second token of the pool by address sort order + * Returns fee The fee collected upon every swap in the pool, denominated in hundredths of a bip + * Returns tickSpacing The minimum number of ticks between initialized ticks + * @return + */ + @External(readonly = true) + public Parameters parameters (); +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/BitMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/BitMath.java new file mode 100644 index 000000000..616bf2994 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/BitMath.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package network.balanced.score.core.dex.libs; + +import java.math.BigInteger; +import network.balanced.score.core.dex.utils.IntUtils; +import score.Context; + +public class BitMath { + + /** + * @notice Returns the index of the most significant bit of the number, + * where the least significant bit is at index 0 and the most significant bit is at index 255 + * @dev The function satisfies the property: + * x >= 2**mostSignificantBit(x) and x < 2**(mostSignificantBit(x)+1) + * @param x the value for which to compute the most significant bit, must be greater than 0 + * @return r the index of the most significant bit + */ + public static int mostSignificantBit(BigInteger x) { + Context.require(x.compareTo(BigInteger.ZERO) > 0); + char r = 0; + + if (x.compareTo(new BigInteger("100000000000000000000000000000000", 16)) >= 0) { + x = x.shiftRight(128); + r += 128; + } + if (x.compareTo(new BigInteger("10000000000000000", 16)) >= 0) { + x = x.shiftRight(64); + r += 64; + } + if (x.compareTo(new BigInteger("100000000", 16)) >= 0) { + x = x.shiftRight(32); + r += 32; + } + if (x.compareTo(new BigInteger("10000", 16)) >= 0) { + x = x.shiftRight(16); + r += 16; + } + if (x.compareTo(new BigInteger("100", 16)) >= 0) { + x = x.shiftRight(8); + r += 8; + } + if (x.compareTo(new BigInteger("10", 16)) >= 0) { + x = x.shiftRight(4); + r += 4; + } + if (x.compareTo(new BigInteger("4", 16)) >= 0) { + x = x.shiftRight(2); + r += 2; + } + if (x.compareTo(new BigInteger("2", 16)) >= 0) { + r += 1; + } + + return r; + } + + public static int leastSignificantBit(BigInteger x) { + Context.require(x.compareTo(BigInteger.ZERO) > 0); + + char r = 255; + + if (x.and(IntUtils.MAX_UINT128).compareTo(BigInteger.ZERO) > 0) { + r -= 128; + } else { + x = x.shiftRight(128); + } + if (x.and(IntUtils.MAX_UINT64).compareTo(BigInteger.ZERO) > 0) { + r -= 64; + } else { + x = x.shiftRight(64); + } + if (x.and(IntUtils.MAX_UINT32).compareTo(BigInteger.ZERO) > 0) { + r -= 32; + } else { + x = x.shiftRight(32); + } + if (x.and(IntUtils.MAX_UINT16).compareTo(BigInteger.ZERO) > 0) { + r -= 16; + } else { + x = x.shiftRight(16); + } + if (x.and(IntUtils.MAX_UINT8).compareTo(BigInteger.ZERO) > 0) { + r -= 8; + } else { + x = x.shiftRight(8); + } + if (x.and(BigInteger.valueOf(0xf)).compareTo(BigInteger.ZERO) > 0) { + r -= 4; + } else { + x = x.shiftRight(4); + } + if (x.and(BigInteger.valueOf(0x3)).compareTo(BigInteger.ZERO) > 0) { + r -= 2; + } else { + x = x.shiftRight(2); + } + if (x.and(BigInteger.valueOf(0x1)).compareTo(BigInteger.ZERO) > 0) { + r -= 1; + } + + return r; + } + +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint128.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint128.java new file mode 100644 index 000000000..8c60221e3 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint128.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.libs; + +import java.math.BigInteger; + +// @title FixedPoint128 +// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) +public class FixedPoint128 { + public final static BigInteger Q128 = new BigInteger("100000000000000000000000000000000", 16); +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint96.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint96.java new file mode 100644 index 000000000..9742672b1 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint96.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.libs; + +import java.math.BigInteger; + +/// @title FixedPoint96 +/// @notice A library for handling binary fixed point numbers, see https://en.wikipedia.org/wiki/Q_(number_format) +public class FixedPoint96 { + public static final int RESOLUTION = 96; + public static final BigInteger Q96 = new BigInteger("1000000000000000000000000", 16); +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FullMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FullMath.java new file mode 100644 index 000000000..47b68aef2 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FullMath.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.libs; + +import static network.balanced.score.core.dex.utils.IntUtils.MAX_UINT256; +import static java.math.BigInteger.ONE; +import static java.math.BigInteger.ZERO; + +import java.math.BigInteger; + +import score.Context; + +public class FullMath { + + public static BigInteger mulDivRoundingUp (BigInteger a, BigInteger b, BigInteger denominator) { + BigInteger result = mulDiv(a, b, denominator); + + if (mulmod(a, b, denominator).compareTo(ZERO) > 0) { + Context.require(result.compareTo(MAX_UINT256) < 0); + result = result.add(ONE); + } + + return result; + } + + private static BigInteger mulmod (BigInteger x, BigInteger y, BigInteger m) { + return x.multiply(y).mod(m); + } + + /** + * @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 + * @param a The multiplicand + * @param b The multiplier + * @param denominator The divisor + * @return result The 256-bit result + * @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv + */ + public static BigInteger mulDiv (BigInteger a, BigInteger b, BigInteger denominator) { + // BigInteger can reach 512 bits + return a.multiply(b).divide(denominator); + /* + // 512-bit multiply [prod1 prod0] = a * b + // Compute the product mod 2**256 and mod 2**256 - 1 + // then use the Chinese Remainder Theorem to reconstruct + // the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2**256 + prod0 + + final BigInteger THREE = BigInteger.valueOf(3); + + BigInteger prod0; // Least significant 256 bits of the product + BigInteger prod1; // Most significant 256 bits of the product + + a = uint256(a); + b = uint256(b); + denominator = uint256(denominator); + + BigInteger mm = mulmod(a, b, MAX_UINT256); + prod0 = mulmod256(a, b); + prod1 = sub256(sub256(mm, prod0), lt(mm, prod0)); + + // Handle non-overflow cases, 256 by 256 division + if (prod1.equals(ZERO)) { + Context.require(denominator.compareTo(ZERO) > 0, + "mulDiv: denominator > 0"); + + return prod0.divide(denominator); + } + + // Make sure the result is less than 2**256. + // Also prevents denominator == 0 + Context.require(denominator.compareTo(prod1) > 0, + "mulDiv: denominator > prod1"); + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0] + // Compute remainder using mulmod + BigInteger remainder = mulmod(a, b, denominator); + + // Subtract 256 bit number from 512 bit number + prod1 = sub256(prod1, gt(remainder, prod0)); + prod0 = sub256(prod0, remainder); + + // Factor powers of two out of denominator + // Compute largest power of two divisor of denominator. + // Always >= 1. + BigInteger twos = denominator.negate().and(denominator); + + // Divide denominator by power of two + denominator = denominator.divide(twos); + + // Divide [prod1 prod0] by the factors of two + prod0 = prod0.divide(twos); + + // Shift in bits from prod1 into prod0. For this we need + // to flip `twos` such that it is 2**256 / twos. + // If twos is zero, then it becomes one + twos = sub256(ZERO, twos).divide(twos).add(ONE); + + prod0 = prod0.or(mulmod256(prod1, twos)); + + // Invert denominator mod 2**256 + // Now that denominator is an odd number, it has an inverse + // modulo 2**256 such that denominator * inv = 1 mod 2**256. + // Compute the inverse by starting with a seed that is correct + // correct for four bits. That is, denominator * inv = 1 mod 2**4 + BigInteger inv = mulmod256(denominator, THREE).xor(TWO); + + // Now use Newton-Raphson iteration to improve the precision. + // Thanks to Hensel's lifting lemma, this also works in modular + // arithmetic, doubling the correct bits in each step. + inv = newtonRaphson(denominator, inv); // inverse mod 2**8 + inv = newtonRaphson(denominator, inv); // inverse mod 2**16 + inv = newtonRaphson(denominator, inv); // inverse mod 2**32 + inv = newtonRaphson(denominator, inv); // inverse mod 2**64 + inv = newtonRaphson(denominator, inv); // inverse mod 2**128 + inv = newtonRaphson(denominator, inv); // inverse mod 2**256 + + // Because the division is now exact we can divide by multiplying + // with the modular inverse of denominator. This will give us the + // correct result modulo 2**256. Since the precoditions guarantee + // that the outcome is less than 2**256, this is the final result. + // We don't need to compute the high bits of the result and prod1 + // is no longer required. + return mulmod256(prod0, inv); + */ + } + + // private static BigInteger mulmod256(BigInteger x, BigInteger y) { + // return x.multiply(y).mod(TWO_POW_256); + // } + + // private static BigInteger sub256 (BigInteger a, BigInteger b) { + // BigInteger c = a.subtract(b); + // if (c.compareTo(ZERO) < 0) { + // c = c.add(TWO_POW_256); + // } + // return c; + // } + + // private static BigInteger newtonRaphson (BigInteger denominator, BigInteger inv) { + // BigInteger a = mulmod256(denominator, inv); + // BigInteger b = sub256(TWO, a); + // return mulmod256(inv, b); + // } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/LiquidityMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/LiquidityMath.java new file mode 100644 index 000000000..47f123712 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/LiquidityMath.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.libs; + +import static network.balanced.score.core.dex.utils.IntUtils.uint128; + +import java.math.BigInteger; + +import score.Context; + +public class LiquidityMath { + /** + * @notice Add a signed liquidity delta to liquidity and revert if it overflows or underflows + * @param x The liquidity before change + * @param y The delta by which liquidity should be changed + * @return z The liquidity delta + */ + public static BigInteger addDelta (BigInteger x, BigInteger y) { + BigInteger z; + + if (y.compareTo(BigInteger.ZERO) < 0) { + z = uint128(x.subtract(y.negate())); + Context.require(z.compareTo(x) < 0, + "addDelta: z < x"); + } else { + z = uint128(x.add(y)); + Context.require(z.compareTo(x) >= 0, + "addDelta: z >= x"); + } + + return z; + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/OracleLib.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/OracleLib.java new file mode 100644 index 000000000..5e264f774 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/OracleLib.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.libs; + +import static java.math.BigInteger.ONE; +import static java.math.BigInteger.ZERO; + +import java.math.BigInteger; +import network.balanced.score.core.dex.structs.pool.Oracle; +import score.Context; + +public class OracleLib { + public static Oracle.Observation transform ( + Oracle.Observation observation, + BigInteger blockTimestamp, + int tick, + BigInteger liquidity + ) { + Context.require(blockTimestamp.compareTo(observation.blockTimestamp) >= 0, + "transform: invalid blockTimestamp"); + BigInteger delta = blockTimestamp.subtract(observation.blockTimestamp); + BigInteger tickCumulative = observation.tickCumulative.add(BigInteger.valueOf(tick).multiply(delta)); + BigInteger denominator = liquidity.compareTo(ZERO) > 0 ? liquidity : ONE; + BigInteger secondsPerLiquidityCumulativeX128 = observation.secondsPerLiquidityCumulativeX128.add(delta.shiftLeft(128).divide(denominator)); + return new Oracle.Observation(blockTimestamp, tickCumulative, secondsPerLiquidityCumulativeX128, true); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/PositionLib.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/PositionLib.java new file mode 100644 index 000000000..366c8df90 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/PositionLib.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.libs; + +import static network.balanced.score.core.dex.utils.IntUtils.uint128; + +import java.math.BigInteger; +import network.balanced.score.core.dex.structs.pool.Position; +import score.Context; + +public class PositionLib { + /** + * @notice Credits accumulated fees to a user's position + * @param posInfo The individual position to update + * @param liquidityDelta The change in pool liquidity as a result of the position update + * @param feeGrowthInside0X128 The all-time fee growth in token0, per unit of liquidity, inside the position's tick boundaries + * @param feeGrowthInside1X128 The all-time fee growth in token1, per unit of liquidity, inside the position's tick boundaries + */ + public static void update ( + Position.Info posInfo, + BigInteger liquidityDelta, + BigInteger feeGrowthInside0X128, + BigInteger feeGrowthInside1X128 + ) { + BigInteger liquidityNext; + + if (liquidityDelta.equals(BigInteger.ZERO)) { + Context.require(posInfo.liquidity.compareTo(BigInteger.ZERO) > 0, + "update: pokes aren't allowed for 0 liquidity positions"); // disallow pokes for 0 liquidity positions + liquidityNext = posInfo.liquidity; + } else { + liquidityNext = LiquidityMath.addDelta(posInfo.liquidity, liquidityDelta); + } + + // calculate accumulated fees + BigInteger tokensOwed0 = uint128(FullMath.mulDiv(feeGrowthInside0X128.subtract(posInfo.feeGrowthInside0LastX128), posInfo.liquidity, FixedPoint128.Q128)); + BigInteger tokensOwed1 = uint128(FullMath.mulDiv(feeGrowthInside1X128.subtract(posInfo.feeGrowthInside1LastX128), posInfo.liquidity, FixedPoint128.Q128)); + + // update the position + if (!liquidityDelta.equals(BigInteger.ZERO)) { + posInfo.liquidity = liquidityNext; + } + + posInfo.feeGrowthInside0LastX128 = feeGrowthInside0X128; + posInfo.feeGrowthInside1LastX128 = feeGrowthInside1X128; + + if (tokensOwed0.compareTo(BigInteger.ZERO) > 0 || tokensOwed1.compareTo(BigInteger.ZERO) > 0) { + // overflow is acceptable, have to withdraw before you hit type(uint128).max fees + posInfo.tokensOwed0 = uint128(posInfo.tokensOwed0.add(tokensOwed0)); + posInfo.tokensOwed1 = uint128(posInfo.tokensOwed1.add(tokensOwed1)); + } + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SqrtPriceMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SqrtPriceMath.java new file mode 100644 index 000000000..462c2f4f6 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SqrtPriceMath.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.libs; + +import static java.math.BigInteger.ZERO; + +import java.math.BigInteger; +import network.balanced.score.core.dex.utils.IntUtils; +import score.Context; + +public class SqrtPriceMath { + + /** + * @notice Helper that gets signed token0 delta + * @param sqrtRatioAX96 A sqrt price + * @param sqrtRatioBX96 Another sqrt price + * @param liquidity The change in liquidity for which to compute the amount0 delta + * @return amount0 Amount of token0 corresponding to the passed liquidityDelta between the two prices + */ + public static BigInteger getAmount0Delta ( + BigInteger sqrtRatioAX96, + BigInteger sqrtRatioBX96, + BigInteger liquidity + ) { + return liquidity.compareTo(ZERO) < 0 + ? getAmount0Delta(sqrtRatioAX96, sqrtRatioBX96, liquidity.negate(), false).negate() + : getAmount0Delta(sqrtRatioAX96, sqrtRatioBX96, liquidity, true); + } + + public static BigInteger getAmount0Delta ( + BigInteger sqrtRatioAX96, + BigInteger sqrtRatioBX96, + BigInteger liquidity, + boolean roundUp + ) { + if (sqrtRatioAX96.compareTo(sqrtRatioBX96) > 0) { + BigInteger tmp = sqrtRatioAX96; + sqrtRatioAX96 = sqrtRatioBX96; + sqrtRatioBX96 = tmp; + } + + BigInteger numerator1 = liquidity.shiftLeft(FixedPoint96.RESOLUTION); + BigInteger numerator2 = sqrtRatioBX96.subtract(sqrtRatioAX96); + + Context.require(sqrtRatioAX96.compareTo(ZERO) > 0); + + return roundUp ? + UnsafeMath.divRoundingUp ( + FullMath.mulDivRoundingUp(numerator1, numerator2, sqrtRatioBX96), + sqrtRatioAX96 + ) + : FullMath.mulDiv(numerator1, numerator2, sqrtRatioBX96).divide(sqrtRatioAX96); + } + + public static BigInteger getAmount1Delta ( + BigInteger sqrtRatioAX96, + BigInteger sqrtRatioBX96, + BigInteger liquidity + ) { + return liquidity.compareTo(ZERO) < 0 + ? getAmount1Delta(sqrtRatioAX96, sqrtRatioBX96, liquidity.negate(), false).negate() + : getAmount1Delta(sqrtRatioAX96, sqrtRatioBX96, liquidity, true); + } + + public static BigInteger getAmount1Delta ( + BigInteger sqrtRatioAX96, + BigInteger sqrtRatioBX96, + BigInteger liquidity, + boolean roundUp + ) { + if (sqrtRatioAX96.compareTo(sqrtRatioBX96) > 0) { + BigInteger tmp = sqrtRatioAX96; + sqrtRatioAX96 = sqrtRatioBX96; + sqrtRatioBX96 = tmp; + } + + return + roundUp + ? FullMath.mulDivRoundingUp(liquidity, sqrtRatioBX96.subtract(sqrtRatioAX96), FixedPoint96.Q96) + : FullMath.mulDiv(liquidity, sqrtRatioBX96.subtract(sqrtRatioAX96), FixedPoint96.Q96); + } + + /** + * @notice Gets the next sqrt price given a delta of token0 + * @dev Always rounds up, because in the exact output case (increasing price) we need to move the price at least + * far enough to get the desired output amount, and in the exact input case (decreasing price) we need to move the + * price less in order to not send too much output. + * The most precise formula for this is liquidity * sqrtPX96 / (liquidity +- amount * sqrtPX96), + * if this is impossible because of overflow, we calculate liquidity / (liquidity / sqrtPX96 +- amount). + * @param sqrtPX96 The starting price, i.e. before accounting for the token0 delta + * @param liquidity The amount of usable liquidity + * @param amount How much of token0 to add or remove from virtual reserves + * @param add Whether to add or remove the amount of token0 + * @return The price after adding or removing amount, depending on add + */ + private static BigInteger getNextSqrtPriceFromAmount0RoundingUp ( + BigInteger sqrtPX96, + BigInteger liquidity, + BigInteger amount, + boolean add + ) { + // we short circuit amount == 0 because the result is otherwise not guaranteed to equal the input price + if (amount.equals(ZERO)) { + return sqrtPX96; + } + + BigInteger numerator1 = liquidity.shiftLeft(FixedPoint96.RESOLUTION); + + if (add) { + BigInteger product = amount.multiply(sqrtPX96); + if (product.divide(amount).equals(sqrtPX96)) { + BigInteger denominator = numerator1.add(product); + if (denominator.compareTo(numerator1) >= 0) { + // always fits in 160 bits + return FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator); + } + } + + return UnsafeMath.divRoundingUp(numerator1, numerator1.divide(sqrtPX96).add(amount)); + } else { + // if the product overflows, we know the denominator underflows + // in addition, we must check that the denominator does not underflow + BigInteger product = amount.multiply(sqrtPX96); + Context.require(product.divide(amount).equals(sqrtPX96) && numerator1.compareTo(product) > 0, + "getNextSqrtPriceFromAmount0RoundingUp: denominator underflow"); + + BigInteger denominator = numerator1.subtract(product); + return FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator); + } + } + + /** + * @notice Gets the next sqrt price given a delta of token1 + * @dev Always rounds down, because in the exact output case (decreasing price) we need to move the price at least + * far enough to get the desired output amount, and in the exact input case (increasing price) we need to move the + * price less in order to not send too much output. + * The formula we compute is within <1 wei of the lossless version: sqrtPX96 +- amount / liquidity + * @param sqrtPX96 The starting price, i.e., before accounting for the token1 delta + * @param liquidity The amount of usable liquidity + * @param amount How much of token1 to add, or remove, from virtual reserves + * @param add Whether to add, or remove, the amount of token1 + * @return The price after adding or removing `amount` + */ + private static BigInteger getNextSqrtPriceFromAmount1RoundingDown ( + BigInteger sqrtPX96, + BigInteger liquidity, + BigInteger amount, + boolean add + ) { + // if we're adding (subtracting), rounding down requires rounding the quotient down (up) + // in both cases, avoid a mulDiv for most inputs + if (add) { + BigInteger quotient = amount.compareTo(IntUtils.MAX_UINT160) <= 0 + ? amount.shiftLeft(FixedPoint96.RESOLUTION).divide(liquidity) + : FullMath.mulDiv(amount, FixedPoint96.Q96, liquidity); + + return sqrtPX96.add(quotient); + } else { + BigInteger quotient = amount.compareTo(IntUtils.MAX_UINT160) <= 0 + ? UnsafeMath.divRoundingUp(amount.shiftLeft(FixedPoint96.RESOLUTION), liquidity) + : FullMath.mulDivRoundingUp(amount, FixedPoint96.Q96, liquidity); + Context.require(sqrtPX96.compareTo(quotient) > 0, "getNextSqrtPriceFromAmount1RoundingDown"); + // always fits 160 bits + return sqrtPX96.subtract(quotient); + } + } + + /** + * @notice Gets the next sqrt price given an input amount of token0 or token1 + * @dev Throws if price or liquidity are 0, or if the next price is out of bounds + * @param sqrtPX96 The starting price, i.e., before accounting for the input amount + * @param liquidity The amount of usable liquidity + * @param amountIn How much of token0, or token1, is being swapped in + * @param zeroForOne Whether the amount in is token0 or token1 + * @return sqrtQX96 The price after adding the input amount to token0 or token1 + */ + public static BigInteger getNextSqrtPriceFromInput ( + BigInteger sqrtPX96, + BigInteger liquidity, + BigInteger amountIn, + boolean zeroForOne + ) { + Context.require(sqrtPX96.compareTo(ZERO) > 0, "sqrtPX96 > 0"); + Context.require(liquidity.compareTo(ZERO) > 0, "liquidity > 0"); + + // round to make sure that we don't pass the target price + return zeroForOne ? + getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountIn, true) + : getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountIn, true); + } + + /** + * @notice Gets the next sqrt price given an output amount of token0 or token1 + * @dev Throws if price or liquidity are 0 or the next price is out of bounds + * @param sqrtPX96 The starting price before accounting for the output amount + * @param liquidity The amount of usable liquidity + * @param amountOut How much of token0, or token1, is being swapped out + * @param zeroForOne Whether the amount out is token0 or token1 + * @return sqrtQX96 The price after removing the output amount of token0 or token1 + */ + public static BigInteger getNextSqrtPriceFromOutput ( + BigInteger sqrtPX96, + BigInteger liquidity, + BigInteger amountOut, + boolean zeroForOne + ) { + Context.require(sqrtPX96.compareTo(ZERO) > 0, "getNextSqrtPriceFromOutput: sqrtPX96 > 0"); + Context.require(liquidity.compareTo(ZERO) > 0, "getNextSqrtPriceFromOutput: liquidity > 0"); + + // round to make sure that we pass the target price + return zeroForOne ? + getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountOut, false) + : getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountOut, false); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SwapMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SwapMath.java new file mode 100644 index 000000000..4cc0e385a --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SwapMath.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.libs; + +import static java.math.BigInteger.ZERO; + +import java.math.BigInteger; + +import network.balanced.score.core.dex.structs.pool.ComputeSwapStepResult; +import static network.balanced.score.core.dex.utils.IntUtils.uint256; + +public class SwapMath { + /** + * @notice Computes the result of swapping some amount in, or amount out, given the parameters of the swap + * @dev The fee, plus the amount in, will never exceed the amount remaining if the swap's `amountSpecified` is positive + * @param sqrtRatioCurrentX96 The current sqrt price of the pool + * @param sqrtRatioTargetX96 The price that cannot be exceeded, from which the direction of the swap is inferred + * @param liquidity The usable liquidity + * @param amountRemaining How much input or output amount is remaining to be swapped in/out + * @param feePips The fee taken from the input amount, expressed in hundredths of a bip + * @return sqrtRatioNextX96 The price after swapping the amount in/out, not to exceed the price target + * @return amountIn The amount to be swapped in, of either token0 or token1, based on the direction of the swap + * @return amountOut The amount to be received, of either token0 or token1, based on the direction of the swap + * @return feeAmount The amount of input that will be taken as a fee + */ + public static ComputeSwapStepResult computeSwapStep ( + BigInteger sqrtRatioCurrentX96, + BigInteger sqrtRatioTargetX96, + BigInteger liquidity, + BigInteger amountRemaining, + int feePips + ) { + boolean zeroForOne = sqrtRatioCurrentX96.compareTo(sqrtRatioTargetX96) >= 0; + boolean exactIn = amountRemaining.compareTo(ZERO) >= 0; + final BigInteger TEN_E6 = BigInteger.valueOf(1000000); + + BigInteger sqrtRatioNextX96 = ZERO; + BigInteger amountIn = ZERO; + BigInteger amountOut = ZERO; + BigInteger feeAmount = ZERO; + + if (exactIn) { + BigInteger amountRemainingLessFee = FullMath.mulDiv(amountRemaining, TEN_E6.subtract(BigInteger.valueOf(feePips)), TEN_E6); + amountIn = zeroForOne + ? SqrtPriceMath.getAmount0Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, true) + : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, true); + + if (amountRemainingLessFee.compareTo(amountIn) >= 0) { + sqrtRatioNextX96 = sqrtRatioTargetX96; + } + else { + sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput( + sqrtRatioCurrentX96, + liquidity, + amountRemainingLessFee, + zeroForOne + ); + } + } else { + amountOut = zeroForOne + ? SqrtPriceMath.getAmount1Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, false) + : SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, false); + + if (uint256(amountRemaining.negate()).compareTo(amountOut) >= 0) { + sqrtRatioNextX96 = sqrtRatioTargetX96; + } + else { + sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput( + sqrtRatioCurrentX96, + liquidity, + uint256(amountRemaining.negate()), + zeroForOne + ); + } + } + + + boolean max = sqrtRatioTargetX96.equals(sqrtRatioNextX96); + + // get the input/output amounts + if (zeroForOne) { + amountIn = max && exactIn + ? amountIn + : SqrtPriceMath.getAmount0Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, true); + amountOut = max && !exactIn + ? amountOut + : SqrtPriceMath.getAmount1Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, false); + } else { + amountIn = max && exactIn + ? amountIn + : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, true); + amountOut = max && !exactIn + ? amountOut + : SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, false); + } + + // cap the output amount to not exceed the remaining output amount + if (!exactIn && amountOut.compareTo(uint256(amountRemaining.negate())) > 0) { + amountOut = uint256(amountRemaining.negate()); + } + + if (exactIn && sqrtRatioNextX96 != sqrtRatioTargetX96) { + // we didn't reach the target, so take the remainder of the maximum input as fee + feeAmount = uint256(amountRemaining).subtract(amountIn); + } else { + feeAmount = FullMath.mulDivRoundingUp(amountIn, BigInteger.valueOf(feePips), TEN_E6.subtract(BigInteger.valueOf(feePips))); + } + + return new ComputeSwapStepResult(sqrtRatioNextX96, amountIn, amountOut, feeAmount); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickLib.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickLib.java new file mode 100644 index 000000000..d38528f4f --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickLib.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.libs; + +import java.math.BigInteger; + +public class TickLib { + public static BigInteger tickSpacingToMaxLiquidityPerTick (int tickSpacing) { + int minTick = ((int) ((float) TickMath.MIN_TICK / tickSpacing)) * tickSpacing; + int maxTick = ((int) ((float) TickMath.MAX_TICK / tickSpacing)) * tickSpacing; + int numTicks = ((maxTick - minTick) / tickSpacing) + 1; + // 340282366920938463463374607431768211456 = 2^128 + return new BigInteger("340282366920938463463374607431768211456").divide(BigInteger.valueOf(numTicks)); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickMath.java new file mode 100644 index 000000000..c5fd7d94a --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickMath.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.libs; + +import static java.math.BigInteger.ONE; +import static java.math.BigInteger.ZERO; +import static network.balanced.score.core.dex.utils.MathUtils.gt; +import java.math.BigInteger; +import network.balanced.score.core.dex.utils.IntUtils; +import score.Context; + +// @title Math library for computing sqrt prices from ticks and vice versa +// @notice Computes sqrt price for ticks of size 1.0001, i.e. sqrt(1.0001^tick) as fixed point Q64.96 numbers. Supports +// prices between 2**-128 and 2**128 +public class TickMath { + // @dev The minimum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**-128 + public final static int MIN_TICK = -887272; + // @dev The maximum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**128 + public final static int MAX_TICK = -MIN_TICK; + + // @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) + public final static BigInteger MIN_SQRT_RATIO = new BigInteger("4295128739"); + // @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) + public final static BigInteger MAX_SQRT_RATIO = new BigInteger("1461446703485210103287273052203988822378723970342"); + + public static BigInteger getSqrtRatioAtTick (int tick) { + BigInteger absTick = BigInteger.valueOf(tick).abs(); + Context.require(absTick.compareTo(BigInteger.valueOf(MAX_TICK)) <= 0, + "getSqrtRatioAtTick: tick can't be superior to MAX_TICK"); + + BigInteger ratio = + !absTick.and(BigInteger.valueOf(0x1)).equals(ZERO) ? new BigInteger("fffcb933bd6fad37aa2d162d1a594001", 16) : new BigInteger("100000000000000000000000000000000", 16); + if (!absTick.and(BigInteger.valueOf(0x2)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("fff97272373d413259a46990580e213a", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x4)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("fff2e50f5f656932ef12357cf3c7fdcc", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x8)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("ffe5caca7e10e4e61c3624eaa0941cd0", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x10)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("ffcb9843d60f6159c9db58835c926644", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x20)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("ff973b41fa98c081472e6896dfb254c0", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x40)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("ff2ea16466c96a3843ec78b326b52861", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x80)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("fe5dee046a99a2a811c461f1969c3053", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x100)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("fcbe86c7900a88aedcffc83b479aa3a4", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x200)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("f987a7253ac413176f2b074cf7815e54", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x400)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("f3392b0822b70005940c7a398e4b70f3", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x800)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("e7159475a2c29b7443b29c7fa6e889d9", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x1000)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("d097f3bdfd2022b8845ad8f792aa5825", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x2000)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("a9f746462d870fdf8a65dc1f90e061e5", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x4000)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("70d869a156d2a1b890bb3df62baf32f7", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x8000)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("31be135f97d08fd981231505542fcfa6", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x10000)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("9aa508b5b7a84e1c677de54f3e99bc9", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x20000)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("5d6af8dedb81196699c329225ee604", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x40000)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("2216e584f5fa1ea926041bedfe98", 16))).shiftRight(128); + if (!absTick.and(BigInteger.valueOf(0x80000)).equals(ZERO)) ratio = (ratio.multiply(new BigInteger("48a170391f7dc42444e8fa2", 16))).shiftRight(128); + + if (tick > 0) { + ratio = IntUtils.MAX_UINT256.divide(ratio); + } + + // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96. + // we then downcast because we know the result always fits within 160 bits due to our tick input constraint + // we round up in the division so getTickAtSqrtRatio of the output price is always consistent + return (ratio.shiftRight(32).add(ratio.mod(ONE.shiftLeft(32)).equals(ZERO) ? ZERO : ONE)); + } + + public static int getTickAtSqrtRatio (BigInteger sqrtPriceX96) { + // second inequality must be < because the price can never reach the price at the max tick + Context.require(sqrtPriceX96.compareTo(MIN_SQRT_RATIO) >= 0 + && sqrtPriceX96.compareTo(MAX_SQRT_RATIO) < 0, + "getTickAtSqrtRatio: preconditions failed"); + + BigInteger ratio = sqrtPriceX96.shiftLeft(32); + BigInteger r = ratio; + BigInteger msb = ZERO; + BigInteger f = null; + + f = gt(r, new BigInteger("ffffffffffffffffffffffffffffffff", 16)).shiftLeft(7); + msb = msb.or(f); + r = r.shiftRight(f.intValue()); + + f = gt(r, new BigInteger("ffffffffffffffff", 16)).shiftLeft(6); + msb = msb.or(f); + r = r.shiftRight(f.intValue()); + + f = gt(r, new BigInteger("ffffffff", 16)).shiftLeft(5); + msb = msb.or(f); + r = r.shiftRight(f.intValue()); + + f = gt(r, new BigInteger("ffff", 16)).shiftLeft(4); + msb = msb.or(f); + r = r.shiftRight(f.intValue()); + + f = gt(r, new BigInteger("ff", 16)).shiftLeft(3); + msb = msb.or(f); + r = r.shiftRight(f.intValue()); + + f = gt(r, new BigInteger("f", 16)).shiftLeft(2); + msb = msb.or(f); + r = r.shiftRight(f.intValue()); + + f = gt(r, new BigInteger("3", 16)).shiftLeft(1); + msb = msb.or(f); + r = r.shiftRight(f.intValue()); + + f = gt(r, new BigInteger("1", 16)); + msb = msb.or(f); + + if (msb.compareTo(BigInteger.valueOf(128)) >= 0) { + r = ratio.shiftRight(msb.intValue() - 127); + } else { + r = ratio.shiftLeft(127 - msb.intValue()); + } + + BigInteger log_2 = msb.subtract(BigInteger.valueOf(128)).shiftLeft(64); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(63)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(62)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(61)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(60)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(59)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(58)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(57)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(56)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(55)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(54)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(53)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(52)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(51)); + r = r.shiftRight(f.intValue()); + + r = r.multiply(r).shiftRight(127); + f = r.shiftRight(128); + log_2 = log_2.or(f.shiftLeft(50)); + + BigInteger log_sqrt10001 = log_2.multiply(new BigInteger("255738958999603826347141")); // 128.128 number + int tickLow = log_sqrt10001.subtract(new BigInteger("3402992956809132418596140100660247210")).shiftRight(128).intValue(); + int tickHi = log_sqrt10001.add(new BigInteger("291339464771989622907027621153398088495")).shiftRight(128).intValue(); + + return tickLow == tickHi ? tickLow : getSqrtRatioAtTick(tickHi).compareTo(sqrtPriceX96) <= 0 ? tickHi : tickLow; + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/UnsafeMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/UnsafeMath.java new file mode 100644 index 000000000..fc629d4d5 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/UnsafeMath.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.libs; + +import java.math.BigInteger; +import static network.balanced.score.core.dex.utils.MathUtils.gt; + +public class UnsafeMath { + public static BigInteger divRoundingUp(BigInteger x, BigInteger y) { + return x.divide(y).add(gt(x.mod(y), BigInteger.ZERO)); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/ConcentratedLiquidityPoolDeployer.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/ConcentratedLiquidityPoolDeployer.java new file mode 100644 index 000000000..d528196ed --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/ConcentratedLiquidityPoolDeployer.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.models; + +import network.balanced.score.core.dex.structs.factory.Parameters; +import network.balanced.score.lib.utils.Names; +import score.Address; +import score.Context; +import score.VarDB; +import score.annotation.External; + +public class ConcentratedLiquidityPoolDeployer { + + // ================================================ + // Consts + // ================================================ + // Contract class name + private static final String NAME = Names.POOL_DEPLOYER; + + // ================================================ + // DB Variables + // ================================================ + public final VarDB parameters = Context.newVarDB(NAME + "_parameters", Parameters.class); + + // ================================================ + // Methods + // ================================================ + public Address deploy(byte[] contractBytes, Address factory, Address token0, Address token1, int fee, int tickSpacing) { + this.parameters.set(new Parameters(factory, token0, token1, fee, tickSpacing)); + Address pool = Context.deploy(contractBytes); + this.parameters.set(null); + return pool; + } + + public void update ( + Address pool, + byte[] contractBytes, + Address factory, + Address token0, + Address token1, + int fee, + int tickSpacing + ) { + this.parameters.set(new Parameters(factory, token0, token1, fee, tickSpacing)); + + Address result = Context.deploy(pool, contractBytes); + Context.require(result.equals(pool), + NAME + "::update: invalid pool address"); + + this.parameters.set(null); + } + + @External(readonly = true) + public Parameters parameters () { + return this.parameters.get(); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Observations.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Observations.java new file mode 100644 index 000000000..07bfb4733 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Observations.java @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.models; + +import static java.math.BigInteger.ONE; +import static java.math.BigInteger.ZERO; + +import java.math.BigInteger; +import network.balanced.score.core.dex.libs.OracleLib; +import network.balanced.score.core.dex.structs.pool.BeforeAfterObservation; +import network.balanced.score.core.dex.structs.pool.InitializeResult; +import network.balanced.score.core.dex.structs.pool.ObserveResult; +import network.balanced.score.core.dex.structs.pool.Oracle; +import network.balanced.score.core.dex.structs.pool.Oracle.Observation; +import network.balanced.score.core.dex.utils.MathUtils; +import score.Context; +import score.DictDB; +import score.VarDB; + +public class Observations { + // ================================================ + // Consts + // ================================================ + // Contract class name + private static final String NAME = "ObservationsDB"; + private static final BigInteger TWO_POWER_32 = MathUtils.pow(BigInteger.TWO, 32); + + // ================================================ + // DB Variables + // ================================================ + // Returns data about a specific observation index + private final DictDB observations = Context.newDictDB(NAME + "_observations", Oracle.Observation.class); + private final VarDB oldestIndex = Context.newVarDB(NAME + "_oldestIndex", Integer.class); + + // ================================================ + // Methods + // ================================================ + public Oracle.Observation get (int index) { + return this.observations.getOrDefault(index, Oracle.Observation.empty()); + } + + private void setOldestIndex (int currentIndex, int cardinality) { + Integer oldestIndex = this.oldestIndex.get(); + if (oldestIndex == null) { + // Not initialized, oldest = current index + this.oldestIndex.set(currentIndex); + } + else if (oldestIndex == currentIndex) { + // new oldest = next one to the previous one + this.oldestIndex.set((currentIndex + 1) % cardinality); + } + } + + public void set (int index, Oracle.Observation observation) { + this.observations.set(index, observation); + } + + private boolean lte (BigInteger time, BigInteger a, BigInteger b) { + // if there hasn't been overflow, no need to adjust + if (a.compareTo(time) <= 0 && b.compareTo(time) <= 0) { + return a.compareTo(b) <= 0; + } + + BigInteger aAdjusted = a.compareTo(time) > 0 ? a : a.add(TWO_POWER_32); + BigInteger bAdjusted = b.compareTo(time) > 0 ? b : b.add(TWO_POWER_32); + + return aAdjusted.compareTo(bAdjusted) <= 0; + } + + public InitializeResult initialize (BigInteger time) { + Oracle.Observation observation = new Oracle.Observation(time, ZERO, ZERO, true); + this.set(0, observation); + this.setOldestIndex(0, 1); + return new InitializeResult(1, 1); + } + + /** + * @notice Fetches the observations beforeOrAt and atOrAfter a target, i.e. where [beforeOrAt, atOrAfter] is satisfied. + * The result may be the same observation, or adjacent observations. + * @dev The answer must be contained in the array, used when the target is located within the stored observation + * boundaries: older than the most recent observation and younger, or the same age as, the oldest observation + * @param time The current block.timestamp + * @param target The timestamp at which the reserved observation should be for + * @param index The index of the observation that was most recently written to the observations array + * @param cardinality The number of populated elements in the oracle array + * @return The observation recorded and after + */ + private BeforeAfterObservation binarySearch ( + BigInteger time, + BigInteger target, + int index, + int cardinality + ) { + int l = (index + 1) % cardinality; // oldest observation + int r = l + cardinality - 1; // newest observation + int i; + + Oracle.Observation beforeOrAt = null; + Oracle.Observation atOrAfter = null; + + while (true) { + i = (l + r) / 2; + + beforeOrAt = this.get(i % cardinality); + + // we've landed on an uninitialized tick, keep searching higher (more recently) + if (!beforeOrAt.initialized) { + l = i + 1; + continue; + } + + atOrAfter = this.get((i + 1) % cardinality); + + boolean targetAtOrAfter = lte(time, beforeOrAt.blockTimestamp, target); + + // check if we've found the answer! + if (targetAtOrAfter && lte(time, target, atOrAfter.blockTimestamp)) { + break; + } + + if (!targetAtOrAfter) { + r = i - 1; + } else { + l = i + 1; + } + } + + return new BeforeAfterObservation(beforeOrAt, atOrAfter); + } + + private BeforeAfterObservation getSurroundingObservations ( + BigInteger time, + BigInteger target, + int tick, + int index, + BigInteger liquidity, + int cardinality + ) { + // optimistically set before to the newest observation + Oracle.Observation beforeOrAt = this.get(index); + + // if the target is chronologically at or after the newest observation, we can early return + if (lte(time, beforeOrAt.blockTimestamp, target)) { + if (beforeOrAt.blockTimestamp.equals(target)) { + // if newest observation equals target, we're in the same block, so we can ignore atOrAfter + Oracle.Observation atOrAfter = Oracle.Observation.empty(); + return new BeforeAfterObservation(beforeOrAt, atOrAfter); + } else { + // otherwise, we need to transform + return new BeforeAfterObservation(beforeOrAt, OracleLib.transform(beforeOrAt, target, tick, liquidity)); + } + } + + // now, set before to the oldest observation + beforeOrAt = this.get((index + 1) % cardinality); + if (!beforeOrAt.initialized) { + beforeOrAt = this.get(0); + } + + // ensure that the target is chronologically at or after the oldest observation + Context.require(lte(time, beforeOrAt.blockTimestamp, target), + "getSurroundingObservations: too old"); + + // if we've reached this point, we have to binary search + return binarySearch(time, target, index, cardinality); + } + + public class ObserveSingleResult { + public BigInteger tickCumulative; + public BigInteger secondsPerLiquidityCumulativeX128; + + ObserveSingleResult (BigInteger tickCumulative, BigInteger secondsPerLiquidityCumulativeX128) { + this.tickCumulative = tickCumulative; + this.secondsPerLiquidityCumulativeX128 = secondsPerLiquidityCumulativeX128; + } + } + + public ObserveSingleResult observeSingle (BigInteger time, BigInteger secondsAgo, int tick, int index, BigInteger liquidity, int cardinality) { + if (secondsAgo.equals(ZERO)) { + Oracle.Observation last = this.get(index); + if (!last.blockTimestamp.equals(time)) { + last = OracleLib.transform(last, time, tick, liquidity); + return new ObserveSingleResult(last.tickCumulative, last.secondsPerLiquidityCumulativeX128); + } + } + + BigInteger target = time.subtract(secondsAgo); + + BeforeAfterObservation result = getSurroundingObservations(time, target, tick, index, liquidity, cardinality); + Oracle.Observation beforeOrAt = result.beforeOrAt; + Oracle.Observation atOrAfter = result.atOrAfter; + + if (target.equals(beforeOrAt.blockTimestamp)) { + // we're at the left boundary + return new ObserveSingleResult(beforeOrAt.tickCumulative, beforeOrAt.secondsPerLiquidityCumulativeX128); + } else if (target.equals(atOrAfter.blockTimestamp)) { + // we're at the right boundary + return new ObserveSingleResult(atOrAfter.tickCumulative, atOrAfter.secondsPerLiquidityCumulativeX128); + } else { + // we're in the middle + BigInteger observationTimeDelta = atOrAfter.blockTimestamp.subtract(beforeOrAt.blockTimestamp); + BigInteger targetDelta = target.subtract(beforeOrAt.blockTimestamp); + return new ObserveSingleResult( + beforeOrAt.tickCumulative.add( + atOrAfter.tickCumulative.subtract(beforeOrAt.tickCumulative).divide(observationTimeDelta).multiply(targetDelta) + ), + beforeOrAt.secondsPerLiquidityCumulativeX128.add( + atOrAfter.secondsPerLiquidityCumulativeX128.subtract(beforeOrAt.secondsPerLiquidityCumulativeX128).multiply(targetDelta).divide(observationTimeDelta) + ) + ); + } + } + + public ObserveResult observe (BigInteger time, BigInteger[] secondsAgos, int tick, int index, BigInteger liquidity, int cardinality) { + Context.require(cardinality > 0, + "observe: cardinality must be superior to 0"); + + BigInteger[] tickCumulatives = new BigInteger[secondsAgos.length]; + BigInteger[] secondsPerLiquidityCumulativeX128s = new BigInteger[secondsAgos.length]; + + for (int i = 0; i < secondsAgos.length; i++) { + ObserveSingleResult result = observeSingle(time, secondsAgos[i], tick, index, liquidity, cardinality); + tickCumulatives[i] = result.tickCumulative; + secondsPerLiquidityCumulativeX128s[i] = result.secondsPerLiquidityCumulativeX128; + } + + return new ObserveResult(tickCumulatives, secondsPerLiquidityCumulativeX128s); + } + + /** + * @notice Prepares the oracle array to store up to `next` observations + * @param current The current next cardinality of the oracle array + * @param next The proposed next cardinality which will be populated in the oracle array + * @return next The next cardinality which will be populated in the oracle array + */ + public int grow (int current, int next) { + Context.require(current > 0, + "grow: current must be superior to 0"); + + // no-op if the passed next value isn't greater than the current next value + if (next <= current) { + return current; + } + + // this data will not be used because the initialized boolean is still false + for (int i = current; i < next; i++) { + Oracle.Observation observation = this.get(i); + observation.blockTimestamp = ONE; + this.set(i, observation); + this.setOldestIndex(i, next); + } + + return next; + } + + public class WriteResult { + public int observationIndex; + public int observationCardinality; + public WriteResult (int observationIndex, int observationCardinality) { + this.observationIndex = observationIndex; + this.observationCardinality = observationCardinality; + } + } + + public WriteResult write ( + int index, + BigInteger blockTimestamp, + int tick, + BigInteger liquidity, + int cardinality, + int cardinalityNext + ) { + Oracle.Observation last = this.get(index); + + // early return if we've already written an observation this block + if (last.blockTimestamp.equals(blockTimestamp)) { + return new WriteResult(index, cardinality); + } + + int cardinalityUpdated = cardinality; + // if the conditions are right, we can bump the cardinality + if (cardinalityNext > cardinality && index == (cardinality - 1)) { + cardinalityUpdated = cardinalityNext; + } + + int indexUpdated = (index + 1) % cardinalityUpdated; + this.set(indexUpdated, OracleLib.transform(last, blockTimestamp, tick, liquidity)); + this.setOldestIndex(indexUpdated, cardinalityUpdated); + + return new WriteResult(indexUpdated, cardinalityUpdated); + } + + public Observation getOldest () { + return this.get(this.oldestIndex.getOrDefault(0)); + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Positions.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Positions.java new file mode 100644 index 000000000..b7fa06481 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Positions.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package network.balanced.score.core.dex.models; + +import network.balanced.score.core.dex.structs.pool.Position; +import network.balanced.score.core.dex.utils.BytesUtils; +import score.Address; +import score.Context; +import score.DictDB; + +public class Positions { + // ================================================ + // Consts + // ================================================ + // Contract class name + private static final String NAME = "PositionsDB"; + + // Returns the information about a position by the position's key + private final DictDB positions = Context.newDictDB(NAME + "_positions", Position.Info.class); + + public Position.Info get (byte[] key) { + var position = this.positions.get(key); + return position == null ? Position.Info.empty() : position; + } + + public void set (byte[] key, Position.Info value) { + this.positions.set(key, value); + } + + public static byte[] getKey ( + Address owner, + int tickLower, + int tickUpper + ) { + return Context.hash("sha3-256", + BytesUtils.concat( + owner.toByteArray(), + BytesUtils.intToBytes(tickLower), + BytesUtils.intToBytes(tickUpper) + ) + ); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/TickBitmap.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/TickBitmap.java new file mode 100644 index 000000000..7559f9338 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/TickBitmap.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.models; + +import static java.math.BigInteger.ONE; +import static java.math.BigInteger.ZERO; +import static network.balanced.score.core.dex.utils.IntUtils.uint8; +import java.math.BigInteger; +import network.balanced.score.core.dex.libs.BitMath; +import network.balanced.score.core.dex.structs.pool.NextInitializedTickWithinOneWordResult; +import network.balanced.score.core.dex.utils.IntUtils; +import score.Context; +import score.DictDB; + +public class TickBitmap { + + // ================================================ + // Consts + // ================================================ + // Class name + private static final String NAME = "TickBitmapDB"; + + // ================================================ + // DB Variables + // ================================================ + // Returns 256 packed tick initialized boolean values. See TickBitmap for more information + private final DictDB tickBitmap = Context.newDictDB(NAME + "_tickBitmap", BigInteger.class); + + // ================================================ + // Methods + // ================================================ + public class PositionResult { + public int wordPos; + public int bitPos; + + public PositionResult (int wordPos, int bitPos) { + this.wordPos = wordPos; + this.bitPos = bitPos; + } + } + + public BigInteger get (int index) { + return this.tickBitmap.getOrDefault(index, ZERO); + } + + /** + * @notice Computes the position in the mapping where the initialized bit for a tick lives + * @param tick The tick for which to compute the position + * @return wordPos The key in the mapping containing the word in which the bit is stored + * @return bitPos The bit position in the word where the flag is stored + */ + private PositionResult position (int tick) { + int wordPos = tick >> 8; + int bitPos = uint8(tick % 256); + return new PositionResult(wordPos, bitPos); + } + + /** + * @notice Flips the initialized state for a given tick from false to true, or vice versa + * @param tick The tick to flip + * @param tickSpacing The spacing between usable ticks + */ + public void flipTick ( + int tick, + int tickSpacing + ) { + // ensure that the tick is spaced + Context.require(tick % tickSpacing == 0, + "flipTick: tick isn't spaced"); + + var result = position(tick / tickSpacing); + int wordPos = result.wordPos; + int bitPos = result.bitPos; + + BigInteger mask = ONE.shiftLeft(bitPos); + BigInteger packedTick = this.get(wordPos); + + this.tickBitmap.set(wordPos, packedTick.xor(mask)); + } + + public NextInitializedTickWithinOneWordResult nextInitializedTickWithinOneWord ( + int tick, + int tickSpacing, + boolean lte + ) { + + int compressed = tick / tickSpacing; + + if (tick < 0 && tick % tickSpacing != 0) { + compressed--; // round towards negative infinity + } + + if (lte) { + var position = position(compressed); + int wordPos = position.wordPos; + int bitPos = position.bitPos; + + var oneShifted = ONE.shiftLeft(bitPos); + // all the 1s at or to the right of the current bitPos + BigInteger mask = oneShifted.subtract(ONE).add(oneShifted); + BigInteger masked = this.get(wordPos).and(mask); + + // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word + boolean initialized = !masked.equals(ZERO); + // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick + int tickNext = initialized + ? (compressed - (bitPos - BitMath.mostSignificantBit(masked))) * tickSpacing + : (compressed - bitPos) * tickSpacing; + + return new NextInitializedTickWithinOneWordResult (tickNext, initialized); + } else { + // start from the word of the next tick, since the current tick state doesn't matter + var position = position(compressed + 1); + int wordPos = position.wordPos; + int bitPos = position.bitPos; + // all the 1s at or to the left of the bitPos + BigInteger mask = ONE.shiftLeft(bitPos).subtract(ONE).not(); + BigInteger masked = this.get(wordPos).and(mask); + + // if there are no initialized ticks to the left of the current tick, return leftmost in the word + boolean initialized = !masked.equals(ZERO); + // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick + int tickNext = initialized + ? (compressed + 1 + BitMath.leastSignificantBit(masked) - bitPos) * tickSpacing + : (compressed + 1 + IntUtils.MAX_UINT8.intValue() - bitPos) * tickSpacing; + + return new NextInitializedTickWithinOneWordResult (tickNext, initialized); + } + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Ticks.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Ticks.java new file mode 100644 index 000000000..0576934e9 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Ticks.java @@ -0,0 +1,220 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.models; + +import static java.math.BigInteger.ZERO; + +import java.math.BigInteger; +import network.balanced.score.core.dex.libs.LiquidityMath; +import network.balanced.score.core.dex.structs.pool.Tick; +import network.balanced.score.core.dex.structs.pool.Tick.Info; +import network.balanced.score.core.dex.utils.EnumerableMap; +import network.balanced.score.core.dex.utils.EnumerableSet; +import score.Context; + +public class Ticks { + // ================================================ + // Consts + // ================================================ + // Class name + private static final String NAME = "TicksDB"; + + // ================================================ + // DB Variables + // ================================================ + // Look up information about a specific tick in the pool + private final EnumerableMap ticks = new EnumerableMap<>(NAME + "_ticks", Integer.class, Tick.Info.class); + private final EnumerableSet initialized = new EnumerableSet<>(NAME + "_initialized", Integer.class); + + // ================================================ + // Methods + // ================================================ + public Info get (int key) { + var result = this.ticks.get(key); + return result == null ? Tick.Info.empty(key) : result; + } + + public int initializedSize () { + return this.initialized.length(); + } + + public int initialized (int index) { + return this.initialized.get(index); + } + + private void set (int key, Tick.Info value) { + this.ticks.set(key, value); + if (value != null && value.initialized) { + this.initialized.add(key); + } else { + // either deleted or uninitialized + this.initialized.remove(key); + } + } + + public class UpdateResult { + public Info info; + public boolean flipped; + public UpdateResult(Info info, boolean flipped) { + this.info = info; + this.flipped = flipped; + } + } + + /** + * @notice Updates a tick and returns true if the tick was flipped from initialized to uninitialized, or vice versa + * @param tick The tick that will be updated + * @param tickCurrent The current tick + * @param liquidityDelta A new amount of liquidity to be added (subtracted) when tick is crossed from left to right (right to left) + * @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0 + * @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1 + * @param secondsPerLiquidityCumulativeX128 The all-time seconds per max(1, liquidity) of the pool + * @param tickCumulative The tick * time elapsed since the pool was first initialized + * @param time The current block timestamp cast to a uint32 + * @param upper true for updating a position's upper tick, or false for updating a position's lower tick + * @param maxLiquidity The maximum liquidity allocation for a single tick + * @return flipped Whether the tick was flipped from initialized to uninitialized, or vice versa + */ + public UpdateResult update ( + int tick, + int tickCurrent, + BigInteger liquidityDelta, + BigInteger feeGrowthGlobal0X128, + BigInteger feeGrowthGlobal1X128, + BigInteger secondsPerLiquidityCumulativeX128, + BigInteger tickCumulative, + BigInteger time, + boolean upper, + BigInteger maxLiquidity + ) { + Tick.Info info = this.get(tick); + BigInteger liquidityGrossBefore = info.liquidityGross; + BigInteger liquidityGrossAfter = LiquidityMath.addDelta(liquidityGrossBefore, liquidityDelta); + + Context.require(liquidityGrossAfter.compareTo(maxLiquidity) <= 0, + "update: liquidityGrossAfter <= maxLiquidity"); + + boolean flipped = (liquidityGrossAfter.equals(ZERO)) != (liquidityGrossBefore.equals(ZERO)); + + if (liquidityGrossBefore.equals(ZERO)) { + // by convention, we assume that all growth before a tick was initialized happened _below_ the tick + if (tick <= tickCurrent) { + info.feeGrowthOutside0X128 = feeGrowthGlobal0X128; + info.feeGrowthOutside1X128 = feeGrowthGlobal1X128; + info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128; + info.tickCumulativeOutside = tickCumulative; + info.secondsOutside = time; + } + info.initialized = true; + } + + info.liquidityGross = liquidityGrossAfter; + + // when the lower (upper) tick is crossed left to right (right to left), liquidity must be added (removed) + info.liquidityNet = upper + ? info.liquidityNet.subtract(liquidityDelta) + : info.liquidityNet.add(liquidityDelta); + + this.set(tick, info); + return new UpdateResult(info, flipped); + } + + public class GetFeeGrowthInsideResult { + public BigInteger feeGrowthInside0X128; + public BigInteger feeGrowthInside1X128; + public GetFeeGrowthInsideResult (BigInteger feeGrowthInside0X128, BigInteger feeGrowthInside1X128) { + this.feeGrowthInside0X128 = feeGrowthInside0X128; + this.feeGrowthInside1X128 = feeGrowthInside1X128; + } + } + + public GetFeeGrowthInsideResult getFeeGrowthInside ( + int tickLower, + int tickUpper, + int tickCurrent, + BigInteger feeGrowthGlobal0X128, + BigInteger feeGrowthGlobal1X128 + ) { + Tick.Info lower = this.get(tickLower); + Tick.Info upper = this.get(tickUpper); + + // calculate fee growth below + BigInteger feeGrowthBelow0X128; + BigInteger feeGrowthBelow1X128; + + if (tickCurrent >= tickLower) { + feeGrowthBelow0X128 = lower.feeGrowthOutside0X128; + feeGrowthBelow1X128 = lower.feeGrowthOutside1X128; + } else { + feeGrowthBelow0X128 = feeGrowthGlobal0X128.subtract(lower.feeGrowthOutside0X128); + feeGrowthBelow1X128 = feeGrowthGlobal1X128.subtract(lower.feeGrowthOutside1X128); + } + + // calculate fee growth above + BigInteger feeGrowthAbove0X128; + BigInteger feeGrowthAbove1X128; + + if (tickCurrent < tickUpper) { + feeGrowthAbove0X128 = upper.feeGrowthOutside0X128; + feeGrowthAbove1X128 = upper.feeGrowthOutside1X128; + } else { + feeGrowthAbove0X128 = feeGrowthGlobal0X128.subtract(upper.feeGrowthOutside0X128); + feeGrowthAbove1X128 = feeGrowthGlobal1X128.subtract(upper.feeGrowthOutside1X128); + } + + return new GetFeeGrowthInsideResult( + feeGrowthGlobal0X128.subtract(feeGrowthBelow0X128).subtract(feeGrowthAbove0X128), + feeGrowthGlobal1X128.subtract(feeGrowthBelow1X128).subtract(feeGrowthAbove1X128) + ); + } + + /** + * @notice Clears tick data + * @param tick The tick that will be cleared + */ + public void clear(int tick) { + this.set(tick, null); + } + + /** + * @notice Transitions to next tick as needed by price movement + * @param tick The destination tick of the transition + * @param feeGrowthGlobal0X128 The all-time global fee growth, per unit of liquidity, in token0 + * @param feeGrowthGlobal1X128 The all-time global fee growth, per unit of liquidity, in token1 + * @param secondsPerLiquidityCumulativeX128 The current seconds per liquidity + * @param tickCumulative The tick * time elapsed since the pool was first initialized + * @param time The current block.timestamp + * @return liquidityNet The amount of liquidity added (subtracted) when tick is crossed from left to right (right to left) + */ + public Info cross ( + int tick, + BigInteger feeGrowthGlobal0X128, + BigInteger feeGrowthGlobal1X128, + BigInteger secondsPerLiquidityCumulativeX128, + BigInteger tickCumulative, + BigInteger time + ) { + Tick.Info info = this.get(tick); + info.feeGrowthOutside0X128 = feeGrowthGlobal0X128.subtract(info.feeGrowthOutside0X128); + info.feeGrowthOutside1X128 = feeGrowthGlobal1X128.subtract(info.feeGrowthOutside1X128); + info.secondsPerLiquidityOutsideX128 = secondsPerLiquidityCumulativeX128.subtract(info.secondsPerLiquidityOutsideX128); + info.tickCumulativeOutside = tickCumulative.subtract(info.tickCumulativeOutside); + info.secondsOutside = time.subtract(info.secondsOutside); + this.set(tick, info); + return info; + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/factory/Parameters.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/factory/Parameters.java new file mode 100644 index 000000000..b381133a6 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/factory/Parameters.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.factory; + +import java.math.BigInteger; +import java.util.Map; + +import score.Address; +import score.ObjectReader; +import score.ObjectWriter; + +public class Parameters { + public Address factory; + public Address token0; + public Address token1; + public Integer fee; + public Integer tickSpacing; + + public static void writeObject(ObjectWriter w, Parameters obj) { + w.write(obj.factory); + w.write(obj.token0); + w.write(obj.token1); + w.write(obj.fee); + w.write(obj.tickSpacing); + } + + public static Parameters readObject(ObjectReader r) { + return new Parameters( + r.readAddress(), // factory, + r.readAddress(), // token0, + r.readAddress(), // token1, + r.readInt(), // fee, + r.readInt() // tickSpacing + ); + } + + public Parameters ( + Address factory, + Address token0, + Address token1, + Integer fee, + Integer tickSpacing + ) { + this.factory = factory; + this.token0 = token0; + this.token1 = token1; + this.fee = fee; + this.tickSpacing = tickSpacing; + } + + public static Parameters fromMap (Object call) { + @SuppressWarnings("unchecked") + Map map = (Map) call; + return new Parameters ( + (Address) map.get("factory"), + (Address) map.get("token0"), + (Address) map.get("token1"), + ((BigInteger) map.get("fee")).intValue(), + ((BigInteger) map.get("tickSpacing")).intValue() + ); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/BeforeAfterObservation.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/BeforeAfterObservation.java new file mode 100644 index 000000000..6b2c2e3b3 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/BeforeAfterObservation.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +public class BeforeAfterObservation { + public Oracle.Observation beforeOrAt; + public Oracle.Observation atOrAfter; + + public BeforeAfterObservation (Oracle.Observation beforeOrAt, Oracle.Observation atOrAfter) { + this.beforeOrAt = beforeOrAt; + this.atOrAfter = atOrAfter; + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ComputeSwapStepResult.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ComputeSwapStepResult.java new file mode 100644 index 000000000..4c5834d74 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ComputeSwapStepResult.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; + +public class ComputeSwapStepResult { + public BigInteger sqrtRatioNextX96; + public BigInteger amountIn; + public BigInteger amountOut; + public BigInteger feeAmount; + public ComputeSwapStepResult ( + BigInteger sqrtRatioNextX96, + BigInteger amountIn, + BigInteger amountOut, + BigInteger feeAmount + ) { + this.sqrtRatioNextX96 = sqrtRatioNextX96; + this.amountIn = amountIn; + this.amountOut = amountOut; + this.feeAmount = feeAmount; + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/InitializeResult.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/InitializeResult.java new file mode 100644 index 000000000..fbfc570b5 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/InitializeResult.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +public class InitializeResult { + public int cardinality; + public int cardinalityNext; + + public InitializeResult (int cardinality, int cardinalityNext) { + this.cardinality = cardinality; + this.cardinalityNext = cardinalityNext; + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/MintCallbackData.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/MintCallbackData.java new file mode 100644 index 000000000..456d36efb --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/MintCallbackData.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import score.Address; +import score.ObjectReader; +import score.ObjectWriter; + +public class MintCallbackData { + public PoolAddress.PoolKey poolKey; + public Address payer; + + public MintCallbackData (PoolAddress.PoolKey poolKey, Address payer) { + this.poolKey = poolKey; + this.payer = payer; + } + + public static MintCallbackData readObject(ObjectReader reader) { + PoolAddress.PoolKey poolKey = reader.read(PoolAddress.PoolKey.class); + Address payer = reader.readAddress(); + return new MintCallbackData(poolKey, payer); + } + + public static void writeObject(ObjectWriter w, MintCallbackData obj) { + w.write(obj.poolKey); + w.write(obj.payer); + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ModifyPositionParams.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ModifyPositionParams.java new file mode 100644 index 000000000..cab30311b --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ModifyPositionParams.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; + +import score.Address; + +public class ModifyPositionParams { + // the address that owns the position + public Address owner; + // the lower and upper tick of the position + public int tickLower; + public int tickUpper; + // any change in liquidity + public BigInteger liquidityDelta; + + public ModifyPositionParams(Address recipient, int tickLower, int tickUpper, BigInteger amount) { + this.owner = recipient; + this.tickLower = tickLower; + this.tickUpper = tickUpper; + this.liquidityDelta = amount; + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ModifyPositionResult.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ModifyPositionResult.java new file mode 100644 index 000000000..f7be0f172 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ModifyPositionResult.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; + +public class ModifyPositionResult { + public PositionStorage positionStorage; + public BigInteger amount0; + public BigInteger amount1; + + public ModifyPositionResult (PositionStorage positionStorage, BigInteger amount0, BigInteger amount1) { + this.positionStorage = positionStorage; + this.amount0 = amount0; + this.amount1 = amount1; + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/NextInitializedTickWithinOneWordResult.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/NextInitializedTickWithinOneWordResult.java new file mode 100644 index 000000000..a92d6e35a --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/NextInitializedTickWithinOneWordResult.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +public class NextInitializedTickWithinOneWordResult { + public int tickNext; + public boolean initialized; + + public NextInitializedTickWithinOneWordResult () {} + + public NextInitializedTickWithinOneWordResult ( + int tickNext, + boolean initialized + ) { + this.tickNext = tickNext; + this.initialized = initialized; + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ObserveResult.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ObserveResult.java new file mode 100644 index 000000000..947af5cdd --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ObserveResult.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; +import network.balanced.score.core.dex.utils.ArrayUtils; + +public class ObserveResult { + // Cumulative tick values as of each `secondsAgos` from the current block timestamp + public BigInteger[] tickCumulatives; + // Cumulative seconds per liquidity-in-range value as of each `secondsAgos` from the current block + public BigInteger[] secondsPerLiquidityCumulativeX128s; + + public ObserveResult (BigInteger[] tickCumulatives, BigInteger[] secondsPerLiquidityCumulativeX128s) { + this.tickCumulatives = tickCumulatives; + this.secondsPerLiquidityCumulativeX128s = secondsPerLiquidityCumulativeX128s; + } + + @SuppressWarnings("unchecked") + public static ObserveResult fromMap (Object call) { + Map map = (Map) call; + var tickCumulatives = (List) map.get("tickCumulatives"); + var secondsPerLiquidityCumulativeX128s = (List) map.get("secondsPerLiquidityCumulativeX128s"); + + return new ObserveResult ( + ArrayUtils.fromList(tickCumulatives), + ArrayUtils.fromList(secondsPerLiquidityCumulativeX128s) + ); + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Oracle.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Oracle.java new file mode 100644 index 000000000..e930e253b --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Oracle.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import static java.math.BigInteger.ZERO; +import java.math.BigInteger; +import java.util.Map; + +import score.ObjectReader; +import score.ObjectWriter; + +public class Oracle { + public static class Observation { + // the block timestamp of the observation + public BigInteger blockTimestamp; + // the tick accumulator, i.e. tick * time elapsed since the pool was first initialized + public BigInteger tickCumulative; + // the seconds per liquidity, i.e. seconds elapsed / max(1, liquidity) since the pool was first initialized + public BigInteger secondsPerLiquidityCumulativeX128; + // whether or not the observation is initialized + public Boolean initialized; + + public Observation ( + BigInteger blockTimestamp, + BigInteger tickCumulative, + BigInteger secondsPerLiquidityCumulativeX128, + Boolean initialized + ) { + this.blockTimestamp = blockTimestamp; + this.tickCumulative = tickCumulative; + this.secondsPerLiquidityCumulativeX128 = secondsPerLiquidityCumulativeX128; + this.initialized = initialized; + } + + public static void writeObject(ObjectWriter w, Observation obj) { + w.write(obj.blockTimestamp); + w.write(obj.tickCumulative); + w.write(obj.secondsPerLiquidityCumulativeX128); + w.write(obj.initialized); + } + + public static Observation readObject(ObjectReader r) { + return new Observation( + r.readBigInteger(), // blockTimestamp, + r.readBigInteger(), // tickCumulative, + r.readBigInteger(), // secondsPerLiquidityCumulativeX128, + r.readBoolean() // initialized, + ); + } + + public static Observation fromMap(Object call) { + @SuppressWarnings("unchecked") + Map map = (Map) call; + return new Observation( + (BigInteger) map.get("blockTimestamp"), + (BigInteger) map.get("tickCumulative"), + (BigInteger) map.get("secondsPerLiquidityCumulativeX128"), + (Boolean) map.get("initialized") + ); + } + + public static Oracle.Observation empty () { + return new Oracle.Observation(ZERO, ZERO, ZERO, false); + } + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PairAmounts.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PairAmounts.java new file mode 100644 index 000000000..c25807054 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PairAmounts.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; +import java.util.Map; + +public class PairAmounts { + // Amount of token0 + public BigInteger amount0; + // Amount of token1 + public BigInteger amount1; + + public PairAmounts (BigInteger amount0, BigInteger amount1) { + this.amount0 = amount0; + this.amount1 = amount1; + } + + public static PairAmounts fromMap (Object call) { + @SuppressWarnings("unchecked") + Map map = (Map) call; + return new PairAmounts ( + (BigInteger) map.get("amount0"), + (BigInteger) map.get("amount1") + ); + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolAddress.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolAddress.java new file mode 100644 index 000000000..f5b8c5994 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolAddress.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import score.Address; +import score.ObjectReader; +import score.ObjectWriter; + +public class PoolAddress { + public static class PoolKey { + public Address token0; + public Address token1; + public int fee; + + public PoolKey(Address token0, Address token1, int fee) { + this.token0 = token0; + this.token1 = token1; + this.fee = fee; + } + + public static PoolKey readObject(ObjectReader reader) { + Address token0 = reader.readAddress(); + Address token1 = reader.readAddress(); + int fee = reader.readInt(); + return new PoolKey(token0, token1, fee); + } + + public static void writeObject(ObjectWriter w, PoolKey obj) { + w.write(obj.token0); + w.write(obj.token1); + w.write(obj.fee); + } + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolData.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolData.java new file mode 100644 index 000000000..30cb8ffc0 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolData.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package network.balanced.score.core.dex.structs.pool; + +import score.Address; + +public class PoolData { + // The first token of the given pool + public Address tokenA; + // The second token of the given pool + public Address tokenB; + // The fee level of the pool + public int fee; + + public PoolData (Address tokenA, Address tokenB, int fee) { + this.tokenA = tokenA; + this.tokenB = tokenB; + this.fee = fee; + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolSettings.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolSettings.java new file mode 100644 index 000000000..5a201981b --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolSettings.java @@ -0,0 +1,68 @@ +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; +import java.util.Map; +import score.Address; + +public class PoolSettings { + // Contract name + public String name; + + // The contract that deployed the pool + public Address factory; + + // The first of the two tokens of the pool, sorted by address + public Address token0; + + // The second of the two tokens of the pool, sorted by address + public Address token1; + + // The pool's fee in hundredths of a bip, i.e. 1e-6 + public int fee; + + // The pool tick spacing + // @dev Ticks can only be used at multiples of this value, minimum of 1 and always positive + // e.g.: a tickSpacing of 3 means ticks can be initialized every 3rd tick, i.e., ..., -6, -3, 0, 3, 6, ... + // This value is an int to avoid casting even though it is always positive. + public int tickSpacing; + + // The maximum amount of position liquidity that can use any tick in the range + // @dev This parameter is enforced per tick to prevent liquidity from overflowing an int at any point, and + // also prevents out-of-range liquidity from being used to prevent adding in-range liquidity to a pool + // @return The max amount of liquidity per tick + public BigInteger maxLiquidityPerTick; + + public PoolSettings ( + Address factory, + Address token0, + Address token1, + Integer fee, + Integer tickSpacing, + BigInteger maxLiquidityPerTick, + String name + ) { + this.factory = factory; + this.token0 = token0; + this.token1 = token1; + this.fee = fee; + this.tickSpacing = tickSpacing; + this.maxLiquidityPerTick = maxLiquidityPerTick; + this.name = name; + } + + public PoolSettings () {} + + public static PoolSettings fromMap(Object call) { + @SuppressWarnings("unchecked") + Map map = (Map) call; + return new PoolSettings ( + (Address) map.get("factory"), + (Address) map.get("token0"), + (Address) map.get("token1"), + ((BigInteger) map.get("fee")).intValue(), + ((BigInteger) map.get("tickSpacing")).intValue(), + (BigInteger) map.get("maxLiquidityPerTick"), + (String) map.get("name") + ); + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Position.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Position.java new file mode 100644 index 000000000..b453efb86 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Position.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import static java.math.BigInteger.ZERO; +import java.math.BigInteger; +import java.util.Map; + +import score.ObjectReader; +import score.ObjectWriter; + +public class Position { + public static class Info { + // the amount of liquidity owned by this position + public BigInteger liquidity; + + // fee growth per unit of liquidity as of the last update to liquidity or fees owed + public BigInteger feeGrowthInside0LastX128; + public BigInteger feeGrowthInside1LastX128; + + // the fees owed to the position owner in token0/token1 + public BigInteger tokensOwed0; + public BigInteger tokensOwed1; + + public Info ( + BigInteger liquidity, + BigInteger feeGrowthInside0LastX128, + BigInteger feeGrowthInside1LastX128, + BigInteger tokensOwed0, + BigInteger tokensOwed1 + ) { + this.liquidity = liquidity; + this.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; + this.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; + this.tokensOwed0 = tokensOwed0; + this.tokensOwed1 = tokensOwed1; + } + + public static void writeObject(ObjectWriter w, Info obj) { + w.write(obj.liquidity); + w.write(obj.feeGrowthInside0LastX128); + w.write(obj.feeGrowthInside1LastX128); + w.write(obj.tokensOwed0); + w.write(obj.tokensOwed1); + } + + public static Info readObject(ObjectReader r) { + return new Info( + r.readBigInteger(), // liquidity + r.readBigInteger(), // feeGrowthInside0LastX128 + r.readBigInteger(), // feeGrowthInside1LastX128 + r.readBigInteger(), // tokensOwed0 + r.readBigInteger() // tokensOwed1 + ); + } + + public static Info fromMap(Object call) { + @SuppressWarnings("unchecked") + Map map = (Map) call; + return new Info( + (BigInteger) map.get("liquidity"), + (BigInteger) map.get("feeGrowthInside0LastX128"), + (BigInteger) map.get("feeGrowthInside1LastX128"), + (BigInteger) map.get("tokensOwed0"), + (BigInteger) map.get("tokensOwed1") + ); + } + + public static Position.Info empty () { + return new Position.Info (ZERO, ZERO, ZERO, ZERO, ZERO); + } + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PositionStorage.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PositionStorage.java new file mode 100644 index 000000000..d2cdc2a98 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PositionStorage.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +public class PositionStorage { + public Position.Info position; + public byte[] key; + + public PositionStorage(Position.Info position, byte[] positionKey) { + this.position = position; + this.key = positionKey; + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ProtocolFees.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ProtocolFees.java new file mode 100644 index 000000000..7231fbc68 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ProtocolFees.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; +import java.util.Map; + +import score.ObjectReader; +import score.ObjectWriter; + +// accumulated protocol fees in token0/token1 units +public class ProtocolFees { + public BigInteger token0; + public BigInteger token1; + + public ProtocolFees ( + BigInteger token0, + BigInteger token1 + ) { + this.token0 = token0; + this.token1 = token1; + } + + public static void writeObject(ObjectWriter w, ProtocolFees obj) { + w.write(obj.token0); + w.write(obj.token1); + } + + public static ProtocolFees readObject(ObjectReader r) { + return new ProtocolFees( + r.readBigInteger(), // token0 + r.readBigInteger() // token1 + ); + } + + public static ProtocolFees fromMap(Object call) { + @SuppressWarnings("unchecked") + Map map = (Map) call; + return new ProtocolFees ( + (BigInteger) map.get("token0"), + (BigInteger) map.get("token1") + ); + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Slot0.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Slot0.java new file mode 100644 index 000000000..d1c72e229 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Slot0.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; +import java.util.Map; +import score.ObjectReader; +import score.ObjectWriter; + +public class Slot0 { + // The current price of the pool as a sqrt(token1/token0) Q64.96 value + public BigInteger sqrtPriceX96; + // The current tick of the pool, i.e. according to the last tick transition that was run. + // This value may not always be equal to SqrtTickMath.getTickAtSqrtRatio(sqrtPriceX96) if the price is on a tick boundary + public int tick; + // The index of the last oracle observation that was written + public int observationIndex; + // the current maximum number of observations that are being stored + public int observationCardinality; + // the next maximum number of observations to store, triggered in observations.write + public int observationCardinalityNext; + // The current protocol fee as a percentage of the swap fee taken on withdrawal + // represented as an integer denominator (1/x)% + // 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. + public int feeProtocol; + // Whether the pool is locked + public boolean unlocked; + + public static void writeObject (ObjectWriter w, Slot0 obj) { + w.write(obj.sqrtPriceX96); + w.write(obj.tick); + w.write(obj.observationIndex); + w.write(obj.observationCardinality); + w.write(obj.observationCardinalityNext); + w.write(obj.feeProtocol); + w.write(obj.unlocked); + } + + public static Slot0 readObject(ObjectReader r) { + return new Slot0( + r.readBigInteger(), // sqrtPriceX96 + r.readInt(), // tick + r.readInt(), // observationIndex + r.readInt(), // observationCardinality + r.readInt(), // observationCardinalityNext + r.readInt(), // feeProtocol + r.readBoolean() // unlocked + ); + } + + public static Slot0 fromMap(Object call) { + @SuppressWarnings("unchecked") + Map map = (Map) call; + return new Slot0( + (BigInteger) map.get("sqrtPriceX96"), + ((BigInteger) map.get("tick")).intValue(), + ((BigInteger) map.get("observationIndex")).intValue(), + ((BigInteger) map.get("observationCardinality")).intValue(), + ((BigInteger) map.get("observationCardinalityNext")).intValue(), + ((BigInteger) map.get("feeProtocol")).intValue(), + (Boolean) map.get("unlocked") + ); + } + + public Slot0 ( + BigInteger sqrtPriceX96, + int tick, + int observationIndex, + int observationCardinality, + int observationCardinalityNext, + int feeProtocol, + boolean unlocked + ) { + this.sqrtPriceX96 = sqrtPriceX96; + this.tick = tick; + this.observationIndex = observationIndex; + this.observationCardinality = observationCardinality; + this.observationCardinalityNext = observationCardinalityNext; + this.feeProtocol = feeProtocol; + this.unlocked = unlocked; + } + + public Slot0 () {} +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SnapshotCumulativesInsideResult.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SnapshotCumulativesInsideResult.java new file mode 100644 index 000000000..aafed722a --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SnapshotCumulativesInsideResult.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; +import java.util.Map; + +public class SnapshotCumulativesInsideResult { + public BigInteger tickCumulativeInside; + public BigInteger secondsPerLiquidityInsideX128; + public BigInteger secondsInside; + + public SnapshotCumulativesInsideResult (BigInteger tickCumulativeInside, BigInteger secondsPerLiquidityInsideX128, BigInteger secondsInside) { + this.tickCumulativeInside = tickCumulativeInside; + this.secondsPerLiquidityInsideX128 = secondsPerLiquidityInsideX128; + this.secondsInside = secondsInside; + } + + public static SnapshotCumulativesInsideResult fromMap(Object call) { + @SuppressWarnings("unchecked") + Map map = (Map) call; + return new SnapshotCumulativesInsideResult ( + (BigInteger) map.get("tickCumulativeInside"), + (BigInteger) map.get("secondsPerLiquidityInsideX128"), + (BigInteger) map.get("secondsInside") + ); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/StepComputations.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/StepComputations.java new file mode 100644 index 000000000..2fb710a51 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/StepComputations.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; + +public class StepComputations { + // the price at the beginning of the step + public BigInteger sqrtPriceStartX96; + // the next tick to swap to from the current tick in the swap direction + public int tickNext; + // whether tickNext is initialized or not + public boolean initialized; + // sqrt(price) for the next tick (1/0) + public BigInteger sqrtPriceNextX96; + // how much is being swapped in in this step + public BigInteger amountIn; + // how much is being swapped out + public BigInteger amountOut; + // how much fee is being paid in + public BigInteger feeAmount; +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SwapCache.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SwapCache.java new file mode 100644 index 000000000..a1660275c --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SwapCache.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; + +public class SwapCache { + // the protocol fee for the input token + public int feeProtocol; + // liquidity at the beginning of the swap + public BigInteger liquidityStart; + // the timestamp of the current block + public BigInteger blockTimestamp; + // the current value of the tick accumulator, computed only if we cross an initialized tick + public BigInteger tickCumulative; + // the current value of seconds per liquidity accumulator, computed only if we cross an initialized tick + public BigInteger secondsPerLiquidityCumulativeX128; + // whether we've computed and cached the above two accumulators + public boolean computedLatestObservation; + + public SwapCache( + BigInteger liquidityStart, + BigInteger blockTimestamp, + int feeProtocol, + BigInteger secondsPerLiquidityCumulativeX128, + BigInteger tickCumulative, + boolean computedLatestObservation + ) { + this.liquidityStart = liquidityStart; + this.blockTimestamp = blockTimestamp; + this.feeProtocol = feeProtocol; + this.secondsPerLiquidityCumulativeX128 = secondsPerLiquidityCumulativeX128; + this.tickCumulative = tickCumulative; + this.computedLatestObservation = computedLatestObservation; + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SwapState.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SwapState.java new file mode 100644 index 000000000..ebfe961ff --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SwapState.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import java.math.BigInteger; + +// the top level state of the swap, the results of which are recorded in storage at the end +public class SwapState { + // the amount remaining to be swapped in/out of the input/output asset + public BigInteger amountSpecifiedRemaining; + // the amount already swapped out/in of the output/input asset + public BigInteger amountCalculated; + // current sqrt(price) + public BigInteger sqrtPriceX96; + // the tick associated with the current price + public int tick; + // the global fee growth of the input token + public BigInteger feeGrowthGlobalX128; + // amount of input token paid as protocol fee + public BigInteger protocolFee; + // the current liquidity in range + public BigInteger liquidity; + + public SwapState( + BigInteger amountSpecifiedRemaining, + BigInteger amountCalculated, + BigInteger sqrtPriceX96, + int tick, + BigInteger feeGrowthGlobalX128, + BigInteger protocolFee, + BigInteger liquidity + ) { + this.amountSpecifiedRemaining = amountSpecifiedRemaining; + this.amountCalculated = amountCalculated; + this.sqrtPriceX96 = sqrtPriceX96; + this.tick = tick; + this.feeGrowthGlobalX128 = feeGrowthGlobalX128; + this.protocolFee = protocolFee; + this.liquidity = liquidity; + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Tick.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Tick.java new file mode 100644 index 000000000..d9c757416 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Tick.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.structs.pool; + +import static java.math.BigInteger.ZERO; +import java.math.BigInteger; +import java.util.Map; + +import score.ObjectReader; +import score.ObjectWriter; + +public class Tick { + public static class Info { + // the tick index + public Integer index; + + // the total position liquidity that references this tick + public BigInteger liquidityGross; + + // amount of net liquidity added (subtracted) when tick is crossed from left to right (right to left), + public BigInteger liquidityNet; + + // fee growth per unit of liquidity on the _other_ side of this tick (relative to the current tick) + // only has relative meaning, not absolute — the value depends on when the tick is initialized + public BigInteger feeGrowthOutside0X128; + public BigInteger feeGrowthOutside1X128; + + // the cumulative tick value on the other side of the tick + public BigInteger tickCumulativeOutside; + + // the seconds per unit of liquidity on the _other_ side of this tick (relative to the current tick) + // only has relative meaning, not absolute — the value depends on when the tick is initialized + public BigInteger secondsPerLiquidityOutsideX128; + + // the seconds spent on the other side of the tick (relative to the current tick) + // only has relative meaning, not absolute — the value depends on when the tick is initialized + public BigInteger secondsOutside; + + // true if the tick is initialized, i.e. the value is exactly equivalent to the expression liquidityGross != 0 + // these 8 bits are set to prevent fresh sstores when crossing newly initialized ticks + // Outside values can only be used if the tick is initialized, i.e. if liquidityGross is greater than 0. + // In addition, these values are only relative and must be used only in comparison to previous snapshots for + // a specific position. + public boolean initialized; + + public Info ( + Integer index, + BigInteger liquidityGross, + BigInteger liquidityNet, + BigInteger feeGrowthOutside0X128, + BigInteger feeGrowthOutside1X128, + BigInteger tickCumulativeOutside, + BigInteger secondsPerLiquidityOutsideX128, + BigInteger secondsOutside, + boolean initialized + ) { + this.index = index; + this.liquidityGross = liquidityGross; + this.liquidityNet = liquidityNet; + this.feeGrowthOutside0X128 = feeGrowthOutside0X128; + this.feeGrowthOutside1X128 = feeGrowthOutside1X128; + this.tickCumulativeOutside = tickCumulativeOutside; + this.secondsPerLiquidityOutsideX128 = secondsPerLiquidityOutsideX128; + this.secondsOutside = secondsOutside; + this.initialized = initialized; + } + + public static void writeObject(ObjectWriter w, Info obj) { + w.write(obj.index); + w.write(obj.liquidityGross); + w.write(obj.liquidityNet); + w.write(obj.feeGrowthOutside0X128); + w.write(obj.feeGrowthOutside1X128); + w.write(obj.tickCumulativeOutside); + w.write(obj.secondsPerLiquidityOutsideX128); + w.write(obj.secondsOutside); + w.write(obj.initialized); + } + + public static Info readObject(ObjectReader r) { + return new Info( + r.readInt(), // index + r.readBigInteger(), // liquidityGross + r.readBigInteger(), // liquidityNet + r.readBigInteger(), // feeGrowthOutside0X128 + r.readBigInteger(), // feeGrowthOutside1X128 + r.readBigInteger(), // tickCumulativeOutside + r.readBigInteger(), // secondsPerLiquidityOutsideX128 + r.readBigInteger(), // secondsOutside + r.readBoolean() // initialized + ); + } + + public static Info fromMap(Object call) { + @SuppressWarnings("unchecked") + Map map = (Map) call; + return new Info( + ((BigInteger) map.get("index")).intValueExact(), + (BigInteger) map.get("liquidityGross"), + (BigInteger) map.get("liquidityNet"), + (BigInteger) map.get("feeGrowthOutside0X128"), + (BigInteger) map.get("feeGrowthOutside1X128"), + (BigInteger) map.get("tickCumulativeOutside"), + (BigInteger) map.get("secondsPerLiquidityOutsideX128"), + (BigInteger) map.get("secondsOutside"), + (Boolean) map.get("initialized") + ); + } + + public static Info empty(int index) { + return new Tick.Info(index, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, ZERO, false); + } + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/AddressUtils.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/AddressUtils.java new file mode 100644 index 000000000..5280f7136 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/AddressUtils.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.utils; + +import java.math.BigInteger; + +import score.Address; + +public class AddressUtils { + public static final Address ZERO_ADDRESS = new Address(new byte[Address.LENGTH]); + + public static int compareTo (Address a, Address b) { + return new BigInteger(a.toByteArray()).compareTo(new BigInteger(b.toByteArray())); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/ArrayUtils.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/ArrayUtils.java new file mode 100644 index 000000000..85878c564 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/ArrayUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.utils; + +import java.math.BigInteger; +import java.util.List; +import score.Address; + +public class ArrayUtils { + public static BigInteger[] arrayCopy (BigInteger[] array) { + BigInteger[] result = new BigInteger[array.length]; + System.arraycopy(array, 0, result, 0, array.length); + return result; + } + + public static void arrayFill (T[] array, T fill) { + int size = array.length; + for (int i = 0; i < size; i++) { + array[i] = fill; + } + } + + public static BigInteger[] newFill (int size, BigInteger fill) { + BigInteger[] result = new BigInteger[size]; + arrayFill(result, fill); + return result; + } + + public static Integer[] newFill (int size, Integer fill) { + Integer[] result = new Integer[size]; + arrayFill(result, fill); + return result; + } + + public static Address[] newFill (int size, Address fill) { + Address[] result = new Address[size]; + arrayFill(result, fill); + return result; + } + + public static String toString (Object[] array) { + StringBuilder sb = new StringBuilder(); + sb.append('['); + for (int i = 0; i < array.length; i++) { + sb.append(array[i].toString()); + if (i != array.length - 1) sb.append(", "); + } + sb.append(']'); + return sb.toString(); + } + + public static boolean contains (Object[] array, Object item) { + for (Object current : array) { + if (current.equals(item)) { + return true; + } + } + return false; + } + + public static BigInteger[] fromList (List list) { + final int size = list.size(); + BigInteger[] result = new BigInteger[size]; + for (int i = 0; i < size; i++) { + result[i] = list.get(i); + } + return result; + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/BytesUtils.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/BytesUtils.java new file mode 100644 index 000000000..6d823a60d --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/BytesUtils.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.utils; + +import score.Context; + +public class BytesUtils { + + public final static int INT_SIZE = 4; + + // Assume big endianess + public static int bytesToInt (byte[] bytearray) { + Context.require(bytearray.length == INT_SIZE, + "bytesToInt: Invalid bytearray size"); + return ((bytearray[0] & 0xFF) << 24) + | ((bytearray[1] & 0xFF) << 16) + | ((bytearray[2] & 0xFF) << 8) + | ((bytearray[3] & 0xFF)); + } + + // Assume big endianess + public static byte[] intToBytes (int data) { + return new byte[] { + (byte)((data >> 24) & 0xff), + (byte)((data >> 16) & 0xff), + (byte)((data >> 8) & 0xff), + (byte)((data >> 0) & 0xff), + }; + } + + public static byte[] concat (byte[] ... array) { + int size = 0; + + for (byte[] item : array) { + size += item.length; + } + + byte[] result = new byte[size]; + int destPos = 0; + + for (byte[] item : array) { + System.arraycopy(item, 0, result, destPos, item.length); + destPos += item.length; + } + + return result; + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/EnumerableMap.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/EnumerableMap.java new file mode 100644 index 000000000..da16709d1 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/EnumerableMap.java @@ -0,0 +1,61 @@ +/* + * Copyright 2021 ICONLOOP Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.utils; + +import score.Context; +import score.DictDB; + +public class EnumerableMap { + private final EnumerableSet keys; + private final DictDB values; + + public EnumerableMap(String id, Class keyClass, Class valueClass) { + this.keys = new EnumerableSet(id + "_keys", keyClass); + this.values = Context.newDictDB(id + "_values", valueClass); + } + + public int size() { + return keys.length(); + } + + public boolean contains(K key) { + return keys.contains(key); + } + + public K getKey(int index) { + return keys.get(index); + } + + public V get(K key) { + return values.get(key); + } + + public V getOrDefault(K key, V value) { + var entry = this.get(key); + return entry != null ? entry : value; + } + + public void set(K key, V value) { + values.set(key, value); + keys.add(key); + } + + public void remove(K key) { + values.set(key, null); + keys.remove(key); + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/EnumerableSet.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/EnumerableSet.java new file mode 100644 index 000000000..bed76d065 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/EnumerableSet.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.utils; + +import score.ArrayDB; +import score.Context; +import score.DictDB; + +public class EnumerableSet { + private final ArrayDB entries; + private final DictDB indexes; + + public EnumerableSet(String id, Class valueClass) { + // array of valueClass + this.entries = Context.newArrayDB(id, valueClass); + // value => array index + this.indexes = Context.newDictDB(id, Integer.class); + } + + public int length() { + return entries.size(); + } + + public V get(int index) { + return entries.get(index); + } + + public Integer indexOf(V value) { + // returns null if value doesn't exist + Integer result = indexes.get(value); + if (result != null) { + return result - 1; + } + return null; + } + + public boolean contains(V value) { + return indexes.get(value) != null; + } + + public void add(V value) { + if (!contains(value)) { + // add new value + entries.add(value); + indexes.set(value, entries.size()); + } + } + + public void remove(V value) { + var valueIndex = indexes.get(value); + if (valueIndex != null) { + // pop and swap with the last entry + int lastIndex = entries.size(); + V lastValue = entries.pop(); + indexes.set(value, null); + if (lastIndex != valueIndex) { + entries.set(valueIndex - 1, lastValue); + indexes.set(lastValue, valueIndex); + } + } + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/ICX.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/ICX.java new file mode 100644 index 000000000..2673a491a --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/ICX.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.utils; + +import java.math.BigInteger; + +import score.Address; +import score.Context; + +public class ICX { + public static Address ADDRESS = Address.fromString("cx1111111111111111111111111111111111111111"); + private static final int DECIMALS = 18; + private static final String SYMBOL = "ICX"; + + public static void transfer ( + Address targetAddress, + BigInteger value + ) { + Context.transfer(targetAddress, value); + } + + public static void transfer ( + Address targetAddress, + BigInteger value, + String method, + Object... params + ) { + if (targetAddress.isContract()) { + Context.call(value, targetAddress, method, params); + } else { + Context.transfer(targetAddress, value); + } + } + + public static Address getAddress () { + return ICX.ADDRESS; + } + + public static boolean isICX (Address token) { + return token.equals(ADDRESS); + } + + public static String symbol () { + return ICX.SYMBOL; + } + + public static int decimals () { + return ICX.DECIMALS; + } + + public static BigInteger balanceOf(Address address) { + return Context.getBalance(address); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/IntUtils.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/IntUtils.java new file mode 100644 index 000000000..f408c71a6 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/IntUtils.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.utils; + +import static java.math.BigInteger.ZERO; + +import java.math.BigInteger; + +public class IntUtils { + public final static BigInteger MAX_UINT8 = new BigInteger("ff", 16); + public final static BigInteger MAX_UINT16 = new BigInteger("ffff", 16); + public final static BigInteger MAX_UINT32 = new BigInteger("ffffffff", 16); + public final static BigInteger MAX_UINT64 = new BigInteger("ffffffffffffffff", 16); + public static final BigInteger MAX_UINT96 = new BigInteger("ffffffffffffffffffffffff", 16); + public final static BigInteger MAX_UINT128 = new BigInteger("ffffffffffffffffffffffffffffffff", 16); + public final static BigInteger MAX_UINT160 = new BigInteger("ffffffffffffffffffffffffffffffffffffffff", 16); + public final static BigInteger MAX_INT256 = new BigInteger("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16); + public final static BigInteger MAX_UINT256 = new BigInteger("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16); + public final static BigInteger TWO_POW_96 = MAX_UINT96.add(BigInteger.ONE); + public final static BigInteger TWO_POW_128 = MAX_UINT128.add(BigInteger.ONE); + public final static BigInteger TWO_POW_160 = MAX_UINT160.add(BigInteger.ONE); + public final static BigInteger TWO_POW_256 = MAX_UINT256.add(BigInteger.ONE); + + public static BigInteger uint128(BigInteger n) { + if (n.compareTo(ZERO) < 0) { + return n.add(TWO_POW_128); + } + return n.mod(TWO_POW_128); + } + + public static BigInteger uint256(BigInteger n) { + if (n.compareTo(ZERO) < 0) { + return n.add(TWO_POW_256); + } + return n.mod(TWO_POW_256); + } + + public static BigInteger uint96(BigInteger n) { + if (n.compareTo(ZERO) < 0) { + return n.add(TWO_POW_96); + } + return n.mod(TWO_POW_96); + } + + public static int uint8(int i) { + return i < 0 ? i + 256 : i; + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/JSONUtils.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/JSONUtils.java new file mode 100644 index 000000000..60dd87934 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/JSONUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.utils; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; + +public class JSONUtils { + public static byte[] method (String method) { + return ("{\"method\": \"" + method + "\"}").getBytes(); + } + + public static byte[] method (String method, JsonObject params) { + JsonObject data = Json.object() + .add("method", method) + .add("params", params); + + byte[] dataBytes = data.toString().getBytes(); + + return dataBytes; + } + + public static JsonObject parse (byte[] _data) { + return Json.parse(new String(_data)).asObject(); + } + + public static JsonObject parse (String _data) { + return Json.parse(_data).asObject(); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/MathUtils.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/MathUtils.java new file mode 100644 index 000000000..688682a46 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/MathUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.utils; + +import java.math.BigDecimal; +import java.math.BigInteger; + +public class MathUtils { + + public static BigInteger lt(BigInteger x, BigInteger y) { + return x.compareTo(y) < 0 ? BigInteger.ONE : BigInteger.ZERO; + } + + public static BigInteger gt (BigInteger x, BigInteger y) { + return x.compareTo(y) > 0 ? BigInteger.ONE : BigInteger.ZERO; + } + + public static BigInteger pow (BigInteger base, int exponent) { + BigInteger result = BigInteger.ONE; + for (int i = 0; i < exponent; i++) { + result = result.multiply(base); + } + return result; + } + + public static BigDecimal pow (BigDecimal base, int exponent) { + BigDecimal result = BigDecimal.ONE; + for (int i = 0; i < exponent; i++) { + result = result.multiply(base); + } + return result; + } + + public static BigInteger pow10 (int exponent) { + return MathUtils.pow(BigInteger.TEN, exponent); + } + + public static BigDecimal pow10_decimal (int exponent) { + return MathUtils.pow(BigDecimal.TEN, exponent); + } + + public static BigInteger min (BigInteger a, BigInteger b) { + return a.compareTo(b) > 0 ? b : a; + } + public static BigInteger max (BigInteger a, BigInteger b) { + return a.compareTo(b) > 0 ? a : b; + } + + public static BigInteger sum (BigInteger[] array) { + BigInteger result = BigInteger.ZERO; + for (var cur : array) { + result = result.add(cur); + } + return result; + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/StringUtils.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/StringUtils.java new file mode 100644 index 000000000..46ffc8607 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/StringUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.utils; + +import java.math.BigInteger; + +import score.Context; + +public class StringUtils { + + public static BigInteger toBigInt (String input) { + if (input.startsWith("0x")) { + return new BigInteger(input.substring(2), 16); + } + + if (input.startsWith("-0x")) { + return new BigInteger(input.substring(3), 16).negate(); + } + + return new BigInteger(input, 10); + } + + /** + * Convert a hexstring with or without leading "0x" to byte array + * @param hexstring a hexstring + * @return a byte array + */ + public static byte[] hexToByteArray(String hexstring) { + /* hexstring must be an even-length string. */ + Context.require(hexstring.length() % 2 == 0, + "hexToByteArray: invalid hexstring length"); + + if (hexstring.startsWith("0x")) { + hexstring = hexstring.substring(2); + } + + int len = hexstring.length(); + byte[] data = new byte[len / 2]; + + for (int i = 0; i < len; i += 2) { + int c1 = Character.digit(hexstring.charAt(i), 16) << 4; + int c2 = Character.digit(hexstring.charAt(i+1), 16); + + if (c1 == -1 || c2 == -1) { + Context.revert("hexToByteArray: invalid hexstring character at pos " + i); + } + + data[i / 2] = (byte) (c1 + c2); + } + return data; + } + + private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); + + public static String byteArrayToHex(byte[] data) { + char[] hexChars = new char[data.length * 2]; + + for (int j = 0; j < data.length; j++) { + int v = data[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + + return new String(hexChars); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/TimeUtils.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/TimeUtils.java new file mode 100644 index 000000000..374818085 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/TimeUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.dex.utils; + +import java.math.BigInteger; + +import score.Context; + +public class TimeUtils { + + public static final BigInteger TO_SECONDS = BigInteger.valueOf(1000 * 1000); + + public static final BigInteger ONE_SECOND = BigInteger.valueOf(1); + public static final BigInteger ONE_MINUTE = BigInteger.valueOf(60).multiply(ONE_SECOND); + public static final BigInteger ONE_HOUR = BigInteger.valueOf(60).multiply(ONE_MINUTE); + public static final BigInteger ONE_DAY = BigInteger.valueOf(24).multiply(ONE_HOUR); + public static final BigInteger ONE_WEEK = BigInteger.valueOf(7).multiply(ONE_DAY); + public static final BigInteger ONE_MONTH = BigInteger.valueOf(30).multiply(ONE_DAY); + public static final BigInteger ONE_YEAR = BigInteger.valueOf(365).multiply(ONE_DAY); + + public static BigInteger timestampToSeconds (long timestamp) { + return BigInteger.valueOf(timestamp).divide(TO_SECONDS); + } + + public static BigInteger now () { + return timestampToSeconds(Context.getBlockTimestamp()); + } +} diff --git a/core-contracts/NonFungiblePositionManager/build.gradle b/core-contracts/NonFungiblePositionManager/build.gradle new file mode 100644 index 000000000..eba3e4b7a --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/build.gradle @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import network.balanced.score.dependencies.Addresses +import network.balanced.score.dependencies.Dependencies + +version = '0.1.0' + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +sourceSets { + intTest { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + java { + srcDirs("src/intTest") + } + } +} + +// for integration tests +configurations { + intTestImplementation.extendsFrom testImplementation + intTestAnnotationProcessor.extendsFrom testAnnotationProcessor + intTestRuntimeOnly.extendsFrom testRuntimeOnly +} + +dependencies { + compileOnly Dependencies.javaeeApi + + implementation Dependencies.javaeeScorex + implementation Dependencies.minimalJson + implementation project(':score-lib') + implementation project(':Dex') + implementation project(':irc721') + + testImplementation Dependencies.javaeeUnitTest + testImplementation Dependencies.mockitoCore + testImplementation Dependencies.mockitoInline + + + testImplementation project(':test-lib') + testImplementation Dependencies.junitJupiter + testRuntimeOnly Dependencies.junitJupiterEngine + + intTestImplementation project(":score-client") + intTestAnnotationProcessor project(":score-client") + intTestImplementation Dependencies.iconSdk + intTestImplementation Dependencies.jacksonDatabind +} + +optimizedJar { + mainClassName = 'network.balanced.score.core.positionmgr.NonFungiblePositionManager' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + enableDebug = false +} + +deployJar { + endpoints { + sejong { + uri = 'https://sejong.net.solidwallet.io/api/v3' + nid = 0x53 + } + berlin { + uri = 'https://berlin.net.solidwallet.io/api/v3' + nid = 0x7 + to = Addresses.berlin.dex + } + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + to = Addresses.lisbon.dex + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + mainnet { + to = Addresses.mainnet.dex + uri = 'https://ctz.solidwallet.io/api/v3' + nid = 0x1 + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + arg("_governance", Addresses.mainnet.governance) + } +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + csv.required = false + html.outputLocation = layout.buildDirectory.dir('jacocoHtml') + } +} + +task integrationTest(type: Test, dependsOn: optimizedJar) { + useJUnitPlatform() + options { + testLogging.showStandardStreams = true + description = 'Runs integration tests.' + group = 'verification' + testClassesDirs = sourceSets.intTest.output.classesDirs + classpath = sourceSets.intTest.runtimeClasspath + systemProperty 'Dex', project.tasks.optimizedJar.outputJarName + project.extensions.deployJar.arguments.each { + arg -> systemProperty arg.name, arg.value + } + + } + +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/NonFungiblePositionManager.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/NonFungiblePositionManager.java new file mode 100644 index 000000000..3147969bf --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/NonFungiblePositionManager.java @@ -0,0 +1,652 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr; + +import static java.math.BigInteger.ONE; +import static java.math.BigInteger.ZERO; + +import java.math.BigInteger; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; +import com.eclipsesource.json.JsonValue; +import network.balanced.score.core.dex.interfaces.pool.IBalancedMintCallback; +import network.balanced.score.core.dex.libs.FixedPoint128; +import network.balanced.score.core.dex.libs.FullMath; +import network.balanced.score.core.dex.models.Positions; +import team.iconation.standards.token.irc721.IRC721Enumerable; + +import score.Address; +import score.Context; +import score.DictDB; +import score.VarDB; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; +import scorex.io.Reader; +import scorex.io.StringReader; + +import static network.balanced.score.core.dex.utils.IntUtils.uint128; +import network.balanced.score.core.positionmgr.interfaces.IBalancedLiquidityManagement; +import network.balanced.score.core.positionmgr.libs.PoolAddressLib; +import network.balanced.score.core.positionmgr.structs.AddLiquidityParams; +import network.balanced.score.core.positionmgr.implementation.BalancedLiquidityManagement; +import network.balanced.score.core.dex.interfaces.pool.IConcentratedLiquidityPool; +import network.balanced.score.core.dex.structs.pool.PairAmounts; +import network.balanced.score.core.dex.structs.pool.Position; +import network.balanced.score.core.dex.structs.pool.PoolAddress.PoolKey; +import network.balanced.score.core.positionmgr.interfaces.INonfungibleTokenPositionDescriptor; +import network.balanced.score.core.positionmgr.structs.CollectParams; +import network.balanced.score.core.positionmgr.structs.DecreaseLiquidityParams; +import network.balanced.score.core.positionmgr.structs.IncreaseLiquidityParams; +import network.balanced.score.core.positionmgr.structs.IncreaseLiquidityResult; +import network.balanced.score.core.positionmgr.structs.MintParams; +import network.balanced.score.core.positionmgr.structs.MintResult; +import network.balanced.score.core.positionmgr.structs.NFTPosition; +import network.balanced.score.core.positionmgr.structs.PositionInformation; +import network.balanced.score.core.dex.utils.TimeUtils; + +// @title NFT positions +// @notice Wraps Balanced positions in the IRC non-fungible token interface +public class NonFungiblePositionManager extends IRC721Enumerable + implements IBalancedLiquidityManagement, + IBalancedMintCallback +{ + // ================================================ + // Consts + // ================================================ + // Contract class name + public static final String NAME = "NonFungiblePositionManager"; + + // Contract name + private final String name; + + // Factory + private final Address factory; + + // Liquidity Manager + private final BalancedLiquidityManagement liquidityMgr; + + // ================================================ + // DB Variables + // ================================================ + /// @dev IDs of pools assigned by this contract + private final DictDB poolIds = Context.newDictDB(NAME + "_poolIds", BigInteger.class); + + /// @dev Pool keys by pool ID, to save on SSTOREs for position data + private final DictDB poolIdToPoolKey = Context.newDictDB(NAME + "_poolIdToPoolKey", PoolKey.class); + + /// @dev The token ID position data + private final DictDB positions = Context.newDictDB(NAME + "_positions", NFTPosition.class); + + /// @dev The ID of the next token that will be minted. Skips 0 + private final VarDB nextId = Context.newVarDB(NAME + "_nextId", BigInteger.class); + /// @dev The ID of the next pool that is used for the first time. Skips 0 + private final VarDB nextPoolId = Context.newVarDB(NAME + "_nextPoolId", BigInteger.class); + + /// @dev The address of the token descriptor contract, which handles generating token URIs for position tokens + private final Address tokenDescriptor; + + // ================================================ + // Event Logs + // ================================================ + /// @notice Emitted when liquidity is increased for a position NFT + /// @dev Also emitted when a token is minted + /// @param tokenId The ID of the token for which liquidity was increased + /// @param liquidity The amount by which liquidity for the NFT position was increased + /// @param amount0 The amount of token0 that was paid for the increase in liquidity + /// @param amount1 The amount of token1 that was paid for the increase in liquidity + @EventLog + public void IncreaseLiquidity ( + BigInteger tokenId, + BigInteger liquidity, + BigInteger amount0, + BigInteger amount1 + ) {} + + /// @notice Emitted when liquidity is decreased for a position NFT + /// @param tokenId The ID of the token for which liquidity was decreased + /// @param liquidity The amount by which liquidity for the NFT position was decreased + /// @param amount0 The amount of token0 that was accounted for the decrease in liquidity + /// @param amount1 The amount of token1 that was accounted for the decrease in liquidity + @EventLog + public void DecreaseLiquidity ( + BigInteger tokenId, + BigInteger liquidity, + BigInteger amount0, + BigInteger amount1 + ) {} + + /// @notice Emitted when tokens are collected for a position NFT + /// @dev The amounts reported may not be exactly equivalent to the amounts transferred, due to rounding behavior + /// @param tokenId The ID of the token for which underlying tokens were collected + /// @param recipient The address of the account that received the collected tokens + /// @param amount0 The amount of token0 owed to the position that was collected + /// @param amount1 The amount of token1 owed to the position that was collected + @EventLog + public void Collect ( + BigInteger tokenId, + Address recipient, + BigInteger amount0Collect, + BigInteger amount1Collect + ) {} + + // ================================================ + // Methods + // ================================================ + /** + * @notice Contract constructor + */ + public NonFungiblePositionManager ( + Address factory, + Address tokenDescriptor + ) { + super("Balanced Positions NFT-V1", "BLN-POS"); + + this.liquidityMgr = new BalancedLiquidityManagement(factory); + this.name = "Balanced NFT Position Manager"; + this.factory = factory; + this.tokenDescriptor = tokenDescriptor; + + if (this.nextId.get() == null) { + this.nextId.set(ONE); + } + if (this.nextPoolId.get() == null) { + this.nextPoolId.set(ONE); + } + } + + /** + * @notice Returns the position information associated with a given token ID. + * @dev Throws if the token ID is not valid. + * + * Access: Everyone + * + * @param tokenId The ID of the token that represents the position + * @return the position + */ + @External(readonly = true) + public PositionInformation positions ( + BigInteger tokenId + ) { + NFTPosition position = this.positions.getOrDefault(tokenId, NFTPosition.empty()); + Context.require(!position.poolId.equals(ZERO), + "positions: Invalid token ID"); + + PoolKey poolKey = this.poolIdToPoolKey.get(position.poolId); + return new PositionInformation( + position.nonce, + position.operator, + poolKey.token0, + poolKey.token1, + poolKey.fee, + position.tickLower, + position.tickUpper, + position.liquidity, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.tokensOwed0, + position.tokensOwed1 + ); + } + + /// @dev Caches a pool key + private BigInteger cachePoolKey (Address pool, PoolKey poolKey) { + BigInteger poolId = this.poolIds.get(pool); + if (poolId == null) { + // increment poolId + poolId = this.nextPoolId.get(); + this.nextPoolId.set(poolId.add(ONE)); + + this.poolIds.set(pool, poolId); + this.poolIdToPoolKey.set(poolId, poolKey); + } + + return poolId; + } + + /** + * @notice Creates a new position wrapped in a NFT + * + * Access: Everyone + * + * @dev Call this when the pool does exist and is initialized. Note that if the pool is created but not initialized, the call will fail. + * @param params The params necessary to mint a position, encoded as `MintParams` + * @return tokenId The ID of the token that represents the minted position + * @return liquidity The amount of liquidity for this position + * @return amount0 The amount of token0 + * @return amount1 The amount of token1 + */ + @External + public MintResult mint ( + MintParams params + ) { + this.checkDeadline(params.deadline); + var result = this.liquidityMgr.addLiquidity(new AddLiquidityParams( + params.token0, + params.token1, + params.fee, + Context.getAddress(), + params.tickLower, + params.tickUpper, + params.amount0Desired, + params.amount1Desired, + params.amount0Min, + params.amount1Min + )); + + BigInteger liquidity = result.liquidity; + BigInteger amount0 = result.amount0; + BigInteger amount1 = result.amount1; + Address pool = result.pool; + + // increment ID + BigInteger tokenId = this.nextId.get(); + this.nextId.set(tokenId.add(ONE)); + this._mint(params.recipient, tokenId); + + byte[] positionKey = Positions.getKey(Context.getAddress(), params.tickLower, params.tickUpper); + Position.Info poolPos = IConcentratedLiquidityPool.positions(pool, positionKey); + + // idempotent set + BigInteger poolId = cachePoolKey(pool, PoolAddressLib.getPoolKey(params.token0, params.token1, params.fee)); + + this.positions.set(tokenId, new NFTPosition( + ZERO, + ZERO_ADDRESS, + poolId, + params.tickLower, + params.tickUpper, + liquidity, + poolPos.feeGrowthInside0LastX128, + poolPos.feeGrowthInside1LastX128, + ZERO, + ZERO + )); + + this.IncreaseLiquidity(tokenId, liquidity, amount0, amount1); + + return new MintResult(tokenId, liquidity, amount0, amount1); + } + + @External(readonly = true) + public String tokenURI (BigInteger tokenId) { + _exists(tokenId); + return INonfungibleTokenPositionDescriptor.tokenURI(this.tokenDescriptor, Context.getAddress(), tokenId); + } + + /** + * @notice Increases the amount of liquidity in an existing position, with tokens paid by the `Context.getCaller()` + * @param params tokenId The ID of the token for which liquidity is being increased, + * amount0Desired The desired amount of token0 to be spent, + * amount1Desired The desired amount of token1 to be spent, + * amount0Min The minimum amount of token0 to spend, which serves as a slippage check, + * amount1Min The minimum amount of token1 to spend, which serves as a slippage check, + * deadline The time by which the transaction must be included to effect the change + * @return liquidity The new liquidity amount as a result of the increase + * @return amount0 The amount of token0 to achieve resulting liquidity + * @return amount1 The amount of token1 to achieve resulting liquidity + */ + @External + public IncreaseLiquidityResult increaseLiquidity ( + IncreaseLiquidityParams params + ) { + this.checkDeadline(params.deadline); + + // OK + NFTPosition positionStorage = this.positions.get(params.tokenId); + PoolKey poolKey = this.poolIdToPoolKey.get(positionStorage.poolId); + + var result = this.liquidityMgr.addLiquidity(new AddLiquidityParams( + poolKey.token0, + poolKey.token1, + poolKey.fee, + Context.getAddress(), + positionStorage.tickLower, + positionStorage.tickUpper, + params.amount0Desired, + params.amount1Desired, + params.amount0Min, + params.amount1Min + )); + + BigInteger liquidity = result.liquidity; + BigInteger amount0 = result.amount0; + BigInteger amount1 = result.amount1; + Address pool = result.pool; + + byte[] positionKey = Positions.getKey(Context.getAddress(), positionStorage.tickLower, positionStorage.tickUpper); + + // this is now updated to the current transaction + Position.Info poolPos = IConcentratedLiquidityPool.positions(pool, positionKey); + + BigInteger feeGrowthInside0LastX128 = poolPos.feeGrowthInside0LastX128; + BigInteger feeGrowthInside1LastX128 = poolPos.feeGrowthInside1LastX128; + + // calculate accumulated fees + BigInteger tokensOwed0 = uint128(FullMath.mulDiv(feeGrowthInside0LastX128.subtract(positionStorage.feeGrowthInside0LastX128), positionStorage.liquidity, FixedPoint128.Q128)); + BigInteger tokensOwed1 = uint128(FullMath.mulDiv(feeGrowthInside1LastX128.subtract(positionStorage.feeGrowthInside1LastX128), positionStorage.liquidity, FixedPoint128.Q128)); + + positionStorage.tokensOwed0 = positionStorage.tokensOwed0.add(tokensOwed0); + positionStorage.tokensOwed1 = positionStorage.tokensOwed1.add(tokensOwed1); + + positionStorage.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; + positionStorage.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; + positionStorage.liquidity = positionStorage.liquidity.add(liquidity); + + this.positions.set(params.tokenId, positionStorage); + this.IncreaseLiquidity(params.tokenId, liquidity, amount0, amount1); + + return new IncreaseLiquidityResult(liquidity, amount0, amount1); + } + + /** + * @notice Decreases the amount of liquidity in a position and accounts it to the position + * @param params tokenId The ID of the token for which liquidity is being decreased, + * amount The amount by which liquidity will be decreased, + * amount0Min The minimum amount of token0 that should be accounted for the burned liquidity, + * amount1Min The minimum amount of token1 that should be accounted for the burned liquidity, + * deadline The time by which the transaction must be included to effect the change + * @return amount0 The amount of token0 accounted to the position's tokens owed + * @return amount1 The amount of token1 accounted to the position's tokens owed + */ + @External + public PairAmounts decreaseLiquidity ( + DecreaseLiquidityParams params + ) { + this.isAuthorizedForToken(params.tokenId); + this.checkDeadline(params.deadline); + + // OK + Context.require(params.liquidity.compareTo(ZERO) > 0, + "decreaseLiquidity: liquidity must be superior to zero"); + + NFTPosition positionStorage = this.positions.get(params.tokenId); + BigInteger positionLiquidity = positionStorage.liquidity; + Context.require(positionLiquidity.compareTo(params.liquidity) >= 0, + "decreaseLiquidity: invalid liquidity"); + + PoolKey poolKey = this.poolIdToPoolKey.get(positionStorage.poolId); + Address pool = PoolAddressLib.getPool(this.factory, poolKey); + + PairAmounts pairAmounts = IConcentratedLiquidityPool.burn(pool, positionStorage.tickLower, positionStorage.tickUpper, params.liquidity); + BigInteger amount0 = pairAmounts.amount0; + BigInteger amount1 = pairAmounts.amount1; + + Context.require(amount0.compareTo(params.amount0Min) >= 0 && amount1.compareTo(params.amount1Min) >= 0, + "decreaseLiquidity: Price slippage check"); + + byte[] positionKey = Positions.getKey(Context.getAddress(), positionStorage.tickLower, positionStorage.tickUpper); + // this is now updated to the current transaction + Position.Info poolPos = IConcentratedLiquidityPool.positions(pool, positionKey); + + BigInteger feeGrowthInside0LastX128 = poolPos.feeGrowthInside0LastX128; + BigInteger feeGrowthInside1LastX128 = poolPos.feeGrowthInside1LastX128; + + // calculate accumulated fees + BigInteger tokensOwed0 = uint128(amount0).add(uint128(FullMath.mulDiv(feeGrowthInside0LastX128.subtract(positionStorage.feeGrowthInside0LastX128), positionLiquidity, FixedPoint128.Q128))); + BigInteger tokensOwed1 = uint128(amount1).add(uint128(FullMath.mulDiv(feeGrowthInside1LastX128.subtract(positionStorage.feeGrowthInside1LastX128), positionLiquidity, FixedPoint128.Q128))); + + positionStorage.tokensOwed0 = positionStorage.tokensOwed0.add(tokensOwed0); + positionStorage.tokensOwed1 = positionStorage.tokensOwed1.add(tokensOwed1); + + positionStorage.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; + positionStorage.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; + // subtraction is safe because we checked positionLiquidity is gte params.liquidity + positionStorage.liquidity = positionLiquidity.subtract(params.liquidity); + + this.DecreaseLiquidity(params.tokenId, params.liquidity, amount0, amount1); + + this.positions.set(params.tokenId, positionStorage); + return new PairAmounts(amount0, amount1); + } + + /** + * @notice Collects up to a maximum amount of fees owed to a specific position to the recipient + * @param params tokenId The ID of the NFT for which tokens are being collected, + * recipient The account that should receive the tokens, + * amount0Max The maximum amount of token0 to collect, + * amount1Max The maximum amount of token1 to collect + * @return amount0 The amount of fees collected in token0 + * @return amount1 The amount of fees collected in token1 + */ + @External + public PairAmounts collect (CollectParams params) { + isAuthorizedForToken(params.tokenId); + + Context.require(params.amount0Max.compareTo(ZERO) > 0 || params.amount1Max.compareTo(ZERO) > 0, + "collect: amount0Max and amount1Max cannot be both zero"); + + // allow collecting to the nft position manager address with address 0 + Address recipient = params.recipient.equals(ZERO_ADDRESS) ? Context.getAddress() : params.recipient; + + NFTPosition positionStorage = this.positions.get(params.tokenId); + PoolKey poolKey = this.poolIdToPoolKey.get(positionStorage.poolId); + Address pool = PoolAddressLib.getPool(this.factory, poolKey); + + BigInteger tokensOwed0 = positionStorage.tokensOwed0; + BigInteger tokensOwed1 = positionStorage.tokensOwed1; + + // trigger an update of the position fees owed and fee growth snapshots if it has any liquidity + if (positionStorage.liquidity.compareTo(ZERO) > 0) { + IConcentratedLiquidityPool.burn(pool, positionStorage.tickLower, positionStorage.tickUpper, ZERO); + var positionKey = Positions.getKey(Context.getAddress(), positionStorage.tickLower, positionStorage.tickUpper); + Position.Info poolPos = IConcentratedLiquidityPool.positions(pool, positionKey); + BigInteger feeGrowthInside0LastX128 = poolPos.feeGrowthInside0LastX128; + BigInteger feeGrowthInside1LastX128 = poolPos.feeGrowthInside1LastX128; + + // calculate accumulated fees + tokensOwed0 = tokensOwed0.add(uint128(FullMath.mulDiv(feeGrowthInside0LastX128.subtract(positionStorage.feeGrowthInside0LastX128), positionStorage.liquidity, FixedPoint128.Q128))); + tokensOwed1 = tokensOwed1.add(uint128(FullMath.mulDiv(feeGrowthInside1LastX128.subtract(positionStorage.feeGrowthInside1LastX128), positionStorage.liquidity, FixedPoint128.Q128))); + + positionStorage.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; + positionStorage.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; + } + + // compute the arguments to give to the pool#collect method + BigInteger amount0Collect, amount1Collect; + amount0Collect = (params.amount0Max.compareTo(tokensOwed0) > 0) ? tokensOwed0 : params.amount0Max; + amount1Collect = (params.amount1Max.compareTo(tokensOwed1) > 0) ? tokensOwed1 : params.amount1Max; + + // the actual amounts collected are returned + PairAmounts collected = IConcentratedLiquidityPool.collect ( + pool, + recipient, + positionStorage.tickLower, + positionStorage.tickUpper, + amount0Collect, + amount1Collect + ); + BigInteger amount0 = collected.amount0; + BigInteger amount1 = collected.amount1; + + // sometimes there will be a few less loops than expected due to rounding down in core, but we just subtract the full amount expected + // instead of the actual amount so we can burn the token + positionStorage.tokensOwed0 = tokensOwed0.subtract(amount0Collect); + positionStorage.tokensOwed1 = tokensOwed1.subtract(amount1Collect); + + this.Collect(params.tokenId, recipient, amount0Collect, amount1Collect); + + this.positions.set(params.tokenId, positionStorage); + return new PairAmounts(amount0, amount1); + } + + /** + * @notice Burns a token ID, which deletes it from the NFT contract. + * The token must have 0 liquidity and all tokens must be collected first. + * @param tokenId The ID of the token that is being burned + */ + @External + public void burn (BigInteger tokenId) { + isAuthorizedForToken(tokenId); + + NFTPosition positionStorage = this.positions.get(tokenId); + + Context.require( + positionStorage.liquidity.equals(ZERO) + && positionStorage.tokensOwed0.equals(ZERO) + && positionStorage.tokensOwed1.equals(ZERO), + "burn: Not cleared" + ); + + this.positions.set(tokenId, null); + this._burn(tokenId); + } + + // ================================================ + // Implements LiquidityManager + // ================================================ + /** + * @notice Called to `Context.getCaller()` after minting liquidity to a position from ConcentratedLiquidityPool#mint. + * @dev In the implementation you must pay the pool tokens owed for the minted liquidity. + * The caller of this method must be checked to be a ConcentratedLiquidityPool deployed by the canonical BalancedFactory. + * @param amount0Owed The amount of token0 due to the pool for the minted liquidity + * @param amount1Owed The amount of token1 due to the pool for the minted liquidity + * @param data Any data passed through by the caller via the mint call + */ + @External + public void balancedMintCallback ( + BigInteger amount0Owed, + BigInteger amount1Owed, + byte[] data + ) { + // Context.println("[Callback] paying " + amount0Owed + " / " + amount1Owed + " to " + Context.call(Context.getCaller(), "name")); + this.liquidityMgr.balancedMintCallback(amount0Owed, amount1Owed, data); + } + + /** + * @notice Remove funds from the liquidity manager previously deposited by `Context.getCaller` + * + * @param token The token address to withdraw + */ + @External + public void withdraw (Address token) { + this.liquidityMgr.withdraw(token); + } + + /** + * @notice Remove all funds from the liquidity manager previously deposited by `Context.getCaller` + */ + @External + public void withdraw_all () { + this.liquidityMgr.withdraw_all(); + } + + /** + * @notice Add ICX funds to the liquidity manager + */ + @External + @Payable + public void depositIcx () { + this.liquidityMgr.depositIcx(); + } + + @External + public void tokenFallback (Address _from, BigInteger _value, @Optional byte[] _data) throws Exception { + Reader reader = new StringReader(new String(_data)); + JsonValue input = Json.parse(reader); + JsonObject root = input.asObject(); + String method = root.get("method").asString(); + Address token = Context.getCaller(); + + switch (method) + { + /** + * @notice Add IRC2 funds to the liquidity manager + */ + case "deposit": { + deposit(_from, token, _value); + break; + } + + default: + Context.revert("tokenFallback: Unimplemented tokenFallback action"); + } + } + + // @External - this method is external through tokenFallback + public void deposit(Address caller, Address tokenIn, BigInteger amountIn) { + this.liquidityMgr.deposit(caller, tokenIn, amountIn); + } + + // ReadOnly methods + @External(readonly = true) + public BigInteger deposited(Address user, Address token) { + return this.liquidityMgr.deposited(user, token); + } + + @External(readonly = true) + public int depositedTokensSize(Address user) { + return this.liquidityMgr.depositedTokensSize(user); + } + + @External(readonly = true) + public Address depositedToken(Address user, int index) { + return this.liquidityMgr.depositedToken(user, index); + } + + // ================================================ + // Overrides + // ================================================ + /// @dev Overrides _approve to use the operator in the position, which is packed with the position permit nonce + @Override + protected void _approve (Address to, BigInteger tokenId) { + var position = this.positions.getOrDefault(tokenId, NFTPosition.empty()); + position.operator = to; + this.positions.set(tokenId, position); + this.Approval(ownerOf(tokenId), to, tokenId); + } + + @Override + @External(readonly=true) + public Address getApproved(BigInteger tokenId) { + Context.require(this._exists(tokenId), + "getApproved: approved query for nonexistent token"); + return this.positions.get(tokenId).operator; + } + + // ================================================ + // Checks + // ================================================ + /** + * Check if transaction hasn't reached the deadline + */ + private void checkDeadline(BigInteger deadline) { + final BigInteger now = TimeUtils.now(); + Context.require(now.compareTo(deadline) <= 0, + "checkDeadline: Transaction too old"); + } + + private void isAuthorizedForToken (BigInteger tokenId) { + Context.require(_isApprovedOrOwner(Context.getCaller(), tokenId), + "checkAuthorizedForToken: Not approved"); + } + + // ================================================ + // Public variable getters + // ================================================ + @External(readonly = true) + public String name() { + return this.name; + } + + @External(readonly = true) + public Address factory() { + return this.factory; + } +} diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/implementation/BalancedLiquidityManagement.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/implementation/BalancedLiquidityManagement.java new file mode 100644 index 000000000..c5c63948b --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/implementation/BalancedLiquidityManagement.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.implementation; + +import network.balanced.score.core.dex.interfaces.irc2.IIRC2ICX; +import network.balanced.score.core.dex.libs.TickMath; +import network.balanced.score.core.positionmgr.interfaces.IBalancedLiquidityManagement; +import network.balanced.score.core.positionmgr.interfaces.IBalancedLiquidityManagementAddLiquidity; +import network.balanced.score.core.positionmgr.libs.CallbackValidation; +import network.balanced.score.core.positionmgr.libs.LiquidityAmounts; +import network.balanced.score.core.positionmgr.libs.PeripheryPayments; +import network.balanced.score.core.positionmgr.libs.PoolAddressLib; +import score.Address; +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import network.balanced.score.core.dex.interfaces.pool.IConcentratedLiquidityPool; +import network.balanced.score.core.dex.structs.pool.MintCallbackData; +import network.balanced.score.core.dex.structs.pool.PairAmounts; +import network.balanced.score.core.dex.structs.pool.PoolAddress.PoolKey; +import network.balanced.score.core.dex.utils.EnumerableMap; +import network.balanced.score.core.dex.utils.ICX; +import network.balanced.score.core.positionmgr.structs.AddLiquidityParams; +import network.balanced.score.core.positionmgr.structs.AddLiquidityResult; +import static java.math.BigInteger.ZERO; + +import java.math.BigInteger; + +public class BalancedLiquidityManagement + implements IBalancedLiquidityManagement, + IBalancedLiquidityManagementAddLiquidity +{ + // ================================================ + // Consts + // ================================================ + // Contract class name + private static final String NAME = "BalancedLiquidityManagement"; + + // address of the Balanced factory + private final Address factory; + + // ================================================ + // DB Variables + // ================================================ + // User => Token => Amount + private EnumerableMap depositedMap (Address user) { + return new EnumerableMap<>(NAME + "_deposited_" + user, Address.class, BigInteger.class); + } + + // ================================================ + // Event Logs + // ================================================ + + // ================================================ + // Methods + // ================================================ + /** + * @notice Contract constructor + */ + public BalancedLiquidityManagement( + Address _factory + ) { + this.factory = _factory; + } + + /** + * @notice Called to `Context.getCaller()` after minting liquidity to a position from ConcentratedLiquidityPool#mint. + * @dev In the implementation you must pay the pool tokens owed for the minted liquidity. + * The caller of this method must be checked to be a ConcentratedLiquidityPool deployed by the canonical BalancedFactory. + * @param amount0Owed The amount of token0 due to the pool for the minted liquidity + * @param amount1Owed The amount of token1 due to the pool for the minted liquidity + * @param data Any data passed through by the caller via the mint call + */ + // @External + public void balancedMintCallback ( + BigInteger amount0Owed, + BigInteger amount1Owed, + byte[] data + ) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", data); + MintCallbackData decoded = reader.read(MintCallbackData.class); + CallbackValidation.verifyCallback(this.factory, decoded.poolKey); + + if (amount0Owed.compareTo(ZERO) > 0) { + pay(decoded.payer, decoded.poolKey.token0, amount0Owed); + } + + if (amount1Owed.compareTo(ZERO) > 0) { + pay(decoded.payer, decoded.poolKey.token1, amount1Owed); + } + } + + private void pay (Address payer, Address token, BigInteger owed) { + final Address caller = Context.getCaller(); + checkEnoughDeposited(payer, token, owed); + + // Remove funds from deposited + var depositedUser = this.depositedMap(payer); + BigInteger oldBalance = depositedUser.getOrDefault(token, ZERO); + if (oldBalance.equals(owed)) { + // All funds were payed + depositedUser.remove(token); + } else { + // Only a portion of the deposit funds were payed + depositedUser.set(token, oldBalance.subtract(owed)); + } + + // Actually transfer the tokens + PeripheryPayments.pay(token, caller, owed); + } + + /** + * @notice Add liquidity to an initialized pool + * @dev Liquidity must have been provided beforehand + */ + public AddLiquidityResult addLiquidity (AddLiquidityParams params) { + PoolKey poolKey = new PoolKey(params.token0, params.token1, params.fee); + + Address pool = PoolAddressLib.getPool(this.factory, poolKey); + Context.require(pool != null, "addLiquidity: pool doesn't exist"); + + // compute the liquidity amount + BigInteger sqrtPriceX96 = IConcentratedLiquidityPool.slot0(pool).sqrtPriceX96; + BigInteger sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower); + BigInteger sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper); + + BigInteger liquidity = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + sqrtRatioAX96, + sqrtRatioBX96, + params.amount0Desired, + params.amount1Desired + ); + + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + writer.write(new MintCallbackData(poolKey, Context.getCaller())); + + PairAmounts amounts = IConcentratedLiquidityPool.mint ( + pool, + params.recipient, + params.tickLower, + params.tickUpper, + liquidity, + writer.toByteArray() + ); + + Context.require( + amounts.amount0.compareTo(params.amount0Min) >= 0 + && amounts.amount1.compareTo(params.amount1Min) >= 0, + "addLiquidity: Price slippage check" + ); + + return new AddLiquidityResult (liquidity, amounts.amount0, amounts.amount1, pool); + } + + + // @External + // @Payable + public void depositIcx () { + deposit(Context.getCaller(), ICX.getAddress(), Context.getValue()); + } + + /** + * @notice Add funds to the liquidity manager + */ + // @External - this method is external through tokenFallback + public void deposit ( + Address caller, + Address tokenIn, + BigInteger amountIn + ) { + // --- Checks --- + Context.require(amountIn.compareTo(ZERO) > 0, + "deposit: Deposit amount cannot be less or equal to 0"); + + // --- OK from here --- + var depositedUser = this.depositedMap(caller); + BigInteger oldBalance = depositedUser.getOrDefault(tokenIn, ZERO); + depositedUser.set(tokenIn, oldBalance.add(amountIn)); + } + + /** + * @notice Remove funds from the liquidity manager + */ + // @External + public void withdraw (Address token) { + final Address caller = Context.getCaller(); + + var depositedUser = this.depositedMap(caller); + BigInteger amount = depositedUser.getOrDefault(token, ZERO); + depositedUser.remove(token); + + if (amount.compareTo(ZERO) > 0) { + IIRC2ICX.transfer(token, caller, amount, "withdraw"); + } + } + + /** + * @notice Remove all funds from the liquidity manager + */ + // @External + public void withdraw_all () { + final Address caller = Context.getCaller(); + + var depositedUser = this.depositedMap(caller); + int size = depositedUser.size(); + + for (int i = 0; i < size; i++) { + Address token = depositedUser.getKey(0); + BigInteger amount = depositedUser.getOrDefault(token, ZERO); + depositedUser.remove(token); + + if (amount.compareTo(ZERO) > 0) { + IIRC2ICX.transfer(token, caller, amount, "withdraw"); + } + } + } + + // ================================================ + // Checks + // ================================================ + private void checkEnoughDeposited (Address address, Address token, BigInteger amount) { + var userBalance = this.deposited(address, token); + // Context.println("[Callee][checkEnoughDeposited][" + IIRC2ICX.symbol(token) + "] " + userBalance + " / " + amount); + Context.require(userBalance.compareTo(amount) >= 0, + NAME + "::checkEnoughDeposited: user didn't deposit enough funds (" + userBalance + " / " + amount + ")"); + } + + // ================================================ + // Public variable getters + // ================================================ + /** + * Returns the amount of tokens previously deposited for a given user and token + * + * @param user A user address who made a deposit + * @param token A token address + */ + // @External(readonly = true) + public BigInteger deposited (Address user, Address token) { + return this.depositedMap(user).getOrDefault(token, ZERO); + } + + /** + * Returns the size of the token list deposited + * + * @param user A user address who made a deposit + */ + // @External(readonly = true) + public int depositedTokensSize (Address user) { + return this.depositedMap(user).size(); + } + + /** + * Returns the token address in the list given an index + * + * @param user A user address who made a deposit + * @param index The deposited token list index + */ + // @External(readonly = true) + public Address depositedToken (Address user, int index) { + return this.depositedMap(user).getKey(index); + } +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/IBalancedLiquidityManagement.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/IBalancedLiquidityManagement.java new file mode 100644 index 000000000..03ca1882d --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/IBalancedLiquidityManagement.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.interfaces; + +import java.math.BigInteger; +import score.Address; + +public interface IBalancedLiquidityManagement +{ + /** + * @notice Add IRC2 funds to the liquidity manager + */ + // @External - this method is external through tokenFallback + public void deposit ( + Address caller, + Address tokenIn, + BigInteger amountIn + ); + + + /** + * @notice Add ICX funds to the liquidity manager + */ + // @External + // @Payable + public void depositIcx (); + + /** + * @notice Remove funds from the liquidity manager + */ + public void withdraw (Address token); + + /** + * @notice Remove all funds from the liquidity manager + */ + public void withdraw_all (); + + + // ================================================ + // Public variable getters + // ================================================ + // @External(readonly = true) + public BigInteger deposited (Address user, Address token); + + // @External(readonly = true) + public int depositedTokensSize (Address user); + + // @External(readonly = true) + public Address depositedToken (Address user, int index); +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/IBalancedLiquidityManagementAddLiquidity.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/IBalancedLiquidityManagementAddLiquidity.java new file mode 100644 index 000000000..1a9cd0da2 --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/IBalancedLiquidityManagementAddLiquidity.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.interfaces; + +import java.math.BigInteger; +import network.balanced.score.core.dex.interfaces.pool.IBalancedMintCallback; +import network.balanced.score.core.positionmgr.structs.AddLiquidityParams; +import network.balanced.score.core.positionmgr.structs.AddLiquidityResult; + +public interface IBalancedLiquidityManagementAddLiquidity + extends IBalancedMintCallback +{ + /** + * @notice Called to `Context.getCaller()` after minting liquidity to a position from ConcentratedLiquidityPool#mint. + * @dev In the implementation you must pay the pool tokens owed for the minted liquidity. + * The caller of this method must be checked to be a ConcentratedLiquidityPool deployed by the canonical BalancedFactory. + * @param amount0Owed The amount of token0 due to the pool for the minted liquidity + * @param amount1Owed The amount of token1 due to the pool for the minted liquidity + * @param data Any data passed through by the caller via the mint call + */ + // @External + public void balancedMintCallback ( + BigInteger amount0Owed, + BigInteger amount1Owed, + byte[] data + ); + + /** + * @notice Add liquidity to an initialized pool + * @dev Liquidity must have been provided beforehand + */ + // @External + public AddLiquidityResult addLiquidity (AddLiquidityParams params); +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/INonfungibleTokenPositionDescriptor.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/INonfungibleTokenPositionDescriptor.java new file mode 100644 index 000000000..bd8f23a10 --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/INonfungibleTokenPositionDescriptor.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.interfaces; + +import java.math.BigInteger; +import score.Address; +import score.Context; + +public class INonfungibleTokenPositionDescriptor { + // Write methods + + // ReadOnly methods + public static String tokenURI ( + Address positionDescriptor, + Address positionManager, + BigInteger tokenId + ) { + return (String) Context.call(positionDescriptor, "tokenURI", positionManager, tokenId); + } +} diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/CallbackValidation.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/CallbackValidation.java new file mode 100644 index 000000000..2d347790f --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/CallbackValidation.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.libs; + +import network.balanced.score.core.dex.structs.pool.PoolAddress.PoolKey; +import score.Address; +import score.Context; + +public class CallbackValidation { + + /** + * @notice Returns the address of a valid Balanced Pool + * @param factory The contract address of the Balanced factory + * @param tokenA The contract address of either token0 or token1 + * @param tokenB The contract address of the other token + * @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip + * @return pool The pool contract address + */ + public static Address verifyCallback (Address factory, Address tokenA, Address tokenB, int fee) { + return verifyCallback (factory, PoolAddressLib.getPoolKey(tokenA, tokenB, fee)); + } + + /** + * @notice Returns the address of a valid Balanced Pool + * @param factory The contract address of the Balanced factory + * @param poolKey The identifying key of the pool + * @return pool The pool contract address + */ + public static Address verifyCallback (Address factory, PoolKey poolKey) { + Address pool = PoolAddressLib.getPool(factory, poolKey); + Context.require(Context.getCaller().equals(pool), "verifyCallback: failed"); + return pool; + } +} diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/LiquidityAmounts.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/LiquidityAmounts.java new file mode 100644 index 000000000..da772b013 --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/LiquidityAmounts.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.libs; + +import static network.balanced.score.core.dex.utils.IntUtils.uint128; + +import java.math.BigInteger; +import network.balanced.score.core.dex.libs.FixedPoint96; +import network.balanced.score.core.dex.libs.FullMath; + +public class LiquidityAmounts { + + /** + * @notice Computes the maximum amount of liquidity received for a given amount of token0, token1, the current + * pool prices and the prices at the tick boundaries + * @param sqrtRatioX96 A sqrt price representing the current pool prices + * @param sqrtRatioAX96 A sqrt price representing the first tick boundary + * @param sqrtRatioBX96 A sqrt price representing the second tick boundary + * @param amount0 The amount of token0 being sent in + * @param amount1 The amount of token1 being sent in + * @return liquidity The maximum amount of liquidity received + */ + public static BigInteger getLiquidityForAmounts ( + BigInteger sqrtRatioX96, + BigInteger sqrtRatioAX96, + BigInteger sqrtRatioBX96, + BigInteger amount0, + BigInteger amount1 + ) { + BigInteger liquidity = BigInteger.ZERO; + + if (sqrtRatioAX96.compareTo(sqrtRatioBX96) > 0) { + BigInteger tmp = sqrtRatioAX96; + sqrtRatioAX96 = sqrtRatioBX96; + sqrtRatioBX96 = tmp; + } + + if (sqrtRatioX96.compareTo(sqrtRatioAX96) <= 0) { + liquidity = getLiquidityForAmount0(sqrtRatioAX96, sqrtRatioBX96, amount0); + } else if (sqrtRatioX96.compareTo(sqrtRatioBX96) < 0) { + BigInteger liquidity0 = getLiquidityForAmount0(sqrtRatioX96, sqrtRatioBX96, amount0); + BigInteger liquidity1 = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioX96, amount1); + liquidity = liquidity0.compareTo(liquidity1) < 0 ? liquidity0 : liquidity1; + } else { + liquidity = getLiquidityForAmount1(sqrtRatioAX96, sqrtRatioBX96, amount1); + } + + return liquidity; + } + + /** + * @notice Computes the amount of token0 for a given amount of liquidity and a price range + * @param sqrtRatioAX96 A sqrt price representing the first tick boundary + * @param sqrtRatioBX96 A sqrt price representing the second tick boundary + * @param liquidity The liquidity being valued + * @return amount0 The amount of token0 + */ + private static BigInteger getLiquidityForAmount0( + BigInteger sqrtRatioAX96, + BigInteger sqrtRatioBX96, + BigInteger amount0 + ) { + if (sqrtRatioAX96.compareTo(sqrtRatioBX96) > 0) { + BigInteger tmp = sqrtRatioAX96; + sqrtRatioAX96 = sqrtRatioBX96; + sqrtRatioBX96 = tmp; + } + + BigInteger intermediate = FullMath.mulDiv(sqrtRatioAX96, sqrtRatioBX96, FixedPoint96.Q96); + return uint128(FullMath.mulDiv(amount0, intermediate, sqrtRatioBX96.subtract(sqrtRatioAX96))); + } + + /** + * @notice Computes the amount of token1 for a given amount of liquidity and a price range + * @param sqrtRatioAX96 A sqrt price representing the first tick boundary + * @param sqrtRatioBX96 A sqrt price representing the second tick boundary + * @param liquidity The liquidity being valued + * @return amount1 The amount of token1 + */ + private static BigInteger getLiquidityForAmount1( + BigInteger sqrtRatioAX96, + BigInteger sqrtRatioBX96, + BigInteger amount1 + ) { + if (sqrtRatioAX96.compareTo(sqrtRatioBX96) > 0) { + BigInteger tmp = sqrtRatioAX96; + sqrtRatioAX96 = sqrtRatioBX96; + sqrtRatioBX96 = tmp; + } + + return uint128(FullMath.mulDiv(amount1, FixedPoint96.Q96, sqrtRatioBX96.subtract(sqrtRatioAX96))); + } +} diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/PeripheryPayments.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/PeripheryPayments.java new file mode 100644 index 000000000..ef0dd00e4 --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/PeripheryPayments.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.libs; + +import java.math.BigInteger; +import network.balanced.score.core.dex.interfaces.irc2.IIRC2ICX; +import score.Address; + +public class PeripheryPayments { + + /** + * @param token The token to pay + * @param recipient The entity that will receive payment + * @param value The amount to pay + */ + public static void pay( + Address token, + Address recipient, + BigInteger value + ) { + IIRC2ICX.transfer(token, recipient, value, "deposit"); + } +} diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/PoolAddressLib.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/PoolAddressLib.java new file mode 100644 index 000000000..d68928556 --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/PoolAddressLib.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.libs; + +import network.balanced.score.core.dex.interfaces.factory.IBalancedFactory; +import network.balanced.score.core.dex.structs.pool.PoolAddress.PoolKey; +import network.balanced.score.core.dex.utils.AddressUtils; +import score.Address; + +public class PoolAddressLib { + /** + * @notice Returns PoolKey: the ordered tokens with the matched fee levels + * @param tokenA The first token of a pool, unsorted + * @param tokenB The second token of a pool, unsorted + * @param fee The fee level of the pool + * @return Poolkey The pool details with ordered token0 and token1 assignments + */ + public static PoolKey getPoolKey (Address tokenA, Address tokenB, int fee) { + Address token0 = tokenA; + Address token1 = tokenB; + + if (AddressUtils.compareTo(tokenA, tokenB) > 0) { + token0 = tokenB; + token1 = tokenA; + } + + return new PoolKey(token0, token1, fee); + } + + public static Address getPool (Address factory, PoolKey key) { + return IBalancedFactory.getPool(factory, key.token0, key.token1, key.fee); + } +} diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/AddLiquidityParams.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/AddLiquidityParams.java new file mode 100644 index 000000000..3191b6b89 --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/AddLiquidityParams.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.structs; + +import java.math.BigInteger; + +import score.Address; + +public class AddLiquidityParams { + public Address token0; + public Address token1; + public int fee; + public Address recipient; + public int tickLower; + public int tickUpper; + public BigInteger amount0Desired; + public BigInteger amount1Desired; + public BigInteger amount0Min; + public BigInteger amount1Min; + + public AddLiquidityParams() {} + + public AddLiquidityParams( + Address token0, + Address token1, + int fee, + Address recipient, + int tickLower, + int tickUpper, + BigInteger amount0Desired, + BigInteger amount1Desired, + BigInteger amount0Min, + BigInteger amount1Min + ) { + this.token0 = token0; + this.token1 = token1; + this.fee = fee; + this.recipient = recipient; + this.tickLower = tickLower; + this.tickUpper = tickUpper; + this.amount0Desired = amount0Desired; + this.amount1Desired = amount1Desired; + this.amount0Min = amount0Min; + this.amount1Min = amount1Min; + } +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/AddLiquidityResult.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/AddLiquidityResult.java new file mode 100644 index 000000000..d31de67d3 --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/AddLiquidityResult.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.structs; + +import java.math.BigInteger; + +import score.Address; + +public class AddLiquidityResult { + public BigInteger liquidity; + public BigInteger amount0; + public BigInteger amount1; + public Address pool; + + public AddLiquidityResult ( + BigInteger liquidity, + BigInteger amount0, + BigInteger amount1, + Address pool + ) { + this.liquidity = liquidity; + this.amount0 = amount0; + this.amount1 = amount1; + this.pool = pool; + } +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/CollectParams.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/CollectParams.java new file mode 100644 index 000000000..d384157ae --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/CollectParams.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.structs; + +import java.math.BigInteger; + +import score.Address; + +public class CollectParams { + // The ID of the NFT for which tokens are being collected + public BigInteger tokenId; + // The account that should receive the tokens + public Address recipient; + // The maximum amount of token0 to collect + public BigInteger amount0Max; + // The maximum amount of token1 to collect + public BigInteger amount1Max; + + public CollectParams() {} + + public CollectParams ( + BigInteger tokenId, + Address recipient, + BigInteger amount0Max, + BigInteger amount1Max + ) { + this.tokenId = tokenId; + this.recipient = recipient; + this.amount0Max = amount0Max; + this.amount1Max = amount1Max; + } +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/DecreaseLiquidityParams.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/DecreaseLiquidityParams.java new file mode 100644 index 000000000..855105a6f --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/DecreaseLiquidityParams.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.structs; + +import java.math.BigInteger; + +public class DecreaseLiquidityParams { + // The ID of the token for which liquidity is being decreased + public BigInteger tokenId; + // The amount by which liquidity will be decreased + public BigInteger liquidity; + // The minimum amount of token0 that should be accounted for the burned liquidity + public BigInteger amount0Min; + // The minimum amount of token1 that should be accounted for the burned liquidity + public BigInteger amount1Min; + // The time by which the transaction must be included to effect the change + public BigInteger deadline; + + public DecreaseLiquidityParams () {} + + public DecreaseLiquidityParams ( + BigInteger tokenId, + BigInteger liquidity, + BigInteger amount0Min, + BigInteger amount1Min, + BigInteger deadline + ) { + this.tokenId = tokenId; + this.liquidity = liquidity; + this.amount0Min = amount0Min; + this.amount1Min = amount1Min; + this.deadline = deadline; + } +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/IncreaseLiquidityParams.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/IncreaseLiquidityParams.java new file mode 100644 index 000000000..06ae91a9c --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/IncreaseLiquidityParams.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.structs; + +import java.math.BigInteger; + +public class IncreaseLiquidityParams { + // The ID of the token for which liquidity is being increased + public BigInteger tokenId; + // The desired amount of token0 to be spent + public BigInteger amount0Desired; + // The desired amount of token1 to be spent + public BigInteger amount1Desired; + // The minimum amount of token0 to spend, which serves as a slippage check + public BigInteger amount0Min; + // The minimum amount of token1 to spend, which serves as a slippage check + public BigInteger amount1Min; + // The time by which the transaction must be included to effect the change + public BigInteger deadline; + + public IncreaseLiquidityParams () {} + + public IncreaseLiquidityParams ( + BigInteger tokenId, + BigInteger amount0Desired, + BigInteger amount1Desired, + BigInteger amount0Min, + BigInteger amount1Min, + BigInteger deadline + ) { + this.tokenId = tokenId; + this.amount0Desired = amount0Desired; + this.amount1Desired = amount1Desired; + this.amount0Min = amount0Min; + this.amount1Min = amount1Min; + this.deadline = deadline; + } +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/IncreaseLiquidityResult.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/IncreaseLiquidityResult.java new file mode 100644 index 000000000..60f8d389b --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/IncreaseLiquidityResult.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.structs; + +import java.math.BigInteger; + +public class IncreaseLiquidityResult { + // The new liquidity amount as a result of the increase + public BigInteger liquidity; + // The amount of token0 to achieve resulting liquidity + public BigInteger amount0; + // The amount of token1 to achieve resulting liquidity + public BigInteger amount1; + + public IncreaseLiquidityResult( + BigInteger liquidity, + BigInteger amount0, + BigInteger amount1 + ) { + this.liquidity = liquidity; + this.amount0 = amount0; + this.amount1 = amount1; + } +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/MintParams.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/MintParams.java new file mode 100644 index 000000000..857acd4f2 --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/MintParams.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.structs; + +import java.math.BigInteger; +import java.util.Map; +import score.Address; + +public class MintParams { + // The first token of a pool, unsorted + public Address token0; + // The second token of a pool, unsorted + public Address token1; + // The fee level of the pool + public int fee; + // The lower tick of the position + public int tickLower; + // The upper tick of the position + public int tickUpper; + // The desired amount of token0 to be spent, + public BigInteger amount0Desired; + // The desired amount of token1 to be spent, + public BigInteger amount1Desired; + // The minimum amount of token0 to spend, which serves as a slippage check, + public BigInteger amount0Min; + // The minimum amount of token1 to spend, which serves as a slippage check, + public BigInteger amount1Min; + // The address that received the output of the swap + public Address recipient; + // The unix time after which a mint will fail, to protect against long-pending transactions and wild swings in prices + public BigInteger deadline; + + public MintParams ( + Address token0, + Address token1, + int fee, + int tickLower, + int tickUpper, + BigInteger amount0Desired, + BigInteger amount1Desired, + BigInteger amount0Min, + BigInteger amount1Min, + Address recipient, + BigInteger deadline + ) { + this.token0 = token0; + this.token1 = token1; + this.fee = fee; + this.tickLower = tickLower; + this.tickUpper = tickUpper; + this.amount0Desired = amount0Desired; + this.amount1Desired = amount1Desired; + this.amount0Min = amount0Min; + this.amount1Min = amount1Min; + this.recipient = recipient; + this.deadline = deadline; + } + + public MintParams() {} + + public Map toMap() { + return Map.ofEntries( + Map.entry("token0", this.token0), + Map.entry("token1", this.token1), + Map.entry("fee", this.fee), + Map.entry("tickLower", this.tickLower), + Map.entry("tickUpper", this.tickUpper), + Map.entry("amount0Desired", this.amount0Desired), + Map.entry("amount1Desired", this.amount1Desired), + Map.entry("amount0Min", this.amount0Min), + Map.entry("amount1Min", this.amount1Min), + Map.entry("recipient", this.recipient), + Map.entry("deadline", this.deadline) + ); + } +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/MintResult.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/MintResult.java new file mode 100644 index 000000000..12c65acb0 --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/MintResult.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.structs; + +import java.math.BigInteger; +import java.util.Map; + +public class MintResult { + // The ID of the token that represents the minted position + public BigInteger tokenId; + // The amount of liquidity for this position + public BigInteger liquidity; + // The amount of token0 + public BigInteger amount0; + // The amount of token1 + public BigInteger amount1; + + public MintResult ( + BigInteger tokenId, + BigInteger liquidity, + BigInteger amount0, + BigInteger amount1) { + this.tokenId = tokenId; + this.liquidity = liquidity; + this.amount0 = amount0; + this.amount1 = amount1; + } + + public static MintResult fromMap (Object call) { + @SuppressWarnings("unchecked") + Map map = (Map) call; + return new MintResult ( + (BigInteger) map.get("tokenId"), + (BigInteger) map.get("liquidity"), + (BigInteger) map.get("amount0"), + (BigInteger) map.get("amount1") + ); + } +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/NFTPosition.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/NFTPosition.java new file mode 100644 index 000000000..038e652b2 --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/NFTPosition.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.structs; + +import static network.balanced.score.core.dex.utils.AddressUtils.ZERO_ADDRESS; +import static java.math.BigInteger.ZERO; + +import java.math.BigInteger; + +import score.Address; +import score.ObjectReader; +import score.ObjectWriter; + +// details about the Balanced position +public class NFTPosition { + // the nonce for permits + public BigInteger nonce; + // the address that is approved for spending this token + public Address operator; + // the ID of the pool with which this token is connected + public BigInteger poolId; + // the tick range of the position + public int tickLower; + public int tickUpper; + // the liquidity of the position + public BigInteger liquidity; + // the fee growth of the aggregate position as of the last action on the individual position + public BigInteger feeGrowthInside0LastX128; + public BigInteger feeGrowthInside1LastX128; + // how many uncollected tokens are owed to the position, as of the last computation + public BigInteger tokensOwed0; + public BigInteger tokensOwed1; + + public NFTPosition ( + BigInteger nonce, + Address operator, + BigInteger poolId, + int tickLower, + int tickUpper, + BigInteger liquidity, + BigInteger feeGrowthInside0LastX128, + BigInteger feeGrowthInside1LastX128, + BigInteger tokensOwed0, + BigInteger tokensOwed1 + ) { + this.nonce = nonce; + this.operator = operator; + this.poolId = poolId; + this.tickLower = tickLower; + this.tickUpper = tickUpper; + this.liquidity = liquidity; + this.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; + this.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; + this.tokensOwed0 = tokensOwed0; + this.tokensOwed1 = tokensOwed1; + } + + public static NFTPosition readObject(ObjectReader reader) { + return new NFTPosition( + reader.readBigInteger(), + reader.readAddress(), + reader.readBigInteger(), + reader.readInt(), + reader.readInt(), + reader.readBigInteger(), + reader.readBigInteger(), + reader.readBigInteger(), + reader.readBigInteger(), + reader.readBigInteger() + ); + } + + public static void writeObject(ObjectWriter w, NFTPosition obj) { + w.write(obj.nonce); + w.write(obj.operator); + w.write(obj.poolId); + w.write(obj.tickLower); + w.write(obj.tickUpper); + w.write(obj.liquidity); + w.write(obj.feeGrowthInside0LastX128); + w.write(obj.feeGrowthInside1LastX128); + w.write(obj.tokensOwed0); + w.write(obj.tokensOwed1); + } + + public static NFTPosition empty() { + return new NFTPosition ( + ZERO, + ZERO_ADDRESS, + ZERO, + 0, 0, + ZERO, + ZERO, + ZERO, + ZERO, + ZERO + ); + } +} \ No newline at end of file diff --git a/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/PositionInformation.java b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/PositionInformation.java new file mode 100644 index 000000000..74e80d937 --- /dev/null +++ b/core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/PositionInformation.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.positionmgr.structs; + +import java.math.BigInteger; +import java.util.Map; + +import score.Address; + +public class PositionInformation { + // The nonce for permits + public BigInteger nonce; + // The address that is approved for spending + public Address operator; + // The address of the token0 for a specific pool + public Address token0; + // The address of the token1 for a specific pool + public Address token1; + // The fee associated with the pool + public int fee; + // The lower end of the tick range for the position + public int tickLower; + // The higher end of the tick range for the position + public int tickUpper; + // The liquidity of the position + public BigInteger liquidity; + // The fee growth of token0 as of the last action on the individual position + public BigInteger feeGrowthInside0LastX128; + // The fee growth of token1 as of the last action on the individual position + public BigInteger feeGrowthInside1LastX128; + // The uncollected amount of token0 owed to the position as of the last computation + public BigInteger tokensOwed0; + // The uncollected amount of token1 owed to the position as of the last computation + public BigInteger tokensOwed1; + + public PositionInformation ( + BigInteger nonce, + Address operator, + Address token0, + Address token1, + int fee, + int tickLower, + int tickUpper, + BigInteger liquidity, + BigInteger feeGrowthInside0LastX128, + BigInteger feeGrowthInside1LastX128, + BigInteger tokensOwed0, + BigInteger tokensOwed1 + ) { + this.nonce = nonce; + this.operator = operator; + this.token0 = token0; + this.token1 = token1; + this.fee = fee; + this.tickLower = tickLower; + this.tickUpper = tickUpper; + this.liquidity = liquidity; + this.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; + this.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; + this.tokensOwed0 = tokensOwed0; + this.tokensOwed1 = tokensOwed1; + } + + public static PositionInformation fromMap (Object call) { + @SuppressWarnings("unchecked") + Map map = (Map) call; + return new PositionInformation( + (BigInteger) map.get("nonce"), + (Address) map.get("operator"), + (Address) map.get("token0"), + (Address) map.get("token1"), + ((BigInteger) map.get("fee")).intValue(), + ((BigInteger) map.get("tickLower")).intValue(), + ((BigInteger) map.get("tickUpper")).intValue(), + (BigInteger) map.get("liquidity"), + (BigInteger) map.get("feeGrowthInside0LastX128"), + (BigInteger) map.get("feeGrowthInside1LastX128"), + (BigInteger) map.get("tokensOwed0"), + (BigInteger) map.get("tokensOwed1") + ); + } +} \ No newline at end of file diff --git a/score-lib/src/main/java/network/balanced/score/lib/utils/Names.java b/score-lib/src/main/java/network/balanced/score/lib/utils/Names.java index bf02afe6e..b7eff1795 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/utils/Names.java +++ b/score-lib/src/main/java/network/balanced/score/lib/utils/Names.java @@ -44,6 +44,8 @@ public class Names { public final static String BURNER = "Balanced-ICON Burner"; public final static String SAVINGS = "Balanced Savings"; public final static String TRICKLER = "Balanced Trickler"; + public final static String CONCENTRATED_LIQUIDITY_POOL = "Balanced Concentrated Liquidity Pool"; + public final static String POOL_DEPLOYER = "Balanced Pool Deployer"; public final static String SPOKE_ASSET_MANAGER = "Balanced Spoke Asset Manager"; public final static String SPOKE_XCALL_MANAGER = "Balanced Spoke XCall Manager"; diff --git a/settings.gradle b/settings.gradle index 05fce79a5..022d8cbdf 100644 --- a/settings.gradle +++ b/settings.gradle @@ -135,3 +135,9 @@ project(':SpokeXCallManager').projectDir = file("spoke-contracts/SpokeXCallManag include(':SpokeBalancedDollar') project(':SpokeBalancedDollar').projectDir = file("spoke-contracts/SpokeBalancedDollar") + +include(':NonFungiblePositionManager') +project(':NonFungiblePositionManager').projectDir = file("core-contracts/NonFungiblePositionManager") + +include(':irc721') +project(':irc721').projectDir = file("token-contracts/irc721") \ No newline at end of file diff --git a/token-contracts/irc721/build.gradle b/token-contracts/irc721/build.gradle new file mode 100644 index 000000000..9cd0112b6 --- /dev/null +++ b/token-contracts/irc721/build.gradle @@ -0,0 +1,11 @@ +dependencies { + compileOnly 'foundation.icon:javaee-api:0.9.0' + + testImplementation 'org.mockito:mockito-core:3.3.3' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IIRC721Receiver.java b/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IIRC721Receiver.java new file mode 100644 index 000000000..714a6ceeb --- /dev/null +++ b/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IIRC721Receiver.java @@ -0,0 +1,17 @@ +package team.iconation.standards.token.irc721; + +import java.math.BigInteger; +import score.Address; +import score.Context; + +public class IIRC721Receiver { + public static void onIRC721Received ( + Address to, + Address operator, + Address from, + BigInteger tokenId, + Object data + ) { + Context.call(to, "onIRC721Received", operator, from, tokenId, data); + } +} diff --git a/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721.java b/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721.java new file mode 100644 index 000000000..ad9b303e8 --- /dev/null +++ b/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721.java @@ -0,0 +1,393 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package team.iconation.standards.token.irc721; + +import static java.math.BigInteger.ONE; +import static java.math.BigInteger.ZERO; +import static score.Context.require; + +import java.math.BigInteger; + +import score.Address; +import score.BranchDB; +import score.Context; +import score.DictDB; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Optional; + +public class IRC721 { + + // ================================================ + // Consts + // ================================================ + // Zero Address + protected static final Address ZERO_ADDRESS = new Address(new byte[Address.LENGTH]); + // Token name + private final String name; + // Token symbol + private final String symbol; + + // ================================================ + // DB Variables + // ================================================ + // Mapping from token ID to owner address + private final DictDB _owners = Context.newDictDB("owners", Address.class); + // Mapping owner address to token count + private final DictDB _balances = Context.newDictDB("balances", BigInteger.class); + // Mapping from token ID to approved address + private final DictDB _tokenApprovals = Context.newDictDB("tokenApprovals", Address.class); + // Mapping from owner to operator approvals + private final BranchDB> _operatorApprovals = Context.newBranchDB("operatorApprovals", Boolean.class); + + // ================================================ + // Event Logs + // ================================================ + @EventLog + protected void Transfer(Address from, Address to, BigInteger tokenId) {} + + @EventLog + protected void Approval(Address ownerOf, Address to, BigInteger tokenId) {} + + @EventLog + protected void ApprovalForAll(Address owner, Address operator, boolean approved) {} + + // ================================================ + // Methods + // ================================================ + public IRC721 (String name, String symbol) { + this.name = name; + this.symbol = symbol; + } + + @External(readonly = true) + public BigInteger balanceOf(Address owner) { + require(!owner.equals(ZERO_ADDRESS), "IRC721: balance query for the zero address"); + return _balances.getOrDefault(owner, ZERO); + } + + @External(readonly = true) + public Address ownerOf(BigInteger tokenId) { + Address owner = _owners.getOrDefault(tokenId, ZERO_ADDRESS); + require(!owner.equals(ZERO_ADDRESS), "IRC721: owner query for nonexistent token"); + return owner; + } + + @External(readonly = true) + public String name () { + return this.name; + } + + @External(readonly = true) + public String symbol () { + return this.symbol; + } + + @External(readonly = true) + public String tokenURI(BigInteger tokenId) { + require(_exists(tokenId), "IRC721Metadata: URI query for nonexistent token"); + + String baseURI = _baseURI(); + return baseURI.length() > 0 ? baseURI + "|" + tokenId.toString() : ""; + } + + /** + * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each + * token will be the concatenation of the `baseURI` and the `tokenId`. Empty + * by default, can be overriden in child contracts. + */ + protected String _baseURI() { + return ""; + } + + @External + public void approve(Address to, BigInteger tokenId) { + Address owner = ownerOf(tokenId); + require(to != owner, "IRC721: approval to current owner"); + final Address caller = Context.getCaller(); + + require(caller.equals(owner) || isApprovedForAll(owner, caller), + "IRC721: approve caller is not owner nor approved for all" + ); + + _approve(to, tokenId); + } + + @External(readonly = true) + public Address getApproved(BigInteger tokenId) { + require(_exists(tokenId), "IRC721: approved query for nonexistent token"); + + return _tokenApprovals.getOrDefault(tokenId, ZERO_ADDRESS); + } + + @External + public void setApprovalForAll(Address operator, boolean approved) { + final Address caller = Context.getCaller(); + _setApprovalForAll(caller, operator, approved); + } + + @External(readonly = true) + public boolean isApprovedForAll(Address owner, Address operator) { + return _operatorApprovals.at(owner).getOrDefault(operator, false); + } + + @External + public void transferFrom( + Address from, + Address to, + BigInteger tokenId + ) { + final Address caller = Context.getCaller(); + require(_isApprovedOrOwner(caller, tokenId), "IRC721: transfer caller is not owner nor approved"); + _transfer(from, to, tokenId); + } + + @External + public void safeTransferFrom( + Address from, + Address to, + BigInteger tokenId, + @Optional byte[] _data + ) { + final Address caller = Context.getCaller(); + require(_isApprovedOrOwner(caller, tokenId), "IRC721: transfer caller is not owner nor approved"); + _safeTransfer(from, to, tokenId, _data); + } + + /** + * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the IRC721 protocol to prevent tokens from being forever locked. + * + * `_data` is additional data, it has no specified format and it is sent in call to `to`. + * + * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. + * implement alternative mechanisms to perform token transfer, such as signature-based. + * + * Requirements: + * + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + * - `tokenId` token must exist and be owned by `from`. + * - If `to` refers to a smart contract, it must implement {IIRC721Receiver-onIRC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + private void _safeTransfer( + Address from, + Address to, + BigInteger tokenId, + @Optional byte[] _data + ) { + _transfer(from, to, tokenId); + require(_checkOnIRC721Received(from, to, tokenId, _data), "IRC721: transfer to non IRC721Receiver implementer"); + } + + /** + * @dev Returns whether `tokenId` exists. + * + * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}. + * + * Tokens start existing when they are minted (`_mint`), + * and stop existing when they are burned (`_burn`). + */ + protected boolean _exists(BigInteger tokenId) { + return _owners.get(tokenId) != null; + } + + + /** + * @dev Returns whether `spender` is allowed to manage `tokenId`. + * + * Requirements: + * + * - `tokenId` must exist. + */ + protected boolean _isApprovedOrOwner(Address spender, BigInteger tokenId) { + require(_exists(tokenId), "IRC721: operator query for nonexistent token"); + Address owner = ownerOf(tokenId); + return (spender.equals(owner) || getApproved(tokenId).equals(spender) || isApprovedForAll(owner, spender)); + } + + /** + * @dev Safely mints `tokenId` and transfers it to `to`. + * + * Requirements: + * + * - `tokenId` must not exist. + * - If `to` refers to a smart contract, it must implement {IIRC721Receiver-onIRC721Received}, which is called upon a safe transfer. + * + * Emits a {Transfer} event. + */ + protected void _safeMint( + Address to, + BigInteger tokenId, + @Optional byte[] _data + ) { + _mint(to, tokenId); + require( + _checkOnIRC721Received(ZERO_ADDRESS, to, tokenId, _data), + "IRC721: transfer to non IRC721Receiver implementer" + ); + } + + /** + * @dev Mints `tokenId` and transfers it to `to`. + * + * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible + * + * Requirements: + * + * - `tokenId` must not exist. + * - `to` cannot be the zero address. + * + * Emits a {Transfer} event. + */ + protected void _mint(Address to, BigInteger tokenId) { + require(!to.equals(ZERO_ADDRESS), "IRC721: mint to the zero address"); + require(!_exists(tokenId), "IRC721: token already minted"); + + _beforeTokenTransfer(ZERO_ADDRESS, to, tokenId); + + _balances.set(to, _balances.getOrDefault(to, ZERO).add(ONE)); + _owners.set(tokenId, to); + + this.Transfer(ZERO_ADDRESS, to, tokenId); + } + + /** + * @dev Destroys `tokenId`. + * The approval is cleared when the token is burned. + * + * Requirements: + * + * - `tokenId` must exist. + * + * Emits a {Transfer} event. + */ + protected void _burn (BigInteger tokenId) { + Address owner = ownerOf(tokenId); + + _beforeTokenTransfer(owner, ZERO_ADDRESS, tokenId); + + // Clear approvals + _approve(ZERO_ADDRESS, tokenId); + + _balances.set(owner, _balances.get(owner).subtract(ONE)); + _owners.set(tokenId, null); + + this.Transfer(owner, ZERO_ADDRESS, tokenId); + } + + /** + * @dev Transfers `tokenId` from `from` to `to`. + * As opposed to {transferFrom}, this imposes no restrictions on Context.getCaller(). + * + * Requirements: + * + * - `to` cannot be the zero address. + * - `tokenId` token must be owned by `from`. + * + * Emits a {Transfer} event. + */ + protected void _transfer( + Address from, + Address to, + BigInteger tokenId + ) { + require(ownerOf(tokenId).equals(from), "IRC721: transfer of token that is not own"); + require(!to.equals(ZERO_ADDRESS), "IRC721: transfer to the zero address"); + + _beforeTokenTransfer(from, to, tokenId); + + // Clear approvals from the previous owner + _approve(ZERO_ADDRESS, tokenId); + + _balances.set(from, _balances.get(from).subtract(ONE)); + _balances.set(to, _balances.getOrDefault(to, ZERO).add(ONE)); + _owners.set(tokenId, to); + + this.Transfer(from, to, tokenId); + } + + /** + * @dev Approve `to` to operate on `tokenId` + * + * Emits a {Approval} event. + */ + protected void _approve(Address to, BigInteger tokenId) { + _tokenApprovals.set(tokenId, to); + this.Approval(ownerOf(tokenId), to, tokenId); + } + + /** + * @dev Approve `operator` to operate on all of `owner` tokens + * + * Emits a {ApprovalForAll} event. + */ + protected void _setApprovalForAll( + Address owner, + Address operator, + boolean approved + ) { + require(owner != operator, "IRC721: approve to caller"); + _operatorApprovals.at(owner).set(operator, approved); + this.ApprovalForAll(owner, operator, approved); + } + + /** + * @dev Internal function to invoke {IIRC721Receiver-onIRC721Received} on a target address. + * The call is not executed if the target address is not a contract. + * + * @param from address representing the previous owner of the given token ID + * @param to target address that will receive the tokens + * @param tokenId uint256 ID of the token to be transferred + * @param _data bytes optional data to send along with the call + * @return bool whether the call correctly returned the expected magic value + */ + protected boolean _checkOnIRC721Received( + Address from, + Address to, + BigInteger tokenId, + byte[] _data + ) { + if (to.isContract()) { + IIRC721Receiver.onIRC721Received(to, Context.getCaller(), from, tokenId, _data != null ? _data : "".getBytes()); + } + + return true; + } + + /** + * @dev Hook that is called before any token transfer. This includes minting + * and burning. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, ``from``'s `tokenId` will be burned. + * - `from` and `to` are never both zero. + * + * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. + */ + protected void _beforeTokenTransfer( + Address from, + Address to, + BigInteger tokenId + ) {} +} diff --git a/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721Enumerable.java b/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721Enumerable.java new file mode 100644 index 000000000..9c3991e44 --- /dev/null +++ b/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721Enumerable.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package team.iconation.standards.token.irc721; + +import static java.math.BigInteger.ONE; +import static java.math.BigInteger.ZERO; +import static score.Context.require; + +import java.math.BigInteger; + +import score.Address; +import score.BranchDB; +import score.Context; +import score.DictDB; +import score.VarDB; +import score.annotation.External; + +public class IRC721Enumerable extends IRC721 { + // ================================================ + // DB Variables + // ================================================ + // Mapping from owner to list of owned token IDs + final BranchDB> _ownedTokens; + + // Mapping from token ID to index of the owner tokens list + final DictDB _ownedTokensIndex; + + // Array with all token ids, used for enumeration + final VarDB _allTokensLength; + final DictDB _allTokens; + + // Mapping from token id to position in the allTokens array + final DictDB _allTokensIndex; + + // ================================================ + // Methods + // ================================================ + public IRC721Enumerable(String name, String symbol) { + super(name, symbol); + + _ownedTokens = Context.newBranchDB(symbol + "_ownedTokens", BigInteger.class); + _ownedTokensIndex = Context.newDictDB(symbol + "_ownedTokensIndex", BigInteger.class); + _allTokensLength = Context.newVarDB(symbol + "_allTokensLength", BigInteger.class); + _allTokens = Context.newDictDB(symbol + "_allTokens", BigInteger.class); + _allTokensIndex = Context.newDictDB(symbol + "_allTokensIndex", BigInteger.class); + + if (_allTokensLength.get() == null) { + _allTokensLength.set(ZERO); + } + } + + /** + * @dev Returns a token ID owned by `owner` at a given `index` of its token list. + * Use along with {balanceOf} to enumerate all of ``owner``'s tokens. + */ + @External(readonly = true) + public BigInteger tokenOfOwnerByIndex (Address owner, BigInteger index) { + require(index.compareTo(balanceOf(owner)) < 0, + "tokenOfOwnerByIndex: owner index out of bounds"); + return this._ownedTokens.at(owner).get(index); + } + + /** + * @dev Returns the total amount of tokens stored by the contract. + */ + @External(readonly = true) + public BigInteger totalSupply() { + return _allTokensLength.get(); + } + + /** + * @dev Returns a token ID at a given `index` of all the tokens stored by the contract. + * Use along with {totalSupply} to enumerate all tokens. + */ + @External(readonly = true) + public BigInteger tokenByIndex(BigInteger index) { + require(index.compareTo(totalSupply()) < 0, + "tokenByIndex: global index out of bounds"); + return _allTokens.get(index); + } + + /** + * @dev Hook that is called before any token transfer. + * This includes minting and burning. + * + * Calling conditions: + * + * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be + * transferred to `to`. + * - When `from` is zero, `tokenId` will be minted for `to`. + * - When `to` is zero, ``from``'s `tokenId` will be burned. + * - `from` cannot be the zero address. + * - `to` cannot be the zero address. + */ + @Override + protected void _beforeTokenTransfer( + Address from, + Address to, + BigInteger tokenId + ) { + super._beforeTokenTransfer(from, to, tokenId); + + if (from.equals(ZERO_ADDRESS)) { + _addTokenToAllTokensEnumeration(tokenId); + } else if (from != to) { + _removeTokenFromOwnerEnumeration(from, tokenId); + } + if (to.equals(ZERO_ADDRESS)) { + _removeTokenFromAllTokensEnumeration(tokenId); + } else if (to != from) { + _addTokenToOwnerEnumeration(to, tokenId); + } + } + + /** + * @dev Private function to add a token to this extension's ownership-tracking data structures. + * @param to address representing the new owner of the given token ID + * @param tokenId ID of the token to be added to the tokens list of the given address + */ + private void _addTokenToOwnerEnumeration(Address to, BigInteger tokenId) { + BigInteger length = balanceOf(to); + _ownedTokens.at(to).set(length, tokenId); + _ownedTokensIndex.set(tokenId, length); + } + + /** + * @dev Private function to add a token to this extension's token tracking data structures. + * @param tokenId ID of the token to be added to the tokens list + */ + private void _addTokenToAllTokensEnumeration(BigInteger tokenId) { + BigInteger oldLength = _allTokensLength.get(); + _allTokensIndex.set(tokenId, oldLength); + _allTokens.set(oldLength, tokenId); + _allTokensLength.set(oldLength.add(ONE)); + } + + /** + * @dev Private function to remove a token from this extension's ownership-tracking data structures. Note that + * while the token is not assigned a new owner, the `_ownedTokensIndex` mapping is _not_ updated: this allows for + * gas optimizations e.g. when performing a transfer operation (avoiding double writes). + * This has O(1) time complexity, but alters the order of the _ownedTokens array. + * @param from address representing the previous owner of the given token ID + * @param tokenId ID of the token to be removed from the tokens list of the given address + */ + private void _removeTokenFromOwnerEnumeration(Address from, BigInteger tokenId) { + // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and + // then delete the last slot (swap and pop). + + BigInteger lastTokenIndex = balanceOf(from).subtract(ONE); + BigInteger tokenIndex = _ownedTokensIndex.get(tokenId); + + // When the token to delete is the last token, the swap operation is unnecessary + if (tokenIndex != lastTokenIndex) { + BigInteger lastTokenId = _ownedTokens.at(from).get(lastTokenIndex); + _ownedTokens.at(from).set(tokenIndex, lastTokenId); // Move the last token to the slot of the to-delete token + _ownedTokensIndex.set(lastTokenId, tokenIndex); // Update the moved token's index + } + + // This also deletes the contents at the last position of the array + _ownedTokensIndex.set(tokenId, null); + _ownedTokens.at(from).set(lastTokenIndex, null); + } + + /** + * @dev Private function to remove a token from this extension's token tracking data structures. + * This has O(1) time complexity, but alters the order of the _allTokens array. + * @param tokenId ID of the token to be removed from the tokens list + */ + private void _removeTokenFromAllTokensEnumeration(BigInteger tokenId) { + // To prevent a gap in the tokens array, we store the last token in the index of the token to delete, and + // then delete the last slot (swap and pop). + + BigInteger lastTokenIndex = _allTokensLength.get().subtract(ONE); + BigInteger tokenIndex = _allTokensIndex.get(tokenId); + + // When the token to delete is the last token, the swap operation is unnecessary. However, since this occurs so + // rarely (when the last minted token is burnt) that we still do the swap here to avoid the gas cost of adding + // an 'if' statement (like in _removeTokenFromOwnerEnumeration) + BigInteger lastTokenId = _allTokens.get(lastTokenIndex); + + _allTokens.set(tokenIndex, lastTokenId); // Move the last token to the slot of the to-delete token + _allTokensIndex.set(lastTokenId, tokenIndex); // Update the moved token's index + + // This also deletes the contents at the last position of the array + _allTokensIndex.set(tokenId, null); + // pop + _allTokens.set(lastTokenIndex, null); + _allTokensLength.set(lastTokenIndex); + } +} diff --git a/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721Receiver.java b/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721Receiver.java new file mode 100644 index 000000000..159501750 --- /dev/null +++ b/token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721Receiver.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package team.iconation.standards.token.irc721; + +import java.math.BigInteger; +import score.Address; + +public interface IRC721Receiver { + public void onIRC721Received (Address caller, Address from, BigInteger tokenId, byte[] data); +}