From fffe754a8b2fd15ddbec819eb275f07fa555247f Mon Sep 17 00:00:00 2001 From: Spl3en Date: Mon, 8 Jul 2024 22:16:04 +0200 Subject: [PATCH 1/3] Add initial Balanced Liquidity Pool implementation --- core-contracts/Dex/build.gradle | 1 - .../score/core/dex/DexIntegrationTest.java | 385 ----- .../LpTransferableOnContinuousModeTest.java | 163 -- .../score/core/dex/MultipleAddTest.java | 231 --- .../core/dex/NonStakedLPRewardsTest.java | 168 -- .../score/core/dex/SwapRemoveAndFeeTest.java | 197 --- .../core/dex/testtokens/DexIntTestToken.jar | Bin 3689 -> 0 bytes .../balanced/score/core/dex/AbstractDex.java | 820 --------- .../balanced/score/core/dex/BalancedPool.java | 1499 +++++++++++++++++ .../score/core/dex/BalancedPoolFactory.java | 395 +++++ .../score/core/dex/DexDBVariables.java | 151 -- .../balanced/score/core/dex/DexImpl.java | 519 ------ .../score/core/dex/db/LinkedListDB.java | 202 --- .../balanced/score/core/dex/db/NodeDB.java | 92 - .../interfaces/factory/IBalancedFactory.java | 46 + .../score/core/dex/interfaces/irc2/IIRC2.java | 50 + .../core/dex/interfaces/irc2/IIRC2ICX.java | 73 + .../dex/interfaces/irc2/IRC2ICXParam.java | 24 + .../pool/IBalancedFlashCallback.java | 34 + .../pool/IBalancedMintCallback.java | 35 + .../dex/interfaces/pool/IBalancedPool.java | 195 +++ .../interfaces/pool/IBalancedPoolCallee.java | 52 + .../pool/IBalancedSwapCallback.java | 34 + .../pooldeployer/IBalancedPoolDeployer.java | 40 + .../score/core/dex/librairies/BitMath.java | 118 ++ .../core/dex/librairies/FixedPoint128.java | 25 + .../core/dex/librairies/FixedPoint96.java | 26 + .../score/core/dex/librairies/FullMath.java | 162 ++ .../core/dex/librairies/LiquidityMath.java | 47 + .../score/core/dex/librairies/OracleLib.java | 41 + .../core/dex/librairies/PositionLib.java | 67 + .../core/dex/librairies/SqrtPriceMath.java | 229 +++ .../score/core/dex/librairies/SwapMath.java | 125 ++ .../score/core/dex/librairies/TickLib.java | 29 + .../score/core/dex/librairies/TickMath.java | 202 +++ .../score/core/dex/librairies/UnsafeMath.java | 26 + .../core/dex/models/BalancedPoolDeployer.java | 71 + .../score/core/dex/models/Observations.java | 310 ++++ .../score/core/dex/models/Positions.java | 58 + .../score/core/dex/models/TickBitmap.java | 144 ++ .../balanced/score/core/dex/models/Ticks.java | 220 +++ .../core/dex/structs/factory/Parameters.java | 76 + .../structs/pool/BeforeAfterObservation.java | 27 + .../structs/pool/ComputeSwapStepResult.java | 37 + .../dex/structs/pool/InitializeResult.java | 27 + .../dex/structs/pool/MintCallbackData.java | 42 + .../structs/pool/ModifyPositionParams.java | 38 + .../structs/pool/ModifyPositionResult.java | 31 + ...extInitializedTickWithinOneWordResult.java | 32 + .../core/dex/structs/pool/ObserveResult.java | 46 + .../score/core/dex/structs/pool/Oracle.java | 80 + .../core/dex/structs/pool/PairAmounts.java | 41 + .../core/dex/structs/pool/PoolAddress.java | 48 + .../score/core/dex/structs/pool/PoolData.java | 35 + .../core/dex/structs/pool/PoolSettings.java | 68 + .../score/core/dex/structs/pool/Position.java | 87 + .../dex/structs/pool/PositionStorage.java | 27 + .../core/dex/structs/pool/ProtocolFees.java | 58 + .../score/core/dex/structs/pool/Slot0.java | 99 ++ .../pool/SnapshotCumulativesInsideResult.java | 42 + .../dex/structs/pool/StepComputations.java | 36 + .../core/dex/structs/pool/SwapCache.java | 50 + .../core/dex/structs/pool/SwapState.java | 55 + .../score/core/dex/structs/pool/Tick.java | 128 ++ .../{LPMetadataDB.java => AddressUtils.java} | 18 +- .../score/core/dex/utils/ArrayUtils.java | 83 + .../score/core/dex/utils/BytesUtils.java | 62 + .../balanced/score/core/dex/utils/Check.java | 47 - .../balanced/score/core/dex/utils/Const.java | 41 - .../score/core/dex/utils/EnumerableMap.java | 61 + .../score/core/dex/utils/EnumerableSet.java | 76 + .../balanced/score/core/dex/utils/ICX.java | 68 + .../score/core/dex/utils/IntUtils.java | 62 + .../score/core/dex/utils/JSONUtils.java | 44 + .../score/core/dex/utils/MathUtils.java | 70 + .../score/core/dex/utils/StringUtils.java | 80 + .../score/core/dex/utils/TimeUtils.java | 42 + .../balanced/score/core/dex/DexTestBase.java | 150 -- .../balanced/score/core/dex/DexTestCore.java | 1012 ----------- .../core/dex/DexTestSettersAndGetters.java | 658 -------- .../score/core/dex/db/LinkedListDBTest.java | 167 -- .../balanced/score/lib/utils/Names.java | 2 + 82 files changed, 6246 insertions(+), 5013 deletions(-) delete mode 100644 core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/DexIntegrationTest.java delete mode 100644 core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/LpTransferableOnContinuousModeTest.java delete mode 100644 core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/MultipleAddTest.java delete mode 100644 core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/NonStakedLPRewardsTest.java delete mode 100644 core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/SwapRemoveAndFeeTest.java delete mode 100644 core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/testtokens/DexIntTestToken.jar delete mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/AbstractDex.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPool.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPoolFactory.java delete mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexDBVariables.java delete mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexImpl.java delete mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/LinkedListDB.java delete mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/NodeDB.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/factory/IBalancedFactory.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IIRC2.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IIRC2ICX.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/irc2/IRC2ICXParam.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedFlashCallback.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedMintCallback.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPool.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPoolCallee.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedSwapCallback.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IBalancedPoolDeployer.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/BitMath.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FixedPoint128.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FixedPoint96.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FullMath.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/LiquidityMath.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/OracleLib.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/PositionLib.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/SqrtPriceMath.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/SwapMath.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/TickLib.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/TickMath.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/UnsafeMath.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/BalancedPoolDeployer.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Observations.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Positions.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/TickBitmap.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/Ticks.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/factory/Parameters.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/BeforeAfterObservation.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ComputeSwapStepResult.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/InitializeResult.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/MintCallbackData.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ModifyPositionParams.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ModifyPositionResult.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/NextInitializedTickWithinOneWordResult.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ObserveResult.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Oracle.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PairAmounts.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolAddress.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolData.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PoolSettings.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Position.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/PositionStorage.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/ProtocolFees.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Slot0.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SnapshotCumulativesInsideResult.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/StepComputations.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SwapCache.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/SwapState.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/structs/pool/Tick.java rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/{LPMetadataDB.java => AddressUtils.java} (61%) create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/ArrayUtils.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/BytesUtils.java delete mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Check.java delete mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Const.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/EnumerableMap.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/EnumerableSet.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/ICX.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/IntUtils.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/JSONUtils.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/MathUtils.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/StringUtils.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/TimeUtils.java delete mode 100644 core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestBase.java delete mode 100644 core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestCore.java delete mode 100644 core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestSettersAndGetters.java delete mode 100644 core-contracts/Dex/src/test/java/network/balanced/score/core/dex/db/LinkedListDBTest.java diff --git a/core-contracts/Dex/build.gradle b/core-contracts/Dex/build.gradle index 37ee967af..a849ee0bf 100644 --- a/core-contracts/Dex/build.gradle +++ b/core-contracts/Dex/build.gradle @@ -104,7 +104,6 @@ deployJar { keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' parameters { - arg("_governance", Addresses.mainnet.governance) } } diff --git a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/DexIntegrationTest.java b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/DexIntegrationTest.java deleted file mode 100644 index 9238ee571..000000000 --- a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/DexIntegrationTest.java +++ /dev/null @@ -1,385 +0,0 @@ -/* - * 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. - */ - -package network.balanced.score.core.dex; - -import com.eclipsesource.json.JsonArray; -import foundation.icon.icx.Wallet; -import foundation.icon.jsonrpc.Address; -import foundation.icon.score.client.DefaultScoreClient; -import network.balanced.score.lib.interfaces.*; -import network.balanced.score.lib.interfaces.dex.DexTestScoreClient; -import network.balanced.score.lib.test.integration.Balanced; -import network.balanced.score.lib.test.integration.Env; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.math.BigInteger; -import java.util.Map; - -import static foundation.icon.score.client.DefaultScoreClient._deploy; -import static network.balanced.score.core.dex.utils.Const.SICXICX_MARKET_NAME; -import static network.balanced.score.lib.test.integration.BalancedUtils.createParameter; -import static network.balanced.score.lib.test.integration.BalancedUtils.createTransaction; -import static network.balanced.score.lib.test.integration.ScoreIntegrationTest.createWalletWithBalance; -import static network.balanced.score.lib.utils.Constants.EXA; -import static org.junit.jupiter.api.Assertions.*; - -class DexIntegrationTest { - - private static final Env.Chain chain = Env.getDefaultChain(); - private static Balanced balanced; - private static Wallet userWallet; - private static Wallet tUserWallet; - private static Wallet testOwnerWallet; - - private static DefaultScoreClient dexScoreClient; - private static DefaultScoreClient governanceScoreClient; - private static DefaultScoreClient stakingScoreClient; - private static DefaultScoreClient sIcxScoreClient; - private static DefaultScoreClient balnScoreClient; - private static DefaultScoreClient rewardsScoreClient; - private static DefaultScoreClient tokenAClient; - private static DefaultScoreClient tokenBClient; - private static DefaultScoreClient tokenCClient; - private static DefaultScoreClient tokenDClient; - private static DefaultScoreClient daoFundScoreClient; - - private static final File jarfile = new File("src/intTest/java/network/balanced/score/core/dex/testtokens" + - "/DexIntTestToken.jar"); - - static { - try { - tUserWallet = createWalletWithBalance(BigInteger.valueOf(500).multiply(EXA)); - userWallet = createWalletWithBalance(BigInteger.valueOf(800).multiply(EXA)); - - balanced = new Balanced(); - testOwnerWallet = balanced.owner; - - tokenAClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, jarfile.getPath(), - Map.of("name", "Test Token", "symbol", "TT")); - tokenBClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, jarfile.getPath(), - Map.of("name", "Test Base Token", "symbol", "TB")); - tokenCClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, jarfile.getPath(), - Map.of("name", "Test Third Token", "symbol", "TTD")); - tokenDClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, jarfile.getPath(), - Map.of("name", "Test Fourth Token", "symbol", "TFD")); - - balanced.setupBalanced(); - dexScoreClient = balanced.dex; - governanceScoreClient = balanced.governance; - stakingScoreClient = balanced.staking; - sIcxScoreClient = balanced.sicx; - balnScoreClient = balanced.baln; - rewardsScoreClient = balanced.rewards; - daoFundScoreClient = balanced.daofund; - - Rewards rewards = new RewardsScoreClient(balanced.rewards); - Loans loans = new LoansScoreClient(balanced.loans); - BalancedToken baln = new BalancedTokenScoreClient(balanced.baln); - Sicx sicx = new SicxScoreClient(balanced.sicx); - StakedLP stakedLp = new StakedLPScoreClient(balanced.stakedLp); - - } catch (Exception e) { - e.printStackTrace(); - System.out.println("Error on init test: " + e.getMessage()); - } - - } - - private static final Address tokenAAddress = tokenAClient._address(); - private static final Address tokenBAddress = tokenBClient._address(); - private static final Address tokenCAddress = tokenCClient._address(); - private static final Address tokenDAddress = tokenDClient._address(); - - private static final Address userAddress = Address.of(userWallet); - private static final Address tUserAddress = Address.of(tUserWallet); - - private static final DexTestScoreClient ownerDexTestScoreClient = new DexTestScoreClient(chain.getEndpointURL(), - chain.networkId, testOwnerWallet, tokenAAddress); - private static final DexTestScoreClient ownerDexTestBaseScoreClient = new DexTestScoreClient(chain.getEndpointURL(), - chain.networkId, testOwnerWallet, tokenBAddress); - private static final DexTestScoreClient ownerDexTestThirdScoreClient = - new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, tokenCAddress); - private static final DexTestScoreClient ownerDexTestFourthScoreClient = - new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, tokenDAddress); - private static final DexScoreClient dexUserScoreClient = new DexScoreClient(dexScoreClient.endpoint(), - dexScoreClient._nid(), userWallet, dexScoreClient._address()); - private static final Staking userStakeScoreClient = new StakingScoreClient(dexScoreClient.endpoint(), - dexScoreClient._nid(), userWallet, stakingScoreClient._address()); - private static final SicxScoreClient userSicxScoreClient = new SicxScoreClient(dexScoreClient.endpoint(), - dexScoreClient._nid(), userWallet, sIcxScoreClient._address()); - static Rewards userWalletRewardsClient = new RewardsScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), - userWallet, rewardsScoreClient._address()); - private static final BalancedTokenScoreClient userBalnScoreClient = - new BalancedTokenScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, - balnScoreClient._address()); - private static final DexTestScoreClient userDexTestScoreClient = new DexTestScoreClient(dexScoreClient.endpoint(), - dexScoreClient._nid(), userWallet, tokenAAddress); - private static final DexTestScoreClient userDexTestBaseScoreClient = - new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, tokenBAddress); - private static final DexTestScoreClient userDexTestThirdScoreClient = - new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, tokenCAddress); - private static final DexTestScoreClient userDexTestFourthScoreClient = - new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, tokenDAddress); - - private static final GovernanceScoreClient governanceDexScoreClient = - new GovernanceScoreClient(governanceScoreClient); - private static final RewardsScoreClient userRewardScoreClient = new RewardsScoreClient(rewardsScoreClient); - private static final DAOfundScoreClient userDaoFundScoreClient = new DAOfundScoreClient(daoFundScoreClient); - - private static final DefaultScoreClient userClient = new DefaultScoreClient(chain.getEndpointURL(), - chain.networkId, userWallet, DefaultScoreClient.ZERO_ADDRESS); - - private static final DefaultScoreClient tUserClient = new DefaultScoreClient(chain.getEndpointURL(), - chain.networkId, tUserWallet, DefaultScoreClient.ZERO_ADDRESS); - - - @Test - @Order(3) - void testICXTransferSwapEarningAndCancelOrder() { - assertEquals(SICXICX_MARKET_NAME, dexUserScoreClient.getPoolName(BigInteger.ONE)); - BigInteger defaultPoolId = dexUserScoreClient.lookupPid(SICXICX_MARKET_NAME); - assertEquals(BigInteger.ONE, defaultPoolId); - - Map poolStats = dexUserScoreClient.getPoolStats(defaultPoolId); - assertEquals(poolStats.get("base_token").toString(), sIcxScoreClient._address().toString()); - assertNull(poolStats.get("quote_token")); - assertEquals(hexToBigInteger(poolStats.get("base").toString()), BigInteger.ZERO); - assertEquals(hexToBigInteger(poolStats.get("quote").toString()), BigInteger.ZERO); - assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()), BigInteger.ZERO); - - //test icx transfer and verify stats - balanced.syncDistributions(); - userClient._transfer(dexScoreClient._address(), BigInteger.valueOf(200).multiply(EXA), null); - poolStats = dexUserScoreClient.getPoolStats(defaultPoolId); - - assertEquals(poolStats.get("base_token").toString(), sIcxScoreClient._address().toString()); - assertNull(poolStats.get("quote_token")); - assertEquals(hexToBigInteger(poolStats.get("base").toString()), BigInteger.ZERO); - assertEquals(hexToBigInteger(poolStats.get("quote").toString()), BigInteger.valueOf(200).multiply(EXA)); - assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()), BigInteger.valueOf(200).multiply(EXA)); - BigInteger beforeSwapPrice = hexToBigInteger(poolStats.get("price").toString()); - - //test swap - byte[] data = "testData".getBytes(); - ((StakingScoreClient) userStakeScoreClient).stakeICX(BigInteger.valueOf(100).multiply(EXA), userAddress, data); - - byte[] swapIcx = "{\"method\":\"_swap_icx\",\"params\":{\"none\":\"none\"}}".getBytes(); - userSicxScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(50).multiply(EXA), swapIcx); - - poolStats = dexUserScoreClient.getPoolStats(defaultPoolId); - BigInteger afterSwapPrice = hexToBigInteger(poolStats.get("price").toString()); - // price should be same after swap - assertEquals(beforeSwapPrice, afterSwapPrice); - - defaultPoolId = dexUserScoreClient.lookupPid(SICXICX_MARKET_NAME); - poolStats = dexUserScoreClient.getPoolStats(defaultPoolId); - - assertEquals(poolStats.get("base_token").toString(), sIcxScoreClient._address().toString()); - assertNull(poolStats.get("quote_token")); - assertEquals(hexToBigInteger(poolStats.get("base").toString()), BigInteger.ZERO); - assertEquals(hexToBigInteger(poolStats.get("quote").toString()).divide(EXA), BigInteger.valueOf(150)); - assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()).divide(EXA), BigInteger.valueOf(150)); - - System.out.println(" day is: " + dexUserScoreClient.getDay()); - waitForADay(); - //release lock by distributing rewards - balanced.syncDistributions(); - //verify sicx earning and make withdraw - BigInteger sicxEarning = dexUserScoreClient.getSicxEarnings(userAddress); - assertNotNull(sicxEarning); - dexUserScoreClient.withdrawSicxEarnings(sicxEarning); - - balanced.syncDistributions(); - dexUserScoreClient.cancelSicxicxOrder(); - } - - /*@Test - @Order(4) - void testBalnPoolTokenTransferableOnContinuousRewards(){ - - if(dexUserScoreClient.getContinuousRewardsDay()==null) { - governanceDexScoreClient.setContinuousRewardsDay(dexUserScoreClient.getDay().add(BigInteger.ONE)); - } - waitForADay(); - balanced.syncDistributions(); - //continuous starts - byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); - mintAndTransferTestTokens(tokenDeposit); - dexUserScoreClient.add(Address.fromString(dexTestBaseScoreAddress), Address.fromString - (dexTestFourthScoreClient._address().toString()), BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf - (50).multiply(EXA), false); - BigInteger poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestBaseScoreAddress), Address - .fromString(dexTestFourthScoreAddress)); - //assert pool id is less than 5 - assert poolId.compareTo(BigInteger.valueOf(6)) < 0; - BigInteger liquidity = (BigInteger.valueOf(50).multiply(EXA).multiply(BigInteger.valueOf(50).multiply(EXA))) - .sqrt(); - BigInteger balance = dexUserScoreClient.balanceOf(userAddress, poolId); - BigInteger tUsersPrevBalance = dexUserScoreClient.balanceOf(tUserAddress, poolId); - - assertEquals(balance, liquidity); - dexUserScoreClient.transfer(tUserAddress, BigInteger.valueOf(5).multiply(EXA), poolId, new byte[0]); - BigInteger tUsersBalance = dexUserScoreClient.balanceOf(tUserAddress, poolId); - assertEquals(tUsersPrevBalance.add(BigInteger.valueOf(5).multiply(EXA)), tUsersBalance); - }*/ - - @Test - @Order(6) - void testWithdraw() { - byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); - this.mintAndTransferTestTokens(tokenDeposit); - BigInteger withdrawAMount = BigInteger.valueOf(50); - BigInteger balanceBeforeWithdraw = dexUserScoreClient.depositOfUser(userAddress, tokenAAddress); - //withdraw test token - dexUserScoreClient.withdraw(tokenAAddress, withdrawAMount); - - BigInteger balanceAfterWithdraw = dexUserScoreClient.depositOfUser(userAddress, tokenAAddress); - - assert balanceBeforeWithdraw.equals(balanceAfterWithdraw.add(withdrawAMount)); - } - - @Test - @Order(7) - void testLpTokensAndTransfer() { - byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); - mintAndTransferTestTokens(tokenDeposit); - transferSicxToken(); - dexUserScoreClient.add(tokenBAddress, Address.fromString(sIcxScoreClient._address().toString()), - BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(25).multiply(EXA), false, BigInteger.valueOf(100)); - mintAndTransferTestTokens(tokenDeposit); - transferSicxToken(); - dexUserScoreClient.add(Address.fromString(sIcxScoreClient._address().toString()), tokenAAddress, - BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(25).multiply(EXA), false, BigInteger.valueOf(100)); - mintAndTransferTestTokens(tokenDeposit); - transferSicxToken(); - dexUserScoreClient.add(tokenBAddress, tokenAAddress, BigInteger.valueOf(50).multiply(EXA), - BigInteger.valueOf(25).multiply(EXA), false, BigInteger.valueOf(100)); - mintAndTransferTestTokens(tokenDeposit); - transferSicxToken(); - dexUserScoreClient.add(Address.fromString(sIcxScoreClient._address().toString()), tokenCAddress, - BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(25).multiply(EXA), false, BigInteger.valueOf(100)); - mintAndTransferTestTokens(tokenDeposit); - transferSicxToken(); - dexUserScoreClient.add(tokenBAddress, tokenCAddress, BigInteger.valueOf(50).multiply(EXA), - BigInteger.valueOf(25).multiply(EXA), false, BigInteger.valueOf(100)); - - waitForADay(); - - //take pool id > 5 so that it can be transferred - BigInteger poolId = dexUserScoreClient.getPoolId(tokenBAddress, tokenCAddress); - System.out.println(poolId); - BigInteger balance = dexUserScoreClient.balanceOf(userAddress, poolId); - assertNotEquals(balance, BigInteger.ZERO); - dexUserScoreClient.transfer(Address.fromString(tUserAddress.toString()), BigInteger.valueOf(5).multiply(EXA), - poolId, new byte[0]); - BigInteger tUsersBalance = dexUserScoreClient.balanceOf(Address.fromString(tUserAddress.toString()), poolId); - assert BigInteger.ZERO.compareTo(tUsersBalance) < 0; - - } - - @Test - @Order(8) - void testNonContinuousAndContinuousReward() { - balanced.syncDistributions(); - BigInteger balnHolding = userRewardScoreClient.getBalnHolding(tUserAddress.toString()); - tUserClient._transfer(dexScoreClient._address(), BigInteger.valueOf(200).multiply(EXA), null); - - - balanced.syncDistributions(); - System.out.println("Baln total supply is: " + userBalnScoreClient.totalSupply()); - BigInteger updatedBalnHolding = userRewardScoreClient.getBalnHolding(tUserAddress.toString()); - System.out.println("baln holding: " + balnHolding); - System.out.println("updated baln holding: " + updatedBalnHolding); - assert balnHolding.compareTo(updatedBalnHolding) < 0; - BigInteger beforeSleepDay = dexUserScoreClient.getDay(); - try { - Thread.sleep(5000); //wait some time - } catch (Exception e) { - System.out.println(e.getMessage()); - } - - BigInteger nextUpdatedBalnHolding = userRewardScoreClient.getBalnHolding(tUserAddress.toString()); - assertEquals(beforeSleepDay, dexUserScoreClient.getDay()); - - System.out.println("updated baln holding: " + updatedBalnHolding); - System.out.println("next updated baln holding: " + nextUpdatedBalnHolding); - assert updatedBalnHolding.compareTo(nextUpdatedBalnHolding) < 0; - - } - - void transferSicxToken() { - byte[] data = "testData".getBytes(); - ((StakingScoreClient) userStakeScoreClient).stakeICX(BigInteger.valueOf(80).multiply(EXA), userAddress, data); - - byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); - userSicxScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(60).multiply(EXA), tokenDeposit); - } - - void mintAndTransferTestTokens(byte[] tokenDeposit) { - - ownerDexTestScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); - ownerDexTestBaseScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); - ownerDexTestThirdScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); - ownerDexTestFourthScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); - - - //deposit base token - userDexTestBaseScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), - tokenDeposit); - //deposit quote token - userDexTestScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), tokenDeposit); - userDexTestThirdScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), - tokenDeposit); - userDexTestFourthScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), - tokenDeposit); - - //check isQuoteCoinAllowed for test token if not added - if (!dexUserScoreClient.isQuoteCoinAllowed(tokenAAddress)) { - dexAddQuoteCoin(tokenAAddress); - } - if (!dexUserScoreClient.isQuoteCoinAllowed(tokenBAddress)) { - dexAddQuoteCoin(tokenBAddress); - } - if (!dexUserScoreClient.isQuoteCoinAllowed(tokenCAddress)) { - dexAddQuoteCoin(tokenCAddress); - } - if (!dexUserScoreClient.isQuoteCoinAllowed(tokenDAddress)) { - dexAddQuoteCoin(tokenDAddress); - } - } - - private static void dexAddQuoteCoin(Address address) { - JsonArray addQuoteCoinParameters = new JsonArray() - .add(createParameter(address)); - - JsonArray actions = new JsonArray() - .add(createTransaction(balanced.dex._address(), "addQuoteCoin", addQuoteCoinParameters)); - - balanced.ownerClient.governance.execute(actions.toString()); - } - - void waitForADay() { - balanced.increaseDay(1); - } - - BigInteger hexToBigInteger(String hex) { - return new BigInteger(hex.replace("0x", ""), 16); - } - -} diff --git a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/LpTransferableOnContinuousModeTest.java b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/LpTransferableOnContinuousModeTest.java deleted file mode 100644 index 07e861029..000000000 --- a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/LpTransferableOnContinuousModeTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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. - */ - -package network.balanced.score.core.dex; - -import com.eclipsesource.json.JsonArray; -import foundation.icon.icx.Wallet; -import foundation.icon.jsonrpc.Address; -import foundation.icon.score.client.DefaultScoreClient; -import network.balanced.score.lib.interfaces.DexScoreClient; -import network.balanced.score.lib.interfaces.GovernanceScoreClient; -import network.balanced.score.lib.interfaces.dex.DexTestScoreClient; -import network.balanced.score.lib.test.integration.Balanced; -import network.balanced.score.lib.test.integration.Env; -import network.balanced.score.lib.test.integration.ScoreIntegrationTest; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.math.BigInteger; -import java.util.Map; - -import static foundation.icon.score.client.DefaultScoreClient._deploy; -import static network.balanced.score.lib.test.integration.BalancedUtils.createParameter; -import static network.balanced.score.lib.test.integration.BalancedUtils.createTransaction; -import static network.balanced.score.lib.utils.Constants.EXA; -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class LpTransferableOnContinuousModeTest { - - private static final Env.Chain chain = Env.getDefaultChain(); - private static Balanced balanced; - private static Wallet userWallet; - private static Wallet tUserWallet; - private static Wallet testOwnerWallet; - private static DefaultScoreClient dexScoreClient; - private static DefaultScoreClient governanceScoreClient; - private static DefaultScoreClient dexTestBaseScoreClient; - private static DefaultScoreClient dexTestFourthScoreClient; - - - static DefaultScoreClient daoFund; - private static final File jarfile = new File("src/intTest/java/network/balanced/score/core/dex/testtokens" + - "/DexIntTestToken.jar"); - - static { - try { - balanced = new Balanced(); - testOwnerWallet = balanced.owner; - userWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(800).multiply(EXA)); - tUserWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(500).multiply(EXA)); - dexTestBaseScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, - jarfile.getPath(), Map.of("name", "Test Base Token", "symbol", "TB")); - dexTestFourthScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, - jarfile.getPath(), Map.of("name", "Test Fourth Token", "symbol", "TFD")); - balanced.setupBalanced(); - dexScoreClient = balanced.dex; - governanceScoreClient = balanced.governance; - - } catch (Exception e) { - e.printStackTrace(); - System.out.println("Error on init test: " + e.getMessage()); - } - - } - - private static final String dexTestBaseScoreAddress = dexTestBaseScoreClient._address().toString(); - private static final String dexTestFourthScoreAddress = dexTestFourthScoreClient._address().toString(); - - private static final Address userAddress = Address.of(userWallet); - private static final Address tUserAddress = Address.of(tUserWallet); - - private static final DexTestScoreClient ownerDexTestBaseScoreClient = new DexTestScoreClient(chain.getEndpointURL(), - chain.networkId, testOwnerWallet, DefaultScoreClient.address(dexTestBaseScoreAddress)); - private static final DexTestScoreClient ownerDexTestFourthScoreClient = - new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, - DefaultScoreClient.address(dexTestFourthScoreAddress)); - private static final DexScoreClient dexUserScoreClient = new DexScoreClient(dexScoreClient.endpoint(), - dexScoreClient._nid(), userWallet, dexScoreClient._address()); - - private static final DexTestScoreClient userDexTestBaseScoreClient = - new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, - DefaultScoreClient.address(dexTestBaseScoreAddress)); - private static final DexTestScoreClient userDexTestFourthScoreClient = - new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, - DefaultScoreClient.address(dexTestFourthScoreAddress)); - private static final GovernanceScoreClient governanceDexScoreClient = - new GovernanceScoreClient(governanceScoreClient); - - - @Test - @Order(4) - void testBalnPoolTokenTransferableOnContinuousRewards() { - byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); - mintAndTransferTestTokens(tokenDeposit); - dexUserScoreClient.add(Address.fromString(dexTestBaseScoreAddress), - Address.fromString(dexTestFourthScoreClient._address().toString()), - BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(50).multiply(EXA), false, BigInteger.valueOf(100)); - BigInteger poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestBaseScoreAddress), - Address.fromString(dexTestFourthScoreAddress)); - //assert pool id is less than 5 - assert poolId.compareTo(BigInteger.valueOf(6)) < 0; - BigInteger liquidity = - (BigInteger.valueOf(50).multiply(EXA).multiply(BigInteger.valueOf(50).multiply(EXA))).sqrt(); - BigInteger balance = dexUserScoreClient.balanceOf(userAddress, poolId); - BigInteger tUsersPrevBalance = dexUserScoreClient.balanceOf(tUserAddress, poolId); - - assertEquals(balance, liquidity); - dexUserScoreClient.transfer(tUserAddress, BigInteger.valueOf(5).multiply(EXA), poolId, new byte[0]); - BigInteger tUsersBalance = dexUserScoreClient.balanceOf(tUserAddress, poolId); - assertEquals(tUsersPrevBalance.add(BigInteger.valueOf(5).multiply(EXA)), tUsersBalance); - } - - void mintAndTransferTestTokens(byte[] tokenDeposit) { - - ownerDexTestBaseScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); - ownerDexTestFourthScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); - - - //deposit base token - userDexTestBaseScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), - tokenDeposit); - //deposit quote token - userDexTestFourthScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), - tokenDeposit); - - //check isQuoteCoinAllowed for test token if not added - - if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestBaseScoreAddress))) { - dexAddQuoteCoin(new Address(dexTestBaseScoreAddress)); - } - if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestFourthScoreAddress))) { - dexAddQuoteCoin(new Address(dexTestFourthScoreAddress)); - } - } - - void dexAddQuoteCoin(Address address) { - JsonArray addQuoteCoinParameters = new JsonArray() - .add(createParameter(address)); - - JsonArray actions = new JsonArray() - .add(createTransaction(balanced.dex._address(), "addQuoteCoin", addQuoteCoinParameters)); - - balanced.ownerClient.governance.execute(actions.toString()); - } - - void waitForADay() { - balanced.increaseDay(1); - } -} diff --git a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/MultipleAddTest.java b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/MultipleAddTest.java deleted file mode 100644 index a2f095d38..000000000 --- a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/MultipleAddTest.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * 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. - */ - -package network.balanced.score.core.dex; - -import com.eclipsesource.json.JsonArray; -import foundation.icon.icx.Wallet; -import foundation.icon.jsonrpc.Address; -import foundation.icon.score.client.DefaultScoreClient; -import foundation.icon.score.client.RevertedException; -import network.balanced.score.lib.interfaces.*; -import network.balanced.score.lib.interfaces.dex.DexTestScoreClient; -import network.balanced.score.lib.test.integration.Balanced; -import network.balanced.score.lib.test.integration.Env; -import network.balanced.score.lib.test.integration.ScoreIntegrationTest; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestMethodOrder; -import org.junit.jupiter.api.function.Executable; -import score.UserRevertedException; - -import java.io.File; -import java.math.BigInteger; -import java.util.Map; - -import static foundation.icon.score.client.DefaultScoreClient._deploy; -import static network.balanced.score.lib.test.integration.BalancedUtils.createParameter; -import static network.balanced.score.lib.test.integration.BalancedUtils.createTransaction; -import static network.balanced.score.lib.utils.Constants.EXA; -import static org.junit.jupiter.api.Assertions.*; - -@TestMethodOrder(MethodOrderer.OrderAnnotation.class) -public class MultipleAddTest { - private static final Env.Chain chain = Env.getDefaultChain(); - private static Wallet userWallet; - private static Wallet testOwnerWallet; - private static Balanced balanced; - private static DefaultScoreClient dexScoreClient; - private static DefaultScoreClient governanceScoreClient; - private static DefaultScoreClient dexTestThirdScoreClient; - private static DefaultScoreClient dexTestFourthScoreClient; - - - static DefaultScoreClient daoFund; - private static final File jarfile = new File("src/intTest/java/network/balanced/score/core/dex/testtokens" + - "/DexIntTestToken.jar"); - - static { - try { - balanced = new Balanced(); - testOwnerWallet = balanced.owner; - userWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(800).multiply(EXA)); - dexTestThirdScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, - jarfile.getPath(), Map.of("name", "Test Third Token", "symbol", "TTD")); - dexTestFourthScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, - jarfile.getPath(), Map.of("name", "Test Fourth Token", "symbol", "TFD")); - balanced.setupBalanced(); - dexScoreClient = balanced.dex; - governanceScoreClient = balanced.governance; - - } catch (Exception e) { - e.printStackTrace(); - System.out.println("Error on init test: " + e.getMessage()); - } - - } - - private static final String dexTestThirdScoreAddress = dexTestThirdScoreClient._address().toString(); - private static final String dexTestFourthScoreAddress = dexTestFourthScoreClient._address().toString(); - - private static final Address userAddress = Address.of(userWallet); - - private static final DexTestScoreClient ownerDexTestThirdScoreClient = - new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, - DefaultScoreClient.address(dexTestThirdScoreAddress)); - private static final DexTestScoreClient ownerDexTestFourthScoreClient = - new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, - DefaultScoreClient.address(dexTestFourthScoreAddress)); - private static final DexScoreClient dexUserScoreClient = new DexScoreClient(dexScoreClient.endpoint(), - dexScoreClient._nid(), userWallet, dexScoreClient._address()); - private static final DexTestScoreClient userDexTestThirdScoreClient = - new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, - DefaultScoreClient.address(dexTestThirdScoreAddress)); - private static final DexTestScoreClient userDexTestFourthScoreClient = - new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, - DefaultScoreClient.address(dexTestFourthScoreAddress)); - private static final GovernanceScoreClient governanceDexScoreClient = - new GovernanceScoreClient(governanceScoreClient); - - @Test - @Order(4) - void testMultipleAdd() { - //testMultipleAdd - BigInteger previousUserBalance = ownerDexTestFourthScoreClient.balanceOf(userAddress); - BigInteger previousSecondUserBalance = ownerDexTestThirdScoreClient.balanceOf(userAddress); - byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); - - this.mintAndTransferTestTokens(tokenDeposit); - //add the pool of test token and sicx - dexUserScoreClient.add(Address.fromString(dexTestThirdScoreAddress), - Address.fromString(dexTestFourthScoreAddress), BigInteger.valueOf(50).multiply(EXA), - BigInteger.valueOf(50).multiply(EXA), true, BigInteger.valueOf(100)); - BigInteger poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestThirdScoreAddress), - Address.fromString(dexTestFourthScoreAddress)); - Map poolStats = dexUserScoreClient.getPoolStats(poolId); - assertNull(poolStats.get("name")); - assertEquals(poolStats.get("base_token").toString(), dexTestThirdScoreAddress); - assertEquals(poolStats.get("quote_token").toString(), dexTestFourthScoreAddress); - assertEquals(hexToBigInteger(poolStats.get("base").toString()), BigInteger.valueOf(50).multiply(EXA)); - assertEquals(hexToBigInteger(poolStats.get("quote").toString()), BigInteger.valueOf(50).multiply(EXA)); - assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()), BigInteger.valueOf(50).multiply(EXA)); - assertEquals(hexToBigInteger(poolStats.get("price").toString()), BigInteger.ONE.multiply(EXA)); - assertEquals(hexToBigInteger(poolStats.get("base_decimals").toString()), BigInteger.valueOf(18)); - assertEquals(hexToBigInteger(poolStats.get("quote_decimals").toString()), BigInteger.valueOf(18)); - assertEquals(hexToBigInteger(poolStats.get("min_quote").toString()), BigInteger.ZERO); - - // after lp is added to the pool, remaining balance is checked - assertEquals(previousUserBalance.add(BigInteger.valueOf(150).multiply(EXA)), - ownerDexTestFourthScoreClient.balanceOf(userAddress)); - assertEquals(previousSecondUserBalance.add(BigInteger.valueOf(150).multiply(EXA)), - ownerDexTestThirdScoreClient.balanceOf(userAddress)); - - this.mintAndTransferTestTokens(tokenDeposit); - - dexUserScoreClient.add(Address.fromString(dexTestThirdScoreAddress), - Address.fromString(dexTestFourthScoreAddress), BigInteger.valueOf(50).multiply(EXA), - BigInteger.valueOf(50).multiply(EXA), true, BigInteger.valueOf(100)); - - // after lp is added to the pool, remaining balance is checked - assertEquals(BigInteger.valueOf(300).multiply(EXA), ownerDexTestFourthScoreClient.balanceOf(userAddress)); - assertEquals(BigInteger.valueOf(300).multiply(EXA), ownerDexTestThirdScoreClient.balanceOf(userAddress)); - - poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestThirdScoreAddress), - Address.fromString(dexTestFourthScoreAddress)); - poolStats = dexUserScoreClient.getPoolStats(poolId); - assertNull(poolStats.get("name")); - assertEquals(poolStats.get("base_token").toString(), dexTestThirdScoreAddress); - assertEquals(poolStats.get("quote_token").toString(), dexTestFourthScoreAddress); - assertEquals(hexToBigInteger(poolStats.get("base").toString()), BigInteger.valueOf(100).multiply(EXA)); - assertEquals(hexToBigInteger(poolStats.get("quote").toString()), BigInteger.valueOf(100).multiply(EXA)); - assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()), BigInteger.valueOf(100).multiply(EXA)); - assertEquals(hexToBigInteger(poolStats.get("price").toString()), BigInteger.ONE.multiply(EXA)); - assertEquals(hexToBigInteger(poolStats.get("base_decimals").toString()), BigInteger.valueOf(18)); - assertEquals(hexToBigInteger(poolStats.get("quote_decimals").toString()), BigInteger.valueOf(18)); - assertEquals(hexToBigInteger(poolStats.get("min_quote").toString()), BigInteger.ZERO); - - //change name and verify - setMarketName(poolId, "DTT/DTBT"); - Map updatedPoolStats = dexUserScoreClient.getPoolStats(poolId); - assertEquals(updatedPoolStats.get("name").toString(), "DTT/DTBT"); - } - - @Test - @Order(5) - void AddLiquidity_failOnHigherSlippage() {; - byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); - - this.mintAndTransferTestTokens(tokenDeposit); - //add the pool of test token and sicx - dexUserScoreClient.add(Address.fromString(dexTestThirdScoreAddress), - Address.fromString(dexTestFourthScoreAddress), BigInteger.valueOf(50).multiply(EXA), - BigInteger.valueOf(50).multiply(EXA), true, BigInteger.valueOf(100)); - - this.mintAndTransferTestTokens(tokenDeposit); - - Executable userLiquiditySupply = () -> dexUserScoreClient.add(Address.fromString(dexTestThirdScoreAddress), - Address.fromString(dexTestFourthScoreAddress), BigInteger.valueOf(50).multiply(EXA), - BigInteger.valueOf(51).multiply(EXA), true, BigInteger.valueOf(100)); - - assertThrows(UserRevertedException.class, userLiquiditySupply); - } - - void mintAndTransferTestTokens(byte[] tokenDeposit) { - - ownerDexTestThirdScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); - ownerDexTestFourthScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); - - - userDexTestThirdScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), - tokenDeposit); - userDexTestFourthScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), - tokenDeposit); - - //check isQuoteCoinAllowed for test token if not added - if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestThirdScoreAddress))) { - dexAddQuoteCoin(new Address(dexTestThirdScoreAddress)); - } - if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestFourthScoreAddress))) { - dexAddQuoteCoin(new Address(dexTestFourthScoreAddress)); - } - } - - void dexAddQuoteCoin(Address address) { - JsonArray addQuoteCoinParameters = new JsonArray() - .add(createParameter(address)); - - JsonArray actions = new JsonArray() - .add(createTransaction(balanced.dex._address(), "addQuoteCoin", addQuoteCoinParameters)); - - balanced.ownerClient.governance.execute(actions.toString()); - } - - void setMarketName(BigInteger poolID, String name) { - JsonArray setMarketNameParameters = new JsonArray() - .add(createParameter(poolID)) - .add(createParameter(name)); - - JsonArray actions = new JsonArray() - .add(createTransaction(balanced.dex._address(), "setMarketName", setMarketNameParameters)); - - balanced.ownerClient.governance.execute(actions.toString()); - } - - BigInteger hexToBigInteger(String hex) { - return new BigInteger(hex.replace("0x", ""), 16); - } -} diff --git a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/NonStakedLPRewardsTest.java b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/NonStakedLPRewardsTest.java deleted file mode 100644 index 6167fc600..000000000 --- a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/NonStakedLPRewardsTest.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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. - */ - -package network.balanced.score.core.dex; - -import foundation.icon.icx.Wallet; -import foundation.icon.jsonrpc.Address; -import foundation.icon.jsonrpc.model.TransactionResult; -import foundation.icon.score.client.DefaultScoreClient; -import network.balanced.score.lib.interfaces.*; -import network.balanced.score.lib.test.integration.Balanced; -import network.balanced.score.lib.test.integration.Env; -import network.balanced.score.lib.test.integration.ScoreIntegrationTest; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.math.BigInteger; -import java.util.function.Consumer; - -import static network.balanced.score.lib.utils.Constants.EXA; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class NonStakedLPRewardsTest { - - private static StakingScoreClient staking; - private static LoansScoreClient loans; - private static RewardsScoreClient rewards; - private static BalancedTokenScoreClient baln; - - static Env.Chain chain = Env.getDefaultChain(); - private static Balanced balanced; - private static Wallet userWallet; - private static DefaultScoreClient dexScoreClient; - private static DefaultScoreClient governanceScoreClient; - private static DefaultScoreClient sIcxScoreClient; - private static DefaultScoreClient balnScoreClient; - private static DefaultScoreClient rewardsScoreClient; - - - private static DefaultScoreClient daoFund; - static File jarfile = new File("src/intTest/java/network/balanced/score/core/dex/testtokens/DexIntTestToken.jar"); - - static { - try { - balanced = new Balanced(); - Wallet testOwnerWallet = balanced.owner; - userWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(800).multiply(EXA)); - Wallet tUserWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(500).multiply(EXA)); - balanced.setupBalanced(); - dexScoreClient = balanced.dex; - governanceScoreClient = balanced.governance; - DefaultScoreClient stakingScoreClient = balanced.staking; - sIcxScoreClient = balanced.sicx; - DefaultScoreClient dividendScoreClient = balanced.dividends; - balnScoreClient = balanced.baln; - rewardsScoreClient = balanced.rewards; - DefaultScoreClient feeHandlerScoreClient = balanced.feehandler; - daoFund = balanced.daofund; - staking = new StakingScoreClient(stakingScoreClient); - rewards = new RewardsScoreClient(rewardsScoreClient); - loans = new LoansScoreClient(balanced.loans); - baln = new BalancedTokenScoreClient(balnScoreClient); - Sicx sicx = new SicxScoreClient(sIcxScoreClient); - StakedLP stakedLp = new StakedLPScoreClient(balanced.stakedLp); - - } catch (Exception e) { - e.printStackTrace(); - System.out.println("Error on init test: " + e.getMessage()); - } - - } - - private static final Address userAddress = Address.of(userWallet); - - private static final DexScoreClient dexUserScoreClient = new DexScoreClient(dexScoreClient.endpoint(), - dexScoreClient._nid(), userWallet, dexScoreClient._address()); - private static final SicxScoreClient userSicxScoreClient = new SicxScoreClient(dexScoreClient.endpoint(), - dexScoreClient._nid(), userWallet, sIcxScoreClient._address()); - private static final RewardsScoreClient userWalletRewardsClient = - new RewardsScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, - rewardsScoreClient._address()); - private static final BalancedTokenScoreClient userBalnScoreClient = - new BalancedTokenScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, - balnScoreClient._address()); - private static final GovernanceScoreClient governanceDexScoreClient = - new GovernanceScoreClient(governanceScoreClient); - private static final DAOfundScoreClient userDaoFundScoreClient = new DAOfundScoreClient(daoFund); - - - @Test - void testNonStakedLpRewards() { - balanced.syncDistributions(); - - byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); - staking.stakeICX(BigInteger.valueOf(100).multiply(BigInteger.TEN.pow(18)), userAddress - , null); - - BigInteger loanAmount = BigInteger.valueOf(150).multiply(BigInteger.TEN.pow(18)); - BigInteger collateral = BigInteger.valueOf(1000).multiply(BigInteger.TEN.pow(18)); - - - loans.depositAndBorrow(collateral, "bnUSD", loanAmount, null, null); - - waitForADay(); - balanced.syncDistributions(); - rewards.claimRewards(null); - - baln.transfer(userAddress, loanAmount, null); - - // deposit base token - userSicxScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(100).multiply(EXA), tokenDeposit); - //deposit quote token - userBalnScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(100).multiply(EXA), tokenDeposit); - dexUserScoreClient.add(balanced.baln._address(), balanced.sicx._address(), - BigInteger.valueOf(100).multiply(EXA), BigInteger.valueOf(100).multiply(EXA), false, BigInteger.valueOf(100)); - - waitForADay(); - balanced.syncDistributions(); - userWalletRewardsClient.claimRewards(null); - - balanced.syncDistributions(); - userWalletRewardsClient.claimRewards(null); - waitForADay(); - - // next day starts - Consumer distributeConsumer = result -> {}; - for (int i = 0; i < 10; i++) { - balanced.ownerClient.rewards.distribute(distributeConsumer); - } - waitForADay(); - - // next day starts - for (int i = 0; i < 10; i++) { - balanced.ownerClient.rewards.distribute(distributeConsumer); - } - // users without staking LP tokens will get 0 rewards - assertEquals(BigInteger.ZERO, rewards.getBalnHolding(userAddress.toString())); - - byte[] stakeLp = "{\"method\":\"_stake\"}".getBytes(); - dexUserScoreClient.transfer(balanced.stakedLp._address(), BigInteger.valueOf(90), BigInteger.valueOf(4), - stakeLp); - - // user gets rewards after lp token is staked - assertTrue(rewards.getBalnHolding(userAddress.toString()).compareTo(BigInteger.ZERO) > 0); - BigInteger previousUserBalance = baln.balanceOf(userAddress); - userWalletRewardsClient.claimRewards(null); - BigInteger newBalance = baln.balanceOf(userAddress); - assertTrue(newBalance.compareTo(previousUserBalance) > 0); - } - - void waitForADay() { - balanced.increaseDay(1); - } -} diff --git a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/SwapRemoveAndFeeTest.java b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/SwapRemoveAndFeeTest.java deleted file mode 100644 index 36c2d5f3e..000000000 --- a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/SwapRemoveAndFeeTest.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * 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. - */ - -package network.balanced.score.core.dex; - -import com.eclipsesource.json.JsonArray; -import foundation.icon.icx.Wallet; -import foundation.icon.jsonrpc.Address; -import foundation.icon.score.client.DefaultScoreClient; -import network.balanced.score.lib.interfaces.Dex; -import network.balanced.score.lib.interfaces.DexScoreClient; -import network.balanced.score.lib.interfaces.GovernanceScoreClient; -import network.balanced.score.lib.interfaces.dex.DexTestScoreClient; -import network.balanced.score.lib.test.integration.Balanced; -import network.balanced.score.lib.test.integration.Env; -import network.balanced.score.lib.test.integration.ScoreIntegrationTest; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; - -import java.io.File; -import java.math.BigInteger; -import java.util.Map; - -import static foundation.icon.score.client.DefaultScoreClient._deploy; -import static network.balanced.score.lib.test.integration.BalancedUtils.createParameter; -import static network.balanced.score.lib.test.integration.BalancedUtils.createTransaction; -import static network.balanced.score.lib.utils.Constants.EXA; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -public class SwapRemoveAndFeeTest { - - private static final Env.Chain chain = Env.getDefaultChain(); - private static Balanced balanced; - private static Wallet userWallet; - private static Wallet testOwnerWallet; - private static DefaultScoreClient dexScoreClient; - private static DefaultScoreClient governanceScoreClient; - private static DefaultScoreClient feeHandlerScoreClient; - private static DefaultScoreClient dexIntTestScoreClient; - private static DefaultScoreClient dexTestBaseScoreClient; - private static DefaultScoreClient dexTestThirdScoreClient; - - private static final File jarfile = new File("src/intTest/java/network/balanced/score/core/dex/testtokens" + - "/DexIntTestToken.jar"); - - static { - try { - balanced = new Balanced(); - testOwnerWallet = balanced.owner; - userWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(800).multiply(EXA)); - Wallet tUserWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(500).multiply(EXA)); - dexIntTestScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, - jarfile.getPath(), Map.of("name", "Test Token", "symbol", "TT")); - dexTestBaseScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, - jarfile.getPath(), Map.of("name", "Test Base Token", "symbol", "TB")); - dexTestThirdScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, - jarfile.getPath(), Map.of("name", "Test Third Token", "symbol", "TTD")); - balanced.setupBalanced(); - dexScoreClient = balanced.dex; - governanceScoreClient = balanced.governance; - feeHandlerScoreClient = balanced.feehandler; - - } catch (Exception e) { - e.printStackTrace(); - System.out.println("Error on init test: " + e.getMessage()); - } - - } - - private static final String dexTestScoreAddress = dexIntTestScoreClient._address().toString(); - private static final String dexTestBaseScoreAddress = dexTestBaseScoreClient._address().toString(); - private static final String dexTestThirdScoreAddress = dexTestThirdScoreClient._address().toString(); - - private static final Address userAddress = Address.of(userWallet); - - private static final DexTestScoreClient ownerDexTestScoreClient = new DexTestScoreClient(chain.getEndpointURL(), - chain.networkId, testOwnerWallet, DefaultScoreClient.address(dexTestScoreAddress)); - private static final DexTestScoreClient ownerDexTestBaseScoreClient = new DexTestScoreClient(chain.getEndpointURL(), - chain.networkId, testOwnerWallet, DefaultScoreClient.address(dexTestBaseScoreAddress)); - private static final DexTestScoreClient ownerDexTestThirdScoreClient = - new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, - DefaultScoreClient.address(dexTestThirdScoreAddress)); - private static final Dex dexUserScoreClient = new DexScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), - userWallet, dexScoreClient._address()); - - private static final DexTestScoreClient userDexTestScoreClient = new DexTestScoreClient(dexScoreClient.endpoint(), - dexScoreClient._nid(), userWallet, DefaultScoreClient.address(dexTestScoreAddress)); - private static final DexTestScoreClient userDexTestBaseScoreClient = - new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, - DefaultScoreClient.address(dexTestBaseScoreAddress)); - private static final DexTestScoreClient userDexTestThirdScoreClient = - new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, - DefaultScoreClient.address(dexTestThirdScoreAddress)); - private static final GovernanceScoreClient governanceDexScoreClient = - new GovernanceScoreClient(governanceScoreClient); - - @Test - @Order(5) - void testSwapTokensVerifySendsFeeAndRemove() { - //check balance of fee handler in from token - BigInteger feeBalanceOfTestToken = userDexTestScoreClient.balanceOf(feeHandlerScoreClient._address()); - byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); - this.mintAndTransferTestTokens(tokenDeposit); - - dexUserScoreClient.add(Address.fromString(dexTestBaseScoreAddress), Address.fromString(dexTestScoreAddress), - BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(50).multiply(EXA), true, BigInteger.valueOf(100)); - - BigInteger poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestBaseScoreAddress), - Address.fromString(dexTestScoreAddress)); - assertNotNull(poolId); - //governanceDexScoreClient.disable_fee_handler(); - String swapString = "{\"method\":\"_swap\",\"params\":{\"toToken\":\"" + dexTestBaseScoreAddress + "\"}}"; - userDexTestScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(100).multiply(EXA), - swapString.getBytes()); - Map poolStats = dexUserScoreClient.getPoolStats(poolId); - assertEquals(poolStats.get("base_token").toString(), dexTestBaseScoreAddress); - assertEquals(poolStats.get("quote_token").toString(), dexTestScoreAddress); - assertEquals(hexToBigInteger(poolStats.get("base").toString()).divide(EXA), BigInteger.valueOf(16)); - assertEquals(hexToBigInteger(poolStats.get("quote").toString()).divide(EXA), BigInteger.valueOf(149)); - assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()).divide(EXA), BigInteger.valueOf(50)); - assertEquals(hexToBigInteger(poolStats.get("price").toString()).divide(EXA), BigInteger.valueOf(8)); - assertEquals(hexToBigInteger(poolStats.get("base_decimals").toString()), BigInteger.valueOf(18)); - assertEquals(hexToBigInteger(poolStats.get("quote_decimals").toString()), BigInteger.valueOf(18)); - assertEquals(hexToBigInteger(poolStats.get("min_quote").toString()), BigInteger.ZERO); - BigInteger updatedFeeBalanceOfTestToken = userDexTestScoreClient.balanceOf(feeHandlerScoreClient._address()); - assert updatedFeeBalanceOfTestToken.compareTo(feeBalanceOfTestToken) > 0; - assertEquals(BigInteger.valueOf(150).multiply(EXA).divide(BigInteger.valueOf(1000)), - updatedFeeBalanceOfTestToken); - - waitForADay(); - balanced.syncDistributions(); - BigInteger withdrawAmount = BigInteger.valueOf(5); - BigInteger balanceBefore = dexUserScoreClient.balanceOf(userAddress, poolId); - dexUserScoreClient.remove(poolId, BigInteger.valueOf(5), true); - BigInteger balanceAfter = dexUserScoreClient.balanceOf(userAddress, poolId); - assert balanceAfter.equals(balanceBefore.subtract(withdrawAmount)); - } - - void mintAndTransferTestTokens(byte[] tokenDeposit) { - - ownerDexTestScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); - ownerDexTestBaseScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); - ownerDexTestThirdScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); - - - //deposit base token - userDexTestBaseScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), - tokenDeposit); - //deposit quote token - userDexTestScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), tokenDeposit); - userDexTestThirdScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), - tokenDeposit); - - //check isQuoteCoinAllowed for test token if not added - if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestScoreAddress))) { - dexAddQuoteCoin(new Address(dexTestScoreAddress)); - } - if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestBaseScoreAddress))) { - dexAddQuoteCoin(new Address(dexTestBaseScoreAddress)); - } - if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestThirdScoreAddress))) { - dexAddQuoteCoin(new Address(dexTestThirdScoreAddress)); - } - } - - void dexAddQuoteCoin(Address address) { - JsonArray addQuoteCoinParameters = new JsonArray() - .add(createParameter(address)); - - JsonArray actions = new JsonArray() - .add(createTransaction(balanced.dex._address(), "addQuoteCoin", addQuoteCoinParameters)); - - balanced.ownerClient.governance.execute(actions.toString()); - } - - void waitForADay() { - balanced.increaseDay(1); - } - - BigInteger hexToBigInteger(String hex) { - return new BigInteger(hex.replace("0x", ""), 16); - } -} 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 de679b5c11ba40aea2d2e06117d47de7758c4d2a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3689 zcmai1XHZky77m02P>OVasR1-dFCvH_y(t|+ z1Pn!bO#lJuNV_8N!u@eoX5PE&%%0h2=6q|Nwf6dEf9p0JNW%`GqoV@=0D_eD)7Sxw z0ED`ciX==|Lkgjy3)4_HG=d^DjEz75s^iZNKLP;X5M7-*a7kz{BU}>lsf$=X`ILmWu(n3K z#rplA`hG=In>>|`Sa!?r3UyBv)L1<^yL(TiA|M8JQ5HPcnB?hWo=`7d7umac{?s}& zIdWNBm{`3%Ov^)ajc9tY<{D!4`7|3k2piT^S%~xN84vn|bh;D+#kRW7i`lQxt3Ki^ zhu7q84CK#2+sMy(@J%yL4H*1_@` zLjpk|`trl-)#)dhTP051t!{Yc0FqR}hZ)}$=b#6H)nBLZrO<8UpP^i>c{{suu4g|J z1FKV;&>O(xdM+~Nm@!b!!k_y8)svP|&s)%YPIo;#{;A|2+Yg3rg27-9u(bwQL);sz zael7jQ)5?0BUiiyk~Ka(H97BjqFGW$N247U_uL$f@5m@30a<&AurBxzGgRVZ7rd&S zy_eyhP0xAl+&nvOW*{$+mJ4_qC@u`dcm!!fgn_26K@R%v1(tTYkVis5w%bfJA)kN{ zP%So;w}TB7DGPMam2~E(%3NVU@@7W*hKR5MFaF&p=TJ3?$&?d@Mn62K{=tMMZbH{uVTW(LE7D~Y$dHv)oYBe(Az#`SZ4 zh?wxs7F9nC=}cV_YUx|A{d_0kB|>6!g)AnjlfXyZxN=sa+cr1tk&#~0}Ehh%x_mfL#`=(f3889Dd!!2TD# zam|IH^nc1cmd2kE(C&@3qZ@0se<35M>glhE&myt4P`k9c@ndG6^zG;>3g)NT*%e*| zyO5LOEyU_8$uAY8`tscir=x|Cwp!8Ynk_u4Y8(-Q6Hn7!kDP$R6hL2{kAXgU_p=Ic4 z`pce>rT)XY)v6AD{svB@yz2cFiFbE=oMQGF+g98x%bCMVoLp!60*+dlfdOl{HwjdXwvzO$N*d z*;gq{`UdcLL_ce`rqVVIiPmpZxT)D{dosC6!kEgN%#n% zcaDiyFMwE`OH}FPxvzM=r(wu+$^MM0bG{zmzS-LM40rb0i`CQeJyENoo_Ug<%=G$B ztVu38xd*TSXCkkpw+)VXa1$oC+a0)@=1=tV+1d%V4&-m#-*`YJQ>09%_UX^gQ4~xL z^T@D9?%|~I^n8m9NF|gXD~@N1x*r`U5bJ^@(l6g2%}QDd+%UN?Tst$4A9%0q6X<18 zhS#jA^vPBXKr6xrD;Udsr}S%z8;hZ?n!Hn`U!V-pJZ-T40ZgscxAgfcEifBbT z>m(+s3UIr*#C)Lv-M+EsP?{*NIAcDl)WrRk*6H!_qHCv^UJvV!$t z(35mI;zrBrxFz#gEUoR!>p<7d;ZZc9b%*KbuSNx*zp$8he}H%kLOfz$6)}P??g9Mo ze73r!C!$wg@SNplcpQ|kO~wG;w(9susKyW0#@*hcbERn^`q?CQm=7r%q;xKpBo6w{ z8};mEt+gw;h=aJeA=)W}yoVF=)lx3m#zVpoQM+PxV}QHwpKb`6uO}$iI^O{IXuEYr zKQAn+<@e!|mkV0>bmnIBLZ?N3mfnU@D=V|uSSM1QGq0JuQ4>VnW#O`7Sw>y%6VAG84E|5_2uur7p}? zceqmBc&_C_M;U8u-eY{xD?JyOQ)t?_|7hpKzM1xs2<)=ann^`k6I$HSRx6PeSBl^E zdu!lsY14x$OMR$g7C$L^zoy7(Wa=Oy30Ep=?UkLzA-x@UIlqe0uI4mH$~L&)J1m(` zXTRz>5xdDI`yS!St*|k-zzm*!glMMGryX?NL`h(!`{&5=Wp?ZQS%(R*dQPK$`F@+Wi!mB zcH?@YU_ffc^+dkTZftVs+YqPR;s<2&h*e9-EweaWeT!Tc$0IzK z`#W2c2*n8(jByJ7n{02)UYHEuUz4#t#y`~32p)S9`?KBOXKV!X=D1Y%uEoI8k{W?4 z`QyIJ^OgCnTaVv@Ibg;_V$LzUXvrZvm_}C0mwtDCe?#`oCEi!O&7zgbvV2xTKI%Mv&wP zCf_Pr!(P?xv6lP=#ZB)8*)YRm)D+L&6zTf8_}zR@4*bx126MSVzD~>6;B(s3>NG!k z+1~8A2ItH_2@8zB9LT(*R1@hy+_gos`o3(Cp*g~+)3EQcFOxI@!f1SWcCO}BcMBkZ$amYQ$Fxcca#Hy65)p@(5xwLM+^RKcEVTYbs+?ClI0 zYaG)=H|tdExf*pBNTnCNqBggrr+o!A;haITlb`S|V_M|Q93x>a6q978H1u>kYweW? zScSE$dDl^o!10FFuh^NQ)lp5;2Ae65?m|+3P%&P1+J1ecb_nF1`DiH5WTP!pIgI6v zZvkOcERBeK}`GBh~5ay(#j1ycV~8_uG&L|e{;)XY1w2%n@4`_C5K9^C{9(uqk| zd^)kXAa&hZ71b_q+Qyz5x2z-0*;PayiYau=VE*_=HLR(IrB_Z!R`^t!Kdr-|K^@61 z3QqS>LHAB8{Mo_2*g>#J^B@}fS;Xh6ahp1Q*{dO5RGv+W&Q>DCB*L2?K}RLgz(Nq4 z4^Z4^LF@Z=t5I7k^qQI@k_X;pMFU4NQEKu9d|$&s_xRuVerQ*wd+@H=BEkRtsSwNT zddT76>uFYZQ#Ti}J`YJx(2x`<*GpPu&xo1OaHBo(i0b&lrm9+eJqHE=B$ z`5m1}(JUu*@JaNqYT|b^7YG12sb@~2e=YRy=v)f=r2e=K2T=|M002`S3?;J;hU2&Y E0)w>;IRF3v diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/AbstractDex.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/AbstractDex.java deleted file mode 100644 index 44a27835a..000000000 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/AbstractDex.java +++ /dev/null @@ -1,820 +0,0 @@ -/* - * Copyright (c) 2022-2023 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 network.balanced.score.core.dex.db.NodeDB; -import network.balanced.score.lib.interfaces.Dex; -import network.balanced.score.lib.structs.PrepDelegations; -import network.balanced.score.lib.structs.RewardsDataEntry; -import network.balanced.score.lib.utils.BalancedFloorLimits; -import network.balanced.score.lib.utils.FloorLimited; -import score.Address; -import score.BranchDB; -import score.Context; -import score.DictDB; -import score.annotation.EventLog; -import score.annotation.External; -import scorex.util.ArrayList; -import scorex.util.HashMap; - -import java.math.BigInteger; -import java.util.List; -import java.util.Map; - -import static network.balanced.score.core.dex.DexDBVariables.*; -import static network.balanced.score.core.dex.utils.Check.isValidPercent; -import static network.balanced.score.core.dex.utils.Check.isValidPoolId; -import static network.balanced.score.core.dex.utils.Const.*; -import static network.balanced.score.lib.utils.BalancedAddressManager.*; -import static network.balanced.score.lib.utils.Check.onlyGovernance; -import static network.balanced.score.lib.utils.Constants.*; -import static network.balanced.score.lib.utils.Math.pow; - -public abstract class AbstractDex extends FloorLimited implements Dex { - - public AbstractDex(Address _governance) { - if (governance.get() == null) { - governance.set(_governance); - - // Set Default Fee Rates - poolLpFee.set(BigInteger.valueOf(15L)); - poolBalnFee.set(BigInteger.valueOf(15L)); - icxConversionFee.set(BigInteger.valueOf(70L)); - icxBalnFee.set(BigInteger.valueOf(30L)); - - nonce.set(2); - currentDay.set(BigInteger.valueOf(1L)); - namedMarkets.set(SICXICX_MARKET_NAME, SICXICX_POOL_ID); - marketsToNames.set(SICXICX_POOL_ID, SICXICX_MARKET_NAME); - dexOn.set(true); - } - setGovernance(governance.get()); - } - - @EventLog(indexed = 2) - public void Swap(BigInteger _id, Address _baseToken, Address _fromToken, Address _toToken, - Address _sender, Address _receiver, BigInteger _fromValue, BigInteger _toValue, - BigInteger _timestamp, BigInteger _lpFees, BigInteger _balnFees, BigInteger _poolBase, - BigInteger _poolQuote, BigInteger _endingPrice, BigInteger _effectiveFillPrice) { - } - - @EventLog(indexed = 3) - public void MarketAdded(BigInteger _id, Address _baseToken, - Address _quoteToken, BigInteger _baseValue, BigInteger _quoteValue) { - } - - @EventLog(indexed = 3) - public void Add(BigInteger _id, Address _owner, BigInteger _value, BigInteger _base, BigInteger _quote) { - } - - @EventLog(indexed = 3) - public void Remove(BigInteger _id, Address _owner, BigInteger _value, BigInteger _base, BigInteger _quote) { - } - - @EventLog(indexed = 2) - public void Deposit(Address _token, Address _owner, BigInteger _value) { - } - - @EventLog(indexed = 2) - public void Withdraw(Address _token, Address _owner, BigInteger _value) { - } - - @EventLog(indexed = 2) - public void ClaimSicxEarnings(Address _owner, BigInteger _value) { - } - - @EventLog(indexed = 3) - public void TransferSingle(Address _operator, Address _from, Address _to, BigInteger _id, BigInteger _value) { - } - - @External(readonly = true) - public String name() { - return TAG; - } - - - @External - public void updateAddress(String name) { - resetAddress(name); - } - - @External(readonly = true) - public Address getAddress(String name) { - return getAddressByName(name); - } - - @External - public void turnDexOn() { - onlyGovernance(); - dexOn.set(true); - } - - @External(readonly = true) - public boolean getDexOn() { - return dexOn.get(); - } - - @External - public void setPoolLpFee(BigInteger _value) { - onlyGovernance(); - poolLpFee.set(_value); - } - - @External - public void setPoolBalnFee(BigInteger _value) { - onlyGovernance(); - poolBalnFee.set(_value); - } - - @External - public void setIcxConversionFee(BigInteger _value) { - onlyGovernance(); - icxConversionFee.set(_value); - } - - @External - public void setIcxBalnFee(BigInteger _value) { - onlyGovernance(); - icxBalnFee.set(_value); - } - - @External(readonly = true) - public Map getFees() { - BigInteger icxBalnFees = icxBalnFee.get(); - BigInteger icxConversionFees = icxConversionFee.get(); - BigInteger poolBalnFees = poolBalnFee.get(); - BigInteger poolLpFees = poolLpFee.get(); - Map feesMapping = new HashMap<>(); - feesMapping.put("icx_total", icxBalnFees.add(icxConversionFees)); - feesMapping.put("pool_total", poolBalnFees.add(poolLpFees)); - feesMapping.put("pool_lp_fee", poolLpFees); - feesMapping.put("pool_baln_fee", poolBalnFees); - feesMapping.put("icx_conversion_fee", icxConversionFees); - feesMapping.put("icx_baln_fee", icxBalnFees); - return feesMapping; - } - - @External - public void setMarketName(BigInteger _id, String _name) { - onlyGovernance(); - namedMarkets.set(_name, _id.intValue()); - marketsToNames.set(_id.intValue(), _name); - } - - @External - public void setOracleProtection(BigInteger pid, BigInteger percentage) { - onlyGovernance(); - isValidPoolId(pid); - isValidPercent(percentage.intValue()); - validateTokenOraclePrice(poolBase.get(pid.intValue())); - validateTokenOraclePrice(poolQuote.get(pid.intValue())); - - oracleProtection.set(pid, percentage); - } - - private void validateTokenOraclePrice(Address token) { - BigInteger price = getOraclePrice(token); - Context.require(price != null && !price.equals(BigInteger.ZERO), - "Token must be supported by the balanced Oracle"); - } - - private BigInteger getOraclePrice(Address token) { - String symbol = (String) Context.call(token, "symbol"); - return (BigInteger) Context.call(getBalancedOracle(), "getPriceInUSD", symbol); - } - - protected void oracleProtection(Integer pid, BigInteger priceBase) { - BigInteger poolId = BigInteger.valueOf(pid); - BigInteger oracleProtectionPercentage = oracleProtection.get(poolId); - if (oracleProtectionPercentage == null || oracleProtectionPercentage.signum() == 0) { - return; - } - - Address quoteToken = poolQuote.get(pid); - Address baseToken = poolBase.get(pid); - BigInteger oraclePriceQuote = getOraclePrice(quoteToken); - BigInteger oraclePriceBase = getOraclePrice(baseToken); - - BigInteger oraclePriceBaseRatio = oraclePriceBase.multiply(EXA).divide(oraclePriceQuote); - BigInteger oracleProtectionExa = oraclePriceBaseRatio.multiply(oracleProtectionPercentage).divide(POINTS); - - Context.require(priceBase.compareTo(oraclePriceBaseRatio.add(oracleProtectionExa)) <= 0 && priceBase.compareTo(oraclePriceBaseRatio.subtract(oracleProtectionExa)) >= 0, TAG + ": oracle protection price violated"); - } - - @External(readonly = true) - public String getPoolName(BigInteger _id) { - return marketsToNames.get(_id.intValue()); - } - - @External - public void addQuoteCoin(Address _address) { - onlyGovernance(); - quoteCoins.add(_address); - } - - @External(readonly = true) - public boolean isQuoteCoinAllowed(Address _address) { - return quoteCoins.contains(_address); - } - - @External(readonly = true) - public BigInteger getDay() { - BigInteger blockTime = BigInteger.valueOf(Context.getBlockTimestamp()); - BigInteger timeDelta = blockTime.subtract(timeOffset.get()); - return timeDelta.divide(MICRO_SECONDS_IN_A_DAY); - } - - @External - public void setTimeOffset(BigInteger _delta_time) { - onlyGovernance(); - timeOffset.set(_delta_time); - } - - @External(readonly = true) - public BigInteger getTimeOffset() { - return timeOffset.get(); - } - - @External - public boolean precompute(BigInteger snap, BigInteger batch_size) { - return true; - } - - @External(readonly = true) - public BigInteger getDeposit(Address _tokenAddress, Address _user) { - return deposit.at(_tokenAddress).getOrDefault(_user, BigInteger.ZERO); - } - - @External(readonly = true) - public BigInteger getSicxEarnings(Address _user) { - return sicxEarnings.getOrDefault(_user, BigInteger.ZERO); - } - - @External(readonly = true) - public BigInteger getPoolId(Address _token1Address, Address _token2Address) { - return BigInteger.valueOf(poolId.at(_token1Address).get(_token2Address)); - } - - @External(readonly = true) - public BigInteger getNonce() { - return BigInteger.valueOf(nonce.getOrDefault(0)); - } - - @External(readonly = true) - public List getNamedPools() { - List namedPools = new ArrayList<>(); - namedPools.addAll(namedMarkets.keys()); - return namedPools; - } - - @External(readonly = true) - public BigInteger lookupPid(String _name) { - return BigInteger.valueOf(namedMarkets.get(_name)); - } - - @External(readonly = true) - public BigInteger getPoolTotal(BigInteger _id, Address _token) { - return poolTotal.at(_id.intValue()).getOrDefault(_token, BigInteger.ZERO); - } - - @External(readonly = true) - public Address getPoolBase(BigInteger _id) { - return poolBase.get(_id.intValue()); - } - - @External(readonly = true) - public Address getPoolQuote(BigInteger _id) { - return poolQuote.get(_id.intValue()); - } - - @External(readonly = true) - public BigInteger getOracleProtection(BigInteger pid) { - return oracleProtection.get(pid); - } - - @External(readonly = true) - public BigInteger getQuotePriceInBase(BigInteger _id) { - isValidPoolId(_id); - - if (_id.intValue() == SICXICX_POOL_ID) { - return ((EXA.multiply(EXA)).divide(getSicxRate())); - } - - return priceOfAInB(_id.intValue(), poolQuote, poolBase); - } - - @External(readonly = true) - public BigInteger getBasePriceInQuote(BigInteger _id) { - isValidPoolId(_id); - - if (_id.intValue() == SICXICX_POOL_ID) { - return getSicxRate(); - } - - return priceOfAInB(_id.intValue(), poolBase, poolQuote); - } - - private BigInteger priceOfAInB(Integer id, DictDB tokenA, DictDB tokenB) { - Address ATokenAddress = tokenA.get(id); - Address BTokenAddress = tokenB.get(id); - - DictDB totalTokensInPool = poolTotal.at(id); - BigInteger ATokenTotal = totalTokensInPool.get(ATokenAddress); - BigInteger BTokenTotal = totalTokensInPool.get(BTokenAddress); - - return BTokenTotal.multiply(EXA).divide(ATokenTotal); - } - - @External(readonly = true) - public BigInteger getBalnPrice() { - return getBasePriceInQuote(BigInteger.valueOf(poolId.at(getBaln()).get(getBnusd()))); - } - - @External(readonly = true) - public BigInteger getSicxBnusdPrice() { - return getBasePriceInQuote(BigInteger.valueOf(poolId.at(getSicx()).get(getBnusd()))); - } - - @External(readonly = true) - public BigInteger getBnusdValue(String _name) { - // Should eventually only handle sICX/ICX - Integer _id = namedMarkets.get(_name); - return getLPBnusdValue(_id); - } - - @External(readonly = true) - public BigInteger getLPBnusdValue(int _id) { - if (_id == SICXICX_POOL_ID) { - BigInteger icxTotal = icxQueueTotal.getOrDefault(BigInteger.ZERO); - return (icxTotal.multiply(getSicxBnusdPrice())).divide(getSicxRate()); - } - - Address poolQuoteToken = poolQuote.get(_id); - Address sicxAddress = getSicx(); - Address bnusdAddress = getBnusd(); - - if (poolQuoteToken.equals(sicxAddress)) { - BigInteger sicxTotal = poolTotal.at(_id).get(sicxAddress).multiply(BigInteger.TWO); - return getSicxBnusdPrice().multiply(sicxTotal).divide(EXA); - } else if (poolQuoteToken.equals(bnusdAddress)) { - return poolTotal.at(_id).get(bnusdAddress).multiply(BigInteger.TWO); - } - - return BigInteger.ZERO; - } - - @External(readonly = true) - public BigInteger getPrice(BigInteger _id) { - return this.getBasePriceInQuote(_id); - } - - @External(readonly = true) - public BigInteger getPriceByName(String _name) { - return getPrice(BigInteger.valueOf(namedMarkets.get(_name))); - } - - @External(readonly = true) - public BigInteger getICXBalance(Address _address) { - BigInteger orderId = icxQueueOrderId.get(_address); - if (orderId == null) { - return BigInteger.ZERO; - } - return icxQueue.getNode(orderId).getSize(); - } - - @External(readonly = true) - public Map getPoolStats(BigInteger _id) { - isValidPoolId(_id); - Map poolStats = new HashMap<>(); - if (_id.intValue() == SICXICX_POOL_ID) { - poolStats.put("base_token", getSicx()); - poolStats.put("quote_token", null); - poolStats.put("base", BigInteger.ZERO); - poolStats.put("quote", icxQueueTotal.getOrDefault(BigInteger.ZERO)); - poolStats.put("total_supply", icxQueueTotal.getOrDefault(BigInteger.ZERO)); - poolStats.put("price", getPrice(_id)); - poolStats.put("name", SICXICX_MARKET_NAME); - poolStats.put("base_decimals", 18); - poolStats.put("quote_decimals", 18); - poolStats.put("min_quote", getRewardableAmount(null)); - } else { - Address baseToken = poolBase.get(_id.intValue()); - Address quoteToken = poolQuote.get(_id.intValue()); - String name = marketsToNames.get(_id.intValue()); - DictDB totalTokensInPool = poolTotal.at(_id.intValue()); - - poolStats.put("base", totalTokensInPool.get(baseToken)); - poolStats.put("quote", totalTokensInPool.get(quoteToken)); - poolStats.put("base_token", baseToken); - poolStats.put("quote_token", quoteToken); - poolStats.put("total_supply", poolLpTotal.get(_id.intValue())); - poolStats.put("price", getPrice(_id)); - poolStats.put("name", name); - poolStats.put("base_decimals", tokenPrecisions.get(baseToken)); - poolStats.put("quote_decimals", tokenPrecisions.get(quoteToken)); - poolStats.put("min_quote", getRewardableAmount(quoteToken)); - } - return poolStats; - } - - @External(readonly = true) - public Map getPoolStatsForPair(Address _base, Address _quote) { - BigInteger poolId = getPoolId(_base, _quote); - Map poolStats = getPoolStats(poolId); - Map poolStatsWithId = new HashMap<>(); - poolStatsWithId.put("id", poolId); - poolStatsWithId.putAll(poolStats); - - return poolStatsWithId; - } - - @External(readonly = true) - public BigInteger totalDexAddresses(BigInteger _id) { - return BigInteger.valueOf(activeAddresses.get(_id.intValue()).length()); - } - - @External(readonly = true) - public Map getBalanceAndSupply(String _name, String _owner) { - Address owner = Address.fromString(_owner); - if (_name.equals(SICXICX_MARKET_NAME)) { - Map rewardsData = new HashMap<>(); - rewardsData.put("_balance", balanceOf(owner, BigInteger.valueOf(SICXICX_POOL_ID))); - rewardsData.put("_totalSupply", totalSupply(BigInteger.valueOf(SICXICX_POOL_ID))); - return rewardsData; - } - BigInteger poolId = lookupPid(_name); - Context.require(poolId != null, TAG + ": Unsupported data source name"); - - Address stakedLpAddress = getStakedLp(); - BigInteger totalSupply = (BigInteger) Context.call(stakedLpAddress, "totalStaked", poolId); - BigInteger balance = (BigInteger) Context.call(stakedLpAddress, "balanceOf", owner, poolId); - Map rewardsData = new HashMap<>(); - rewardsData.put("_balance", balance); - rewardsData.put("_totalSupply", totalSupply); - return rewardsData; - } - - @External(readonly = true) - public BigInteger getTotalValue(String _name, BigInteger _snapshot_id) { - return totalSupply(BigInteger.valueOf(namedMarkets.get(_name))); - } - - @External - public void permit(BigInteger _id, boolean _permission) { - onlyGovernance(); - active.set(_id.intValue(), _permission); - } - - @External - public void addLpAddresses(BigInteger _poolId, Address[] _addresses) { - onlyGovernance(); - for (Address address : _addresses) { - if (balanceOf(address, _poolId).compareTo(BigInteger.ZERO) > 0) { - activeAddresses.get(_poolId.intValue()).add(address); - } - } - } - - @External(readonly = true) - public BigInteger balanceOf(Address _owner, BigInteger _id) { - if (_id.intValue() == SICXICX_POOL_ID) { - return getICXBalance(_owner); - } else { - return DexDBVariables.balance.at(_id.intValue()).getOrDefault(_owner, BigInteger.ZERO); - } - } - - @External(readonly = true) - public BigInteger totalSupply(BigInteger _id) { - if (_id.intValue() == SICXICX_POOL_ID) { - return icxQueueTotal.getOrDefault(BigInteger.ZERO); - } - - return poolLpTotal.getOrDefault(_id.intValue(), BigInteger.ZERO); - } - - @External - public void delegate(PrepDelegations[] prepDelegations) { - onlyGovernance(); - Context.call(getStaking(), "delegate", (Object) prepDelegations); - } - - private static BigInteger getPriceInUSD(String symbol) { - return (BigInteger) Context.call(getBalancedOracle(), "getLastPriceInUSD", symbol); - } - - protected BigInteger getSicxRate() { - return (BigInteger) Context.call(getStaking(), "getTodayRate"); - } - - boolean isLockingPool(Integer id) { - return id.equals(SICXICX_POOL_ID); - } - - BigInteger getRewardableAmount(Address tokenAddress) { - if (tokenAddress == null) { - return BigInteger.TEN.multiply(EXA); - } else if (getSicx().equals(tokenAddress)) { - return (BigInteger.TEN.multiply(EXA.multiply(EXA))).divide(getSicxRate()); - } else if (getBnusd().equals(tokenAddress)) { - return BigInteger.TEN.multiply(EXA); - } - return BigInteger.ZERO; - } - - void deposit(Address token, Address to, BigInteger amount) { - DictDB depositDetails = deposit.at(token); - BigInteger userBalance = depositDetails.getOrDefault(to, BigInteger.ZERO); - userBalance = userBalance.add(amount); - depositDetails.set(to, userBalance); - Deposit(token, to, amount); - - if (tokenPrecisions.get(token) == null) { - BigInteger decimalValue = (BigInteger) Context.call(token, "decimals"); - tokenPrecisions.set(token, decimalValue); - } - } - - void exchange(Address fromToken, Address toToken, Address sender, - Address receiver, BigInteger value, BigInteger minimumReceive) { - - if (minimumReceive == null) { - minimumReceive = BigInteger.ZERO; - } - - int id = getPoolId(fromToken, toToken).intValue(); - isValidPoolId(id); - Context.require(id != SICXICX_POOL_ID, TAG + ": Not supported on this API, use the ICX swap API."); - Context.require(active.getOrDefault(id, false), TAG + ": Pool is not active"); - - BigInteger lpFees = value.multiply(poolLpFee.get()).divide(FEE_SCALE); - BigInteger balnFees = value.multiply(poolBalnFee.get()).divide(FEE_SCALE); - BigInteger initialBalnFees = balnFees; - BigInteger fees = lpFees.add(balnFees); - - Address poolBaseToken = poolBase.get(id); - boolean isSell = fromToken.equals(poolBaseToken); - Address poolQuoteToken = isSell ? toToken : fromToken; - - // We consider the trade in terms of toToken (token we are trading to), and fromToken (token we are trading - // away) in the pool. It must obey the xy=k constant product formula. - - DictDB totalTokensInPool = poolTotal.at(id); - BigInteger oldFromToken = totalTokensInPool.get(fromToken); - BigInteger oldToToken = totalTokensInPool.get(toToken); - - // We perturb the pool by the asset we are trading in less fees. - // Fees are credited to LPs at the end of the process. - BigInteger inputWithoutFees = value.subtract(fees); - BigInteger newFromToken = oldFromToken.add(inputWithoutFees); - - // Compute the new fromToken according to the constant product formula - BigInteger newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); - - // Send the trader the amount of toToken removed from the pool by the constant product formula - BigInteger sendAmount = oldToToken.subtract(newToToken); - - Context.require(sendAmount.compareTo(BigInteger.ZERO) > 0, TAG + ": Invalid output amount in trade."); - // Revert the transaction if the below slippage, as specified in _minimum_receive - Context.require(sendAmount.compareTo(minimumReceive) >= 0, - TAG + ": MinimumReceiveError: Receive amount " + sendAmount + " below supplied minimum"); - - // Apply fees to fromToken after computing constant product. lpFees are credited to the LPs, the rest are - // sent to BALN holders. - newFromToken = newFromToken.add(lpFees); - - if (isSell) { - oldFromToken = newFromToken; - oldToToken = newToToken; - - newFromToken = oldFromToken.add(balnFees); - newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); - - balnFees = oldToToken.subtract(newToToken); - } - - // Save updated pool totals - totalTokensInPool.set(fromToken, newFromToken); - totalTokensInPool.set(toToken, newToToken); - - // Capture details for event logs - BigInteger totalBase = isSell ? newFromToken : newToToken; - BigInteger totalQuote = isSell ? newToToken : newFromToken; - - BigInteger endingPrice = totalQuote.multiply(EXA).divide(totalBase); - oracleProtection(id, endingPrice); - - // Send the trader their funds - BalancedFloorLimits.verifyWithdraw(toToken, sendAmount); - Context.call(toToken, "transfer", receiver, sendAmount); - - // Send the platform fees to the fee handler SCORE - Context.call(poolQuoteToken, "transfer", getFeehandler(), balnFees); - - // Broadcast pool ending price - BigInteger effectiveFillPrice = (value.multiply(EXA)).divide(sendAmount); - - if (!isSell) { - effectiveFillPrice = (sendAmount.multiply(EXA)).divide(value); - } - - Swap(BigInteger.valueOf(id), poolBaseToken, fromToken, toToken, sender, receiver, value, sendAmount, - BigInteger.valueOf(Context.getBlockTimestamp()), lpFees, initialBalnFees, totalBase, totalQuote, - endingPrice, effectiveFillPrice); - } - - void donate(Address fromToken, Address toToken, BigInteger value) { - int id = getPoolId(fromToken, toToken).intValue(); - isValidPoolId(id); - Context.require(id != SICXICX_POOL_ID, TAG + ": Not supported on this API, use the ICX swap API."); - Context.require(active.getOrDefault(id, false), TAG + ": Pool is not active"); - - DictDB totalTokensInPool = poolTotal.at(id); - BigInteger oldFromToken = totalTokensInPool.get(fromToken); - - BigInteger newFromToken = oldFromToken.add(value); - - totalTokensInPool.set(fromToken, newFromToken); - } - - @External - public void govWithdraw(int id, Address token, BigInteger value) { - onlyGovernance(); - isValidPoolId(id); - Context.require(id != SICXICX_POOL_ID, TAG + ": Not supported on this API, use the ICX swap API."); - - DictDB totalTokensInPool = poolTotal.at(id); - BigInteger oldToken = totalTokensInPool.get(token); - - BigInteger newToken = oldToken.subtract(value); - - totalTokensInPool.set(token, newToken); - Context.call(token, "transfer", getDaofund(), value); - } - - @External - public void govSetPoolTotal(int pid, BigInteger total) { - onlyGovernance(); - poolLpTotal.set(pid, total); - } - - @External - public void govSetUserPoolTotal(int pid, Address user, BigInteger total) { - onlyGovernance(); - BigInteger value = balance.at(pid).get(user); - BigInteger burned = value.subtract(total); - balance.at(pid).set(user, total); - - TransferSingle(Context.getCaller(), user, MINT_ADDRESS, BigInteger.valueOf(pid), burned); - } - - void swapIcx(Address sender, BigInteger value) { - BigInteger sicxIcxPrice = getSicxRate(); - - BigInteger oldIcxTotal = icxQueueTotal.getOrDefault(BigInteger.ZERO); - List data = new ArrayList<>(); - - BigInteger balnFees = (value.multiply(icxBalnFee.get())).divide(FEE_SCALE); - BigInteger conversionFees = value.multiply(icxConversionFee.get()).divide(FEE_SCALE); - BigInteger orderSize = value.subtract(balnFees.add(conversionFees)); - BigInteger orderIcxValue = (orderSize.multiply(sicxIcxPrice)).divide(EXA); - BigInteger lpSicxSize = orderSize.add(conversionFees); - - Context.require(orderIcxValue.compareTo(oldIcxTotal) <= 0, - TAG + ": InsufficientLiquidityError: Not enough ICX suppliers."); - - boolean filled = false; - BigInteger orderRemainingIcx = orderIcxValue; - int iterations = 0; - while (!filled) { - iterations += 1; - if ((icxQueue.size().equals(BigInteger.ZERO)) || (iterations > ICX_QUEUE_FILL_DEPTH)) { - Context.revert(TAG + ": InsufficientLiquidityError: Unable to fill " + orderRemainingIcx + " ICX."); - } - NodeDB counterpartyOrder = icxQueue.getHeadNode(); - Address counterpartyAddress = counterpartyOrder.getUser(); - BigInteger counterpartyIcx = counterpartyOrder.getSize(); - - RewardsDataEntry rewardsEntry = new RewardsDataEntry(); - rewardsEntry._user = counterpartyAddress.toString(); - - BigInteger matchedIcx = counterpartyIcx.min(orderRemainingIcx); - orderRemainingIcx = orderRemainingIcx.subtract(matchedIcx); - - boolean counterpartyFilled = matchedIcx.equals(counterpartyIcx); - if (counterpartyFilled) { - icxQueue.removeHead(); - icxQueueOrderId.set(counterpartyAddress, null); - activeAddresses.get(SICXICX_POOL_ID).remove(counterpartyAddress); - rewardsEntry._balance = BigInteger.ZERO; - } else { - BigInteger newCounterpartyValue = counterpartyIcx.subtract(matchedIcx); - counterpartyOrder.setSize(newCounterpartyValue); - rewardsEntry._balance = newCounterpartyValue; - } - - data.add(rewardsEntry); - - BigInteger lpSicxEarnings = (lpSicxSize.multiply(matchedIcx)).divide(orderIcxValue); - BigInteger newSicxEarnings = getSicxEarnings(counterpartyAddress).add(lpSicxEarnings); - sicxEarnings.set(counterpartyAddress, newSicxEarnings); - - if (orderRemainingIcx.compareTo(BigInteger.ZERO) == 0) { - filled = true; - } - } - - BigInteger newIcxTotal = oldIcxTotal.subtract(orderIcxValue); - icxQueueTotal.set(newIcxTotal); - BigInteger effectiveFillPrice = (orderIcxValue.multiply(EXA)).divide(value); - Address sicxAddress = getSicx(); - Swap(BigInteger.valueOf(SICXICX_POOL_ID), sicxAddress, sicxAddress, EOA_ZERO, sender, sender, value, - orderIcxValue, BigInteger.valueOf(Context.getBlockTimestamp()), conversionFees, balnFees, newIcxTotal - , BigInteger.ZERO, sicxIcxPrice, effectiveFillPrice); - - Context.call(getRewards(), "updateBalanceAndSupplyBatch", SICXICX_MARKET_NAME, newIcxTotal, data); - Context.call(sicxAddress, "transfer", getFeehandler(), balnFees); - BalancedFloorLimits.verifyNativeWithdraw(orderIcxValue); - Context.transfer(sender, orderIcxValue); - } - - - private BigInteger getUnitValue(Address tokenAddress) { - if (tokenAddress == null) { - return EXA; - } else { - return pow(BigInteger.TEN, tokenPrecisions.get(tokenAddress).intValue()); - } - } - - BigInteger snapshotValueAt(BigInteger _snapshot_id, - BranchDB> snapshot) { - Context.require(_snapshot_id.compareTo(BigInteger.ZERO) >= 0, - TAG + ": Snapshot id is equal to or greater then Zero."); - BigInteger low = BigInteger.ZERO; - BigInteger high = snapshot.at(LENGTH).getOrDefault(BigInteger.ZERO, BigInteger.ZERO); - - while (low.compareTo(high) < 0) { - BigInteger mid = (low.add(high)).divide(BigInteger.TWO); - if (snapshot.at(IDS).getOrDefault(mid, BigInteger.ZERO).compareTo(_snapshot_id) > 0) { - high = mid; - } else { - low = mid.add(BigInteger.ONE); - } - } - - if (snapshot.at(IDS).getOrDefault(BigInteger.ZERO, BigInteger.ZERO).equals(_snapshot_id)) { - return snapshot.at(VALUES).getOrDefault(BigInteger.ZERO, BigInteger.ZERO); - } else if (low.equals(BigInteger.ZERO)) { - return BigInteger.ZERO; - } - - BigInteger matchedIndex = low.subtract(BigInteger.ONE); - return snapshot.at(VALUES).getOrDefault(matchedIndex, BigInteger.ZERO); - } - - void _transfer(Address from, Address to, BigInteger value, Integer id, byte[] data) { - - Context.require(!isLockingPool(id), TAG + ": Nontransferable token id"); - Context.require(value.compareTo(BigInteger.ZERO) >= 0, - TAG + ": Transferring value cannot be less than 0."); - - DictDB poolLpBalanceOfUser = balance.at(id); - BigInteger fromBalance = poolLpBalanceOfUser.getOrDefault(from, BigInteger.ZERO); - - Context.require(fromBalance.compareTo(value) >= 0, TAG + ": Out of balance"); - - poolLpBalanceOfUser.set(from, poolLpBalanceOfUser.get(from).subtract(value)); - poolLpBalanceOfUser.set(to, poolLpBalanceOfUser.getOrDefault(to, BigInteger.ZERO).add(value)); - Address stakedLpAddress = getStakedLp(); - - if (!from.equals(stakedLpAddress) && !to.equals(stakedLpAddress)) { - if (value.compareTo(BigInteger.ZERO) > 0) { - activeAddresses.get(id).add(to); - } - - if ((fromBalance.subtract(value)).equals(BigInteger.ZERO)) { - activeAddresses.get(id).remove(from); - } - } - TransferSingle(from, from, to, BigInteger.valueOf(id), value); - - if (to.isContract()) { - Context.call(to, "onIRC31Received", from, from, id, value, data); - } - } -} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPool.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPool.java new file mode 100644 index 000000000..9dc0a6880 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPool.java @@ -0,0 +1,1499 @@ +/* + * 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.IBalancedPoolCallee; +import network.balanced.score.core.dex.librairies.FixedPoint128; +import network.balanced.score.core.dex.librairies.FullMath; +import network.balanced.score.core.dex.librairies.LiquidityMath; +import network.balanced.score.core.dex.librairies.PositionLib; +import network.balanced.score.core.dex.librairies.SqrtPriceMath; +import network.balanced.score.core.dex.librairies.SwapMath; +import network.balanced.score.core.dex.librairies.TickLib; +import network.balanced.score.core.dex.librairies.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 BalancedPool { + // ================================================ + // 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 + * @dev This contract should be not deployed, as this class is abstract anyway. + * See {@code BalancedPoolFactored} constructor for the actual pool deployed on the network + */ + protected BalancedPool (Parameters parameters) { + // Initialize settings + this.settings = new PoolSettings ( + parameters.factory, + parameters.token0, + parameters.token1, + parameters.fee, + parameters.tickSpacing, + TickLib.tickSpacingToMaxLiquidityPerTick(parameters.tickSpacing), + "Balanced 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(); + } + + IBalancedPoolCallee.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(); + IBalancedPoolCallee.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(); + IBalancedPoolCallee.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); + } + + IBalancedPoolCallee.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/BalancedPoolFactory.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPoolFactory.java new file mode 100644 index 000000000..18ce9c6c0 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPoolFactory.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.BalancedPoolDeployer; +import network.balanced.score.core.dex.interfaces.pooldeployer.IBalancedPoolDeployer; +import network.balanced.score.core.dex.structs.factory.Parameters; +import network.balanced.score.core.dex.interfaces.pool.IBalancedPool; +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 BalancedPoolFactory implements IBalancedPoolDeployer { + + // ================================================ + // Consts + // ================================================ + + // Contract class name + private static final String NAME = "BalancedPoolFactory"; + + // 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 IBalancedPoolDeployer + private final BalancedPoolDeployer 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 BalancedPoolFactory() { + this.poolDeployer = new BalancedPoolDeployer(); + + 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 = IBalancedPool.token0(pool); + Address token1 = IBalancedPool.token1(pool); + int fee = IBalancedPool.fee(pool); + int tickSpacing = IBalancedPool.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 IBalancedPoolDeployer --- + @External(readonly = true) + public Parameters parameters() { + return this.poolDeployer.parameters(); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexDBVariables.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexDBVariables.java deleted file mode 100644 index 179670678..000000000 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexDBVariables.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2022-2023 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 network.balanced.score.core.dex.db.LinkedListDB; -import network.balanced.score.core.dex.utils.LPMetadataDB; -import network.balanced.score.lib.utils.IterableDictDB; -import network.balanced.score.lib.utils.SetDB; -import score.*; - -import java.math.BigInteger; - -public class DexDBVariables { - - private static final String ACCOUNT_BALANCE_SNAPSHOT = "account_balance_snapshot"; - private static final String TOTAL_SUPPLY_SNAPSHOT = "total_supply_snapshot"; - private static final String QUOTE_COINS = "quote_coins"; - private static final String ICX_QUEUE_TOTAL = "icx_queue_total"; - private static final String GOVERNANCE_ADDRESS = "governance_address"; - private static final String NAMED_MARKETS = "named_markets"; - private static final String DEX_ON = "dex_on"; - - private static final String CURRENT_DAY = "current_day"; - private static final String TIME_OFFSET = "time_offset"; - private static final String REWARDS_DONE = "rewards_done"; - private static final String DIVIDENDS_DONE = "dividends_done"; - private static final String DEPOSIT = "deposit"; - private static final String POOL_ID = "poolId"; - private static final String NONCE = "nonce"; - private static final String POOL_TOTAL = "poolTotal"; - private static final String POOL_LP_TOTAL = "poolLPTotal"; - private static final String BALANCE = "balances"; - private static final String BALN_SNAPSHOT = "balnSnapshot"; - private static final String POOL_LP_FEE = "pool_lp_fee"; - private static final String POOL_BALN_FEE = "pool_baln_fee"; - private static final String ICX_CONVERSION_FEE = "icx_conversion_fee"; - private static final String ICX_BALN_FEE = "icx_baln_fee"; - private static final String BASE_TOKEN = "baseToken"; - private static final String QUOTE_TOKEN = "quoteToken"; - private static final String ACTIVE_POOL = "activePool"; - private static final String ICX_QUEUE = "icxQueue"; - private static final String ICX_QUEUE_ORDER_ID = "icxQueueOrderId"; - private static final String SICX_EARNINGS = "sicxEarnings"; - private static final String MARKETS_NAMES = "marketsToNames"; - private static final String TOKEN_PRECISIONS = "token_precisions"; - private static final String CURRENT_TX = "current_tx"; - private static final String CONTINUOUS_REWARDS_DAY = "continuous_rewards_day"; - public static final String VERSION = "version"; - public static final String ORACLE_PROTECTION = "oracle_protection"; - - - final static VarDB
governance = Context.newVarDB(GOVERNANCE_ADDRESS, Address.class); - public final static VarDB dexOn = Context.newVarDB(DEX_ON, Boolean.class); - - // Deposits - Map: token_address -> user_address -> value - final static BranchDB> deposit = Context.newBranchDB(DEPOSIT, - BigInteger.class); - // Pool IDs - Map: token address -> opposite token_address -> id - final static BranchDB> poolId = Context.newBranchDB(POOL_ID, Integer.class); - - public final static VarDB nonce = Context.newVarDB(NONCE, Integer.class); - - // Total amount of each type of token in a pool - // Map: pool_id -> token_address -> value - final static BranchDB> poolTotal = Context.newBranchDB(POOL_TOTAL, - BigInteger.class); - - // Total LP Tokens in pool - // Map: pool_id -> total LP tokens - final static DictDB poolLpTotal = Context.newDictDB(POOL_LP_TOTAL, BigInteger.class); - - // User Balances - // Map: pool_id -> user address -> lp token balance - final static BranchDB> balance = Context.newBranchDB(BALANCE, - BigInteger.class); - - // Map: pool_id -> user address -> ids/values/length -> length/0 -> value - final static BranchDB>>> accountBalanceSnapshot = - Context.newBranchDB(ACCOUNT_BALANCE_SNAPSHOT, BigInteger.class); - // Map: pool_id -> ids/values/length -> length/0 -> value - final static BranchDB>> totalSupplySnapshot = - Context.newBranchDB(TOTAL_SUPPLY_SNAPSHOT, BigInteger.class); - - // Map: pool_id -> ids/values/length -> length/0 -> value - final static BranchDB>> balnSnapshot = - Context.newBranchDB(BALN_SNAPSHOT, BigInteger.class); - - // Rewards/timekeeping logic - final static VarDB currentDay = Context.newVarDB(CURRENT_DAY, BigInteger.class); - final static VarDB timeOffset = Context.newVarDB(TIME_OFFSET, BigInteger.class); - final static VarDB rewardsDone = Context.newVarDB(REWARDS_DONE, Boolean.class); - final static VarDB dividendsDone = Context.newVarDB(DIVIDENDS_DONE, Boolean.class); - - final static LPMetadataDB activeAddresses = new LPMetadataDB(); - // Pools must use one of these as quote currency - final static SetDB
quoteCoins = new SetDB<>(QUOTE_COINS, Address.class, null); - - // All fees are divided by `FEE_SCALE` in const - final static VarDB poolLpFee = Context.newVarDB(POOL_LP_FEE, BigInteger.class); - final static VarDB poolBalnFee = Context.newVarDB(POOL_BALN_FEE, BigInteger.class); - final static VarDB icxConversionFee = Context.newVarDB(ICX_CONVERSION_FEE, BigInteger.class); - final static VarDB icxBalnFee = Context.newVarDB(ICX_BALN_FEE, BigInteger.class); - - // Map: pool_id -> base token address - final static DictDB poolBase = Context.newDictDB(BASE_TOKEN, Address.class); - // Map: pool_id -> quote token address - final static DictDB poolQuote = Context.newDictDB(QUOTE_TOKEN, Address.class); - final static DictDB active = Context.newDictDB(ACTIVE_POOL, Boolean.class); - - final static LinkedListDB icxQueue = new LinkedListDB(ICX_QUEUE); - - // Map: user_address -> order id - final static DictDB icxQueueOrderId = Context.newDictDB(ICX_QUEUE_ORDER_ID, BigInteger.class); - - // Map: user_address -> integer of unclaimed earnings - final static DictDB sicxEarnings = Context.newDictDB(SICX_EARNINGS, BigInteger.class); - final static VarDB icxQueueTotal = Context.newVarDB(ICX_QUEUE_TOTAL, BigInteger.class); - - - final static IterableDictDB namedMarkets = new IterableDictDB<>(NAMED_MARKETS, Integer.class, - String.class, true); - - final static DictDB marketsToNames = Context.newDictDB(MARKETS_NAMES, String.class); - - final static DictDB tokenPrecisions = Context.newDictDB(TOKEN_PRECISIONS, BigInteger.class); - - // VarDB used to track the current sent transaction. This helps bound iterations. - final static VarDB currentTx = Context.newVarDB(CURRENT_TX, byte[].class); - - // Activation of continuous rewards day - final static VarDB continuousRewardsDay = Context.newVarDB(CONTINUOUS_REWARDS_DAY, BigInteger.class); - - public static final VarDB currentVersion = Context.newVarDB(VERSION, String.class); - - //Map: pid -> percentage - public final static DictDB oracleProtection = Context.newDictDB(ORACLE_PROTECTION, BigInteger.class); -} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexImpl.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexImpl.java deleted file mode 100644 index 2726cc6e3..000000000 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexImpl.java +++ /dev/null @@ -1,519 +0,0 @@ -/* - * Copyright (c) 2022-2023 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 com.eclipsesource.json.Json; -import com.eclipsesource.json.JsonObject; -import network.balanced.score.core.dex.db.NodeDB; -import network.balanced.score.lib.structs.RewardsDataEntry; -import network.balanced.score.lib.utils.BalancedFloorLimits; -import network.balanced.score.lib.utils.Versions; -import score.Address; -import score.BranchDB; -import score.Context; -import score.DictDB; -import score.annotation.External; -import score.annotation.Optional; -import score.annotation.Payable; -import scorex.util.ArrayList; - -import java.math.BigInteger; -import java.util.List; - -import static network.balanced.score.core.dex.DexDBVariables.*; -import static network.balanced.score.core.dex.utils.Check.isDexOn; -import static network.balanced.score.core.dex.utils.Check.isValidPercent; -import static network.balanced.score.core.dex.utils.Const.*; -import static network.balanced.score.lib.utils.BalancedAddressManager.getRewards; -import static network.balanced.score.lib.utils.BalancedAddressManager.getSicx; -import static network.balanced.score.lib.utils.Check.checkStatus; -import static network.balanced.score.lib.utils.Constants.EXA; -import static network.balanced.score.lib.utils.Constants.POINTS; -import static network.balanced.score.lib.utils.Math.convertToNumber; -import static score.Context.require; - -public class DexImpl extends AbstractDex { - - public DexImpl(Address _governance) { - super(_governance); - if (currentVersion.getOrDefault("").equals(Versions.DEX)) { - Context.revert("Can't Update same version of code"); - } - currentVersion.set(Versions.DEX); - } - - @External(readonly = true) - public String version() { - return currentVersion.getOrDefault(""); - } - - @Payable - public void fallback() { - isDexOn(); - checkStatus(); - - BigInteger orderValue = Context.getValue(); - require(orderValue.compareTo(BigInteger.TEN.multiply(EXA)) >= 0, - TAG + ": Minimum pool contribution is 10 ICX"); - - Address user = Context.getCaller(); - BigInteger oldOrderValue = BigInteger.ZERO; - BigInteger orderId = icxQueueOrderId.getOrDefault(user, BigInteger.ZERO); - - // Upsert Order, so we can bump to the back of the queue - if (orderId.compareTo(BigInteger.ZERO) > 0) { - NodeDB node = icxQueue.getNode(orderId); - oldOrderValue = node.getSize(); - orderValue = orderValue.add(oldOrderValue); - icxQueue.remove(orderId); - } - - // Insert order to the back of the queue - BigInteger nextTailId = icxQueue.getTailId().add(BigInteger.ONE); - icxQueue.append(orderValue, user, nextTailId); - icxQueueOrderId.set(user, nextTailId); - - // Update total ICX queue size - BigInteger oldIcxTotal = icxQueueTotal.getOrDefault(BigInteger.ZERO); - BigInteger currentIcxTotal = oldIcxTotal.add(orderValue).subtract(oldOrderValue); - icxQueueTotal.set(currentIcxTotal); - - activeAddresses.get(SICXICX_POOL_ID).add(user); - - sendRewardsData(user, orderValue, currentIcxTotal); - } - - @External - public void cancelSicxicxOrder() { - isDexOn(); - checkStatus(); - - Address user = Context.getCaller(); - BigInteger orderId = icxQueueOrderId.getOrDefault(user, BigInteger.ZERO); - - require(orderId.compareTo(BigInteger.ZERO) > 0, TAG + ": No open order in sICX/ICX queue."); - - NodeDB order = icxQueue.getNode(orderId); - BigInteger withdrawAmount = order.getSize(); - BigInteger oldIcxTotal = icxQueueTotal.get(); - BigInteger currentIcxTotal = oldIcxTotal.subtract(withdrawAmount); - - icxQueueTotal.set(currentIcxTotal); - icxQueue.remove(orderId); - icxQueueOrderId.set(user, null); - activeAddresses.get(SICXICX_POOL_ID).remove(user); - - sendRewardsData(user, BigInteger.ZERO, currentIcxTotal); - - BalancedFloorLimits.verifyNativeWithdraw(withdrawAmount); - Context.transfer(user, withdrawAmount); - } - - private void sendRewardsData(Address user, BigInteger amount, BigInteger oldIcxTotal) { - List rewardsList = new ArrayList<>(); - RewardsDataEntry rewardsEntry = new RewardsDataEntry(); - rewardsEntry._user = user.toString(); - rewardsEntry._balance = amount; - rewardsList.add(rewardsEntry); - Context.call(getRewards(), "updateBalanceAndSupply", SICXICX_MARKET_NAME, oldIcxTotal, user.toString(), amount); - } - - @External - public void xTokenFallback(String _from, BigInteger _value, byte[] _data) { - isDexOn(); - - String unpackedData = new String(_data); - require(!unpackedData.equals(""), "Token Fallback: Data can't be empty"); - - JsonObject json = Json.parse(unpackedData).asObject(); - - String method = json.get("method").asString(); - Address fromToken = Context.getCaller(); - - require(_value.compareTo(BigInteger.ZERO) > 0, TAG + ": Invalid token transfer value"); - - if (method.equals("_deposit")) { - JsonObject params = json.get("params").asObject(); - Address to = Address.fromString(params.get("address").asString()); - deposit(fromToken, to, _value); - } else {// If no supported method was sent, revert the transaction - Context.revert(100, TAG + ": Unsupported method supplied"); - } - } - - @External - public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { - isDexOn(); - checkStatus(); - - // Parse the transaction data submitted by the user - String unpackedData = new String(_data); - require(!unpackedData.equals(""), "Token Fallback: Data can't be empty"); - - JsonObject json = Json.parse(unpackedData).asObject(); - - String method = json.get("method").asString(); - Address fromToken = Context.getCaller(); - - require(_value.compareTo(BigInteger.ZERO) > 0, TAG + ": Invalid token transfer value"); - - // Call an internal method based on the "method" param sent in tokenFallBack - switch (method) { - case "_deposit": { - deposit(fromToken, _from, _value); - break; - } - case "_swap_icx": { - require(fromToken.equals(getSicx()), - TAG + ": InvalidAsset: _swap_icx can only be called with sICX"); - swapIcx(_from, _value); - break; - } - case "_swap": { - - // Parse the slippage sent by the user in minimumReceive. - // If none is sent, use the maximum. - JsonObject params = json.get("params").asObject(); - BigInteger minimumReceive = BigInteger.ZERO; - if (params.contains("minimumReceive")) { - minimumReceive = convertToNumber(params.get("minimumReceive")); - require(minimumReceive.signum() >= 0, - TAG + ": Must specify a positive number for minimum to receive"); - } - - // Check if an alternative recipient of the swap is set. - Address receiver; - if (params.contains("receiver")) { - receiver = Address.fromString(params.get("receiver").asString()); - } else { - receiver = _from; - } - - // Get destination coin from the swap - require(params.contains("toToken"), TAG + ": No toToken specified in swap"); - Address toToken = Address.fromString(params.get("toToken").asString()); - - // Perform the swap - exchange(fromToken, toToken, _from, receiver, _value, minimumReceive); - break; - } - case "_donate": { - JsonObject params = json.get("params").asObject(); - require(params.contains("toToken"), TAG + ": No toToken specified in swap"); - Address toToken = Address.fromString(params.get("toToken").asString()); - donate(fromToken, toToken, _value); - break; - } - default: - // If no supported method was sent, revert the transaction - Context.revert(100, TAG + ": Unsupported method supplied"); - break; - } - } - - @External - public void transfer(Address _to, BigInteger _value, BigInteger _id, @Optional byte[] _data) { - isDexOn(); - checkStatus(); - if (_data == null) { - _data = new byte[0]; - } - _transfer(Context.getCaller(), _to, _value, _id.intValue(), _data); - } - - @External - public void withdraw(Address _token, BigInteger _value) { - isDexOn(); - checkStatus(); - require(_value.compareTo(BigInteger.ZERO) > 0, TAG + ": Must specify a positive amount"); - Address sender = Context.getCaller(); - DictDB depositDetails = deposit.at(_token); - BigInteger deposit_amount = depositDetails.getOrDefault(sender, BigInteger.ZERO); - require(_value.compareTo(deposit_amount) <= 0, TAG + ": Insufficient Balance"); - - depositDetails.set(sender, deposit_amount.subtract(_value)); - - Withdraw(_token, sender, _value); - BalancedFloorLimits.verifyWithdraw(_token, _value); - Context.call(_token, "transfer", sender, _value); - } - - @External(readonly = true) - public BigInteger depositOfUser(Address _owner, Address _token) { - DictDB depositDetails = deposit.at(_token); - return depositDetails.getOrDefault(_owner, BigInteger.ZERO); - } - - @External - public void remove(BigInteger _id, BigInteger _value, @Optional boolean _withdraw) { - isDexOn(); - checkStatus(); - Address user = Context.getCaller(); - Address baseToken = poolBase.get(_id.intValue()); - require(baseToken != null, TAG + ": invalid pool id"); - DictDB userLPBalance = balance.at(_id.intValue()); - BigInteger userBalance = userLPBalance.getOrDefault(user, BigInteger.ZERO); - - require(active.getOrDefault(_id.intValue(), false), TAG + ": Pool is not active"); - require(_value.compareTo(BigInteger.ZERO) > 0, TAG + " Cannot withdraw a negative or zero balance"); - require(_value.compareTo(userBalance) <= 0, TAG + ": Insufficient balance"); - - Address quoteToken = poolQuote.get(_id.intValue()); - DictDB totalTokensInPool = poolTotal.at(_id.intValue()); - BigInteger totalBase = totalTokensInPool.get(baseToken); - BigInteger totalQuote = totalTokensInPool.get(quoteToken); - BigInteger totalLPToken = poolLpTotal.get(_id.intValue()); - - BigInteger userQuoteLeft = ((userBalance.subtract(_value)).multiply(totalQuote)).divide(totalLPToken); - - if (userQuoteLeft.compareTo(getRewardableAmount(quoteToken)) < 0) { - _value = userBalance; - activeAddresses.get(_id.intValue()).remove(user); - } - - BigInteger baseToWithdraw = _value.multiply(totalBase).divide(totalLPToken); - BigInteger quoteToWithdraw = _value.multiply(totalQuote).divide(totalLPToken); - - BigInteger newBase = totalBase.subtract(baseToWithdraw); - BigInteger newQuote = totalQuote.subtract(quoteToWithdraw); - BigInteger newUserBalance = userBalance.subtract(_value); - BigInteger newTotal = totalLPToken.subtract(_value); - - require(newTotal.compareTo(MIN_LIQUIDITY) >= 0, - TAG + ": Cannot withdraw pool past minimum LP token amount"); - - totalTokensInPool.set(baseToken, newBase); - totalTokensInPool.set(quoteToken, newQuote); - userLPBalance.set(user, newUserBalance); - poolLpTotal.set(_id.intValue(), newTotal); - - Remove(_id, user, _value, baseToWithdraw, quoteToWithdraw); - TransferSingle(user, user, MINT_ADDRESS, _id, _value); - - DictDB userBaseDeposit = deposit.at(baseToken); - BigInteger depositedBase = userBaseDeposit.getOrDefault(user, BigInteger.ZERO); - userBaseDeposit.set(user, depositedBase.add(baseToWithdraw)); - - DictDB userQuoteDeposit = deposit.at(quoteToken); - BigInteger depositedQuote = userQuoteDeposit.getOrDefault(user, BigInteger.ZERO); - userQuoteDeposit.set(user, depositedQuote.add(quoteToWithdraw)); - - if (_withdraw) { - withdraw(baseToken, baseToWithdraw); - withdraw(quoteToken, quoteToWithdraw); - } - - } - - @External - public void add(Address _baseToken, Address _quoteToken, BigInteger _baseValue, BigInteger _quoteValue, - @Optional boolean _withdraw_unused, @Optional BigInteger _slippagePercentage) { - isDexOn(); - checkStatus(); - isValidPercent(_slippagePercentage.intValue()); - - Address user = Context.getCaller(); - - // We check if there is a previously seen pool with this id. - // If none is found (return 0), we create a new pool. - Integer id = poolId.at(_baseToken).getOrDefault(_quoteToken, 0); - - require(_baseToken != _quoteToken, TAG + ": Pool must contain two token contracts"); - // Check base/quote balances are valid - require(_baseValue.compareTo(BigInteger.ZERO) > 0, - TAG + ": Cannot send 0 or negative base token"); - require(_quoteValue.compareTo(BigInteger.ZERO) > 0, - TAG + ": Cannot send 0 or negative quote token"); - - BigInteger userDepositedBase = deposit.at(_baseToken).getOrDefault(user, BigInteger.ZERO); - BigInteger userDepositedQuote = deposit.at(_quoteToken).getOrDefault(user, BigInteger.ZERO); - - // Check deposits are sufficient to cover balances - require(userDepositedBase.compareTo(_baseValue) >= 0, - TAG + ": Insufficient base asset funds deposited"); - require(userDepositedQuote.compareTo(_quoteValue) >= 0, - TAG + ": Insufficient quote asset funds deposited"); - - BigInteger baseToCommit = _baseValue; - BigInteger quoteToCommit = _quoteValue; - - // Initialize pool total variables - BigInteger liquidity; - BigInteger poolBaseAmount = BigInteger.ZERO; - BigInteger poolQuoteAmount = BigInteger.ZERO; - BigInteger poolLpAmount = poolLpTotal.getOrDefault(id, BigInteger.ZERO); - BigInteger userLpAmount = balance.at(id).getOrDefault(user, BigInteger.ZERO); - - // We need to only supply new base and quote in the pool ratio. - // If there isn't a pool yet, we can form one with the supplied ratios. - - if (id == 0) { - // No pool exists for this pair, we should create a new one. - - // Issue the next pool id to this pool - if (!quoteCoins.contains(_quoteToken)) { - Context.revert(TAG + " : QuoteNotAllowed: Supplied quote token not in permitted set."); - } - - Integer nextPoolNonce = nonce.get(); - poolId.at(_baseToken).set(_quoteToken, nextPoolNonce); - poolId.at(_quoteToken).set(_baseToken, nextPoolNonce); - id = nextPoolNonce; - nonce.set(nextPoolNonce + 1); - - active.set(id, true); - poolBase.set(id, _baseToken); - poolQuote.set(id, _quoteToken); - - liquidity = (_baseValue.multiply(_quoteValue)).sqrt(); - require(liquidity.compareTo(MIN_LIQUIDITY) >= 0, - TAG + ": Initial LP tokens must exceed " + MIN_LIQUIDITY); - MarketAdded(BigInteger.valueOf(id), _baseToken, _quoteToken, _baseValue, _quoteValue); - } else { - // Pool already exists, supply in the permitted order. - Address poolBaseAddress = poolBase.get(id); - Address poolQuoteAddress = poolQuote.get(id); - - require((poolBaseAddress.equals(_baseToken)) && (poolQuoteAddress.equals(_quoteToken)), - TAG + ": Must supply " + _baseToken.toString() + " as base and " + _quoteToken.toString() + - " as quote"); - - // We can only commit in the ratio of the pool. We determine this as: - // Min(ratio of quote from base, ratio of base from quote) - // Any assets not used are refunded - - DictDB totalTokensInPool = poolTotal.at(id); - poolBaseAmount = totalTokensInPool.get(_baseToken); - poolQuoteAmount = totalTokensInPool.get(_quoteToken); - - BigInteger poolPrice = poolBaseAmount.multiply(EXA).divide(poolQuoteAmount); - BigInteger priceOfAssetToCommit = baseToCommit.multiply(EXA).divide(quoteToCommit); - - require( - (priceOfAssetToCommit.subtract(poolPrice)).abs().compareTo(_slippagePercentage.multiply(poolPrice).divide(POINTS)) <= 0, - TAG + " : insufficient slippage provided" - ); - - BigInteger baseFromQuote = _quoteValue.multiply(poolBaseAmount).divide(poolQuoteAmount); - BigInteger quoteFromBase = _baseValue.multiply(poolQuoteAmount).divide(poolBaseAmount); - - if (quoteFromBase.compareTo(_quoteValue) <= 0) { - quoteToCommit = quoteFromBase; - } else { - baseToCommit = baseFromQuote; - } - - BigInteger liquidityFromBase = (poolLpAmount.multiply(baseToCommit)).divide(poolBaseAmount); - BigInteger liquidityFromQuote = (poolLpAmount.multiply(quoteToCommit)).divide(poolQuoteAmount); - - liquidity = liquidityFromBase.min(liquidityFromQuote); - require(liquidity.compareTo(BigInteger.ZERO) >= 0, - TAG + ": LP tokens to mint is less than zero"); - } - - // Apply the funds to the pool - poolBaseAmount = poolBaseAmount.add(baseToCommit); - poolQuoteAmount = poolQuoteAmount.add(quoteToCommit); - - DictDB totalTokensInPool = poolTotal.at(id); - totalTokensInPool.set(_baseToken, poolBaseAmount); - totalTokensInPool.set(_quoteToken, poolQuoteAmount); - - // Deduct the user's deposit - userDepositedBase = userDepositedBase.subtract(baseToCommit); - deposit.at(_baseToken).set(user, userDepositedBase); - userDepositedQuote = userDepositedQuote.subtract(quoteToCommit); - deposit.at(_quoteToken).set(user, userDepositedQuote); - - // Credit the user LP Tokens - userLpAmount = userLpAmount.add(liquidity); - poolLpAmount = poolLpAmount.add(liquidity); - - balance.at(id).set(user, userLpAmount); - poolLpTotal.set(id, poolLpAmount); - Add(BigInteger.valueOf(id), user, liquidity, baseToCommit, quoteToCommit); - TransferSingle(user, MINT_ADDRESS, user, BigInteger.valueOf(id), liquidity); - - activeAddresses.get(id).add(user); - - if (userDepositedBase.compareTo(BigInteger.ZERO) > 0) { - withdraw(_baseToken, userDepositedBase); - } - - if (userDepositedQuote.compareTo(BigInteger.ZERO) > 0) { - withdraw(_quoteToken, userDepositedQuote); - } - } - - @External - public void withdrawSicxEarnings(@Optional BigInteger _value) { - isDexOn(); - checkStatus(); - if (_value == null) { - _value = BigInteger.ZERO; - } - Address sender = Context.getCaller(); - BigInteger sicxEarning = getSicxEarnings(sender); - if (_value.equals(BigInteger.ZERO)) { - _value = sicxEarning; - } - - require(_value.compareTo(BigInteger.ZERO) > 0, - TAG + ": InvalidAmountError: Please send a positive amount."); - require(_value.compareTo(sicxEarning) <= 0, TAG + ": Insufficient balance."); - - sicxEarnings.set(sender, sicxEarning.subtract(_value)); - ClaimSicxEarnings(sender, _value); - Address sICX = getSicx(); - BalancedFloorLimits.verifyWithdraw(sICX, _value); - Context.call(sICX, "transfer", sender, _value); - } - - @External - public void onIRC31Received(Address _operator, Address _from, BigInteger _id, BigInteger _value, byte[] _data) { - checkStatus(); - Context.revert(TAG + ": IRC31 Tokens not accepted"); - } - - // TODO remove when dividends no longer use this - @External(readonly = true) - public BigInteger balanceOfAt(Address _account, BigInteger _id, BigInteger _snapshot_id, @Optional boolean _twa) { - - int poolId = _id.intValue(); - BranchDB> snapshot = accountBalanceSnapshot.at(poolId).at(_account); - return snapshotValueAt(_snapshot_id, snapshot); - - } - - @External(readonly = true) - public BigInteger totalSupplyAt(BigInteger _id, BigInteger _snapshot_id, @Optional boolean _twa) { - - int poolId = _id.intValue(); - BranchDB> snapshot = totalSupplySnapshot.at(poolId); - return snapshotValueAt(_snapshot_id, snapshot); - } - - @External(readonly = true) - public BigInteger totalBalnAt(BigInteger _id, BigInteger _snapshot_id, @Optional boolean _twa) { - - int poolId = _id.intValue(); - BranchDB> snapshot = balnSnapshot.at(poolId); - return snapshotValueAt(_snapshot_id, snapshot); - } - -} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/LinkedListDB.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/LinkedListDB.java deleted file mode 100644 index b465a75da..000000000 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/LinkedListDB.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * 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. - */ - -package network.balanced.score.core.dex.db; - -import score.Address; -import score.Context; -import score.UserRevertedException; -import score.VarDB; - -import java.math.BigInteger; - - -public class LinkedListDB { - - private static final String NAME = "_LINKED_LISTDB"; - public static final BigInteger DEFAULT_NODE_ID = BigInteger.ZERO; - - private final VarDB headId; - private final VarDB tailId; - private final VarDB length; - private final String name; - - - public LinkedListDB(String key) { - this.name = key + NAME; - this.headId = Context.newVarDB(this.name + "_head_id", BigInteger.class); - this.tailId = Context.newVarDB(this.name + "_tail_id", BigInteger.class); - this.length = Context.newVarDB(this.name + "_length", BigInteger.class); - } - - public BigInteger getTailId() { - return tailId.getOrDefault(BigInteger.ZERO); - } - - public BigInteger size() { - return length.getOrDefault(BigInteger.ZERO); - } - - public NodeDB createNodeInstance(BigInteger nodeId) { - return new NodeDB(nodeId.toString() + name); - } - - public void updateNode(BigInteger nodeId, BigInteger size, Address user) { - NodeDB node = createNodeInstance(nodeId); - Context.require(node.exists(), "There is no node of the provided node id."); - node.setValues(size, user); - } - - public BigInteger append(BigInteger size, Address user, BigInteger nodeId) { - NodeDB nodeToAppend = createNode(size, user, nodeId); - if (length.getOrDefault(BigInteger.ZERO).equals(BigInteger.ZERO)) { - headId.set(nodeId); - } else { - BigInteger tailId = this.tailId.getOrDefault(DEFAULT_NODE_ID); - NodeDB tail = getNode(tailId); - tail.setNext(nodeId); - nodeToAppend.setPrev(tailId); - } - tailId.set(nodeId); - length.set(length.getOrDefault(BigInteger.ZERO).add(BigInteger.ONE)); - return nodeId; - } - - public NodeDB getNode(BigInteger nodeId) { - if (nodeId == null || nodeId.equals(DEFAULT_NODE_ID)) { - throw new UserRevertedException("Invalid Node Id"); - } - NodeDB node = createNodeInstance(nodeId); - if (!node.exists()) { - LinkedNodeNotFound(name, nodeId); - } - return node; - } - - public NodeDB getTailNode() { - BigInteger tailId = this.tailId.getOrDefault(DEFAULT_NODE_ID); - if (tailId == null || tailId.equals(DEFAULT_NODE_ID)) { - Context.revert("Empty Linked list"); - } - return getNode(tailId); - } - - public NodeDB getHeadNode() { - BigInteger headId = this.headId.getOrDefault(DEFAULT_NODE_ID); - if (headId == null || headId.equals(DEFAULT_NODE_ID)) { - Context.revert("Empty Linked list"); - } - return getNode(headId); - } - - - public NodeDB createNode(BigInteger size, Address user, - BigInteger nodeId) { - NodeDB node = createNodeInstance(nodeId); - if (node.exists()) { - LinkedNodeAlreadyExists(name, nodeId); - } - node.setValues(size, user); - return node; - } - - - public void removeHead() { - BigInteger size = length.getOrDefault(BigInteger.ZERO); - if (size.equals(BigInteger.ZERO)) { - return; - } - if (size.equals(BigInteger.ONE)) { - clear(); - return; - } - - NodeDB oldHead = getNode(headId.getOrDefault(DEFAULT_NODE_ID)); - BigInteger newHead = oldHead.getNext(); - headId.set(newHead); - getNode(newHead).setPrev(DEFAULT_NODE_ID); - oldHead.delete(); - length.set(size.subtract(BigInteger.ONE)); - } - - public void removeTail() { - BigInteger size = length.getOrDefault(BigInteger.ZERO); - if (size.equals(BigInteger.ZERO)) { - return; - } - if (size.equals(BigInteger.ONE)) { - clear(); - return; - } - - NodeDB oldTail = getNode(tailId.getOrDefault(DEFAULT_NODE_ID)); - BigInteger newTail = oldTail.getPrev(); - tailId.set(newTail); - getNode(newTail).setNext(DEFAULT_NODE_ID); - oldTail.delete(); - length.set(size.subtract(BigInteger.ONE)); - } - - public void remove(BigInteger curId) { - BigInteger size = length.getOrDefault(BigInteger.ZERO); - if (size.equals(BigInteger.ZERO)) { - return; - } - if (curId.equals(headId.getOrDefault(DEFAULT_NODE_ID))) { - removeHead(); - } else if (curId.equals(tailId.getOrDefault(DEFAULT_NODE_ID))) { - removeTail(); - } else { - NodeDB nodeToRemove = getNode(curId); - BigInteger nextNodeId = nodeToRemove.getNext(); - NodeDB nextNode = getNode(nextNodeId); - BigInteger previousNodeId = nodeToRemove.getPrev(); - NodeDB previousNode = getNode(previousNodeId); - nextNode.setPrev(previousNodeId); - previousNode.setNext(nextNodeId); - nodeToRemove.delete(); - length.set(size.subtract(BigInteger.ONE)); - } - } - - public void clear() { - BigInteger currentId = headId.getOrDefault(DEFAULT_NODE_ID); - if (currentId == null || currentId.equals(DEFAULT_NODE_ID)) { - return; - } - NodeDB nodeToRemove = getNode(currentId); - BigInteger tailId = this.tailId.getOrDefault(DEFAULT_NODE_ID); - while (!currentId.equals(tailId)) { - currentId = nodeToRemove.getNext(); - nodeToRemove.delete(); - nodeToRemove = getNode(currentId); - } - nodeToRemove.delete(); - - this.tailId.set(null); - this.headId.set(null); - this.length.set(null); - } - - private void LinkedNodeAlreadyExists(String name, BigInteger nodeId) { - Context.revert("Linked List " + name + " already exists of nodeId." + nodeId.toString()); - } - - private void LinkedNodeNotFound(String name, BigInteger nodeId) { - Context.revert("Linked List " + name + " Node not found of nodeId " + nodeId.toString()); - } - -} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/NodeDB.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/NodeDB.java deleted file mode 100644 index 470536673..000000000 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/NodeDB.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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. - */ - -package network.balanced.score.core.dex.db; - -import score.Address; -import score.Context; -import score.VarDB; - -import java.math.BigInteger; - -import static network.balanced.score.core.dex.db.LinkedListDB.DEFAULT_NODE_ID; - -public class NodeDB { - - private static final String NAME = "_NODEDB"; - - private final VarDB size; - private final VarDB
user; - private final VarDB next; - private final VarDB prev; - - public NodeDB(String key) { - String name = key + NAME; - this.size = Context.newVarDB(name + "_value1", BigInteger.class); - this.user = Context.newVarDB(name + "_value2", Address.class); - this.next = Context.newVarDB(name + "_next", BigInteger.class); - this.prev = Context.newVarDB(name + "_prev", BigInteger.class); - } - - public void delete() { - size.set(null); - user.set(null); - prev.set(null); - next.set(null); - } - - public boolean exists() { - return (size.get() != null) && (user.get() != null); - } - - public BigInteger getSize() { - return size.getOrDefault(BigInteger.ZERO); - } - - public void setSize(BigInteger value) { - size.set(value); - } - - public Address getUser() { - return user.get(); - } - - public void setUser(Address user) { - this.user.set(user); - } - - public void setValues(BigInteger size, Address user) { - this.size.set(size); - this.user.set(user); - } - - public BigInteger getNext() { - return next.getOrDefault(DEFAULT_NODE_ID); - } - - public void setNext(BigInteger nextId) { - next.set(nextId); - } - - public BigInteger getPrev() { - return prev.getOrDefault(DEFAULT_NODE_ID); - } - - public void setPrev(BigInteger prev_id) { - prev.set(prev_id); - } - -} 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..b744e2288 --- /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 BalancedPool#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 BalancedPool deployed by the canonical BalancedPoolFactory. + * @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/IBalancedPool.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPool.java new file mode 100644 index 000000000..5cc459a9d --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPool.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 IBalancedPool { + + // 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/IBalancedPoolCallee.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPoolCallee.java new file mode 100644 index 000000000..165858421 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPoolCallee.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 IBalancedPoolCallee { + + // 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/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..e4497fbfc --- /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 `BalancedPool::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 BalancedPool deployed by the canonical BalancedPoolFactory. 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/pooldeployer/IBalancedPoolDeployer.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IBalancedPoolDeployer.java new file mode 100644 index 000000000..75a4b7c9a --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IBalancedPoolDeployer.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 IBalancedPoolDeployer { + + // ================================================ + // 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/librairies/BitMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/BitMath.java new file mode 100644 index 000000000..c81934bcb --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/librairies/FixedPoint128.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FixedPoint128.java new file mode 100644 index 000000000..685a22bb8 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/librairies/FixedPoint96.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FixedPoint96.java new file mode 100644 index 000000000..e0799f572 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/librairies/FullMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FullMath.java new file mode 100644 index 000000000..071fa63f2 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/librairies/LiquidityMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/LiquidityMath.java new file mode 100644 index 000000000..80fd1cccb --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/librairies/OracleLib.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/OracleLib.java new file mode 100644 index 000000000..8c0b55b24 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/librairies/PositionLib.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/PositionLib.java new file mode 100644 index 000000000..f5b056337 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/librairies/SqrtPriceMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/SqrtPriceMath.java new file mode 100644 index 000000000..3466eadec --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/librairies/SwapMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/SwapMath.java new file mode 100644 index 000000000..874944d9d --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/librairies/TickLib.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/TickLib.java new file mode 100644 index 000000000..d4a1ac6ab --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/librairies/TickMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/TickMath.java new file mode 100644 index 000000000..8a420aaf8 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/librairies/UnsafeMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/UnsafeMath.java new file mode 100644 index 000000000..207c5d152 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/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.librairies; + +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/BalancedPoolDeployer.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/BalancedPoolDeployer.java new file mode 100644 index 000000000..fafd4ca1a --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/BalancedPoolDeployer.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 BalancedPoolDeployer { + + // ================================================ + // 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..cc6ec3619 --- /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.librairies.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..a2f7b2d81 --- /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.librairies.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..502a555ae --- /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.librairies.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/LPMetadataDB.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/AddressUtils.java similarity index 61% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/LPMetadataDB.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/AddressUtils.java index 97f1b9914..5280f7136 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/LPMetadataDB.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/AddressUtils.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2022-2022 Balanced.network. + * 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 + * 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, @@ -16,14 +16,14 @@ package network.balanced.score.core.dex.utils; -import network.balanced.score.lib.utils.EnumerableSetDB; -import score.Address; +import java.math.BigInteger; -public class LPMetadataDB { - private static final String LP_METADATA_PREFIX = "lp"; +import score.Address; - public EnumerableSetDB
get(Integer id) { - return new EnumerableSetDB<>(LP_METADATA_PREFIX + id, Address.class); - } +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/Check.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Check.java deleted file mode 100644 index 8b5f6758a..000000000 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Check.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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. - */ - -package network.balanced.score.core.dex.utils; - -import score.Context; - -import java.math.BigInteger; - -import static network.balanced.score.core.dex.DexDBVariables.dexOn; -import static network.balanced.score.core.dex.DexDBVariables.nonce; -import static network.balanced.score.core.dex.utils.Const.TAG; -import static network.balanced.score.lib.utils.Constants.POINTS; - -public class Check { - - public static void isDexOn() { - Context.require(dexOn.getOrDefault(false), "NotLaunched: Function cannot be called " + - "before the DEX is turned on"); - } - - public static void isValidPoolId(BigInteger id) { - isValidPoolId(id.intValue()); - } - - public static void isValidPoolId(Integer id) { - Context.require(id > 0 && id <= nonce.get(), TAG + ": Invalid pool ID"); - } - - public static void isValidPercent(Integer percent) { - Context.require(percent >= 0 && percent <= POINTS.intValue(), TAG + ": Invalid percentage"); - } - -} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Const.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Const.java deleted file mode 100644 index 871efd547..000000000 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Const.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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. - */ - -package network.balanced.score.core.dex.utils; - -import network.balanced.score.lib.utils.Names; -import score.Address; - -import java.math.BigInteger; - -import static network.balanced.score.lib.utils.Constants.EOA_ZERO; -import static network.balanced.score.lib.utils.Constants.MICRO_SECONDS_IN_A_DAY; - -public class Const { - - public static final String SICXICX_MARKET_NAME = "sICX/ICX"; - - public static final int SICXICX_POOL_ID = 1; - public static final BigInteger MIN_LIQUIDITY = BigInteger.valueOf(1_000); - public static final BigInteger FEE_SCALE = BigInteger.valueOf(10_000); - public static final Integer ICX_QUEUE_FILL_DEPTH = 50; - public static final Address MINT_ADDRESS = EOA_ZERO; - public static final String TAG = Names.DEX; - - public static final String IDS = "ids"; - public static final String VALUES = "values"; - public static final String LENGTH = "length"; -} 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/Dex/src/test/java/network/balanced/score/core/dex/DexTestBase.java b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestBase.java deleted file mode 100644 index f3e2e9917..000000000 --- a/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestBase.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (c) 2022-2023 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 com.iconloop.score.test.Account; -import com.iconloop.score.test.Score; -import com.iconloop.score.test.ServiceManager; -import network.balanced.score.lib.test.UnitTest; -import network.balanced.score.lib.test.mock.MockBalanced; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.stubbing.Answer; -import score.Address; -import score.Context; -import score.annotation.Optional; - -import java.math.BigInteger; -import java.util.HashMap; -import java.util.Map; - -import static network.balanced.score.lib.utils.Constants.EXA; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; - - -class DexTestBase extends UnitTest { - protected static final ServiceManager sm = getServiceManager(); - protected static Account ownerAccount = sm.createAccount(); - protected static Account adminAccount = sm.createAccount(); - protected static Account prep_address = sm.createAccount(); - - int scoreCount = 0; - protected MockBalanced mockBalanced; - protected Account governanceScore; - protected Account dividendsScore; - protected Account stakingScore; - protected Account rewardsScore; - protected Account bnusdScore; - protected Account balnScore; - protected Account sicxScore; - protected Account feehandlerScore; - protected Account stakedLPScore; - protected Account balancedOracle; - - public static Score dexScore; - public static DexImpl dexScoreSpy; - - protected final MockedStatic contextMock = Mockito.mockStatic(Context.class, Mockito.CALLS_REAL_METHODS); - - public void setup() throws Exception { - mockBalanced = new MockBalanced(sm, ownerAccount); - governanceScore = mockBalanced.governance.account; - dividendsScore = mockBalanced.dividends.account; - stakingScore = mockBalanced.staking.account; - rewardsScore = mockBalanced.rewards.account; - bnusdScore = mockBalanced.bnUSD.account; - balnScore = mockBalanced.baln.account; - sicxScore = mockBalanced.sicx.account; - feehandlerScore = mockBalanced.feehandler.account; - stakedLPScore = mockBalanced.stakedLp.account; - balancedOracle = mockBalanced.balancedOracle.account; - - contextMock.when(() -> Context.call(eq(governanceScore.getAddress()), eq("checkStatus"), any(String.class))).thenReturn(null); - contextMock.when(() -> Context.call(eq(BigInteger.class), any(Address.class), eq("balanceOf"), any(Address.class))).thenReturn(BigInteger.ZERO); - - dexScore = sm.deploy(ownerAccount, DexImpl.class, governanceScore.getAddress()); - dexScore.invoke(governanceScore, "setTimeOffset", BigInteger.valueOf(Context.getBlockTimestamp())); - dexScoreSpy = (DexImpl) spy(dexScore.getInstance()); - dexScore.setInstance(dexScoreSpy); - - dexScore.invoke(governanceScore, "addQuoteCoin", bnusdScore.getAddress()); - dexScore.invoke(governanceScore, "addQuoteCoin", sicxScore.getAddress()); - } - - - protected void depositToken(Account depositor, Account tokenScore, BigInteger value) { - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - dexScore.invoke(tokenScore, "tokenFallback", depositor.getAddress(), value, tokenData("_deposit", - new HashMap<>())); - } - - protected void xDepositToken(String depositor, Account to, Account tokenScore, BigInteger value) { - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - dexScore.invoke(tokenScore, "xTokenFallback", depositor, value, tokenData("_deposit", - Map.of("address", to.getAddress().toString()))); - } - - protected void supplyLiquidity(Account supplier, Account baseTokenScore, Account quoteTokenScore, - BigInteger baseValue, BigInteger quoteValue, @Optional boolean withdrawUnused) { - // Configure dex. - dexScore.invoke(governanceScore, "addQuoteCoin", quoteTokenScore.getAddress()); - - // Mock these cross-contract calls. - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - - // Deposit tokens and supply liquidity. - dexScore.invoke(baseTokenScore, "tokenFallback", supplier.getAddress(), baseValue, tokenData("_deposit", - new HashMap<>())); - dexScore.invoke(quoteTokenScore, "tokenFallback", supplier.getAddress(), quoteValue, tokenData("_deposit", - new HashMap<>())); - dexScore.invoke(supplier, "add", baseTokenScore.getAddress(), quoteTokenScore.getAddress(), baseValue, - quoteValue, withdrawUnused, BigInteger.valueOf(100)); - } - - protected BigInteger computePrice(BigInteger tokenAValue, BigInteger tokenBValue) { - return (tokenAValue.multiply(EXA)).divide(tokenBValue); - } - - protected void supplyIcxLiquidity(Account supplier, BigInteger value) { - contextMock.when(Context::getValue).thenReturn(value); - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("updateBalanceAndSupply"), - any(String.class), any(BigInteger.class), any(String.class), any(BigInteger.class))).thenReturn(null); - supplier.addBalance("ICX", value); - sm.transfer(supplier, dexScore.getAddress(), value); - } - - // Not done yet. Fails for some reason. - protected void swapSicxToIcx(Account sender, BigInteger value, BigInteger sicxIcxConversionRate) { - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - doReturn(sicxIcxConversionRate).when(dexScoreSpy).getSicxRate(); - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("updateBalanceAndSupplyBatch"), any(), - any(), any())).thenReturn(null); - contextMock.when(() -> Context.call(eq(sicxScore.getAddress()), eq("transfer"), - eq(feehandlerScore.getAddress()), any(BigInteger.class))).thenReturn(true); - contextMock.when(() -> Context.transfer(eq(sender.getAddress()), any(BigInteger.class))).thenAnswer((Answer) invocation -> null); - dexScore.invoke(sicxScore, "tokenFallback", sender.getAddress(), value, tokenData("_swap_icx", - new HashMap<>())); - } -} \ No newline at end of file diff --git a/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestCore.java b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestCore.java deleted file mode 100644 index 27fd9f836..000000000 --- a/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestCore.java +++ /dev/null @@ -1,1012 +0,0 @@ -/* - * Copyright (c) 2022-2023 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 com.eclipsesource.json.JsonObject; -import com.iconloop.score.test.Account; -import network.balanced.score.core.dex.utils.Const; -import network.balanced.score.lib.structs.PrepDelegations; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.mockito.stubbing.Answer; -import score.Address; -import score.Context; - -import java.math.BigInteger; -import java.util.HashMap; -import java.util.Map; - -import static network.balanced.score.core.dex.utils.Const.*; -import static network.balanced.score.lib.utils.Constants.EXA; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; - - -public class DexTestCore extends DexTestBase { - - @BeforeEach - public void configureContract() throws Exception { - super.setup(); - } - - @SuppressWarnings("unchecked") - @Test - void fallback() { - Account account = sm.createAccount(); - BigInteger icxValue = BigInteger.valueOf(100).multiply(EXA); - contextMock.when(Context::getValue).thenReturn(icxValue); - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("updateBalanceAndSupply"), - any(String.class), any(BigInteger.class), any(String.class), any(BigInteger.class))).thenReturn(null); - contextMock.when(() -> Context.call(any(Address.class), eq("getTodayRate"))).thenReturn(EXA); - dexScore.invoke(ownerAccount, "fallback"); - - BigInteger poolId = BigInteger.valueOf(SICXICX_POOL_ID); - Map poolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger lpBalance = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), - BigInteger.valueOf(SICXICX_POOL_ID)); - assertEquals(icxValue, lpBalance); - assertEquals(lpBalance, poolStats.get("total_supply")); - - BigInteger additionIcxValue = BigInteger.valueOf(50L).multiply(EXA); - contextMock.when(Context::getValue).thenReturn(additionIcxValue); - dexScore.invoke(ownerAccount, "fallback"); - poolStats = (Map) dexScore.call("getPoolStats", poolId); - lpBalance = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), - BigInteger.valueOf(SICXICX_POOL_ID)); - assertEquals(icxValue.add(additionIcxValue), lpBalance); - assertEquals(icxValue.add(additionIcxValue), poolStats.get("total_supply")); - - dexScore.invoke(account, "fallback"); - poolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger balanceOwner = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), poolId); - BigInteger balanceAccount = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - assertEquals(balanceOwner.add(balanceAccount), poolStats.get("total_supply")); - } - - // Test fails in line with: activeAddresses.get(SICXICX_POOL_ID).remove(user); - @Test - void cancelSicxIcxOrder() { - // Arrange. - Account supplier = sm.createAccount(); - BigInteger value = BigInteger.valueOf(1000).multiply(EXA); - - supplyIcxLiquidity(supplier, value); - sm.getBlock().increase(100000); - - // Mock these. - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("updateBalanceAndSupply"), - any(String.class), any(BigInteger.class), any(String.class), any(BigInteger.class))).thenReturn(null); - contextMock.when(() -> Context.transfer(eq(supplier.getAddress()), eq(value))).thenAnswer((Answer) invocation -> null); - - // Act. - dexScore.invoke(supplier, "cancelSicxicxOrder"); - - // Assert. - BigInteger IcxBalance = (BigInteger) dexScore.call("getICXBalance", supplier.getAddress()); - assertEquals(BigInteger.ZERO, IcxBalance); - } - - @Test - void crossChainDeposit() { - Account user = sm.createAccount(); - BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); - xDepositToken("0x1.ETH/0x123", user, bnusdScore, depositValue); - BigInteger retrievedValue = (BigInteger) dexScore.call("getDeposit", bnusdScore.getAddress(), user.getAddress()); - assertEquals(depositValue, retrievedValue); - } - - @Test - void withdrawSicxEarnings() { - Account depositor = sm.createAccount(); - BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); - BigInteger withdrawValue = BigInteger.valueOf(10).multiply(EXA); - depositToken(depositor, balnScore, depositValue); - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(eq(stakingScore.getAddress()), eq("getTodayRate"))).thenReturn(EXA); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - contextMock.when(() -> Context.transfer(any(Address.class), any(BigInteger.class))).then(invocationOnMock -> null); - BigInteger depositBalance = BigInteger.valueOf(100L).multiply(EXA); - supplyIcxLiquidity(depositor, depositBalance); - - contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("transfer"), eq(depositor.getAddress()), - eq(withdrawValue))).thenReturn(null); - - // Act. - dexScore.invoke(depositor, "withdraw", balnScore.getAddress(), withdrawValue); - - // Assert. - BigInteger currentDepositValue = (BigInteger) dexScore.call("getDeposit", balnScore.getAddress(), - depositor.getAddress()); - assertEquals(depositValue.subtract(withdrawValue), currentDepositValue); - - BigInteger swapValue = BigInteger.valueOf(50L).multiply(EXA); - swapSicxToIcx(depositor, swapValue, EXA); - - BigInteger sicxEarning = (BigInteger) dexScore.call("getSicxEarnings", depositor.getAddress()); - dexScore.invoke(depositor, "withdrawSicxEarnings", sicxEarning); - BigInteger newSicxEarning = (BigInteger) dexScore.call("getSicxEarnings", depositor.getAddress()); - assertEquals(BigInteger.ZERO, newSicxEarning); - } - - @Test - void getSicxEarnings() { - Account depositor = sm.createAccount(); - BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); - BigInteger withdrawValue = BigInteger.valueOf(10).multiply(EXA); - depositToken(depositor, balnScore, depositValue); - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(eq(stakingScore.getAddress()), eq("getTodayRate"))).thenReturn(EXA); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - contextMock.when(() -> Context.transfer(any(Address.class), any(BigInteger.class))).then(invocationOnMock -> null); - BigInteger depositBalance = BigInteger.valueOf(100L).multiply(EXA); - supplyIcxLiquidity(depositor, depositBalance); - - contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("transfer"), eq(depositor.getAddress()), - eq(withdrawValue))).thenReturn(null); - - // Act. - dexScore.invoke(depositor, "withdraw", balnScore.getAddress(), withdrawValue); - - // Assert. - BigInteger currentDepositValue = (BigInteger) dexScore.call("getDeposit", balnScore.getAddress(), - depositor.getAddress()); - assertEquals(depositValue.subtract(withdrawValue), currentDepositValue); - - BigInteger swapValue = BigInteger.valueOf(100L).multiply(EXA); - swapSicxToIcx(depositor, swapValue, EXA); - - BigInteger sicxEarning = (BigInteger) dexScore.call("getSicxEarnings", depositor.getAddress()); - } - - - @Test - void tokenFallback_deposit() { - // Arrange. - Account tokenScoreCaller = balnScore; - Account tokenSender = sm.createAccount(); - BigInteger depositValue = BigInteger.valueOf(1000000000); - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - - // Act. - dexScore.invoke(tokenScoreCaller, "tokenFallback", tokenSender.getAddress(), depositValue, tokenData( - "_deposit", new HashMap<>())); - BigInteger retrievedDepositValue = (BigInteger) dexScore.call("getDeposit", tokenScoreCaller.getAddress(), - tokenSender.getAddress()); - - // Assert. - assertEquals(depositValue, retrievedDepositValue); - } - - @Test - void withdrawTokens_negativeAmount() { - // Arrange. - Account depositor = sm.createAccount(); - BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); - BigInteger withdrawValue = BigInteger.valueOf(-1000).multiply(EXA); - String expectedErrorMessage = "Reverted(0): Balanced DEX: Must specify a positive amount"; - depositToken(depositor, balnScore, depositValue); - - // Act & assert. - Executable withdrawalInvocation = () -> dexScore.invoke(depositor, "withdraw", balnScore.getAddress(), - withdrawValue); - expectErrorMessage(withdrawalInvocation, expectedErrorMessage); - } - - @Test - void withdrawToken_insufficientBalance() { - // Arrange. - Account depositor = sm.createAccount(); - BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); - BigInteger withdrawValue = BigInteger.valueOf(1000).multiply(EXA); - String expectedErrorMessage = "Reverted(0): Balanced DEX: Insufficient Balance"; - depositToken(depositor, balnScore, depositValue); - - // Act & assert. - Executable withdrawalInvocation = () -> dexScore.invoke(depositor, "withdraw", balnScore.getAddress(), - withdrawValue); - expectErrorMessage(withdrawalInvocation, expectedErrorMessage); - } - - @Test - void withdrawToken() { - // Arrange. - Account depositor = sm.createAccount(); - BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); - BigInteger withdrawValue = BigInteger.valueOf(10).multiply(EXA); - depositToken(depositor, balnScore, depositValue); - - contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("transfer"), eq(depositor.getAddress()), - eq(withdrawValue))).thenReturn(null); - - // Act. - dexScore.invoke(depositor, "withdraw", balnScore.getAddress(), withdrawValue); - - // Assert. - BigInteger currentDepositValue = (BigInteger) dexScore.call("getDeposit", balnScore.getAddress(), - depositor.getAddress()); - assertEquals(depositValue.subtract(withdrawValue), currentDepositValue); - } - - @SuppressWarnings("unchecked") - @Test - void addLiquidity() { - Account account = sm.createAccount(); - Account account1 = sm.createAccount(); - - final String data = "{" + - "\"method\": \"_deposit\"" + - "}"; - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - - BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); - //deposit - BigInteger bnusdValue = BigInteger.valueOf(276L).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(100L).multiply(EXA); - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); - dexScore.invoke(bnusdScore, "tokenFallback", account1.getAddress(), bnusdValue, data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account1.getAddress(), bnusdValue, data.getBytes()); - // add liquidity pool - dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); - dexScore.invoke(account1, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); - BigInteger poolId = (BigInteger) dexScore.call("getPoolId", bnusdScore.getAddress(), balnScore.getAddress()); - Map poolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - assertEquals(poolStats.get("base_token"), balnScore.getAddress()); - assertEquals(poolStats.get("quote_token"), bnusdScore.getAddress()); - assertEquals(bnusdValue.multiply(balnValue).sqrt(), balance); - - BigInteger account1_balance = (BigInteger) dexScore.call("balanceOf", account1.getAddress(), poolId); - assertEquals(balance.add(account1_balance), poolStats.get("total_supply")); - } - - @Test - void addLiquidity_higherSlippageFail(){ - // Arrange - Account account = sm.createAccount(); - Account account1 = sm.createAccount(); - - final String data = "{" + - "\"method\": \"_deposit\"" + - "}"; - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - - BigInteger bnusdValue = BigInteger.valueOf(276L).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(100L).multiply(EXA); - BigInteger acc2BnusdValue = BigInteger.valueOf(273L).multiply(EXA); - BigInteger acc2BalnValue = BigInteger.valueOf(100L).multiply(EXA); - - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), balnValue, data.getBytes()); - dexScore.invoke(bnusdScore, "tokenFallback", account1.getAddress(), acc2BnusdValue, data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account1.getAddress(), acc2BalnValue, data.getBytes()); - - - // Act - dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); - Executable addLiquidityInvocation = () -> dexScore.invoke(account1, "add", balnScore.getAddress(), bnusdScore.getAddress(), acc2BalnValue, acc2BnusdValue, false, BigInteger.valueOf(100)); - String expectedErrorMessage = "Reverted(0): Balanced DEX : insufficient slippage provided"; - - // Assert - expectErrorMessage(addLiquidityInvocation, expectedErrorMessage); - } - - @Test - void removeLiquidity() { - // Arrange - remove liquidity arguments. - BigInteger poolId = BigInteger.TWO; - BigInteger lpTokensToRemove = BigInteger.valueOf(1000); - Boolean withdrawTokensOnRemoval = false; - - // Arrange - supply liquidity. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - BigInteger usersLpTokens = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), poolId); - - // Arrange - increase blocks past withdrawal lock. - sm.getBlock().increase(100000000); - - // Act & Assert. - dexScore.invoke(ownerAccount, "remove", poolId, lpTokensToRemove, withdrawTokensOnRemoval); - BigInteger usersLpTokensAfterRemoval = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), - poolId); - assertEquals(usersLpTokens.subtract(lpTokensToRemove), usersLpTokensAfterRemoval); - } - - @SuppressWarnings("unchecked") - @Test - void tokenFallbackSwapFromTokenIs_poolQuote() { - Account account = sm.createAccount(); - - final String data = "{" + - "\"method\": \"_deposit\"" + - "}"; - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - - BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); - //deposit - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), - data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), - data.getBytes()); - // add liquidity pool - dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, - FIFTY.divide(BigInteger.TWO), false, BigInteger.valueOf(100)); - BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); - BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - - Map fees = (Map) dexScore.call("getFees"); - Map poolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger oldFromToken = (BigInteger) poolStats.get("quote"); - BigInteger oldToToken = (BigInteger) poolStats.get("base"); - - BigInteger value = BigInteger.valueOf(100L).multiply(EXA); - BigInteger lp_fee = value.multiply(fees.get("pool_lp_fee")).divide(FEE_SCALE); - BigInteger baln_fee = value.multiply(fees.get("pool_baln_fee")).divide(FEE_SCALE); - BigInteger total_fee = lp_fee.add(baln_fee); - - BigInteger inputWithoutFees = value.subtract(total_fee); - BigInteger newFromToken = oldFromToken.add(inputWithoutFees); - - BigInteger newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); - BigInteger sendAmount = oldToToken.subtract(newToToken); - newFromToken = newFromToken.add(lp_fee); - - // test swap - JsonObject jsonData = new JsonObject(); - JsonObject params = new JsonObject(); - params.add("minimumReceive", BigInteger.valueOf(10L).toString()); - params.add("toToken", balnScore.getAddress().toString()); - jsonData.add("method", "_swap"); - jsonData.add("params", params); - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), value, jsonData.toString().getBytes()); - Map newPoolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger newBalance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - - assertEquals(newFromToken, newPoolStats.get("quote")); - assertEquals(newToToken, newPoolStats.get("base")); - assertEquals(balance, newBalance); - - contextMock.verify(() -> Context.call(eq(bnusdScore.getAddress()), eq("transfer"), - eq(feehandlerScore.getAddress()), eq(baln_fee))); - } - - - @Test - void tokenFallbackSwapFromTokenIs_poolBase() { - Account account = sm.createAccount(); - - final String data = "{" + - "\"method\": \"_deposit\"" + - "}"; - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - - BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); - //deposit - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), - data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), - data.getBytes()); - // add liquidity pool - dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, - FIFTY.divide(BigInteger.TWO), false, BigInteger.valueOf(100)); - BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); - BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - - Map fees = (Map) dexScore.call("getFees"); - Map poolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger oldFromToken = (BigInteger) poolStats.get("base"); - BigInteger oldToToken = (BigInteger) poolStats.get("quote"); - - BigInteger value = BigInteger.valueOf(100L).multiply(EXA); - BigInteger lp_fee = value.multiply(fees.get("pool_lp_fee")).divide(FEE_SCALE); - BigInteger baln_fee = value.multiply(fees.get("pool_baln_fee")).divide(FEE_SCALE); - BigInteger total_fee = lp_fee.add(baln_fee); - - BigInteger inputWithoutFees = value.subtract(total_fee); - BigInteger newFromToken = oldFromToken.add(inputWithoutFees); - - BigInteger newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); - BigInteger sendAmount = oldToToken.subtract(newToToken); - newFromToken = newFromToken.add(lp_fee); - - // swapping fees to quote token - oldFromToken = newFromToken; - oldToToken = newToToken; - BigInteger newFromTokenWithBalnFee = oldFromToken.add(baln_fee); - BigInteger newToTokenAfterFeeSwap = (oldFromToken.multiply(oldToToken)).divide(newFromTokenWithBalnFee); - BigInteger swappedBalnFee = oldToToken.subtract(newToTokenAfterFeeSwap); - - // test swap when from token is pool base token - JsonObject jsonData = new JsonObject(); - JsonObject params = new JsonObject(); - params.add("minimumReceive", BigInteger.valueOf(10L).toString()); - params.add("toToken", bnusdScore.getAddress().toString()); - jsonData.add("method", "_swap"); - jsonData.add("params", params); - dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), value, jsonData.toString().getBytes()); - Map newPoolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger newBalance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - - assertEquals(newFromTokenWithBalnFee, newPoolStats.get("base")); - assertEquals(newToTokenAfterFeeSwap, newPoolStats.get("quote")); - assertEquals(balance, newBalance); - - contextMock.verify(() -> Context.call(eq(bnusdScore.getAddress()), eq("transfer"), - eq(feehandlerScore.getAddress()), eq(swappedBalnFee))); - } - - @SuppressWarnings("unchecked") - @Test - void tokenFallback_donate() { - Account account = sm.createAccount(); - - final String data = "{" + - "\"method\": \"_deposit\"" + - "}"; - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - - BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); - //deposit - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), - data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), - data.getBytes()); - // add liquidity pool - dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, - FIFTY.divide(BigInteger.TWO), false, BigInteger.valueOf(100)); - BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); - Map poolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger initialBase = (BigInteger) poolStats.get("base"); - BigInteger initialQuote = (BigInteger) poolStats.get("quote"); - BigInteger initialSupply = (BigInteger) poolStats.get("total_supply"); - - BigInteger bnusdDonation = BigInteger.valueOf(100L).multiply(EXA); - JsonObject jsonData = new JsonObject(); - JsonObject params = new JsonObject(); - params.add("toToken", balnScore.getAddress().toString()); - jsonData.add("method", "_donate"); - jsonData.add("params", params); - dexScore.invoke(bnusdScore, "tokenFallback", ownerAccount.getAddress(), bnusdDonation, - jsonData.toString().getBytes()); - poolStats = (Map) dexScore.call("getPoolStats", poolId); - - assertEquals(initialBase, poolStats.get("base")); - assertEquals(initialQuote.add(bnusdDonation), poolStats.get("quote")); - - BigInteger balnDonation = BigInteger.valueOf(50L).multiply(EXA); - jsonData = new JsonObject(); - params = new JsonObject(); - params.add("toToken", bnusdScore.getAddress().toString()); - jsonData.add("method", "_donate"); - jsonData.add("params", params); - dexScore.invoke(balnScore, "tokenFallback", ownerAccount.getAddress(), balnDonation, - jsonData.toString().getBytes()); - poolStats = (Map) dexScore.call("getPoolStats", poolId); - - assertEquals(initialBase.add(balnDonation), poolStats.get("base")); - assertEquals(initialQuote.add(bnusdDonation), poolStats.get("quote")); - assertEquals(initialSupply, poolStats.get("total_supply")); - } - - @Test - void tokenfallback_swapSicx() { - Account account = sm.createAccount(); - final String data = "{" + - "\"method\": \"_deposit\"" + - "}"; - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(eq(stakingScore.getAddress()), eq("getTodayRate"))).thenReturn(EXA); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - contextMock.when(() -> Context.transfer(any(Address.class), any(BigInteger.class))).then(invocationOnMock -> null); - - BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); - supplyIcxLiquidity(account, FIFTY.multiply(BigInteger.TEN)); - - // add liquidity pool - BigInteger poolId = BigInteger.valueOf(Const.SICXICX_POOL_ID); - BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - - // test swap - BigInteger swapValue = BigInteger.valueOf(50L).multiply(EXA); - swapSicxToIcx(account, swapValue, EXA); - - BigInteger newBalance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - BigInteger sicxEarning = (BigInteger) dexScore.call("getSicxEarnings", account.getAddress()); - assertEquals(balance.subtract(swapValue.subtract(swapValue.divide(BigInteger.valueOf(100L)))), newBalance); - assertEquals(swapValue.multiply(BigInteger.valueOf(997L)).divide(BigInteger.valueOf(1000L)), sicxEarning); - - } - - @Test - void onIRC31Received() { - // Arrange. - Account irc31Contract = Account.newScoreAccount(1); - Address operator = sm.createAccount().getAddress(); - Address from = sm.createAccount().getAddress(); - BigInteger id = BigInteger.ONE; - BigInteger value = BigInteger.valueOf(100).multiply(EXA); - byte[] data = new byte[0]; - String expectedErrorMessage = "Reverted(0): Balanced DEX: IRC31 Tokens not accepted"; - - // Act and assert. - Executable onIRC31Received = () -> dexScore.invoke(irc31Contract, "onIRC31Received", operator, from, id, - value, data); - expectErrorMessage(onIRC31Received, expectedErrorMessage); - } - - @Test - void transfer() { - Account account = sm.createAccount(); - Account account1 = sm.createAccount(); - - final String data = "{" + - "\"method\": \"_deposit\"" + - "}"; - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - - BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), - data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), - data.getBytes()); - dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, - FIFTY.divide(BigInteger.TWO), false, BigInteger.valueOf(100)); - - BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); - BigInteger transferValue = BigInteger.valueOf(5).multiply(EXA); - BigInteger initialValue = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - dexScore.invoke(account, "transfer", account1.getAddress(), transferValue, poolId, data.getBytes()); - - BigInteger value = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - assertEquals(value, initialValue.subtract(transferValue)); - value = (BigInteger) dexScore.call("balanceOf", account1.getAddress(), poolId); - assertEquals(value, transferValue); - } - - @Test - void transfer_toSelf() { - Account account = sm.createAccount(); - Account account1 = sm.createAccount(); - - final String data = "{" + - "\"method\": \"_deposit\"" + - "}"; - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - - BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), - data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), - data.getBytes()); - dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, - FIFTY.divide(BigInteger.TWO), false, BigInteger.valueOf(100)); - - BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); - BigInteger transferValue = BigInteger.valueOf(5).multiply(EXA); - BigInteger initialValue = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - dexScore.invoke(account, "transfer", account.getAddress(), transferValue, poolId, data.getBytes()); - - BigInteger value = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - assertEquals(initialValue, value); - } - - // In the process of going through this. - @Test - void swap_sicx_icx() { - // Arrange. - BigInteger value = BigInteger.valueOf(10000000).multiply(EXA); - BigInteger sicxIcxConversionRate = new BigInteger("1100758881004412705"); - BigInteger swapValue = BigInteger.valueOf(100).multiply(EXA); - - // Act. - supplyIcxLiquidity(ownerAccount, value); - supplyIcxLiquidity(sm.createAccount(), value); - supplyIcxLiquidity(sm.createAccount(), value); - swapSicxToIcx(ownerAccount, swapValue, sicxIcxConversionRate); - } - - @Test - void getTotalValue() { - Account account = sm.createAccount(); - - final String data = "{" + - "\"method\": \"_deposit\"" + - "}"; - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - - BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); - //deposit - BigInteger bnusdValue = BigInteger.valueOf(276L).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(100L).multiply(EXA); - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); - - dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); - BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); - - String marketName = "BALN/BNUSD"; - dexScore.invoke(governanceScore, "setMarketName", poolId, marketName); - BigInteger totalValue = (BigInteger) dexScore.call("getTotalValue", marketName, BigInteger.ONE); - BigInteger totalSupply = (BigInteger) dexScore.call("totalSupply", poolId); - assertEquals(totalSupply, totalValue); - } - - @SuppressWarnings("unchecked") - @Test - void getPoolStatsWithPair() { - Account account = sm.createAccount(); - - final String data = "{" + - "\"method\": \"_deposit\"" + - "}"; - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - - //deposit - BigInteger bnusdValue = BigInteger.valueOf(276L).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(100L).multiply(EXA); - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); - - // add liquidity pool - dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); - BigInteger poolId = (BigInteger) dexScore.call("getPoolId", bnusdScore.getAddress(), balnScore.getAddress()); - Map poolStats = (Map) dexScore.call("getPoolStatsForPair", - balnScore.getAddress(), bnusdScore.getAddress()); - - assertEquals(poolId, poolStats.get("id")); - assertEquals(balnScore.getAddress(), poolStats.get("base_token")); - assertEquals(bnusdScore.getAddress(), poolStats.get("quote_token")); - } - - @Test - void delegate() { - PrepDelegations prep = new PrepDelegations(); - prep._address = prep_address.getAddress(); - prep._votes_in_per = BigInteger.valueOf(100); - PrepDelegations[] preps = new PrepDelegations[]{prep}; - - contextMock.when(() -> Context.call(eq(stakingScore.getAddress()), eq("delegate"), any())).thenReturn( - "Staking delegate called"); - dexScore.invoke(governanceScore, "delegate", (Object) preps); - - contextMock.verify(() -> Context.call(eq(stakingScore.getAddress()), eq("delegate"), any())); - } - - @Test - void govWithdraw() { - Account account = sm.createAccount(); - Account account1 = sm.createAccount(); - - final String data = "{" + - "\"method\": \"_deposit\"" + - "}"; - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - - BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); - //deposit - BigInteger bnusdValue = BigInteger.valueOf(276L).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(100L).multiply(EXA); - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), balnValue, data.getBytes()); - dexScore.invoke(bnusdScore, "tokenFallback", account1.getAddress(), bnusdValue, data.getBytes()); - dexScore.invoke(balnScore, "tokenFallback", account1.getAddress(), balnValue, data.getBytes()); - // add liquidity pool - dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); - dexScore.invoke(account1, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); - - BigInteger poolId = (BigInteger) dexScore.call("getPoolId", bnusdScore.getAddress(), balnScore.getAddress()); - Map poolStats = (Map) dexScore.call("getPoolStats", poolId); - assertEquals(poolStats.get("base_token"), balnScore.getAddress()); - assertEquals(poolStats.get("quote_token"), bnusdScore.getAddress()); - assertEquals(poolStats.get("base"), balnValue.add(balnValue)); - assertEquals(poolStats.get("quote"), bnusdValue.add(bnusdValue)); - - BigInteger balnWithdrawAmount = BigInteger.TEN.pow(19); - dexScore.invoke(governanceScore, "govWithdraw", 2, balnScore.getAddress(), balnWithdrawAmount); - - BigInteger bnUSDWithdrawAmount = BigInteger.valueOf(3).multiply(BigInteger.TEN.pow(19)); - dexScore.invoke(governanceScore, "govWithdraw", 2, bnusdScore.getAddress(), bnUSDWithdrawAmount); - - poolStats = (Map) dexScore.call("getPoolStats", poolId); - contextMock.verify(() -> Context.call(eq(bnusdScore.getAddress()), eq("transfer"), eq(mockBalanced.daofund.getAddress()), - eq(bnUSDWithdrawAmount))); - contextMock.verify(() -> Context.call(eq(balnScore.getAddress()), eq("transfer"), eq(mockBalanced.daofund.getAddress()), - eq(balnWithdrawAmount))); - assertEquals(poolStats.get("base"), balnValue.add(balnValue).subtract(balnWithdrawAmount)); - assertEquals(poolStats.get("quote"), bnusdValue.add(bnusdValue).subtract(bnUSDWithdrawAmount)); - } - - // initial price of baln: 25/50 = 0.50 - // oracle protection is 18% that is 0.09 for 0.5 - // price of baln after swap: 27.xx/46.xx = 58.xx - // protection covered up to 0.50+0.09=0.59, should pass - @Test - void swap_ForOracleProtection() { - // Arrange - Account account = sm.createAccount(); - BigInteger points = BigInteger.valueOf(1800); - String symbolBase = "BALN"; - String symbolQuote = "bnUSD"; - supplyLiquidity(account, balnScore, bnusdScore, BigInteger.valueOf(50).multiply(EXA), - BigInteger.valueOf(25).multiply(EXA), true); - - contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); - contextMock.when(() -> Context.call(eq(bnusdScore.getAddress()), eq("symbol"))).thenReturn(symbolQuote); - contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolBase))).thenReturn(EXA.divide(BigInteger.TWO)); - contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolQuote))).thenReturn(EXA); - dexScore.invoke(governanceScore, "setOracleProtection", BigInteger.TWO, points); - - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - - BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); - BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - - Map fees = (Map) dexScore.call("getFees"); - Map poolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger oldFromToken = (BigInteger) poolStats.get("quote"); - BigInteger oldToToken = (BigInteger) poolStats.get("base"); - - BigInteger value = BigInteger.valueOf(2L).multiply(EXA); - BigInteger lp_fee = value.multiply(fees.get("pool_lp_fee")).divide(FEE_SCALE); - BigInteger baln_fee = value.multiply(fees.get("pool_baln_fee")).divide(FEE_SCALE); - BigInteger total_fee = lp_fee.add(baln_fee); - - BigInteger inputWithoutFees = value.subtract(total_fee); - BigInteger newFromToken = oldFromToken.add(inputWithoutFees); - - BigInteger newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); - BigInteger sendAmount = oldToToken.subtract(newToToken); - newFromToken = newFromToken.add(lp_fee); - - // Act - JsonObject jsonData = new JsonObject(); - JsonObject params = new JsonObject(); - params.add("minimumReceive", sendAmount.toString()); - params.add("toToken", balnScore.getAddress().toString()); - jsonData.add("method", "_swap"); - jsonData.add("params", params); - dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), value, jsonData.toString().getBytes()); - - // Assert - Map newPoolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger newBalance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - assertEquals(newFromToken, newPoolStats.get("quote")); - assertEquals(newToToken, newPoolStats.get("base")); - assertEquals(balance, newBalance); - - contextMock.verify(() -> Context.call(eq(bnusdScore.getAddress()), eq("transfer"), - eq(feehandlerScore.getAddress()), eq(baln_fee))); - } - - // initial price of baln: 25/50 = 0.50 - // oracle protection is 18% that is 0.08 for 0.5 - // price of baln after swap: 27.xx/46.xx = 58.xx - // protection covered up to 0.50+0.08=0.58, should fail - @Test - void swap_FailForOracleProtection() { - // Arrange - Account account = sm.createAccount(); - BigInteger points = BigInteger.valueOf(1600); - String symbolBase = "BALN"; - String symbolQuote = "bnUSD"; - supplyLiquidity(account, balnScore, bnusdScore, BigInteger.valueOf(50).multiply(EXA), - BigInteger.valueOf(25).multiply(EXA), true); - - contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); - contextMock.when(() -> Context.call(eq(bnusdScore.getAddress()), eq("symbol"))).thenReturn(symbolQuote); - contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolBase))).thenReturn(EXA.divide(BigInteger.TWO)); - contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolQuote))).thenReturn(EXA); - dexScore.invoke(governanceScore, "setOracleProtection", BigInteger.TWO, points); - - // Act - JsonObject jsonData = new JsonObject(); - JsonObject params = new JsonObject(); - params.add("minimumReceive", BigInteger.valueOf(2).toString()); - params.add("toToken", balnScore.getAddress().toString()); - jsonData.add("method", "_swap"); - jsonData.add("params", params); - Executable swapToFail = () -> dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.TWO.multiply(EXA), jsonData.toString().getBytes()); - - // Assert - expectErrorMessage(swapToFail, TAG + ": oracle protection price violated"); - } - - // initial price of baln: 25/25 = 1 - // oracle protection is 18% that is 0.18 for 1 - // price of baln after swap: 26.xx/23.xx = 1.16xx - // protection covered up to 1+0.18=1.18, should pass - @Test - void swap_ForOracleProtectionForBalnSicx() { - // Arrange - Account account = sm.createAccount(); - BigInteger points = BigInteger.valueOf(1800); - String symbolBase = "BALN"; - String symbolQuote = "sICX"; - supplyLiquidity(account, balnScore, sicxScore, BigInteger.valueOf(25).multiply(EXA), - BigInteger.valueOf(25).multiply(EXA), true); - - contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); - contextMock.when(() -> Context.call(eq(sicxScore.getAddress()), eq("symbol"))).thenReturn(symbolQuote); - contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolBase))).thenReturn(EXA); - contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolQuote))).thenReturn(EXA); - dexScore.invoke(governanceScore, "setOracleProtection", BigInteger.TWO, points); - - - contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); - contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); - contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), - any(BigInteger.class))).thenReturn(null); - contextMock.when(() -> Context.call(any(Address.class), eq("getTodayRate"))).thenReturn(EXA); - - BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), sicxScore.getAddress()); - BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - - Map fees = (Map) dexScore.call("getFees"); - Map poolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger oldFromToken = (BigInteger) poolStats.get("quote"); - BigInteger oldToToken = (BigInteger) poolStats.get("base"); - - BigInteger value = BigInteger.valueOf(2L).multiply(EXA); - BigInteger lp_fee = value.multiply(fees.get("pool_lp_fee")).divide(FEE_SCALE); - BigInteger baln_fee = value.multiply(fees.get("pool_baln_fee")).divide(FEE_SCALE); - BigInteger total_fee = lp_fee.add(baln_fee); - - BigInteger inputWithoutFees = value.subtract(total_fee); - BigInteger newFromToken = oldFromToken.add(inputWithoutFees); - - BigInteger newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); - BigInteger sendAmount = oldToToken.subtract(newToToken); - newFromToken = newFromToken.add(lp_fee); - - // Act - JsonObject jsonData = new JsonObject(); - JsonObject params = new JsonObject(); - params.add("minimumReceive", sendAmount.toString()); - params.add("toToken", balnScore.getAddress().toString()); - jsonData.add("method", "_swap"); - jsonData.add("params", params); - dexScore.invoke(sicxScore, "tokenFallback", account.getAddress(), value, jsonData.toString().getBytes()); - - // Assert - Map newPoolStats = (Map) dexScore.call("getPoolStats", poolId); - BigInteger newBalance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); - assertEquals(newFromToken, newPoolStats.get("quote")); - assertEquals(newToToken, newPoolStats.get("base")); - assertEquals(balance, newBalance); - - contextMock.verify(() -> Context.call(eq(sicxScore.getAddress()), eq("transfer"), - eq(feehandlerScore.getAddress()), eq(baln_fee))); - } - - // initial price of baln: 25/25 = 1 - // oracle protection is 16% that is 0.16 for 1 - // price of baln after swap: 26.xx/23.xx = 1.16xx - // protection covered up to 1+0.16=1.16, should fail - @Test - void swap_FailForOracleProtectionForBalnSicx() { - // Arrange - Account account = sm.createAccount(); - BigInteger points = BigInteger.valueOf(1600); - String symbolBase = "BALN"; - String symbolQuote = "sICX"; - supplyLiquidity(account, balnScore, sicxScore, BigInteger.valueOf(25).multiply(EXA), - BigInteger.valueOf(25).multiply(EXA), true); - - contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); - contextMock.when(() -> Context.call(eq(sicxScore.getAddress()), eq("symbol"))).thenReturn(symbolQuote); - contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolBase))).thenReturn(EXA); - contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolQuote))).thenReturn(EXA); - dexScore.invoke(governanceScore, "setOracleProtection", BigInteger.TWO, points); - contextMock.when(() -> Context.call(any(Address.class), eq("getTodayRate"))).thenReturn(EXA); - - // Act - JsonObject jsonData = new JsonObject(); - JsonObject params = new JsonObject(); - params.add("minimumReceive", BigInteger.valueOf(2L).toString()); - params.add("toToken", balnScore.getAddress().toString()); - jsonData.add("method", "_swap"); - jsonData.add("params", params); - Executable swapToFail = () -> dexScore.invoke(sicxScore, "tokenFallback", account.getAddress(), BigInteger.TWO.multiply(EXA), jsonData.toString().getBytes()); - - // Assert - expectErrorMessage(swapToFail, TAG + ": oracle protection price violated"); - } - - @AfterEach - void closeMock() { - contextMock.close(); - } -} \ No newline at end of file diff --git a/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestSettersAndGetters.java b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestSettersAndGetters.java deleted file mode 100644 index 4e4aede91..000000000 --- a/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestSettersAndGetters.java +++ /dev/null @@ -1,658 +0,0 @@ -/* - * 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. - */ - -package network.balanced.score.core.dex; - -import com.iconloop.score.test.Account; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import score.Address; -import score.Context; - -import java.math.BigInteger; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -import static network.balanced.score.lib.utils.BalancedAddressManager.getBalancedOracle; -import static network.balanced.score.lib.utils.Constants.EXA; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.doReturn; - - -public class DexTestSettersAndGetters extends DexTestBase { - - @BeforeEach - public void configureContract() throws Exception { - super.setup(); - } - - @Test - void testName() { - assertEquals(dexScore.call("name"), "Balanced DEX"); - } - - @Test - @SuppressWarnings("unchecked") - void setGetFees() { - // Arrange - fees to be set. - BigInteger poolLpFee = BigInteger.valueOf(100); - BigInteger poolBalnFee = BigInteger.valueOf(200); - BigInteger icxConversionFee = BigInteger.valueOf(300); - BigInteger icxBalnFee = BigInteger.valueOf(400); - - // Arrange - methods to be called and set specified fee. - Map fees = Map.of( - "setPoolLpFee", poolLpFee, - "setPoolBalnFee", poolBalnFee, - "setIcxConversionFee", icxConversionFee, - "setIcxBalnFee", icxBalnFee - ); - - // Arrange - expected result when retrieving fees". - Map expectedResult = Map.of( - "icx_total", icxBalnFee.add(icxConversionFee), - "pool_total", poolBalnFee.add(poolLpFee), - "pool_lp_fee", poolLpFee, - "pool_baln_fee", poolBalnFee, - "icx_conversion_fee", icxConversionFee, - "icx_baln_fee", icxBalnFee - ); - Map returnedFees; - - // Act & assert - set all fees and assert that all fee methods are only settable by governance. - for (Map.Entry fee : fees.entrySet()) { - dexScore.invoke(governanceScore, fee.getKey(), fee.getValue()); - assertOnlyCallableBy(governanceScore.getAddress(), dexScore, fee.getKey(), fee.getValue()); - } - - // Assert - retrieve all fees and check validity. - returnedFees = (Map) dexScore.call("getFees"); - assertEquals(expectedResult, returnedFees); - } - - @Test - @SuppressWarnings("unchecked") - void getBalanceAndSupply_normalPool() { - // Arrange - variables. - String poolName = "bnUSD/BALN"; - BigInteger poolId = BigInteger.TWO; - BigInteger mockBalance = BigInteger.valueOf(100); - BigInteger mockTotalSupply = BigInteger.valueOf(200); - Map expectedData = Map.of( - "_balance", mockBalance, - "_totalSupply", mockTotalSupply - ); - - // Arrange - Setup dex contract. - dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); - - // Arrange - Mock these calls to stakedLP contract. - contextMock.when(() -> Context.call(eq(stakedLPScore.getAddress()), eq("balanceOf"), - eq(ownerAccount.getAddress()), eq(poolId))).thenReturn(mockBalance); - contextMock.when(() -> Context.call(eq(stakedLPScore.getAddress()), eq("totalStaked"), eq(poolId))).thenReturn(mockTotalSupply); - - // Assert. - Map returnedData = (Map) dexScore.call("getBalanceAndSupply", - poolName, ownerAccount.getAddress().toString()); - assertEquals(expectedData, returnedData); - } - - @Test - @SuppressWarnings("unchecked") - void getBalanceAndSupply_sicxIcxPool() { - // Arrange. - Account supplier = sm.createAccount(); - BigInteger icxValue = BigInteger.valueOf(100).multiply(EXA); - String poolName = "sICX/ICX"; - - Map expectedData = Map.of( - "_balance", icxValue, - "_totalSupply", icxValue - ); - - // Act. - supplyIcxLiquidity(supplier, icxValue); - - // Assert. - Map returnedData = (Map) dexScore.call("getBalanceAndSupply", - poolName, supplier.getAddress().toString()); - assertEquals(expectedData, returnedData); - } - - @Test - void turnDexOnAndGetDexOn() { - dexScore.invoke(governanceScore, "turnDexOn"); - assertEquals(true, dexScore.call("getDexOn")); - assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "turnDexOn"); - } - - @Test - void addQuoteCoinAndCheckIfAllowed() { - // Arrange. - Address quoteCoin = Account.newScoreAccount(1).getAddress(); - - // Act. - dexScore.invoke(governanceScore, "addQuoteCoin", quoteCoin); - - // Assert. - Boolean quoteCoinAllowed = (Boolean) dexScore.call("isQuoteCoinAllowed", quoteCoin); - assertEquals(true, quoteCoinAllowed); - assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "addQuoteCoin", quoteCoin); - } - - @Test - void setGetTimeOffSet() { - // Arrange. - BigInteger timeOffset = BigInteger.valueOf(100); - - // Act. - dexScore.invoke(governanceScore, "setTimeOffset", timeOffset); - - // Assert. - BigInteger retrievedTimeOffset = (BigInteger) dexScore.call("getTimeOffset"); - assertEquals(timeOffset, retrievedTimeOffset); - assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "setTimeOffset", timeOffset); - } - - @Test - void setOracleProtection() { - // Arrange - BigInteger points = BigInteger.valueOf(30); - String symbolBase = "BALN"; - String symbolQuote = "bnUSD"; - - supplyLiquidity(sm.createAccount(), balnScore, bnusdScore, BigInteger.valueOf(30000).multiply(EXA), - BigInteger.valueOf(10000).multiply(EXA), true); - - contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); - contextMock.when(() -> Context.call(eq(bnusdScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); - contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolBase))).thenReturn(EXA.divide(BigInteger.TWO)); - contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolQuote))).thenReturn(EXA); - - // Act - dexScore.invoke(governanceScore, "setOracleProtection", BigInteger.TWO, points); - - // Asset - BigInteger oracleProtection = (BigInteger) dexScore.call("getOracleProtection", BigInteger.TWO); - assertEquals(oracleProtection, points); - } - - @Test - void getIcxBalance() { - // Arrange. - Account supplier = sm.createAccount(); - BigInteger value = BigInteger.valueOf(1000).multiply(EXA); - - // Act. - supplyIcxLiquidity(supplier, value); - - // Assert. - BigInteger IcxBalance = (BigInteger) dexScore.call("getICXBalance", supplier.getAddress()); - assertEquals(IcxBalance, value); - } - - @Test - void getSicxEarnings() { - // Supply liquidity to sicx/icx pool. - // Swap some sicx to icx. - // Get and verify earnings. - } - - @Test - void getDeposit() { - // Arrange. - Account depositor = sm.createAccount(); - BigInteger value = BigInteger.valueOf(100).multiply(EXA); - - // Act. - depositToken(depositor, bnusdScore, value); - - // Assert. - BigInteger retrievedValue = (BigInteger) dexScore.call("getDeposit", bnusdScore.getAddress(), - depositor.getAddress()); - assertEquals(value, retrievedValue); - - } - - @Test - void getNonce() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - BigInteger expectedNonce = BigInteger.valueOf(3); - - // Act. - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert. - BigInteger retrievedNonce = (BigInteger) dexScore.call("getNonce"); - assertEquals(expectedNonce, retrievedNonce); - } - - @Test - void getPoolId() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - BigInteger expectedPoolValue = BigInteger.TWO; - - // Act. - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert. - BigInteger retrievedPoolId = (BigInteger) dexScore.call("getPoolId", bnusdScore.getAddress(), - balnScore.getAddress()); - assertEquals(expectedPoolValue, retrievedPoolId); - } - - @Test - void lookupId() { - // Arrange. - String namedMarket = "sICX/ICX"; - BigInteger expectedId = BigInteger.ONE; - - // Assert. - BigInteger retrievedId = (BigInteger) dexScore.call("lookupPid", namedMarket); - assertEquals(expectedId, retrievedId); - } - - @Test - void getPoolTotal() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(10).pow(19); - BigInteger balnValue = BigInteger.valueOf(10).pow(19); - BigInteger poolId = BigInteger.TWO; - - // Act. - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert. - BigInteger retrievedBnusdValue = (BigInteger) dexScore.call("getPoolTotal", poolId, bnusdScore.getAddress()); - BigInteger retrievedBalnValue = (BigInteger) dexScore.call("getPoolTotal", poolId, balnScore.getAddress()); - assertEquals(bnusdValue, retrievedBnusdValue); - assertEquals(balnValue, retrievedBalnValue); - } - - @Test - void balanceOf_normalPool() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(10).pow(19); - BigInteger balnValue = BigInteger.valueOf(10).pow(19); - BigInteger userLpTokenValue = (bnusdValue.multiply(balnValue)).sqrt(); - BigInteger poolId = BigInteger.TWO; - - // Act. - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert. - BigInteger retrievedUserLpTokenValue = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), - poolId); - assertEquals(userLpTokenValue, retrievedUserLpTokenValue); - } - - @Test - void balanceOf_icxSicxPool() { - // Arrange. - BigInteger value = BigInteger.valueOf(100).multiply(EXA); - BigInteger poolId = BigInteger.ONE; - - // Act. - supplyIcxLiquidity(ownerAccount, value); - - // Assert. - BigInteger retrievedBalance = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), poolId); - assertEquals(value, retrievedBalance); - } - - @Test - void totalSupply_SicxIcxPool() { - // Arrange. - BigInteger value = BigInteger.valueOf(100).multiply(EXA); - BigInteger poolId = BigInteger.ONE; - - // Act. - supplyIcxLiquidity(ownerAccount, value); - - // Assert. - BigInteger totalIcxSupply = (BigInteger) dexScore.call("totalSupply", poolId); - assertEquals(value, totalIcxSupply); - } - - @Test - void totalSupply_normalPool() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(10).pow(19); - BigInteger balnValue = BigInteger.valueOf(10).pow(19); - BigInteger totalLpTokens = (bnusdValue.multiply(balnValue)).sqrt(); - BigInteger poolId = BigInteger.TWO; - - // Act. - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert. - BigInteger retrievedTotalLpTokens = (BigInteger) dexScore.call("totalSupply", poolId); - assertEquals(totalLpTokens, retrievedTotalLpTokens); - } - - @Test - @SuppressWarnings("unchecked") - void setGetMarketNames() { - // Arrange. - String poolName = "bnUSD/BALN"; - BigInteger poolId = BigInteger.TWO; - List expectedMarketNames = Arrays.asList("sICX/ICX", poolName); - - // Act. - dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); - - // Assert. - List namedPools = (List) dexScore.call("getNamedPools"); - assertEquals(expectedMarketNames, namedPools); - assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "setMarketName", poolId, poolName); - } - - @Test - void getPoolBaseAndGetPoolQuote() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(10).pow(19); - BigInteger balnValue = BigInteger.valueOf(10).pow(19); - - // Act. - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert. - Address poolBase = (Address) dexScore.call("getPoolBase", BigInteger.TWO); - Address poolQuote = (Address) dexScore.call("getPoolQuote", BigInteger.TWO); - assertEquals(poolBase, bnusdScore.getAddress()); - assertEquals(poolQuote, balnScore.getAddress()); - } - - @Test - void getQuotePriceInBase() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - BigInteger expectedPrice = computePrice(bnusdValue, balnValue); - BigInteger poolId = BigInteger.TWO; - - // Act. - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert. - BigInteger price = (BigInteger) dexScore.call("getQuotePriceInBase", poolId); - assertEquals(expectedPrice, price); - } - - @Test - void getBasePriceInQuote() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - BigInteger expectedPrice = computePrice(balnValue, bnusdValue); - BigInteger poolId = BigInteger.TWO; - - // Act. - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert. - BigInteger price = (BigInteger) dexScore.call("getBasePriceInQuote", poolId); - assertEquals(expectedPrice, price); - } - - @Test - void getPrice() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - BigInteger expectedPrice = computePrice(balnValue, bnusdValue); - BigInteger poolId = BigInteger.TWO; - - // Act. - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert. - BigInteger price = (BigInteger) dexScore.call("getPrice", poolId); - assertEquals(expectedPrice, price); - } - - @Test - void getBalnPrice() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - BigInteger expectedPrice = computePrice(balnValue, bnusdValue); - - // Act. - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert. - BigInteger price = (BigInteger) dexScore.call("getBalnPrice"); - assertEquals(expectedPrice, price); - } - - @Test - void getSicxBnusdPrice() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger sicxValue = BigInteger.valueOf(350).multiply(EXA); - BigInteger expectedPrice = computePrice(bnusdValue, sicxValue); - - // Act. - supplyLiquidity(ownerAccount, sicxScore, bnusdScore, sicxValue, bnusdValue, false); - - // Assert. - BigInteger price = (BigInteger) dexScore.call("getSicxBnusdPrice"); - assertEquals(expectedPrice, price); - } - - @Test - void getBnusdValue_sicxIcxPool() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger sicxValue = BigInteger.valueOf(350).multiply(EXA); - BigInteger icxValue = BigInteger.valueOf(10).multiply(EXA); - BigInteger sicxIcxConversionRate = BigInteger.valueOf(10).multiply(EXA); - String poolName = "sICX/ICX"; - BigInteger expectedPoolValue = - (computePrice(bnusdValue, sicxValue).multiply(icxValue)).divide(sicxIcxConversionRate); - doReturn(sicxIcxConversionRate).when(dexScoreSpy).getSicxRate(); - - // Act. - supplyLiquidity(ownerAccount, sicxScore, bnusdScore, sicxValue, bnusdValue, false); - supplyIcxLiquidity(ownerAccount, icxValue); - - // Assert. - BigInteger poolValue = (BigInteger) dexScore.call("getBnusdValue", poolName); - assertEquals(expectedPoolValue, poolValue); - } - - // @Test - // void getBnusdValue_sicxIsQuote() { - // // Arrange. - // BigInteger balnValue = BigInteger.valueOf(195).multiply(EXA); - // BigInteger sicxValue = BigInteger.valueOf(350).multiply(EXA); - // String poolName = "bnUSD/sICX"; - // BigInteger poolId = BigInteger.TWO; - // BigInteger sicxBnusdPrice = BigInteger.valueOf(10).multiply(EXA); - // BigInteger expectedValue = (sicxValue.multiply(BigInteger.TWO).multiply(sicxBnusdPrice)).divide(EXA); - // doReturn(sicxBnusdPrice).when(dexScoreSpy).getSicxBnusdPrice(); - - // // Act. Why can I not supply with sicx as quote currency? Fails. - // dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); - // supplyLiquidity(ownerAccount, bnusdScore, sicxScore, balnValue, sicxValue, false); - - // // Assert. - // //BigInteger poolValue = (BigInteger) dexScore.call( "getBnusdValue", poolName); - // //assertEquals(expectedValue, poolValue); - // } - - @Test - void getBnusdValue_bnusdIsQuote() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - BigInteger expectedValue = BigInteger.valueOf(195).multiply(EXA).multiply(BigInteger.TWO); - String poolName = "bnUSD/BALN"; - BigInteger poolId = BigInteger.TWO; - - // Act. - dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); - supplyLiquidity(ownerAccount, balnScore, bnusdScore, balnValue, bnusdValue, false); - - // Assert. - BigInteger poolValue = (BigInteger) dexScore.call("getBnusdValue", poolName); - assertEquals(expectedValue, poolValue); - } - - @Test - void getBnusdValue_QuoteNotSupported() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - String poolName = "bnUSD/BALN"; - BigInteger poolId = BigInteger.TWO; - - // Act. - dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); - supplyLiquidity(ownerAccount, bnusdScore, balnScore, balnValue, bnusdValue, false); - - // Assert - BigInteger poolValue = (BigInteger) dexScore.call("getBnusdValue", "bnUSD/BALN"); - assertEquals(BigInteger.ZERO, poolValue); - } - - @Test - void getPriceByName() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - String poolName = "bnUSD/BALN"; - BigInteger poolId = BigInteger.TWO; - BigInteger expectedPrice = computePrice(balnValue, bnusdValue); - - // Act. - dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert - BigInteger price = (BigInteger) dexScore.call("getPriceByName", "bnUSD/BALN"); - assertEquals(expectedPrice, price); - } - - @Test - void getPoolName() { - // Arrange. - String poolName = "bnUSD/BALN"; - BigInteger poolId = BigInteger.TWO; - - // Act. - dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); - - // Assert. - String retrievedPoolName = (String) dexScore.call("getPoolName", poolId); - assertEquals(poolName, retrievedPoolName); - } - - @Test - @SuppressWarnings("unchecked") - void getPoolStats_notSicxIcxPool() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - BigInteger expectedPrice = computePrice(balnValue, bnusdValue); - String poolName = "bnUSD/BALN"; - BigInteger poolId = BigInteger.TWO; - BigInteger tokenDecimals = BigInteger.valueOf(18); - BigInteger minQuote = BigInteger.ZERO; - BigInteger totalLpTokens = new BigInteger("261247009552262626468"); - - Map expectedPoolStats = Map.of( - "base", bnusdValue, - "quote", balnValue, - "base_token", bnusdScore.getAddress(), - "quote_token", balnScore.getAddress(), - "total_supply", totalLpTokens, - "price", expectedPrice, - "name", poolName, - "base_decimals", tokenDecimals, - "quote_decimals", tokenDecimals, - "min_quote", minQuote - ); - - // Act. - dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert. - Map poolStats = (Map) dexScore.call("getPoolStats", poolId); - assertEquals(expectedPoolStats, poolStats); - } - - @Test - void getTotalDexAddresses() { - // Arrange. - BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); - BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); - BigInteger poolId = BigInteger.TWO; - - // Act. - supplyLiquidity(governanceScore, bnusdScore, balnScore, bnusdValue, balnValue, false); - supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); - - // Assert - BigInteger totalDexAddresses = (BigInteger) dexScore.call("totalDexAddresses", BigInteger.TWO); - assertEquals(BigInteger.TWO, totalDexAddresses); - } - - @Test - void permit_OnlyGovernance() { - // Arrange. - BigInteger poolId = BigInteger.ONE; - Boolean permission = true; - - // Assert. - assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "permit", poolId, permission); - } - - @Test - void govWithdraw() { - assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "govWithdraw", 1, dexScore.getAddress(), BigInteger.ZERO); - } - - @Test - void govSetPoolTotal() { - assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "govSetPoolTotal", 1, BigInteger.ZERO); - - } - - @Test - void govSetUserPoolTotal() { - assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "govSetUserPoolTotal", 1, dexScore.getAddress(), BigInteger.ZERO); - } - - @Test - void govSetOracleProtection() { - assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "setOracleProtection", BigInteger.ZERO, BigInteger.TWO); - } - - @AfterEach - void closeMock() { - contextMock.close(); - } -} \ No newline at end of file diff --git a/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/db/LinkedListDBTest.java b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/db/LinkedListDBTest.java deleted file mode 100644 index 3e04035e8..000000000 --- a/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/db/LinkedListDBTest.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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. - */ - -package network.balanced.score.core.dex.db; - -import com.iconloop.score.test.Account; -import com.iconloop.score.test.Score; -import com.iconloop.score.test.ServiceManager; -import com.iconloop.score.test.TestBase; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import score.Address; -import scorex.util.ArrayList; - -import java.math.BigInteger; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public class LinkedListDBTest extends TestBase { - - private static final ServiceManager sm = getServiceManager(); - private static final Account owner = sm.createAccount(); - private static final Address user1 = sm.createAccount().getAddress(); - private static final Address user2 = sm.createAccount().getAddress(); - private static final Address user3 = sm.createAccount().getAddress(); - private static final Address user4 = sm.createAccount().getAddress(); - private static final Address user5 = sm.createAccount().getAddress(); - private static final Address user6 = sm.createAccount().getAddress(); - private static final Address user7 = sm.createAccount().getAddress(); - - private static Score dummyScore; - - public static class DummyScore { - - LinkedListDB linkedListDB = new LinkedListDB("linked_list_db"); - - public DummyScore() { - - } - - public void appendUsers(ArrayList
addresses) { - int id = 1; - for (Address address : addresses) { - linkedListDB.append(BigInteger.valueOf(3), address, BigInteger.valueOf(id++)); - } - } - - public void appendUserWithId(Address address, BigInteger id) { - linkedListDB.append(BigInteger.valueOf(3), address, id); - } - - public BigInteger size() { - return linkedListDB.size(); - } - - public Map headNode() { - NodeDB node = linkedListDB.getHeadNode(); - return Map.of("user", node.getUser(), "size", node.getSize(), "prev", node.getPrev(), "next", - node.getNext()); - } - - public Map tailNode() { - NodeDB node = linkedListDB.getTailNode(); - return Map.of("user", node.getUser(), "size", node.getSize(), "prev", node.getPrev(), "next", - node.getNext()); - } - - public void updateNode(BigInteger nodeId, BigInteger size, Address user) { - linkedListDB.updateNode(nodeId, size, user); - } - - public Map getNode(BigInteger nodeId) { - NodeDB node = linkedListDB.getNode(nodeId); - return Map.of("user", node.getUser(), "size", node.getSize(), "prev", node.getPrev(), "next", - node.getNext()); - } - - public void removeHead() { - linkedListDB.removeHead(); - } - - public void removeTail() { - linkedListDB.removeTail(); - } - - public void remove(BigInteger nodeId) { - linkedListDB.remove(nodeId); - } - - public void clear() { - linkedListDB.clear(); - } - - } - - @BeforeAll - public static void setup() throws Exception { - dummyScore = sm.deploy(owner, DummyScore.class); - } - - @SuppressWarnings("unchecked") - @Test - public void testUnorderedAddressBag() { - List
users = new ArrayList<>(); - users.add(user1); - users.add(user2); - users.add(user3); - users.add(user4); - users.add(user5); - dummyScore.invoke(owner, "appendUsers", users); - assertEquals(BigInteger.valueOf(5), dummyScore.call("size")); - dummyScore.invoke(owner, "removeHead"); - assertEquals(BigInteger.valueOf(4), dummyScore.call("size")); - dummyScore.invoke(owner, "removeTail"); - assertEquals(BigInteger.valueOf(3), dummyScore.call("size")); - - dummyScore.invoke(owner, "remove", BigInteger.valueOf(3)); - assertEquals(BigInteger.valueOf(2), dummyScore.call("size")); - Map nodeDB = (Map) dummyScore.call("getNode", BigInteger.TWO); - assertEquals(nodeDB.get("size"), BigInteger.valueOf(3)); - assertEquals(nodeDB.get("user"), user2); - assertEquals(nodeDB.get("prev"), BigInteger.ZERO); - assertEquals(nodeDB.get("next"), BigInteger.valueOf(4)); - - dummyScore.invoke(owner, "updateNode", BigInteger.TWO, BigInteger.valueOf(5), user6); - Map nodeTwo = (Map) dummyScore.call("getNode", BigInteger.TWO); - assertEquals(nodeTwo.get("size"), BigInteger.valueOf(5)); - assertEquals(nodeTwo.get("user"), user6); - assertEquals(nodeDB.get("prev"), BigInteger.ZERO); - assertEquals(nodeTwo.get("next"), BigInteger.valueOf(4)); - - Map headNode = (Map) dummyScore.call("headNode"); - Map tailNode = (Map) dummyScore.call("tailNode"); - assertEquals(headNode.get("user"), user6); - assertEquals(headNode.get("next"), BigInteger.valueOf(4)); - - - assertEquals(tailNode.get("user"), user4); - assertEquals(tailNode.get("prev"), BigInteger.valueOf(2)); - - AssertionError e = Assertions.assertThrows(AssertionError.class, () -> dummyScore.call("getNode", BigInteger.valueOf(7))); - assertEquals(e.getMessage(), "Reverted(0): Linked List linked_list_db_LINKED_LISTDB Node not found of nodeId " - + "7"); - - e = Assertions.assertThrows(AssertionError.class, () -> dummyScore.invoke(owner, "appendUserWithId", user7, BigInteger.TWO)); - assertEquals(e.getMessage(), "Reverted(0): Linked List linked_list_db_LINKED_LISTDB already exists of nodeId" + - "." + "2"); - } - - -} 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"; From 0a4de2ed05627c8e68a3eaea2d88f2234eb4a251 Mon Sep 17 00:00:00 2001 From: Spl3en Date: Wed, 7 Aug 2024 17:13:58 +0200 Subject: [PATCH 2/3] Relocate Concentrated Liquidity Pool code next to DEX code --- .gitignore | 1 + core-contracts/Dex/build.gradle | 1 + .../score/core/dex/DexIntegrationTest.java | 385 +++++++ .../LpTransferableOnContinuousModeTest.java | 163 +++ .../score/core/dex/MultipleAddTest.java | 231 ++++ .../core/dex/NonStakedLPRewardsTest.java | 168 +++ .../score/core/dex/SwapRemoveAndFeeTest.java | 197 ++++ .../balanced/score/core/dex/AbstractDex.java | 820 +++++++++++++ ...ol.java => ConcentratedLiquidityPool.java} | 35 +- ... => ConcentratedLiquidityPoolFactory.java} | 28 +- .../score/core/dex/DexDBVariables.java | 151 +++ .../balanced/score/core/dex/DexImpl.java | 519 +++++++++ .../score/core/dex/db/LinkedListDB.java | 202 ++++ .../balanced/score/core/dex/db/NodeDB.java | 92 ++ .../pool/IBalancedMintCallback.java | 4 +- .../pool/IBalancedSwapCallback.java | 4 +- ...l.java => IConcentratedLiquidityPool.java} | 2 +- ... => IConcentratedLiquidityPoolCallee.java} | 2 +- ...> IConcentratedLiquidityPoolDeployer.java} | 2 +- .../dex/{librairies => libs}/BitMath.java | 2 +- .../{librairies => libs}/FixedPoint128.java | 2 +- .../{librairies => libs}/FixedPoint96.java | 2 +- .../dex/{librairies => libs}/FullMath.java | 2 +- .../{librairies => libs}/LiquidityMath.java | 2 +- .../dex/{librairies => libs}/OracleLib.java | 2 +- .../dex/{librairies => libs}/PositionLib.java | 2 +- .../{librairies => libs}/SqrtPriceMath.java | 2 +- .../dex/{librairies => libs}/SwapMath.java | 2 +- .../dex/{librairies => libs}/TickLib.java | 2 +- .../dex/{librairies => libs}/TickMath.java | 2 +- .../dex/{librairies => libs}/UnsafeMath.java | 2 +- ...=> ConcentratedLiquidityPoolDeployer.java} | 2 +- .../score/core/dex/models/Observations.java | 2 +- .../score/core/dex/models/TickBitmap.java | 2 +- .../balanced/score/core/dex/models/Ticks.java | 2 +- .../balanced/score/core/dex/utils/Check.java | 47 + .../balanced/score/core/dex/utils/Const.java | 41 + .../score/core/dex/utils/LPMetadataDB.java | 29 + .../balanced/score/core/dex/DexTestBase.java | 150 +++ .../balanced/score/core/dex/DexTestCore.java | 1012 +++++++++++++++++ .../core/dex/DexTestSettersAndGetters.java | 658 +++++++++++ .../score/core/dex/db/LinkedListDBTest.java | 167 +++ 42 files changed, 5088 insertions(+), 55 deletions(-) create mode 100644 core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/DexIntegrationTest.java create mode 100644 core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/LpTransferableOnContinuousModeTest.java create mode 100644 core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/MultipleAddTest.java create mode 100644 core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/NonStakedLPRewardsTest.java create mode 100644 core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/SwapRemoveAndFeeTest.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/AbstractDex.java rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{BalancedPool.java => ConcentratedLiquidityPool.java} (97%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{BalancedPoolFactory.java => ConcentratedLiquidityPoolFactory.java} (92%) create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexDBVariables.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexImpl.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/LinkedListDB.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/NodeDB.java rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/{IBalancedPool.java => IConcentratedLiquidityPool.java} (99%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/{IBalancedPoolCallee.java => IConcentratedLiquidityPoolCallee.java} (96%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/{IBalancedPoolDeployer.java => IConcentratedLiquidityPoolDeployer.java} (96%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/BitMath.java (98%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/FixedPoint128.java (94%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/FixedPoint96.java (94%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/FullMath.java (99%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/LiquidityMath.java (96%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/OracleLib.java (96%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/PositionLib.java (98%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/SqrtPriceMath.java (99%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/SwapMath.java (99%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/TickLib.java (95%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/TickMath.java (99%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/{librairies => libs}/UnsafeMath.java (94%) rename core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/{BalancedPoolDeployer.java => ConcentratedLiquidityPoolDeployer.java} (95%) create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Check.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Const.java create mode 100644 core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/LPMetadataDB.java create mode 100644 core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestBase.java create mode 100644 core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestCore.java create mode 100644 core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestSettersAndGetters.java create mode 100644 core-contracts/Dex/src/test/java/network/balanced/score/core/dex/db/LinkedListDBTest.java 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/build.gradle b/core-contracts/Dex/build.gradle index a849ee0bf..37ee967af 100644 --- a/core-contracts/Dex/build.gradle +++ b/core-contracts/Dex/build.gradle @@ -104,6 +104,7 @@ deployJar { keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' parameters { + arg("_governance", Addresses.mainnet.governance) } } diff --git a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/DexIntegrationTest.java b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/DexIntegrationTest.java new file mode 100644 index 000000000..9238ee571 --- /dev/null +++ b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/DexIntegrationTest.java @@ -0,0 +1,385 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex; + +import com.eclipsesource.json.JsonArray; +import foundation.icon.icx.Wallet; +import foundation.icon.jsonrpc.Address; +import foundation.icon.score.client.DefaultScoreClient; +import network.balanced.score.lib.interfaces.*; +import network.balanced.score.lib.interfaces.dex.DexTestScoreClient; +import network.balanced.score.lib.test.integration.Balanced; +import network.balanced.score.lib.test.integration.Env; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.math.BigInteger; +import java.util.Map; + +import static foundation.icon.score.client.DefaultScoreClient._deploy; +import static network.balanced.score.core.dex.utils.Const.SICXICX_MARKET_NAME; +import static network.balanced.score.lib.test.integration.BalancedUtils.createParameter; +import static network.balanced.score.lib.test.integration.BalancedUtils.createTransaction; +import static network.balanced.score.lib.test.integration.ScoreIntegrationTest.createWalletWithBalance; +import static network.balanced.score.lib.utils.Constants.EXA; +import static org.junit.jupiter.api.Assertions.*; + +class DexIntegrationTest { + + private static final Env.Chain chain = Env.getDefaultChain(); + private static Balanced balanced; + private static Wallet userWallet; + private static Wallet tUserWallet; + private static Wallet testOwnerWallet; + + private static DefaultScoreClient dexScoreClient; + private static DefaultScoreClient governanceScoreClient; + private static DefaultScoreClient stakingScoreClient; + private static DefaultScoreClient sIcxScoreClient; + private static DefaultScoreClient balnScoreClient; + private static DefaultScoreClient rewardsScoreClient; + private static DefaultScoreClient tokenAClient; + private static DefaultScoreClient tokenBClient; + private static DefaultScoreClient tokenCClient; + private static DefaultScoreClient tokenDClient; + private static DefaultScoreClient daoFundScoreClient; + + private static final File jarfile = new File("src/intTest/java/network/balanced/score/core/dex/testtokens" + + "/DexIntTestToken.jar"); + + static { + try { + tUserWallet = createWalletWithBalance(BigInteger.valueOf(500).multiply(EXA)); + userWallet = createWalletWithBalance(BigInteger.valueOf(800).multiply(EXA)); + + balanced = new Balanced(); + testOwnerWallet = balanced.owner; + + tokenAClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, jarfile.getPath(), + Map.of("name", "Test Token", "symbol", "TT")); + tokenBClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, jarfile.getPath(), + Map.of("name", "Test Base Token", "symbol", "TB")); + tokenCClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, jarfile.getPath(), + Map.of("name", "Test Third Token", "symbol", "TTD")); + tokenDClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, jarfile.getPath(), + Map.of("name", "Test Fourth Token", "symbol", "TFD")); + + balanced.setupBalanced(); + dexScoreClient = balanced.dex; + governanceScoreClient = balanced.governance; + stakingScoreClient = balanced.staking; + sIcxScoreClient = balanced.sicx; + balnScoreClient = balanced.baln; + rewardsScoreClient = balanced.rewards; + daoFundScoreClient = balanced.daofund; + + Rewards rewards = new RewardsScoreClient(balanced.rewards); + Loans loans = new LoansScoreClient(balanced.loans); + BalancedToken baln = new BalancedTokenScoreClient(balanced.baln); + Sicx sicx = new SicxScoreClient(balanced.sicx); + StakedLP stakedLp = new StakedLPScoreClient(balanced.stakedLp); + + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Error on init test: " + e.getMessage()); + } + + } + + private static final Address tokenAAddress = tokenAClient._address(); + private static final Address tokenBAddress = tokenBClient._address(); + private static final Address tokenCAddress = tokenCClient._address(); + private static final Address tokenDAddress = tokenDClient._address(); + + private static final Address userAddress = Address.of(userWallet); + private static final Address tUserAddress = Address.of(tUserWallet); + + private static final DexTestScoreClient ownerDexTestScoreClient = new DexTestScoreClient(chain.getEndpointURL(), + chain.networkId, testOwnerWallet, tokenAAddress); + private static final DexTestScoreClient ownerDexTestBaseScoreClient = new DexTestScoreClient(chain.getEndpointURL(), + chain.networkId, testOwnerWallet, tokenBAddress); + private static final DexTestScoreClient ownerDexTestThirdScoreClient = + new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, tokenCAddress); + private static final DexTestScoreClient ownerDexTestFourthScoreClient = + new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, tokenDAddress); + private static final DexScoreClient dexUserScoreClient = new DexScoreClient(dexScoreClient.endpoint(), + dexScoreClient._nid(), userWallet, dexScoreClient._address()); + private static final Staking userStakeScoreClient = new StakingScoreClient(dexScoreClient.endpoint(), + dexScoreClient._nid(), userWallet, stakingScoreClient._address()); + private static final SicxScoreClient userSicxScoreClient = new SicxScoreClient(dexScoreClient.endpoint(), + dexScoreClient._nid(), userWallet, sIcxScoreClient._address()); + static Rewards userWalletRewardsClient = new RewardsScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), + userWallet, rewardsScoreClient._address()); + private static final BalancedTokenScoreClient userBalnScoreClient = + new BalancedTokenScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, + balnScoreClient._address()); + private static final DexTestScoreClient userDexTestScoreClient = new DexTestScoreClient(dexScoreClient.endpoint(), + dexScoreClient._nid(), userWallet, tokenAAddress); + private static final DexTestScoreClient userDexTestBaseScoreClient = + new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, tokenBAddress); + private static final DexTestScoreClient userDexTestThirdScoreClient = + new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, tokenCAddress); + private static final DexTestScoreClient userDexTestFourthScoreClient = + new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, tokenDAddress); + + private static final GovernanceScoreClient governanceDexScoreClient = + new GovernanceScoreClient(governanceScoreClient); + private static final RewardsScoreClient userRewardScoreClient = new RewardsScoreClient(rewardsScoreClient); + private static final DAOfundScoreClient userDaoFundScoreClient = new DAOfundScoreClient(daoFundScoreClient); + + private static final DefaultScoreClient userClient = new DefaultScoreClient(chain.getEndpointURL(), + chain.networkId, userWallet, DefaultScoreClient.ZERO_ADDRESS); + + private static final DefaultScoreClient tUserClient = new DefaultScoreClient(chain.getEndpointURL(), + chain.networkId, tUserWallet, DefaultScoreClient.ZERO_ADDRESS); + + + @Test + @Order(3) + void testICXTransferSwapEarningAndCancelOrder() { + assertEquals(SICXICX_MARKET_NAME, dexUserScoreClient.getPoolName(BigInteger.ONE)); + BigInteger defaultPoolId = dexUserScoreClient.lookupPid(SICXICX_MARKET_NAME); + assertEquals(BigInteger.ONE, defaultPoolId); + + Map poolStats = dexUserScoreClient.getPoolStats(defaultPoolId); + assertEquals(poolStats.get("base_token").toString(), sIcxScoreClient._address().toString()); + assertNull(poolStats.get("quote_token")); + assertEquals(hexToBigInteger(poolStats.get("base").toString()), BigInteger.ZERO); + assertEquals(hexToBigInteger(poolStats.get("quote").toString()), BigInteger.ZERO); + assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()), BigInteger.ZERO); + + //test icx transfer and verify stats + balanced.syncDistributions(); + userClient._transfer(dexScoreClient._address(), BigInteger.valueOf(200).multiply(EXA), null); + poolStats = dexUserScoreClient.getPoolStats(defaultPoolId); + + assertEquals(poolStats.get("base_token").toString(), sIcxScoreClient._address().toString()); + assertNull(poolStats.get("quote_token")); + assertEquals(hexToBigInteger(poolStats.get("base").toString()), BigInteger.ZERO); + assertEquals(hexToBigInteger(poolStats.get("quote").toString()), BigInteger.valueOf(200).multiply(EXA)); + assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()), BigInteger.valueOf(200).multiply(EXA)); + BigInteger beforeSwapPrice = hexToBigInteger(poolStats.get("price").toString()); + + //test swap + byte[] data = "testData".getBytes(); + ((StakingScoreClient) userStakeScoreClient).stakeICX(BigInteger.valueOf(100).multiply(EXA), userAddress, data); + + byte[] swapIcx = "{\"method\":\"_swap_icx\",\"params\":{\"none\":\"none\"}}".getBytes(); + userSicxScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(50).multiply(EXA), swapIcx); + + poolStats = dexUserScoreClient.getPoolStats(defaultPoolId); + BigInteger afterSwapPrice = hexToBigInteger(poolStats.get("price").toString()); + // price should be same after swap + assertEquals(beforeSwapPrice, afterSwapPrice); + + defaultPoolId = dexUserScoreClient.lookupPid(SICXICX_MARKET_NAME); + poolStats = dexUserScoreClient.getPoolStats(defaultPoolId); + + assertEquals(poolStats.get("base_token").toString(), sIcxScoreClient._address().toString()); + assertNull(poolStats.get("quote_token")); + assertEquals(hexToBigInteger(poolStats.get("base").toString()), BigInteger.ZERO); + assertEquals(hexToBigInteger(poolStats.get("quote").toString()).divide(EXA), BigInteger.valueOf(150)); + assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()).divide(EXA), BigInteger.valueOf(150)); + + System.out.println(" day is: " + dexUserScoreClient.getDay()); + waitForADay(); + //release lock by distributing rewards + balanced.syncDistributions(); + //verify sicx earning and make withdraw + BigInteger sicxEarning = dexUserScoreClient.getSicxEarnings(userAddress); + assertNotNull(sicxEarning); + dexUserScoreClient.withdrawSicxEarnings(sicxEarning); + + balanced.syncDistributions(); + dexUserScoreClient.cancelSicxicxOrder(); + } + + /*@Test + @Order(4) + void testBalnPoolTokenTransferableOnContinuousRewards(){ + + if(dexUserScoreClient.getContinuousRewardsDay()==null) { + governanceDexScoreClient.setContinuousRewardsDay(dexUserScoreClient.getDay().add(BigInteger.ONE)); + } + waitForADay(); + balanced.syncDistributions(); + //continuous starts + byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); + mintAndTransferTestTokens(tokenDeposit); + dexUserScoreClient.add(Address.fromString(dexTestBaseScoreAddress), Address.fromString + (dexTestFourthScoreClient._address().toString()), BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf + (50).multiply(EXA), false); + BigInteger poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestBaseScoreAddress), Address + .fromString(dexTestFourthScoreAddress)); + //assert pool id is less than 5 + assert poolId.compareTo(BigInteger.valueOf(6)) < 0; + BigInteger liquidity = (BigInteger.valueOf(50).multiply(EXA).multiply(BigInteger.valueOf(50).multiply(EXA))) + .sqrt(); + BigInteger balance = dexUserScoreClient.balanceOf(userAddress, poolId); + BigInteger tUsersPrevBalance = dexUserScoreClient.balanceOf(tUserAddress, poolId); + + assertEquals(balance, liquidity); + dexUserScoreClient.transfer(tUserAddress, BigInteger.valueOf(5).multiply(EXA), poolId, new byte[0]); + BigInteger tUsersBalance = dexUserScoreClient.balanceOf(tUserAddress, poolId); + assertEquals(tUsersPrevBalance.add(BigInteger.valueOf(5).multiply(EXA)), tUsersBalance); + }*/ + + @Test + @Order(6) + void testWithdraw() { + byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); + this.mintAndTransferTestTokens(tokenDeposit); + BigInteger withdrawAMount = BigInteger.valueOf(50); + BigInteger balanceBeforeWithdraw = dexUserScoreClient.depositOfUser(userAddress, tokenAAddress); + //withdraw test token + dexUserScoreClient.withdraw(tokenAAddress, withdrawAMount); + + BigInteger balanceAfterWithdraw = dexUserScoreClient.depositOfUser(userAddress, tokenAAddress); + + assert balanceBeforeWithdraw.equals(balanceAfterWithdraw.add(withdrawAMount)); + } + + @Test + @Order(7) + void testLpTokensAndTransfer() { + byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); + mintAndTransferTestTokens(tokenDeposit); + transferSicxToken(); + dexUserScoreClient.add(tokenBAddress, Address.fromString(sIcxScoreClient._address().toString()), + BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(25).multiply(EXA), false, BigInteger.valueOf(100)); + mintAndTransferTestTokens(tokenDeposit); + transferSicxToken(); + dexUserScoreClient.add(Address.fromString(sIcxScoreClient._address().toString()), tokenAAddress, + BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(25).multiply(EXA), false, BigInteger.valueOf(100)); + mintAndTransferTestTokens(tokenDeposit); + transferSicxToken(); + dexUserScoreClient.add(tokenBAddress, tokenAAddress, BigInteger.valueOf(50).multiply(EXA), + BigInteger.valueOf(25).multiply(EXA), false, BigInteger.valueOf(100)); + mintAndTransferTestTokens(tokenDeposit); + transferSicxToken(); + dexUserScoreClient.add(Address.fromString(sIcxScoreClient._address().toString()), tokenCAddress, + BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(25).multiply(EXA), false, BigInteger.valueOf(100)); + mintAndTransferTestTokens(tokenDeposit); + transferSicxToken(); + dexUserScoreClient.add(tokenBAddress, tokenCAddress, BigInteger.valueOf(50).multiply(EXA), + BigInteger.valueOf(25).multiply(EXA), false, BigInteger.valueOf(100)); + + waitForADay(); + + //take pool id > 5 so that it can be transferred + BigInteger poolId = dexUserScoreClient.getPoolId(tokenBAddress, tokenCAddress); + System.out.println(poolId); + BigInteger balance = dexUserScoreClient.balanceOf(userAddress, poolId); + assertNotEquals(balance, BigInteger.ZERO); + dexUserScoreClient.transfer(Address.fromString(tUserAddress.toString()), BigInteger.valueOf(5).multiply(EXA), + poolId, new byte[0]); + BigInteger tUsersBalance = dexUserScoreClient.balanceOf(Address.fromString(tUserAddress.toString()), poolId); + assert BigInteger.ZERO.compareTo(tUsersBalance) < 0; + + } + + @Test + @Order(8) + void testNonContinuousAndContinuousReward() { + balanced.syncDistributions(); + BigInteger balnHolding = userRewardScoreClient.getBalnHolding(tUserAddress.toString()); + tUserClient._transfer(dexScoreClient._address(), BigInteger.valueOf(200).multiply(EXA), null); + + + balanced.syncDistributions(); + System.out.println("Baln total supply is: " + userBalnScoreClient.totalSupply()); + BigInteger updatedBalnHolding = userRewardScoreClient.getBalnHolding(tUserAddress.toString()); + System.out.println("baln holding: " + balnHolding); + System.out.println("updated baln holding: " + updatedBalnHolding); + assert balnHolding.compareTo(updatedBalnHolding) < 0; + BigInteger beforeSleepDay = dexUserScoreClient.getDay(); + try { + Thread.sleep(5000); //wait some time + } catch (Exception e) { + System.out.println(e.getMessage()); + } + + BigInteger nextUpdatedBalnHolding = userRewardScoreClient.getBalnHolding(tUserAddress.toString()); + assertEquals(beforeSleepDay, dexUserScoreClient.getDay()); + + System.out.println("updated baln holding: " + updatedBalnHolding); + System.out.println("next updated baln holding: " + nextUpdatedBalnHolding); + assert updatedBalnHolding.compareTo(nextUpdatedBalnHolding) < 0; + + } + + void transferSicxToken() { + byte[] data = "testData".getBytes(); + ((StakingScoreClient) userStakeScoreClient).stakeICX(BigInteger.valueOf(80).multiply(EXA), userAddress, data); + + byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); + userSicxScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(60).multiply(EXA), tokenDeposit); + } + + void mintAndTransferTestTokens(byte[] tokenDeposit) { + + ownerDexTestScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); + ownerDexTestBaseScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); + ownerDexTestThirdScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); + ownerDexTestFourthScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); + + + //deposit base token + userDexTestBaseScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), + tokenDeposit); + //deposit quote token + userDexTestScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), tokenDeposit); + userDexTestThirdScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), + tokenDeposit); + userDexTestFourthScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), + tokenDeposit); + + //check isQuoteCoinAllowed for test token if not added + if (!dexUserScoreClient.isQuoteCoinAllowed(tokenAAddress)) { + dexAddQuoteCoin(tokenAAddress); + } + if (!dexUserScoreClient.isQuoteCoinAllowed(tokenBAddress)) { + dexAddQuoteCoin(tokenBAddress); + } + if (!dexUserScoreClient.isQuoteCoinAllowed(tokenCAddress)) { + dexAddQuoteCoin(tokenCAddress); + } + if (!dexUserScoreClient.isQuoteCoinAllowed(tokenDAddress)) { + dexAddQuoteCoin(tokenDAddress); + } + } + + private static void dexAddQuoteCoin(Address address) { + JsonArray addQuoteCoinParameters = new JsonArray() + .add(createParameter(address)); + + JsonArray actions = new JsonArray() + .add(createTransaction(balanced.dex._address(), "addQuoteCoin", addQuoteCoinParameters)); + + balanced.ownerClient.governance.execute(actions.toString()); + } + + void waitForADay() { + balanced.increaseDay(1); + } + + BigInteger hexToBigInteger(String hex) { + return new BigInteger(hex.replace("0x", ""), 16); + } + +} diff --git a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/LpTransferableOnContinuousModeTest.java b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/LpTransferableOnContinuousModeTest.java new file mode 100644 index 000000000..07e861029 --- /dev/null +++ b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/LpTransferableOnContinuousModeTest.java @@ -0,0 +1,163 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex; + +import com.eclipsesource.json.JsonArray; +import foundation.icon.icx.Wallet; +import foundation.icon.jsonrpc.Address; +import foundation.icon.score.client.DefaultScoreClient; +import network.balanced.score.lib.interfaces.DexScoreClient; +import network.balanced.score.lib.interfaces.GovernanceScoreClient; +import network.balanced.score.lib.interfaces.dex.DexTestScoreClient; +import network.balanced.score.lib.test.integration.Balanced; +import network.balanced.score.lib.test.integration.Env; +import network.balanced.score.lib.test.integration.ScoreIntegrationTest; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.math.BigInteger; +import java.util.Map; + +import static foundation.icon.score.client.DefaultScoreClient._deploy; +import static network.balanced.score.lib.test.integration.BalancedUtils.createParameter; +import static network.balanced.score.lib.test.integration.BalancedUtils.createTransaction; +import static network.balanced.score.lib.utils.Constants.EXA; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class LpTransferableOnContinuousModeTest { + + private static final Env.Chain chain = Env.getDefaultChain(); + private static Balanced balanced; + private static Wallet userWallet; + private static Wallet tUserWallet; + private static Wallet testOwnerWallet; + private static DefaultScoreClient dexScoreClient; + private static DefaultScoreClient governanceScoreClient; + private static DefaultScoreClient dexTestBaseScoreClient; + private static DefaultScoreClient dexTestFourthScoreClient; + + + static DefaultScoreClient daoFund; + private static final File jarfile = new File("src/intTest/java/network/balanced/score/core/dex/testtokens" + + "/DexIntTestToken.jar"); + + static { + try { + balanced = new Balanced(); + testOwnerWallet = balanced.owner; + userWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(800).multiply(EXA)); + tUserWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(500).multiply(EXA)); + dexTestBaseScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, + jarfile.getPath(), Map.of("name", "Test Base Token", "symbol", "TB")); + dexTestFourthScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, + jarfile.getPath(), Map.of("name", "Test Fourth Token", "symbol", "TFD")); + balanced.setupBalanced(); + dexScoreClient = balanced.dex; + governanceScoreClient = balanced.governance; + + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Error on init test: " + e.getMessage()); + } + + } + + private static final String dexTestBaseScoreAddress = dexTestBaseScoreClient._address().toString(); + private static final String dexTestFourthScoreAddress = dexTestFourthScoreClient._address().toString(); + + private static final Address userAddress = Address.of(userWallet); + private static final Address tUserAddress = Address.of(tUserWallet); + + private static final DexTestScoreClient ownerDexTestBaseScoreClient = new DexTestScoreClient(chain.getEndpointURL(), + chain.networkId, testOwnerWallet, DefaultScoreClient.address(dexTestBaseScoreAddress)); + private static final DexTestScoreClient ownerDexTestFourthScoreClient = + new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, + DefaultScoreClient.address(dexTestFourthScoreAddress)); + private static final DexScoreClient dexUserScoreClient = new DexScoreClient(dexScoreClient.endpoint(), + dexScoreClient._nid(), userWallet, dexScoreClient._address()); + + private static final DexTestScoreClient userDexTestBaseScoreClient = + new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, + DefaultScoreClient.address(dexTestBaseScoreAddress)); + private static final DexTestScoreClient userDexTestFourthScoreClient = + new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, + DefaultScoreClient.address(dexTestFourthScoreAddress)); + private static final GovernanceScoreClient governanceDexScoreClient = + new GovernanceScoreClient(governanceScoreClient); + + + @Test + @Order(4) + void testBalnPoolTokenTransferableOnContinuousRewards() { + byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); + mintAndTransferTestTokens(tokenDeposit); + dexUserScoreClient.add(Address.fromString(dexTestBaseScoreAddress), + Address.fromString(dexTestFourthScoreClient._address().toString()), + BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(50).multiply(EXA), false, BigInteger.valueOf(100)); + BigInteger poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestBaseScoreAddress), + Address.fromString(dexTestFourthScoreAddress)); + //assert pool id is less than 5 + assert poolId.compareTo(BigInteger.valueOf(6)) < 0; + BigInteger liquidity = + (BigInteger.valueOf(50).multiply(EXA).multiply(BigInteger.valueOf(50).multiply(EXA))).sqrt(); + BigInteger balance = dexUserScoreClient.balanceOf(userAddress, poolId); + BigInteger tUsersPrevBalance = dexUserScoreClient.balanceOf(tUserAddress, poolId); + + assertEquals(balance, liquidity); + dexUserScoreClient.transfer(tUserAddress, BigInteger.valueOf(5).multiply(EXA), poolId, new byte[0]); + BigInteger tUsersBalance = dexUserScoreClient.balanceOf(tUserAddress, poolId); + assertEquals(tUsersPrevBalance.add(BigInteger.valueOf(5).multiply(EXA)), tUsersBalance); + } + + void mintAndTransferTestTokens(byte[] tokenDeposit) { + + ownerDexTestBaseScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); + ownerDexTestFourthScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); + + + //deposit base token + userDexTestBaseScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), + tokenDeposit); + //deposit quote token + userDexTestFourthScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), + tokenDeposit); + + //check isQuoteCoinAllowed for test token if not added + + if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestBaseScoreAddress))) { + dexAddQuoteCoin(new Address(dexTestBaseScoreAddress)); + } + if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestFourthScoreAddress))) { + dexAddQuoteCoin(new Address(dexTestFourthScoreAddress)); + } + } + + void dexAddQuoteCoin(Address address) { + JsonArray addQuoteCoinParameters = new JsonArray() + .add(createParameter(address)); + + JsonArray actions = new JsonArray() + .add(createTransaction(balanced.dex._address(), "addQuoteCoin", addQuoteCoinParameters)); + + balanced.ownerClient.governance.execute(actions.toString()); + } + + void waitForADay() { + balanced.increaseDay(1); + } +} diff --git a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/MultipleAddTest.java b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/MultipleAddTest.java new file mode 100644 index 000000000..a2f095d38 --- /dev/null +++ b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/MultipleAddTest.java @@ -0,0 +1,231 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex; + +import com.eclipsesource.json.JsonArray; +import foundation.icon.icx.Wallet; +import foundation.icon.jsonrpc.Address; +import foundation.icon.score.client.DefaultScoreClient; +import foundation.icon.score.client.RevertedException; +import network.balanced.score.lib.interfaces.*; +import network.balanced.score.lib.interfaces.dex.DexTestScoreClient; +import network.balanced.score.lib.test.integration.Balanced; +import network.balanced.score.lib.test.integration.Env; +import network.balanced.score.lib.test.integration.ScoreIntegrationTest; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.function.Executable; +import score.UserRevertedException; + +import java.io.File; +import java.math.BigInteger; +import java.util.Map; + +import static foundation.icon.score.client.DefaultScoreClient._deploy; +import static network.balanced.score.lib.test.integration.BalancedUtils.createParameter; +import static network.balanced.score.lib.test.integration.BalancedUtils.createTransaction; +import static network.balanced.score.lib.utils.Constants.EXA; +import static org.junit.jupiter.api.Assertions.*; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class MultipleAddTest { + private static final Env.Chain chain = Env.getDefaultChain(); + private static Wallet userWallet; + private static Wallet testOwnerWallet; + private static Balanced balanced; + private static DefaultScoreClient dexScoreClient; + private static DefaultScoreClient governanceScoreClient; + private static DefaultScoreClient dexTestThirdScoreClient; + private static DefaultScoreClient dexTestFourthScoreClient; + + + static DefaultScoreClient daoFund; + private static final File jarfile = new File("src/intTest/java/network/balanced/score/core/dex/testtokens" + + "/DexIntTestToken.jar"); + + static { + try { + balanced = new Balanced(); + testOwnerWallet = balanced.owner; + userWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(800).multiply(EXA)); + dexTestThirdScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, + jarfile.getPath(), Map.of("name", "Test Third Token", "symbol", "TTD")); + dexTestFourthScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, + jarfile.getPath(), Map.of("name", "Test Fourth Token", "symbol", "TFD")); + balanced.setupBalanced(); + dexScoreClient = balanced.dex; + governanceScoreClient = balanced.governance; + + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Error on init test: " + e.getMessage()); + } + + } + + private static final String dexTestThirdScoreAddress = dexTestThirdScoreClient._address().toString(); + private static final String dexTestFourthScoreAddress = dexTestFourthScoreClient._address().toString(); + + private static final Address userAddress = Address.of(userWallet); + + private static final DexTestScoreClient ownerDexTestThirdScoreClient = + new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, + DefaultScoreClient.address(dexTestThirdScoreAddress)); + private static final DexTestScoreClient ownerDexTestFourthScoreClient = + new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, + DefaultScoreClient.address(dexTestFourthScoreAddress)); + private static final DexScoreClient dexUserScoreClient = new DexScoreClient(dexScoreClient.endpoint(), + dexScoreClient._nid(), userWallet, dexScoreClient._address()); + private static final DexTestScoreClient userDexTestThirdScoreClient = + new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, + DefaultScoreClient.address(dexTestThirdScoreAddress)); + private static final DexTestScoreClient userDexTestFourthScoreClient = + new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, + DefaultScoreClient.address(dexTestFourthScoreAddress)); + private static final GovernanceScoreClient governanceDexScoreClient = + new GovernanceScoreClient(governanceScoreClient); + + @Test + @Order(4) + void testMultipleAdd() { + //testMultipleAdd + BigInteger previousUserBalance = ownerDexTestFourthScoreClient.balanceOf(userAddress); + BigInteger previousSecondUserBalance = ownerDexTestThirdScoreClient.balanceOf(userAddress); + byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); + + this.mintAndTransferTestTokens(tokenDeposit); + //add the pool of test token and sicx + dexUserScoreClient.add(Address.fromString(dexTestThirdScoreAddress), + Address.fromString(dexTestFourthScoreAddress), BigInteger.valueOf(50).multiply(EXA), + BigInteger.valueOf(50).multiply(EXA), true, BigInteger.valueOf(100)); + BigInteger poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestThirdScoreAddress), + Address.fromString(dexTestFourthScoreAddress)); + Map poolStats = dexUserScoreClient.getPoolStats(poolId); + assertNull(poolStats.get("name")); + assertEquals(poolStats.get("base_token").toString(), dexTestThirdScoreAddress); + assertEquals(poolStats.get("quote_token").toString(), dexTestFourthScoreAddress); + assertEquals(hexToBigInteger(poolStats.get("base").toString()), BigInteger.valueOf(50).multiply(EXA)); + assertEquals(hexToBigInteger(poolStats.get("quote").toString()), BigInteger.valueOf(50).multiply(EXA)); + assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()), BigInteger.valueOf(50).multiply(EXA)); + assertEquals(hexToBigInteger(poolStats.get("price").toString()), BigInteger.ONE.multiply(EXA)); + assertEquals(hexToBigInteger(poolStats.get("base_decimals").toString()), BigInteger.valueOf(18)); + assertEquals(hexToBigInteger(poolStats.get("quote_decimals").toString()), BigInteger.valueOf(18)); + assertEquals(hexToBigInteger(poolStats.get("min_quote").toString()), BigInteger.ZERO); + + // after lp is added to the pool, remaining balance is checked + assertEquals(previousUserBalance.add(BigInteger.valueOf(150).multiply(EXA)), + ownerDexTestFourthScoreClient.balanceOf(userAddress)); + assertEquals(previousSecondUserBalance.add(BigInteger.valueOf(150).multiply(EXA)), + ownerDexTestThirdScoreClient.balanceOf(userAddress)); + + this.mintAndTransferTestTokens(tokenDeposit); + + dexUserScoreClient.add(Address.fromString(dexTestThirdScoreAddress), + Address.fromString(dexTestFourthScoreAddress), BigInteger.valueOf(50).multiply(EXA), + BigInteger.valueOf(50).multiply(EXA), true, BigInteger.valueOf(100)); + + // after lp is added to the pool, remaining balance is checked + assertEquals(BigInteger.valueOf(300).multiply(EXA), ownerDexTestFourthScoreClient.balanceOf(userAddress)); + assertEquals(BigInteger.valueOf(300).multiply(EXA), ownerDexTestThirdScoreClient.balanceOf(userAddress)); + + poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestThirdScoreAddress), + Address.fromString(dexTestFourthScoreAddress)); + poolStats = dexUserScoreClient.getPoolStats(poolId); + assertNull(poolStats.get("name")); + assertEquals(poolStats.get("base_token").toString(), dexTestThirdScoreAddress); + assertEquals(poolStats.get("quote_token").toString(), dexTestFourthScoreAddress); + assertEquals(hexToBigInteger(poolStats.get("base").toString()), BigInteger.valueOf(100).multiply(EXA)); + assertEquals(hexToBigInteger(poolStats.get("quote").toString()), BigInteger.valueOf(100).multiply(EXA)); + assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()), BigInteger.valueOf(100).multiply(EXA)); + assertEquals(hexToBigInteger(poolStats.get("price").toString()), BigInteger.ONE.multiply(EXA)); + assertEquals(hexToBigInteger(poolStats.get("base_decimals").toString()), BigInteger.valueOf(18)); + assertEquals(hexToBigInteger(poolStats.get("quote_decimals").toString()), BigInteger.valueOf(18)); + assertEquals(hexToBigInteger(poolStats.get("min_quote").toString()), BigInteger.ZERO); + + //change name and verify + setMarketName(poolId, "DTT/DTBT"); + Map updatedPoolStats = dexUserScoreClient.getPoolStats(poolId); + assertEquals(updatedPoolStats.get("name").toString(), "DTT/DTBT"); + } + + @Test + @Order(5) + void AddLiquidity_failOnHigherSlippage() {; + byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); + + this.mintAndTransferTestTokens(tokenDeposit); + //add the pool of test token and sicx + dexUserScoreClient.add(Address.fromString(dexTestThirdScoreAddress), + Address.fromString(dexTestFourthScoreAddress), BigInteger.valueOf(50).multiply(EXA), + BigInteger.valueOf(50).multiply(EXA), true, BigInteger.valueOf(100)); + + this.mintAndTransferTestTokens(tokenDeposit); + + Executable userLiquiditySupply = () -> dexUserScoreClient.add(Address.fromString(dexTestThirdScoreAddress), + Address.fromString(dexTestFourthScoreAddress), BigInteger.valueOf(50).multiply(EXA), + BigInteger.valueOf(51).multiply(EXA), true, BigInteger.valueOf(100)); + + assertThrows(UserRevertedException.class, userLiquiditySupply); + } + + void mintAndTransferTestTokens(byte[] tokenDeposit) { + + ownerDexTestThirdScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); + ownerDexTestFourthScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); + + + userDexTestThirdScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), + tokenDeposit); + userDexTestFourthScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), + tokenDeposit); + + //check isQuoteCoinAllowed for test token if not added + if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestThirdScoreAddress))) { + dexAddQuoteCoin(new Address(dexTestThirdScoreAddress)); + } + if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestFourthScoreAddress))) { + dexAddQuoteCoin(new Address(dexTestFourthScoreAddress)); + } + } + + void dexAddQuoteCoin(Address address) { + JsonArray addQuoteCoinParameters = new JsonArray() + .add(createParameter(address)); + + JsonArray actions = new JsonArray() + .add(createTransaction(balanced.dex._address(), "addQuoteCoin", addQuoteCoinParameters)); + + balanced.ownerClient.governance.execute(actions.toString()); + } + + void setMarketName(BigInteger poolID, String name) { + JsonArray setMarketNameParameters = new JsonArray() + .add(createParameter(poolID)) + .add(createParameter(name)); + + JsonArray actions = new JsonArray() + .add(createTransaction(balanced.dex._address(), "setMarketName", setMarketNameParameters)); + + balanced.ownerClient.governance.execute(actions.toString()); + } + + BigInteger hexToBigInteger(String hex) { + return new BigInteger(hex.replace("0x", ""), 16); + } +} diff --git a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/NonStakedLPRewardsTest.java b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/NonStakedLPRewardsTest.java new file mode 100644 index 000000000..6167fc600 --- /dev/null +++ b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/NonStakedLPRewardsTest.java @@ -0,0 +1,168 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex; + +import foundation.icon.icx.Wallet; +import foundation.icon.jsonrpc.Address; +import foundation.icon.jsonrpc.model.TransactionResult; +import foundation.icon.score.client.DefaultScoreClient; +import network.balanced.score.lib.interfaces.*; +import network.balanced.score.lib.test.integration.Balanced; +import network.balanced.score.lib.test.integration.Env; +import network.balanced.score.lib.test.integration.ScoreIntegrationTest; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.math.BigInteger; +import java.util.function.Consumer; + +import static network.balanced.score.lib.utils.Constants.EXA; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NonStakedLPRewardsTest { + + private static StakingScoreClient staking; + private static LoansScoreClient loans; + private static RewardsScoreClient rewards; + private static BalancedTokenScoreClient baln; + + static Env.Chain chain = Env.getDefaultChain(); + private static Balanced balanced; + private static Wallet userWallet; + private static DefaultScoreClient dexScoreClient; + private static DefaultScoreClient governanceScoreClient; + private static DefaultScoreClient sIcxScoreClient; + private static DefaultScoreClient balnScoreClient; + private static DefaultScoreClient rewardsScoreClient; + + + private static DefaultScoreClient daoFund; + static File jarfile = new File("src/intTest/java/network/balanced/score/core/dex/testtokens/DexIntTestToken.jar"); + + static { + try { + balanced = new Balanced(); + Wallet testOwnerWallet = balanced.owner; + userWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(800).multiply(EXA)); + Wallet tUserWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(500).multiply(EXA)); + balanced.setupBalanced(); + dexScoreClient = balanced.dex; + governanceScoreClient = balanced.governance; + DefaultScoreClient stakingScoreClient = balanced.staking; + sIcxScoreClient = balanced.sicx; + DefaultScoreClient dividendScoreClient = balanced.dividends; + balnScoreClient = balanced.baln; + rewardsScoreClient = balanced.rewards; + DefaultScoreClient feeHandlerScoreClient = balanced.feehandler; + daoFund = balanced.daofund; + staking = new StakingScoreClient(stakingScoreClient); + rewards = new RewardsScoreClient(rewardsScoreClient); + loans = new LoansScoreClient(balanced.loans); + baln = new BalancedTokenScoreClient(balnScoreClient); + Sicx sicx = new SicxScoreClient(sIcxScoreClient); + StakedLP stakedLp = new StakedLPScoreClient(balanced.stakedLp); + + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Error on init test: " + e.getMessage()); + } + + } + + private static final Address userAddress = Address.of(userWallet); + + private static final DexScoreClient dexUserScoreClient = new DexScoreClient(dexScoreClient.endpoint(), + dexScoreClient._nid(), userWallet, dexScoreClient._address()); + private static final SicxScoreClient userSicxScoreClient = new SicxScoreClient(dexScoreClient.endpoint(), + dexScoreClient._nid(), userWallet, sIcxScoreClient._address()); + private static final RewardsScoreClient userWalletRewardsClient = + new RewardsScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, + rewardsScoreClient._address()); + private static final BalancedTokenScoreClient userBalnScoreClient = + new BalancedTokenScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, + balnScoreClient._address()); + private static final GovernanceScoreClient governanceDexScoreClient = + new GovernanceScoreClient(governanceScoreClient); + private static final DAOfundScoreClient userDaoFundScoreClient = new DAOfundScoreClient(daoFund); + + + @Test + void testNonStakedLpRewards() { + balanced.syncDistributions(); + + byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); + staking.stakeICX(BigInteger.valueOf(100).multiply(BigInteger.TEN.pow(18)), userAddress + , null); + + BigInteger loanAmount = BigInteger.valueOf(150).multiply(BigInteger.TEN.pow(18)); + BigInteger collateral = BigInteger.valueOf(1000).multiply(BigInteger.TEN.pow(18)); + + + loans.depositAndBorrow(collateral, "bnUSD", loanAmount, null, null); + + waitForADay(); + balanced.syncDistributions(); + rewards.claimRewards(null); + + baln.transfer(userAddress, loanAmount, null); + + // deposit base token + userSicxScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(100).multiply(EXA), tokenDeposit); + //deposit quote token + userBalnScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(100).multiply(EXA), tokenDeposit); + dexUserScoreClient.add(balanced.baln._address(), balanced.sicx._address(), + BigInteger.valueOf(100).multiply(EXA), BigInteger.valueOf(100).multiply(EXA), false, BigInteger.valueOf(100)); + + waitForADay(); + balanced.syncDistributions(); + userWalletRewardsClient.claimRewards(null); + + balanced.syncDistributions(); + userWalletRewardsClient.claimRewards(null); + waitForADay(); + + // next day starts + Consumer distributeConsumer = result -> {}; + for (int i = 0; i < 10; i++) { + balanced.ownerClient.rewards.distribute(distributeConsumer); + } + waitForADay(); + + // next day starts + for (int i = 0; i < 10; i++) { + balanced.ownerClient.rewards.distribute(distributeConsumer); + } + // users without staking LP tokens will get 0 rewards + assertEquals(BigInteger.ZERO, rewards.getBalnHolding(userAddress.toString())); + + byte[] stakeLp = "{\"method\":\"_stake\"}".getBytes(); + dexUserScoreClient.transfer(balanced.stakedLp._address(), BigInteger.valueOf(90), BigInteger.valueOf(4), + stakeLp); + + // user gets rewards after lp token is staked + assertTrue(rewards.getBalnHolding(userAddress.toString()).compareTo(BigInteger.ZERO) > 0); + BigInteger previousUserBalance = baln.balanceOf(userAddress); + userWalletRewardsClient.claimRewards(null); + BigInteger newBalance = baln.balanceOf(userAddress); + assertTrue(newBalance.compareTo(previousUserBalance) > 0); + } + + void waitForADay() { + balanced.increaseDay(1); + } +} diff --git a/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/SwapRemoveAndFeeTest.java b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/SwapRemoveAndFeeTest.java new file mode 100644 index 000000000..36c2d5f3e --- /dev/null +++ b/core-contracts/Dex/src/intTest/java/network/balanced/score/core/dex/SwapRemoveAndFeeTest.java @@ -0,0 +1,197 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex; + +import com.eclipsesource.json.JsonArray; +import foundation.icon.icx.Wallet; +import foundation.icon.jsonrpc.Address; +import foundation.icon.score.client.DefaultScoreClient; +import network.balanced.score.lib.interfaces.Dex; +import network.balanced.score.lib.interfaces.DexScoreClient; +import network.balanced.score.lib.interfaces.GovernanceScoreClient; +import network.balanced.score.lib.interfaces.dex.DexTestScoreClient; +import network.balanced.score.lib.test.integration.Balanced; +import network.balanced.score.lib.test.integration.Env; +import network.balanced.score.lib.test.integration.ScoreIntegrationTest; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.math.BigInteger; +import java.util.Map; + +import static foundation.icon.score.client.DefaultScoreClient._deploy; +import static network.balanced.score.lib.test.integration.BalancedUtils.createParameter; +import static network.balanced.score.lib.test.integration.BalancedUtils.createTransaction; +import static network.balanced.score.lib.utils.Constants.EXA; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class SwapRemoveAndFeeTest { + + private static final Env.Chain chain = Env.getDefaultChain(); + private static Balanced balanced; + private static Wallet userWallet; + private static Wallet testOwnerWallet; + private static DefaultScoreClient dexScoreClient; + private static DefaultScoreClient governanceScoreClient; + private static DefaultScoreClient feeHandlerScoreClient; + private static DefaultScoreClient dexIntTestScoreClient; + private static DefaultScoreClient dexTestBaseScoreClient; + private static DefaultScoreClient dexTestThirdScoreClient; + + private static final File jarfile = new File("src/intTest/java/network/balanced/score/core/dex/testtokens" + + "/DexIntTestToken.jar"); + + static { + try { + balanced = new Balanced(); + testOwnerWallet = balanced.owner; + userWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(800).multiply(EXA)); + Wallet tUserWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(500).multiply(EXA)); + dexIntTestScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, + jarfile.getPath(), Map.of("name", "Test Token", "symbol", "TT")); + dexTestBaseScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, + jarfile.getPath(), Map.of("name", "Test Base Token", "symbol", "TB")); + dexTestThirdScoreClient = _deploy(chain.getEndpointURL(), chain.networkId, testOwnerWallet, + jarfile.getPath(), Map.of("name", "Test Third Token", "symbol", "TTD")); + balanced.setupBalanced(); + dexScoreClient = balanced.dex; + governanceScoreClient = balanced.governance; + feeHandlerScoreClient = balanced.feehandler; + + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Error on init test: " + e.getMessage()); + } + + } + + private static final String dexTestScoreAddress = dexIntTestScoreClient._address().toString(); + private static final String dexTestBaseScoreAddress = dexTestBaseScoreClient._address().toString(); + private static final String dexTestThirdScoreAddress = dexTestThirdScoreClient._address().toString(); + + private static final Address userAddress = Address.of(userWallet); + + private static final DexTestScoreClient ownerDexTestScoreClient = new DexTestScoreClient(chain.getEndpointURL(), + chain.networkId, testOwnerWallet, DefaultScoreClient.address(dexTestScoreAddress)); + private static final DexTestScoreClient ownerDexTestBaseScoreClient = new DexTestScoreClient(chain.getEndpointURL(), + chain.networkId, testOwnerWallet, DefaultScoreClient.address(dexTestBaseScoreAddress)); + private static final DexTestScoreClient ownerDexTestThirdScoreClient = + new DexTestScoreClient(chain.getEndpointURL(), chain.networkId, testOwnerWallet, + DefaultScoreClient.address(dexTestThirdScoreAddress)); + private static final Dex dexUserScoreClient = new DexScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), + userWallet, dexScoreClient._address()); + + private static final DexTestScoreClient userDexTestScoreClient = new DexTestScoreClient(dexScoreClient.endpoint(), + dexScoreClient._nid(), userWallet, DefaultScoreClient.address(dexTestScoreAddress)); + private static final DexTestScoreClient userDexTestBaseScoreClient = + new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, + DefaultScoreClient.address(dexTestBaseScoreAddress)); + private static final DexTestScoreClient userDexTestThirdScoreClient = + new DexTestScoreClient(dexScoreClient.endpoint(), dexScoreClient._nid(), userWallet, + DefaultScoreClient.address(dexTestThirdScoreAddress)); + private static final GovernanceScoreClient governanceDexScoreClient = + new GovernanceScoreClient(governanceScoreClient); + + @Test + @Order(5) + void testSwapTokensVerifySendsFeeAndRemove() { + //check balance of fee handler in from token + BigInteger feeBalanceOfTestToken = userDexTestScoreClient.balanceOf(feeHandlerScoreClient._address()); + byte[] tokenDeposit = "{\"method\":\"_deposit\",\"params\":{\"none\":\"none\"}}".getBytes(); + this.mintAndTransferTestTokens(tokenDeposit); + + dexUserScoreClient.add(Address.fromString(dexTestBaseScoreAddress), Address.fromString(dexTestScoreAddress), + BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(50).multiply(EXA), true, BigInteger.valueOf(100)); + + BigInteger poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestBaseScoreAddress), + Address.fromString(dexTestScoreAddress)); + assertNotNull(poolId); + //governanceDexScoreClient.disable_fee_handler(); + String swapString = "{\"method\":\"_swap\",\"params\":{\"toToken\":\"" + dexTestBaseScoreAddress + "\"}}"; + userDexTestScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(100).multiply(EXA), + swapString.getBytes()); + Map poolStats = dexUserScoreClient.getPoolStats(poolId); + assertEquals(poolStats.get("base_token").toString(), dexTestBaseScoreAddress); + assertEquals(poolStats.get("quote_token").toString(), dexTestScoreAddress); + assertEquals(hexToBigInteger(poolStats.get("base").toString()).divide(EXA), BigInteger.valueOf(16)); + assertEquals(hexToBigInteger(poolStats.get("quote").toString()).divide(EXA), BigInteger.valueOf(149)); + assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()).divide(EXA), BigInteger.valueOf(50)); + assertEquals(hexToBigInteger(poolStats.get("price").toString()).divide(EXA), BigInteger.valueOf(8)); + assertEquals(hexToBigInteger(poolStats.get("base_decimals").toString()), BigInteger.valueOf(18)); + assertEquals(hexToBigInteger(poolStats.get("quote_decimals").toString()), BigInteger.valueOf(18)); + assertEquals(hexToBigInteger(poolStats.get("min_quote").toString()), BigInteger.ZERO); + BigInteger updatedFeeBalanceOfTestToken = userDexTestScoreClient.balanceOf(feeHandlerScoreClient._address()); + assert updatedFeeBalanceOfTestToken.compareTo(feeBalanceOfTestToken) > 0; + assertEquals(BigInteger.valueOf(150).multiply(EXA).divide(BigInteger.valueOf(1000)), + updatedFeeBalanceOfTestToken); + + waitForADay(); + balanced.syncDistributions(); + BigInteger withdrawAmount = BigInteger.valueOf(5); + BigInteger balanceBefore = dexUserScoreClient.balanceOf(userAddress, poolId); + dexUserScoreClient.remove(poolId, BigInteger.valueOf(5), true); + BigInteger balanceAfter = dexUserScoreClient.balanceOf(userAddress, poolId); + assert balanceAfter.equals(balanceBefore.subtract(withdrawAmount)); + } + + void mintAndTransferTestTokens(byte[] tokenDeposit) { + + ownerDexTestScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); + ownerDexTestBaseScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); + ownerDexTestThirdScoreClient.mintTo(userAddress, BigInteger.valueOf(200).multiply(EXA)); + + + //deposit base token + userDexTestBaseScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), + tokenDeposit); + //deposit quote token + userDexTestScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), tokenDeposit); + userDexTestThirdScoreClient.transfer(dexScoreClient._address(), BigInteger.valueOf(190).multiply(EXA), + tokenDeposit); + + //check isQuoteCoinAllowed for test token if not added + if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestScoreAddress))) { + dexAddQuoteCoin(new Address(dexTestScoreAddress)); + } + if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestBaseScoreAddress))) { + dexAddQuoteCoin(new Address(dexTestBaseScoreAddress)); + } + if (!dexUserScoreClient.isQuoteCoinAllowed(Address.fromString(dexTestThirdScoreAddress))) { + dexAddQuoteCoin(new Address(dexTestThirdScoreAddress)); + } + } + + void dexAddQuoteCoin(Address address) { + JsonArray addQuoteCoinParameters = new JsonArray() + .add(createParameter(address)); + + JsonArray actions = new JsonArray() + .add(createTransaction(balanced.dex._address(), "addQuoteCoin", addQuoteCoinParameters)); + + balanced.ownerClient.governance.execute(actions.toString()); + } + + void waitForADay() { + balanced.increaseDay(1); + } + + BigInteger hexToBigInteger(String hex) { + return new BigInteger(hex.replace("0x", ""), 16); + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/AbstractDex.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/AbstractDex.java new file mode 100644 index 000000000..44a27835a --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/AbstractDex.java @@ -0,0 +1,820 @@ +/* + * Copyright (c) 2022-2023 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 network.balanced.score.core.dex.db.NodeDB; +import network.balanced.score.lib.interfaces.Dex; +import network.balanced.score.lib.structs.PrepDelegations; +import network.balanced.score.lib.structs.RewardsDataEntry; +import network.balanced.score.lib.utils.BalancedFloorLimits; +import network.balanced.score.lib.utils.FloorLimited; +import score.Address; +import score.BranchDB; +import score.Context; +import score.DictDB; +import score.annotation.EventLog; +import score.annotation.External; +import scorex.util.ArrayList; +import scorex.util.HashMap; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +import static network.balanced.score.core.dex.DexDBVariables.*; +import static network.balanced.score.core.dex.utils.Check.isValidPercent; +import static network.balanced.score.core.dex.utils.Check.isValidPoolId; +import static network.balanced.score.core.dex.utils.Const.*; +import static network.balanced.score.lib.utils.BalancedAddressManager.*; +import static network.balanced.score.lib.utils.Check.onlyGovernance; +import static network.balanced.score.lib.utils.Constants.*; +import static network.balanced.score.lib.utils.Math.pow; + +public abstract class AbstractDex extends FloorLimited implements Dex { + + public AbstractDex(Address _governance) { + if (governance.get() == null) { + governance.set(_governance); + + // Set Default Fee Rates + poolLpFee.set(BigInteger.valueOf(15L)); + poolBalnFee.set(BigInteger.valueOf(15L)); + icxConversionFee.set(BigInteger.valueOf(70L)); + icxBalnFee.set(BigInteger.valueOf(30L)); + + nonce.set(2); + currentDay.set(BigInteger.valueOf(1L)); + namedMarkets.set(SICXICX_MARKET_NAME, SICXICX_POOL_ID); + marketsToNames.set(SICXICX_POOL_ID, SICXICX_MARKET_NAME); + dexOn.set(true); + } + setGovernance(governance.get()); + } + + @EventLog(indexed = 2) + public void Swap(BigInteger _id, Address _baseToken, Address _fromToken, Address _toToken, + Address _sender, Address _receiver, BigInteger _fromValue, BigInteger _toValue, + BigInteger _timestamp, BigInteger _lpFees, BigInteger _balnFees, BigInteger _poolBase, + BigInteger _poolQuote, BigInteger _endingPrice, BigInteger _effectiveFillPrice) { + } + + @EventLog(indexed = 3) + public void MarketAdded(BigInteger _id, Address _baseToken, + Address _quoteToken, BigInteger _baseValue, BigInteger _quoteValue) { + } + + @EventLog(indexed = 3) + public void Add(BigInteger _id, Address _owner, BigInteger _value, BigInteger _base, BigInteger _quote) { + } + + @EventLog(indexed = 3) + public void Remove(BigInteger _id, Address _owner, BigInteger _value, BigInteger _base, BigInteger _quote) { + } + + @EventLog(indexed = 2) + public void Deposit(Address _token, Address _owner, BigInteger _value) { + } + + @EventLog(indexed = 2) + public void Withdraw(Address _token, Address _owner, BigInteger _value) { + } + + @EventLog(indexed = 2) + public void ClaimSicxEarnings(Address _owner, BigInteger _value) { + } + + @EventLog(indexed = 3) + public void TransferSingle(Address _operator, Address _from, Address _to, BigInteger _id, BigInteger _value) { + } + + @External(readonly = true) + public String name() { + return TAG; + } + + + @External + public void updateAddress(String name) { + resetAddress(name); + } + + @External(readonly = true) + public Address getAddress(String name) { + return getAddressByName(name); + } + + @External + public void turnDexOn() { + onlyGovernance(); + dexOn.set(true); + } + + @External(readonly = true) + public boolean getDexOn() { + return dexOn.get(); + } + + @External + public void setPoolLpFee(BigInteger _value) { + onlyGovernance(); + poolLpFee.set(_value); + } + + @External + public void setPoolBalnFee(BigInteger _value) { + onlyGovernance(); + poolBalnFee.set(_value); + } + + @External + public void setIcxConversionFee(BigInteger _value) { + onlyGovernance(); + icxConversionFee.set(_value); + } + + @External + public void setIcxBalnFee(BigInteger _value) { + onlyGovernance(); + icxBalnFee.set(_value); + } + + @External(readonly = true) + public Map getFees() { + BigInteger icxBalnFees = icxBalnFee.get(); + BigInteger icxConversionFees = icxConversionFee.get(); + BigInteger poolBalnFees = poolBalnFee.get(); + BigInteger poolLpFees = poolLpFee.get(); + Map feesMapping = new HashMap<>(); + feesMapping.put("icx_total", icxBalnFees.add(icxConversionFees)); + feesMapping.put("pool_total", poolBalnFees.add(poolLpFees)); + feesMapping.put("pool_lp_fee", poolLpFees); + feesMapping.put("pool_baln_fee", poolBalnFees); + feesMapping.put("icx_conversion_fee", icxConversionFees); + feesMapping.put("icx_baln_fee", icxBalnFees); + return feesMapping; + } + + @External + public void setMarketName(BigInteger _id, String _name) { + onlyGovernance(); + namedMarkets.set(_name, _id.intValue()); + marketsToNames.set(_id.intValue(), _name); + } + + @External + public void setOracleProtection(BigInteger pid, BigInteger percentage) { + onlyGovernance(); + isValidPoolId(pid); + isValidPercent(percentage.intValue()); + validateTokenOraclePrice(poolBase.get(pid.intValue())); + validateTokenOraclePrice(poolQuote.get(pid.intValue())); + + oracleProtection.set(pid, percentage); + } + + private void validateTokenOraclePrice(Address token) { + BigInteger price = getOraclePrice(token); + Context.require(price != null && !price.equals(BigInteger.ZERO), + "Token must be supported by the balanced Oracle"); + } + + private BigInteger getOraclePrice(Address token) { + String symbol = (String) Context.call(token, "symbol"); + return (BigInteger) Context.call(getBalancedOracle(), "getPriceInUSD", symbol); + } + + protected void oracleProtection(Integer pid, BigInteger priceBase) { + BigInteger poolId = BigInteger.valueOf(pid); + BigInteger oracleProtectionPercentage = oracleProtection.get(poolId); + if (oracleProtectionPercentage == null || oracleProtectionPercentage.signum() == 0) { + return; + } + + Address quoteToken = poolQuote.get(pid); + Address baseToken = poolBase.get(pid); + BigInteger oraclePriceQuote = getOraclePrice(quoteToken); + BigInteger oraclePriceBase = getOraclePrice(baseToken); + + BigInteger oraclePriceBaseRatio = oraclePriceBase.multiply(EXA).divide(oraclePriceQuote); + BigInteger oracleProtectionExa = oraclePriceBaseRatio.multiply(oracleProtectionPercentage).divide(POINTS); + + Context.require(priceBase.compareTo(oraclePriceBaseRatio.add(oracleProtectionExa)) <= 0 && priceBase.compareTo(oraclePriceBaseRatio.subtract(oracleProtectionExa)) >= 0, TAG + ": oracle protection price violated"); + } + + @External(readonly = true) + public String getPoolName(BigInteger _id) { + return marketsToNames.get(_id.intValue()); + } + + @External + public void addQuoteCoin(Address _address) { + onlyGovernance(); + quoteCoins.add(_address); + } + + @External(readonly = true) + public boolean isQuoteCoinAllowed(Address _address) { + return quoteCoins.contains(_address); + } + + @External(readonly = true) + public BigInteger getDay() { + BigInteger blockTime = BigInteger.valueOf(Context.getBlockTimestamp()); + BigInteger timeDelta = blockTime.subtract(timeOffset.get()); + return timeDelta.divide(MICRO_SECONDS_IN_A_DAY); + } + + @External + public void setTimeOffset(BigInteger _delta_time) { + onlyGovernance(); + timeOffset.set(_delta_time); + } + + @External(readonly = true) + public BigInteger getTimeOffset() { + return timeOffset.get(); + } + + @External + public boolean precompute(BigInteger snap, BigInteger batch_size) { + return true; + } + + @External(readonly = true) + public BigInteger getDeposit(Address _tokenAddress, Address _user) { + return deposit.at(_tokenAddress).getOrDefault(_user, BigInteger.ZERO); + } + + @External(readonly = true) + public BigInteger getSicxEarnings(Address _user) { + return sicxEarnings.getOrDefault(_user, BigInteger.ZERO); + } + + @External(readonly = true) + public BigInteger getPoolId(Address _token1Address, Address _token2Address) { + return BigInteger.valueOf(poolId.at(_token1Address).get(_token2Address)); + } + + @External(readonly = true) + public BigInteger getNonce() { + return BigInteger.valueOf(nonce.getOrDefault(0)); + } + + @External(readonly = true) + public List getNamedPools() { + List namedPools = new ArrayList<>(); + namedPools.addAll(namedMarkets.keys()); + return namedPools; + } + + @External(readonly = true) + public BigInteger lookupPid(String _name) { + return BigInteger.valueOf(namedMarkets.get(_name)); + } + + @External(readonly = true) + public BigInteger getPoolTotal(BigInteger _id, Address _token) { + return poolTotal.at(_id.intValue()).getOrDefault(_token, BigInteger.ZERO); + } + + @External(readonly = true) + public Address getPoolBase(BigInteger _id) { + return poolBase.get(_id.intValue()); + } + + @External(readonly = true) + public Address getPoolQuote(BigInteger _id) { + return poolQuote.get(_id.intValue()); + } + + @External(readonly = true) + public BigInteger getOracleProtection(BigInteger pid) { + return oracleProtection.get(pid); + } + + @External(readonly = true) + public BigInteger getQuotePriceInBase(BigInteger _id) { + isValidPoolId(_id); + + if (_id.intValue() == SICXICX_POOL_ID) { + return ((EXA.multiply(EXA)).divide(getSicxRate())); + } + + return priceOfAInB(_id.intValue(), poolQuote, poolBase); + } + + @External(readonly = true) + public BigInteger getBasePriceInQuote(BigInteger _id) { + isValidPoolId(_id); + + if (_id.intValue() == SICXICX_POOL_ID) { + return getSicxRate(); + } + + return priceOfAInB(_id.intValue(), poolBase, poolQuote); + } + + private BigInteger priceOfAInB(Integer id, DictDB tokenA, DictDB tokenB) { + Address ATokenAddress = tokenA.get(id); + Address BTokenAddress = tokenB.get(id); + + DictDB totalTokensInPool = poolTotal.at(id); + BigInteger ATokenTotal = totalTokensInPool.get(ATokenAddress); + BigInteger BTokenTotal = totalTokensInPool.get(BTokenAddress); + + return BTokenTotal.multiply(EXA).divide(ATokenTotal); + } + + @External(readonly = true) + public BigInteger getBalnPrice() { + return getBasePriceInQuote(BigInteger.valueOf(poolId.at(getBaln()).get(getBnusd()))); + } + + @External(readonly = true) + public BigInteger getSicxBnusdPrice() { + return getBasePriceInQuote(BigInteger.valueOf(poolId.at(getSicx()).get(getBnusd()))); + } + + @External(readonly = true) + public BigInteger getBnusdValue(String _name) { + // Should eventually only handle sICX/ICX + Integer _id = namedMarkets.get(_name); + return getLPBnusdValue(_id); + } + + @External(readonly = true) + public BigInteger getLPBnusdValue(int _id) { + if (_id == SICXICX_POOL_ID) { + BigInteger icxTotal = icxQueueTotal.getOrDefault(BigInteger.ZERO); + return (icxTotal.multiply(getSicxBnusdPrice())).divide(getSicxRate()); + } + + Address poolQuoteToken = poolQuote.get(_id); + Address sicxAddress = getSicx(); + Address bnusdAddress = getBnusd(); + + if (poolQuoteToken.equals(sicxAddress)) { + BigInteger sicxTotal = poolTotal.at(_id).get(sicxAddress).multiply(BigInteger.TWO); + return getSicxBnusdPrice().multiply(sicxTotal).divide(EXA); + } else if (poolQuoteToken.equals(bnusdAddress)) { + return poolTotal.at(_id).get(bnusdAddress).multiply(BigInteger.TWO); + } + + return BigInteger.ZERO; + } + + @External(readonly = true) + public BigInteger getPrice(BigInteger _id) { + return this.getBasePriceInQuote(_id); + } + + @External(readonly = true) + public BigInteger getPriceByName(String _name) { + return getPrice(BigInteger.valueOf(namedMarkets.get(_name))); + } + + @External(readonly = true) + public BigInteger getICXBalance(Address _address) { + BigInteger orderId = icxQueueOrderId.get(_address); + if (orderId == null) { + return BigInteger.ZERO; + } + return icxQueue.getNode(orderId).getSize(); + } + + @External(readonly = true) + public Map getPoolStats(BigInteger _id) { + isValidPoolId(_id); + Map poolStats = new HashMap<>(); + if (_id.intValue() == SICXICX_POOL_ID) { + poolStats.put("base_token", getSicx()); + poolStats.put("quote_token", null); + poolStats.put("base", BigInteger.ZERO); + poolStats.put("quote", icxQueueTotal.getOrDefault(BigInteger.ZERO)); + poolStats.put("total_supply", icxQueueTotal.getOrDefault(BigInteger.ZERO)); + poolStats.put("price", getPrice(_id)); + poolStats.put("name", SICXICX_MARKET_NAME); + poolStats.put("base_decimals", 18); + poolStats.put("quote_decimals", 18); + poolStats.put("min_quote", getRewardableAmount(null)); + } else { + Address baseToken = poolBase.get(_id.intValue()); + Address quoteToken = poolQuote.get(_id.intValue()); + String name = marketsToNames.get(_id.intValue()); + DictDB totalTokensInPool = poolTotal.at(_id.intValue()); + + poolStats.put("base", totalTokensInPool.get(baseToken)); + poolStats.put("quote", totalTokensInPool.get(quoteToken)); + poolStats.put("base_token", baseToken); + poolStats.put("quote_token", quoteToken); + poolStats.put("total_supply", poolLpTotal.get(_id.intValue())); + poolStats.put("price", getPrice(_id)); + poolStats.put("name", name); + poolStats.put("base_decimals", tokenPrecisions.get(baseToken)); + poolStats.put("quote_decimals", tokenPrecisions.get(quoteToken)); + poolStats.put("min_quote", getRewardableAmount(quoteToken)); + } + return poolStats; + } + + @External(readonly = true) + public Map getPoolStatsForPair(Address _base, Address _quote) { + BigInteger poolId = getPoolId(_base, _quote); + Map poolStats = getPoolStats(poolId); + Map poolStatsWithId = new HashMap<>(); + poolStatsWithId.put("id", poolId); + poolStatsWithId.putAll(poolStats); + + return poolStatsWithId; + } + + @External(readonly = true) + public BigInteger totalDexAddresses(BigInteger _id) { + return BigInteger.valueOf(activeAddresses.get(_id.intValue()).length()); + } + + @External(readonly = true) + public Map getBalanceAndSupply(String _name, String _owner) { + Address owner = Address.fromString(_owner); + if (_name.equals(SICXICX_MARKET_NAME)) { + Map rewardsData = new HashMap<>(); + rewardsData.put("_balance", balanceOf(owner, BigInteger.valueOf(SICXICX_POOL_ID))); + rewardsData.put("_totalSupply", totalSupply(BigInteger.valueOf(SICXICX_POOL_ID))); + return rewardsData; + } + BigInteger poolId = lookupPid(_name); + Context.require(poolId != null, TAG + ": Unsupported data source name"); + + Address stakedLpAddress = getStakedLp(); + BigInteger totalSupply = (BigInteger) Context.call(stakedLpAddress, "totalStaked", poolId); + BigInteger balance = (BigInteger) Context.call(stakedLpAddress, "balanceOf", owner, poolId); + Map rewardsData = new HashMap<>(); + rewardsData.put("_balance", balance); + rewardsData.put("_totalSupply", totalSupply); + return rewardsData; + } + + @External(readonly = true) + public BigInteger getTotalValue(String _name, BigInteger _snapshot_id) { + return totalSupply(BigInteger.valueOf(namedMarkets.get(_name))); + } + + @External + public void permit(BigInteger _id, boolean _permission) { + onlyGovernance(); + active.set(_id.intValue(), _permission); + } + + @External + public void addLpAddresses(BigInteger _poolId, Address[] _addresses) { + onlyGovernance(); + for (Address address : _addresses) { + if (balanceOf(address, _poolId).compareTo(BigInteger.ZERO) > 0) { + activeAddresses.get(_poolId.intValue()).add(address); + } + } + } + + @External(readonly = true) + public BigInteger balanceOf(Address _owner, BigInteger _id) { + if (_id.intValue() == SICXICX_POOL_ID) { + return getICXBalance(_owner); + } else { + return DexDBVariables.balance.at(_id.intValue()).getOrDefault(_owner, BigInteger.ZERO); + } + } + + @External(readonly = true) + public BigInteger totalSupply(BigInteger _id) { + if (_id.intValue() == SICXICX_POOL_ID) { + return icxQueueTotal.getOrDefault(BigInteger.ZERO); + } + + return poolLpTotal.getOrDefault(_id.intValue(), BigInteger.ZERO); + } + + @External + public void delegate(PrepDelegations[] prepDelegations) { + onlyGovernance(); + Context.call(getStaking(), "delegate", (Object) prepDelegations); + } + + private static BigInteger getPriceInUSD(String symbol) { + return (BigInteger) Context.call(getBalancedOracle(), "getLastPriceInUSD", symbol); + } + + protected BigInteger getSicxRate() { + return (BigInteger) Context.call(getStaking(), "getTodayRate"); + } + + boolean isLockingPool(Integer id) { + return id.equals(SICXICX_POOL_ID); + } + + BigInteger getRewardableAmount(Address tokenAddress) { + if (tokenAddress == null) { + return BigInteger.TEN.multiply(EXA); + } else if (getSicx().equals(tokenAddress)) { + return (BigInteger.TEN.multiply(EXA.multiply(EXA))).divide(getSicxRate()); + } else if (getBnusd().equals(tokenAddress)) { + return BigInteger.TEN.multiply(EXA); + } + return BigInteger.ZERO; + } + + void deposit(Address token, Address to, BigInteger amount) { + DictDB depositDetails = deposit.at(token); + BigInteger userBalance = depositDetails.getOrDefault(to, BigInteger.ZERO); + userBalance = userBalance.add(amount); + depositDetails.set(to, userBalance); + Deposit(token, to, amount); + + if (tokenPrecisions.get(token) == null) { + BigInteger decimalValue = (BigInteger) Context.call(token, "decimals"); + tokenPrecisions.set(token, decimalValue); + } + } + + void exchange(Address fromToken, Address toToken, Address sender, + Address receiver, BigInteger value, BigInteger minimumReceive) { + + if (minimumReceive == null) { + minimumReceive = BigInteger.ZERO; + } + + int id = getPoolId(fromToken, toToken).intValue(); + isValidPoolId(id); + Context.require(id != SICXICX_POOL_ID, TAG + ": Not supported on this API, use the ICX swap API."); + Context.require(active.getOrDefault(id, false), TAG + ": Pool is not active"); + + BigInteger lpFees = value.multiply(poolLpFee.get()).divide(FEE_SCALE); + BigInteger balnFees = value.multiply(poolBalnFee.get()).divide(FEE_SCALE); + BigInteger initialBalnFees = balnFees; + BigInteger fees = lpFees.add(balnFees); + + Address poolBaseToken = poolBase.get(id); + boolean isSell = fromToken.equals(poolBaseToken); + Address poolQuoteToken = isSell ? toToken : fromToken; + + // We consider the trade in terms of toToken (token we are trading to), and fromToken (token we are trading + // away) in the pool. It must obey the xy=k constant product formula. + + DictDB totalTokensInPool = poolTotal.at(id); + BigInteger oldFromToken = totalTokensInPool.get(fromToken); + BigInteger oldToToken = totalTokensInPool.get(toToken); + + // We perturb the pool by the asset we are trading in less fees. + // Fees are credited to LPs at the end of the process. + BigInteger inputWithoutFees = value.subtract(fees); + BigInteger newFromToken = oldFromToken.add(inputWithoutFees); + + // Compute the new fromToken according to the constant product formula + BigInteger newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); + + // Send the trader the amount of toToken removed from the pool by the constant product formula + BigInteger sendAmount = oldToToken.subtract(newToToken); + + Context.require(sendAmount.compareTo(BigInteger.ZERO) > 0, TAG + ": Invalid output amount in trade."); + // Revert the transaction if the below slippage, as specified in _minimum_receive + Context.require(sendAmount.compareTo(minimumReceive) >= 0, + TAG + ": MinimumReceiveError: Receive amount " + sendAmount + " below supplied minimum"); + + // Apply fees to fromToken after computing constant product. lpFees are credited to the LPs, the rest are + // sent to BALN holders. + newFromToken = newFromToken.add(lpFees); + + if (isSell) { + oldFromToken = newFromToken; + oldToToken = newToToken; + + newFromToken = oldFromToken.add(balnFees); + newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); + + balnFees = oldToToken.subtract(newToToken); + } + + // Save updated pool totals + totalTokensInPool.set(fromToken, newFromToken); + totalTokensInPool.set(toToken, newToToken); + + // Capture details for event logs + BigInteger totalBase = isSell ? newFromToken : newToToken; + BigInteger totalQuote = isSell ? newToToken : newFromToken; + + BigInteger endingPrice = totalQuote.multiply(EXA).divide(totalBase); + oracleProtection(id, endingPrice); + + // Send the trader their funds + BalancedFloorLimits.verifyWithdraw(toToken, sendAmount); + Context.call(toToken, "transfer", receiver, sendAmount); + + // Send the platform fees to the fee handler SCORE + Context.call(poolQuoteToken, "transfer", getFeehandler(), balnFees); + + // Broadcast pool ending price + BigInteger effectiveFillPrice = (value.multiply(EXA)).divide(sendAmount); + + if (!isSell) { + effectiveFillPrice = (sendAmount.multiply(EXA)).divide(value); + } + + Swap(BigInteger.valueOf(id), poolBaseToken, fromToken, toToken, sender, receiver, value, sendAmount, + BigInteger.valueOf(Context.getBlockTimestamp()), lpFees, initialBalnFees, totalBase, totalQuote, + endingPrice, effectiveFillPrice); + } + + void donate(Address fromToken, Address toToken, BigInteger value) { + int id = getPoolId(fromToken, toToken).intValue(); + isValidPoolId(id); + Context.require(id != SICXICX_POOL_ID, TAG + ": Not supported on this API, use the ICX swap API."); + Context.require(active.getOrDefault(id, false), TAG + ": Pool is not active"); + + DictDB totalTokensInPool = poolTotal.at(id); + BigInteger oldFromToken = totalTokensInPool.get(fromToken); + + BigInteger newFromToken = oldFromToken.add(value); + + totalTokensInPool.set(fromToken, newFromToken); + } + + @External + public void govWithdraw(int id, Address token, BigInteger value) { + onlyGovernance(); + isValidPoolId(id); + Context.require(id != SICXICX_POOL_ID, TAG + ": Not supported on this API, use the ICX swap API."); + + DictDB totalTokensInPool = poolTotal.at(id); + BigInteger oldToken = totalTokensInPool.get(token); + + BigInteger newToken = oldToken.subtract(value); + + totalTokensInPool.set(token, newToken); + Context.call(token, "transfer", getDaofund(), value); + } + + @External + public void govSetPoolTotal(int pid, BigInteger total) { + onlyGovernance(); + poolLpTotal.set(pid, total); + } + + @External + public void govSetUserPoolTotal(int pid, Address user, BigInteger total) { + onlyGovernance(); + BigInteger value = balance.at(pid).get(user); + BigInteger burned = value.subtract(total); + balance.at(pid).set(user, total); + + TransferSingle(Context.getCaller(), user, MINT_ADDRESS, BigInteger.valueOf(pid), burned); + } + + void swapIcx(Address sender, BigInteger value) { + BigInteger sicxIcxPrice = getSicxRate(); + + BigInteger oldIcxTotal = icxQueueTotal.getOrDefault(BigInteger.ZERO); + List data = new ArrayList<>(); + + BigInteger balnFees = (value.multiply(icxBalnFee.get())).divide(FEE_SCALE); + BigInteger conversionFees = value.multiply(icxConversionFee.get()).divide(FEE_SCALE); + BigInteger orderSize = value.subtract(balnFees.add(conversionFees)); + BigInteger orderIcxValue = (orderSize.multiply(sicxIcxPrice)).divide(EXA); + BigInteger lpSicxSize = orderSize.add(conversionFees); + + Context.require(orderIcxValue.compareTo(oldIcxTotal) <= 0, + TAG + ": InsufficientLiquidityError: Not enough ICX suppliers."); + + boolean filled = false; + BigInteger orderRemainingIcx = orderIcxValue; + int iterations = 0; + while (!filled) { + iterations += 1; + if ((icxQueue.size().equals(BigInteger.ZERO)) || (iterations > ICX_QUEUE_FILL_DEPTH)) { + Context.revert(TAG + ": InsufficientLiquidityError: Unable to fill " + orderRemainingIcx + " ICX."); + } + NodeDB counterpartyOrder = icxQueue.getHeadNode(); + Address counterpartyAddress = counterpartyOrder.getUser(); + BigInteger counterpartyIcx = counterpartyOrder.getSize(); + + RewardsDataEntry rewardsEntry = new RewardsDataEntry(); + rewardsEntry._user = counterpartyAddress.toString(); + + BigInteger matchedIcx = counterpartyIcx.min(orderRemainingIcx); + orderRemainingIcx = orderRemainingIcx.subtract(matchedIcx); + + boolean counterpartyFilled = matchedIcx.equals(counterpartyIcx); + if (counterpartyFilled) { + icxQueue.removeHead(); + icxQueueOrderId.set(counterpartyAddress, null); + activeAddresses.get(SICXICX_POOL_ID).remove(counterpartyAddress); + rewardsEntry._balance = BigInteger.ZERO; + } else { + BigInteger newCounterpartyValue = counterpartyIcx.subtract(matchedIcx); + counterpartyOrder.setSize(newCounterpartyValue); + rewardsEntry._balance = newCounterpartyValue; + } + + data.add(rewardsEntry); + + BigInteger lpSicxEarnings = (lpSicxSize.multiply(matchedIcx)).divide(orderIcxValue); + BigInteger newSicxEarnings = getSicxEarnings(counterpartyAddress).add(lpSicxEarnings); + sicxEarnings.set(counterpartyAddress, newSicxEarnings); + + if (orderRemainingIcx.compareTo(BigInteger.ZERO) == 0) { + filled = true; + } + } + + BigInteger newIcxTotal = oldIcxTotal.subtract(orderIcxValue); + icxQueueTotal.set(newIcxTotal); + BigInteger effectiveFillPrice = (orderIcxValue.multiply(EXA)).divide(value); + Address sicxAddress = getSicx(); + Swap(BigInteger.valueOf(SICXICX_POOL_ID), sicxAddress, sicxAddress, EOA_ZERO, sender, sender, value, + orderIcxValue, BigInteger.valueOf(Context.getBlockTimestamp()), conversionFees, balnFees, newIcxTotal + , BigInteger.ZERO, sicxIcxPrice, effectiveFillPrice); + + Context.call(getRewards(), "updateBalanceAndSupplyBatch", SICXICX_MARKET_NAME, newIcxTotal, data); + Context.call(sicxAddress, "transfer", getFeehandler(), balnFees); + BalancedFloorLimits.verifyNativeWithdraw(orderIcxValue); + Context.transfer(sender, orderIcxValue); + } + + + private BigInteger getUnitValue(Address tokenAddress) { + if (tokenAddress == null) { + return EXA; + } else { + return pow(BigInteger.TEN, tokenPrecisions.get(tokenAddress).intValue()); + } + } + + BigInteger snapshotValueAt(BigInteger _snapshot_id, + BranchDB> snapshot) { + Context.require(_snapshot_id.compareTo(BigInteger.ZERO) >= 0, + TAG + ": Snapshot id is equal to or greater then Zero."); + BigInteger low = BigInteger.ZERO; + BigInteger high = snapshot.at(LENGTH).getOrDefault(BigInteger.ZERO, BigInteger.ZERO); + + while (low.compareTo(high) < 0) { + BigInteger mid = (low.add(high)).divide(BigInteger.TWO); + if (snapshot.at(IDS).getOrDefault(mid, BigInteger.ZERO).compareTo(_snapshot_id) > 0) { + high = mid; + } else { + low = mid.add(BigInteger.ONE); + } + } + + if (snapshot.at(IDS).getOrDefault(BigInteger.ZERO, BigInteger.ZERO).equals(_snapshot_id)) { + return snapshot.at(VALUES).getOrDefault(BigInteger.ZERO, BigInteger.ZERO); + } else if (low.equals(BigInteger.ZERO)) { + return BigInteger.ZERO; + } + + BigInteger matchedIndex = low.subtract(BigInteger.ONE); + return snapshot.at(VALUES).getOrDefault(matchedIndex, BigInteger.ZERO); + } + + void _transfer(Address from, Address to, BigInteger value, Integer id, byte[] data) { + + Context.require(!isLockingPool(id), TAG + ": Nontransferable token id"); + Context.require(value.compareTo(BigInteger.ZERO) >= 0, + TAG + ": Transferring value cannot be less than 0."); + + DictDB poolLpBalanceOfUser = balance.at(id); + BigInteger fromBalance = poolLpBalanceOfUser.getOrDefault(from, BigInteger.ZERO); + + Context.require(fromBalance.compareTo(value) >= 0, TAG + ": Out of balance"); + + poolLpBalanceOfUser.set(from, poolLpBalanceOfUser.get(from).subtract(value)); + poolLpBalanceOfUser.set(to, poolLpBalanceOfUser.getOrDefault(to, BigInteger.ZERO).add(value)); + Address stakedLpAddress = getStakedLp(); + + if (!from.equals(stakedLpAddress) && !to.equals(stakedLpAddress)) { + if (value.compareTo(BigInteger.ZERO) > 0) { + activeAddresses.get(id).add(to); + } + + if ((fromBalance.subtract(value)).equals(BigInteger.ZERO)) { + activeAddresses.get(id).remove(from); + } + } + TransferSingle(from, from, to, BigInteger.valueOf(id), value); + + if (to.isContract()) { + Context.call(to, "onIRC31Received", from, from, id, value, data); + } + } +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPool.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPool.java similarity index 97% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPool.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPool.java index 9dc0a6880..5d03a8a38 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPool.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPool.java @@ -21,15 +21,15 @@ 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.IBalancedPoolCallee; -import network.balanced.score.core.dex.librairies.FixedPoint128; -import network.balanced.score.core.dex.librairies.FullMath; -import network.balanced.score.core.dex.librairies.LiquidityMath; -import network.balanced.score.core.dex.librairies.PositionLib; -import network.balanced.score.core.dex.librairies.SqrtPriceMath; -import network.balanced.score.core.dex.librairies.SwapMath; -import network.balanced.score.core.dex.librairies.TickLib; -import network.balanced.score.core.dex.librairies.TickMath; +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; @@ -61,7 +61,7 @@ import score.annotation.Optional; import score.annotation.Payable; -public class BalancedPool { +public class ConcentratedLiquidityPool { // ================================================ // Consts // ================================================ @@ -302,10 +302,9 @@ public void TickUpdate ( // ================================================ /** * @notice Contract constructor - * @dev This contract should be not deployed, as this class is abstract anyway. - * See {@code BalancedPoolFactored} constructor for the actual pool deployed on the network + * See {@code ConcentratedLiquidityPoolFactored} constructor for the actual pool deployed on the network */ - protected BalancedPool (Parameters parameters) { + public ConcentratedLiquidityPool (Parameters parameters) { // Initialize settings this.settings = new PoolSettings ( parameters.factory, @@ -314,7 +313,7 @@ protected BalancedPool (Parameters parameters) { parameters.fee, parameters.tickSpacing, TickLib.tickSpacingToMaxLiquidityPerTick(parameters.tickSpacing), - "Balanced Pool (" + IIRC2ICX.symbol(parameters.token0) + " / " + IIRC2ICX.symbol(parameters.token1) + " " + ((float) parameters.fee / 10000) + "%)" + "Balanced Concentrated Liquidity Pool (" + IIRC2ICX.symbol(parameters.token0) + " / " + IIRC2ICX.symbol(parameters.token1) + " " + ((float) parameters.fee / 10000) + "%)" ); // Default values @@ -467,7 +466,7 @@ public PairAmounts mint ( balance1Before = balance1(); } - IBalancedPoolCallee.balancedMintCallback(caller, amount0, amount1, data); + IConcentratedLiquidityPoolCallee.balancedMintCallback(caller, amount0, amount1, data); if (amount0.compareTo(ZERO) > 0) { BigInteger expected = balance0Before.add(amount0); @@ -816,7 +815,7 @@ public PairAmounts swap ( } BigInteger balance0Before = balance0(); - IBalancedPoolCallee.balancedSwapCallback(caller, amount0, amount1, data); + IConcentratedLiquidityPoolCallee.balancedSwapCallback(caller, amount0, amount1, data); Context.require(balance0Before.add(amount0).compareTo(balance0()) <= 0, "swap: the callback didn't charge the payment (1)"); @@ -826,7 +825,7 @@ public PairAmounts swap ( } BigInteger balance1Before = balance1(); - IBalancedPoolCallee.balancedSwapCallback(caller, amount0, amount1, data); + IConcentratedLiquidityPoolCallee.balancedSwapCallback(caller, amount0, amount1, data); Context.require(balance1Before.add(amount1).compareTo(balance1()) <= 0, "swap: the callback didn't charge the payment (2)"); @@ -880,7 +879,7 @@ public void flash ( pay(this.settings.token1, recipient, amount1); } - IBalancedPoolCallee.balancedFlashCallback(caller, fee0, fee1, data); + IConcentratedLiquidityPoolCallee.balancedFlashCallback(caller, fee0, fee1, data); BigInteger balance0After = balance0(); BigInteger balance1After = balance1(); diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPoolFactory.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPoolFactory.java similarity index 92% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPoolFactory.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPoolFactory.java index 18ce9c6c0..e652a6585 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/BalancedPoolFactory.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/ConcentratedLiquidityPoolFactory.java @@ -17,10 +17,10 @@ package network.balanced.score.core.dex; import java.math.BigInteger; -import network.balanced.score.core.dex.models.BalancedPoolDeployer; -import network.balanced.score.core.dex.interfaces.pooldeployer.IBalancedPoolDeployer; +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.IBalancedPool; +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; @@ -35,14 +35,14 @@ * @title Canonical Balanced factory * @notice Deploys Balanced pools and manages ownership and control over pool protocol fees */ -public class BalancedPoolFactory implements IBalancedPoolDeployer { +public class ConcentratedLiquidityPoolFactory implements IConcentratedLiquidityPoolDeployer { // ================================================ // Consts // ================================================ // Contract class name - private static final String NAME = "BalancedPoolFactory"; + private static final String NAME = "ConcentratedLiquidityPoolFactory"; // Contract name private final String name; @@ -56,8 +56,8 @@ public class BalancedPoolFactory implements IBalancedPoolDeployer { protected final VarDB poolContract = Context.newVarDB(NAME + "_poolContract", byte[].class); protected final EnumerableSet
poolsSet = new EnumerableSet
(NAME + "_poolsSet", Address.class); - // Implements IBalancedPoolDeployer - private final BalancedPoolDeployer poolDeployer; + // Implements IConcentratedLiquidityPoolDeployer + private final ConcentratedLiquidityPoolDeployer poolDeployer; // ================================================ // Event Logs @@ -108,8 +108,8 @@ public void PoolUpdated( /** * Contract constructor */ - public BalancedPoolFactory() { - this.poolDeployer = new BalancedPoolDeployer(); + public ConcentratedLiquidityPoolFactory() { + this.poolDeployer = new ConcentratedLiquidityPoolDeployer(); final Address caller = Context.getCaller(); this.name = "Balanced Factory"; @@ -212,10 +212,10 @@ public void updatePool ( checkOwner(); // OK - Address token0 = IBalancedPool.token0(pool); - Address token1 = IBalancedPool.token1(pool); - int fee = IBalancedPool.fee(pool); - int tickSpacing = IBalancedPool.tickSpacing(pool); + Address token0 = IConcentratedLiquidityPool.token0(pool); + Address token1 = IConcentratedLiquidityPool.token1(pool); + int fee = IConcentratedLiquidityPool.fee(pool); + int tickSpacing = IConcentratedLiquidityPool.tickSpacing(pool); this.poolDeployer.update ( pool, @@ -387,7 +387,7 @@ public Address getPool ( return this.getPool.at(token0).at(token1).get(fee); } - // --- Implement IBalancedPoolDeployer --- + // --- 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/DexDBVariables.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexDBVariables.java new file mode 100644 index 000000000..179670678 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexDBVariables.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2022-2023 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 network.balanced.score.core.dex.db.LinkedListDB; +import network.balanced.score.core.dex.utils.LPMetadataDB; +import network.balanced.score.lib.utils.IterableDictDB; +import network.balanced.score.lib.utils.SetDB; +import score.*; + +import java.math.BigInteger; + +public class DexDBVariables { + + private static final String ACCOUNT_BALANCE_SNAPSHOT = "account_balance_snapshot"; + private static final String TOTAL_SUPPLY_SNAPSHOT = "total_supply_snapshot"; + private static final String QUOTE_COINS = "quote_coins"; + private static final String ICX_QUEUE_TOTAL = "icx_queue_total"; + private static final String GOVERNANCE_ADDRESS = "governance_address"; + private static final String NAMED_MARKETS = "named_markets"; + private static final String DEX_ON = "dex_on"; + + private static final String CURRENT_DAY = "current_day"; + private static final String TIME_OFFSET = "time_offset"; + private static final String REWARDS_DONE = "rewards_done"; + private static final String DIVIDENDS_DONE = "dividends_done"; + private static final String DEPOSIT = "deposit"; + private static final String POOL_ID = "poolId"; + private static final String NONCE = "nonce"; + private static final String POOL_TOTAL = "poolTotal"; + private static final String POOL_LP_TOTAL = "poolLPTotal"; + private static final String BALANCE = "balances"; + private static final String BALN_SNAPSHOT = "balnSnapshot"; + private static final String POOL_LP_FEE = "pool_lp_fee"; + private static final String POOL_BALN_FEE = "pool_baln_fee"; + private static final String ICX_CONVERSION_FEE = "icx_conversion_fee"; + private static final String ICX_BALN_FEE = "icx_baln_fee"; + private static final String BASE_TOKEN = "baseToken"; + private static final String QUOTE_TOKEN = "quoteToken"; + private static final String ACTIVE_POOL = "activePool"; + private static final String ICX_QUEUE = "icxQueue"; + private static final String ICX_QUEUE_ORDER_ID = "icxQueueOrderId"; + private static final String SICX_EARNINGS = "sicxEarnings"; + private static final String MARKETS_NAMES = "marketsToNames"; + private static final String TOKEN_PRECISIONS = "token_precisions"; + private static final String CURRENT_TX = "current_tx"; + private static final String CONTINUOUS_REWARDS_DAY = "continuous_rewards_day"; + public static final String VERSION = "version"; + public static final String ORACLE_PROTECTION = "oracle_protection"; + + + final static VarDB
governance = Context.newVarDB(GOVERNANCE_ADDRESS, Address.class); + public final static VarDB dexOn = Context.newVarDB(DEX_ON, Boolean.class); + + // Deposits - Map: token_address -> user_address -> value + final static BranchDB> deposit = Context.newBranchDB(DEPOSIT, + BigInteger.class); + // Pool IDs - Map: token address -> opposite token_address -> id + final static BranchDB> poolId = Context.newBranchDB(POOL_ID, Integer.class); + + public final static VarDB nonce = Context.newVarDB(NONCE, Integer.class); + + // Total amount of each type of token in a pool + // Map: pool_id -> token_address -> value + final static BranchDB> poolTotal = Context.newBranchDB(POOL_TOTAL, + BigInteger.class); + + // Total LP Tokens in pool + // Map: pool_id -> total LP tokens + final static DictDB poolLpTotal = Context.newDictDB(POOL_LP_TOTAL, BigInteger.class); + + // User Balances + // Map: pool_id -> user address -> lp token balance + final static BranchDB> balance = Context.newBranchDB(BALANCE, + BigInteger.class); + + // Map: pool_id -> user address -> ids/values/length -> length/0 -> value + final static BranchDB>>> accountBalanceSnapshot = + Context.newBranchDB(ACCOUNT_BALANCE_SNAPSHOT, BigInteger.class); + // Map: pool_id -> ids/values/length -> length/0 -> value + final static BranchDB>> totalSupplySnapshot = + Context.newBranchDB(TOTAL_SUPPLY_SNAPSHOT, BigInteger.class); + + // Map: pool_id -> ids/values/length -> length/0 -> value + final static BranchDB>> balnSnapshot = + Context.newBranchDB(BALN_SNAPSHOT, BigInteger.class); + + // Rewards/timekeeping logic + final static VarDB currentDay = Context.newVarDB(CURRENT_DAY, BigInteger.class); + final static VarDB timeOffset = Context.newVarDB(TIME_OFFSET, BigInteger.class); + final static VarDB rewardsDone = Context.newVarDB(REWARDS_DONE, Boolean.class); + final static VarDB dividendsDone = Context.newVarDB(DIVIDENDS_DONE, Boolean.class); + + final static LPMetadataDB activeAddresses = new LPMetadataDB(); + // Pools must use one of these as quote currency + final static SetDB
quoteCoins = new SetDB<>(QUOTE_COINS, Address.class, null); + + // All fees are divided by `FEE_SCALE` in const + final static VarDB poolLpFee = Context.newVarDB(POOL_LP_FEE, BigInteger.class); + final static VarDB poolBalnFee = Context.newVarDB(POOL_BALN_FEE, BigInteger.class); + final static VarDB icxConversionFee = Context.newVarDB(ICX_CONVERSION_FEE, BigInteger.class); + final static VarDB icxBalnFee = Context.newVarDB(ICX_BALN_FEE, BigInteger.class); + + // Map: pool_id -> base token address + final static DictDB poolBase = Context.newDictDB(BASE_TOKEN, Address.class); + // Map: pool_id -> quote token address + final static DictDB poolQuote = Context.newDictDB(QUOTE_TOKEN, Address.class); + final static DictDB active = Context.newDictDB(ACTIVE_POOL, Boolean.class); + + final static LinkedListDB icxQueue = new LinkedListDB(ICX_QUEUE); + + // Map: user_address -> order id + final static DictDB icxQueueOrderId = Context.newDictDB(ICX_QUEUE_ORDER_ID, BigInteger.class); + + // Map: user_address -> integer of unclaimed earnings + final static DictDB sicxEarnings = Context.newDictDB(SICX_EARNINGS, BigInteger.class); + final static VarDB icxQueueTotal = Context.newVarDB(ICX_QUEUE_TOTAL, BigInteger.class); + + + final static IterableDictDB namedMarkets = new IterableDictDB<>(NAMED_MARKETS, Integer.class, + String.class, true); + + final static DictDB marketsToNames = Context.newDictDB(MARKETS_NAMES, String.class); + + final static DictDB tokenPrecisions = Context.newDictDB(TOKEN_PRECISIONS, BigInteger.class); + + // VarDB used to track the current sent transaction. This helps bound iterations. + final static VarDB currentTx = Context.newVarDB(CURRENT_TX, byte[].class); + + // Activation of continuous rewards day + final static VarDB continuousRewardsDay = Context.newVarDB(CONTINUOUS_REWARDS_DAY, BigInteger.class); + + public static final VarDB currentVersion = Context.newVarDB(VERSION, String.class); + + //Map: pid -> percentage + public final static DictDB oracleProtection = Context.newDictDB(ORACLE_PROTECTION, BigInteger.class); +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexImpl.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexImpl.java new file mode 100644 index 000000000..2726cc6e3 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/DexImpl.java @@ -0,0 +1,519 @@ +/* + * Copyright (c) 2022-2023 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 com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; +import network.balanced.score.core.dex.db.NodeDB; +import network.balanced.score.lib.structs.RewardsDataEntry; +import network.balanced.score.lib.utils.BalancedFloorLimits; +import network.balanced.score.lib.utils.Versions; +import score.Address; +import score.BranchDB; +import score.Context; +import score.DictDB; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; +import scorex.util.ArrayList; + +import java.math.BigInteger; +import java.util.List; + +import static network.balanced.score.core.dex.DexDBVariables.*; +import static network.balanced.score.core.dex.utils.Check.isDexOn; +import static network.balanced.score.core.dex.utils.Check.isValidPercent; +import static network.balanced.score.core.dex.utils.Const.*; +import static network.balanced.score.lib.utils.BalancedAddressManager.getRewards; +import static network.balanced.score.lib.utils.BalancedAddressManager.getSicx; +import static network.balanced.score.lib.utils.Check.checkStatus; +import static network.balanced.score.lib.utils.Constants.EXA; +import static network.balanced.score.lib.utils.Constants.POINTS; +import static network.balanced.score.lib.utils.Math.convertToNumber; +import static score.Context.require; + +public class DexImpl extends AbstractDex { + + public DexImpl(Address _governance) { + super(_governance); + if (currentVersion.getOrDefault("").equals(Versions.DEX)) { + Context.revert("Can't Update same version of code"); + } + currentVersion.set(Versions.DEX); + } + + @External(readonly = true) + public String version() { + return currentVersion.getOrDefault(""); + } + + @Payable + public void fallback() { + isDexOn(); + checkStatus(); + + BigInteger orderValue = Context.getValue(); + require(orderValue.compareTo(BigInteger.TEN.multiply(EXA)) >= 0, + TAG + ": Minimum pool contribution is 10 ICX"); + + Address user = Context.getCaller(); + BigInteger oldOrderValue = BigInteger.ZERO; + BigInteger orderId = icxQueueOrderId.getOrDefault(user, BigInteger.ZERO); + + // Upsert Order, so we can bump to the back of the queue + if (orderId.compareTo(BigInteger.ZERO) > 0) { + NodeDB node = icxQueue.getNode(orderId); + oldOrderValue = node.getSize(); + orderValue = orderValue.add(oldOrderValue); + icxQueue.remove(orderId); + } + + // Insert order to the back of the queue + BigInteger nextTailId = icxQueue.getTailId().add(BigInteger.ONE); + icxQueue.append(orderValue, user, nextTailId); + icxQueueOrderId.set(user, nextTailId); + + // Update total ICX queue size + BigInteger oldIcxTotal = icxQueueTotal.getOrDefault(BigInteger.ZERO); + BigInteger currentIcxTotal = oldIcxTotal.add(orderValue).subtract(oldOrderValue); + icxQueueTotal.set(currentIcxTotal); + + activeAddresses.get(SICXICX_POOL_ID).add(user); + + sendRewardsData(user, orderValue, currentIcxTotal); + } + + @External + public void cancelSicxicxOrder() { + isDexOn(); + checkStatus(); + + Address user = Context.getCaller(); + BigInteger orderId = icxQueueOrderId.getOrDefault(user, BigInteger.ZERO); + + require(orderId.compareTo(BigInteger.ZERO) > 0, TAG + ": No open order in sICX/ICX queue."); + + NodeDB order = icxQueue.getNode(orderId); + BigInteger withdrawAmount = order.getSize(); + BigInteger oldIcxTotal = icxQueueTotal.get(); + BigInteger currentIcxTotal = oldIcxTotal.subtract(withdrawAmount); + + icxQueueTotal.set(currentIcxTotal); + icxQueue.remove(orderId); + icxQueueOrderId.set(user, null); + activeAddresses.get(SICXICX_POOL_ID).remove(user); + + sendRewardsData(user, BigInteger.ZERO, currentIcxTotal); + + BalancedFloorLimits.verifyNativeWithdraw(withdrawAmount); + Context.transfer(user, withdrawAmount); + } + + private void sendRewardsData(Address user, BigInteger amount, BigInteger oldIcxTotal) { + List rewardsList = new ArrayList<>(); + RewardsDataEntry rewardsEntry = new RewardsDataEntry(); + rewardsEntry._user = user.toString(); + rewardsEntry._balance = amount; + rewardsList.add(rewardsEntry); + Context.call(getRewards(), "updateBalanceAndSupply", SICXICX_MARKET_NAME, oldIcxTotal, user.toString(), amount); + } + + @External + public void xTokenFallback(String _from, BigInteger _value, byte[] _data) { + isDexOn(); + + String unpackedData = new String(_data); + require(!unpackedData.equals(""), "Token Fallback: Data can't be empty"); + + JsonObject json = Json.parse(unpackedData).asObject(); + + String method = json.get("method").asString(); + Address fromToken = Context.getCaller(); + + require(_value.compareTo(BigInteger.ZERO) > 0, TAG + ": Invalid token transfer value"); + + if (method.equals("_deposit")) { + JsonObject params = json.get("params").asObject(); + Address to = Address.fromString(params.get("address").asString()); + deposit(fromToken, to, _value); + } else {// If no supported method was sent, revert the transaction + Context.revert(100, TAG + ": Unsupported method supplied"); + } + } + + @External + public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { + isDexOn(); + checkStatus(); + + // Parse the transaction data submitted by the user + String unpackedData = new String(_data); + require(!unpackedData.equals(""), "Token Fallback: Data can't be empty"); + + JsonObject json = Json.parse(unpackedData).asObject(); + + String method = json.get("method").asString(); + Address fromToken = Context.getCaller(); + + require(_value.compareTo(BigInteger.ZERO) > 0, TAG + ": Invalid token transfer value"); + + // Call an internal method based on the "method" param sent in tokenFallBack + switch (method) { + case "_deposit": { + deposit(fromToken, _from, _value); + break; + } + case "_swap_icx": { + require(fromToken.equals(getSicx()), + TAG + ": InvalidAsset: _swap_icx can only be called with sICX"); + swapIcx(_from, _value); + break; + } + case "_swap": { + + // Parse the slippage sent by the user in minimumReceive. + // If none is sent, use the maximum. + JsonObject params = json.get("params").asObject(); + BigInteger minimumReceive = BigInteger.ZERO; + if (params.contains("minimumReceive")) { + minimumReceive = convertToNumber(params.get("minimumReceive")); + require(minimumReceive.signum() >= 0, + TAG + ": Must specify a positive number for minimum to receive"); + } + + // Check if an alternative recipient of the swap is set. + Address receiver; + if (params.contains("receiver")) { + receiver = Address.fromString(params.get("receiver").asString()); + } else { + receiver = _from; + } + + // Get destination coin from the swap + require(params.contains("toToken"), TAG + ": No toToken specified in swap"); + Address toToken = Address.fromString(params.get("toToken").asString()); + + // Perform the swap + exchange(fromToken, toToken, _from, receiver, _value, minimumReceive); + break; + } + case "_donate": { + JsonObject params = json.get("params").asObject(); + require(params.contains("toToken"), TAG + ": No toToken specified in swap"); + Address toToken = Address.fromString(params.get("toToken").asString()); + donate(fromToken, toToken, _value); + break; + } + default: + // If no supported method was sent, revert the transaction + Context.revert(100, TAG + ": Unsupported method supplied"); + break; + } + } + + @External + public void transfer(Address _to, BigInteger _value, BigInteger _id, @Optional byte[] _data) { + isDexOn(); + checkStatus(); + if (_data == null) { + _data = new byte[0]; + } + _transfer(Context.getCaller(), _to, _value, _id.intValue(), _data); + } + + @External + public void withdraw(Address _token, BigInteger _value) { + isDexOn(); + checkStatus(); + require(_value.compareTo(BigInteger.ZERO) > 0, TAG + ": Must specify a positive amount"); + Address sender = Context.getCaller(); + DictDB depositDetails = deposit.at(_token); + BigInteger deposit_amount = depositDetails.getOrDefault(sender, BigInteger.ZERO); + require(_value.compareTo(deposit_amount) <= 0, TAG + ": Insufficient Balance"); + + depositDetails.set(sender, deposit_amount.subtract(_value)); + + Withdraw(_token, sender, _value); + BalancedFloorLimits.verifyWithdraw(_token, _value); + Context.call(_token, "transfer", sender, _value); + } + + @External(readonly = true) + public BigInteger depositOfUser(Address _owner, Address _token) { + DictDB depositDetails = deposit.at(_token); + return depositDetails.getOrDefault(_owner, BigInteger.ZERO); + } + + @External + public void remove(BigInteger _id, BigInteger _value, @Optional boolean _withdraw) { + isDexOn(); + checkStatus(); + Address user = Context.getCaller(); + Address baseToken = poolBase.get(_id.intValue()); + require(baseToken != null, TAG + ": invalid pool id"); + DictDB userLPBalance = balance.at(_id.intValue()); + BigInteger userBalance = userLPBalance.getOrDefault(user, BigInteger.ZERO); + + require(active.getOrDefault(_id.intValue(), false), TAG + ": Pool is not active"); + require(_value.compareTo(BigInteger.ZERO) > 0, TAG + " Cannot withdraw a negative or zero balance"); + require(_value.compareTo(userBalance) <= 0, TAG + ": Insufficient balance"); + + Address quoteToken = poolQuote.get(_id.intValue()); + DictDB totalTokensInPool = poolTotal.at(_id.intValue()); + BigInteger totalBase = totalTokensInPool.get(baseToken); + BigInteger totalQuote = totalTokensInPool.get(quoteToken); + BigInteger totalLPToken = poolLpTotal.get(_id.intValue()); + + BigInteger userQuoteLeft = ((userBalance.subtract(_value)).multiply(totalQuote)).divide(totalLPToken); + + if (userQuoteLeft.compareTo(getRewardableAmount(quoteToken)) < 0) { + _value = userBalance; + activeAddresses.get(_id.intValue()).remove(user); + } + + BigInteger baseToWithdraw = _value.multiply(totalBase).divide(totalLPToken); + BigInteger quoteToWithdraw = _value.multiply(totalQuote).divide(totalLPToken); + + BigInteger newBase = totalBase.subtract(baseToWithdraw); + BigInteger newQuote = totalQuote.subtract(quoteToWithdraw); + BigInteger newUserBalance = userBalance.subtract(_value); + BigInteger newTotal = totalLPToken.subtract(_value); + + require(newTotal.compareTo(MIN_LIQUIDITY) >= 0, + TAG + ": Cannot withdraw pool past minimum LP token amount"); + + totalTokensInPool.set(baseToken, newBase); + totalTokensInPool.set(quoteToken, newQuote); + userLPBalance.set(user, newUserBalance); + poolLpTotal.set(_id.intValue(), newTotal); + + Remove(_id, user, _value, baseToWithdraw, quoteToWithdraw); + TransferSingle(user, user, MINT_ADDRESS, _id, _value); + + DictDB userBaseDeposit = deposit.at(baseToken); + BigInteger depositedBase = userBaseDeposit.getOrDefault(user, BigInteger.ZERO); + userBaseDeposit.set(user, depositedBase.add(baseToWithdraw)); + + DictDB userQuoteDeposit = deposit.at(quoteToken); + BigInteger depositedQuote = userQuoteDeposit.getOrDefault(user, BigInteger.ZERO); + userQuoteDeposit.set(user, depositedQuote.add(quoteToWithdraw)); + + if (_withdraw) { + withdraw(baseToken, baseToWithdraw); + withdraw(quoteToken, quoteToWithdraw); + } + + } + + @External + public void add(Address _baseToken, Address _quoteToken, BigInteger _baseValue, BigInteger _quoteValue, + @Optional boolean _withdraw_unused, @Optional BigInteger _slippagePercentage) { + isDexOn(); + checkStatus(); + isValidPercent(_slippagePercentage.intValue()); + + Address user = Context.getCaller(); + + // We check if there is a previously seen pool with this id. + // If none is found (return 0), we create a new pool. + Integer id = poolId.at(_baseToken).getOrDefault(_quoteToken, 0); + + require(_baseToken != _quoteToken, TAG + ": Pool must contain two token contracts"); + // Check base/quote balances are valid + require(_baseValue.compareTo(BigInteger.ZERO) > 0, + TAG + ": Cannot send 0 or negative base token"); + require(_quoteValue.compareTo(BigInteger.ZERO) > 0, + TAG + ": Cannot send 0 or negative quote token"); + + BigInteger userDepositedBase = deposit.at(_baseToken).getOrDefault(user, BigInteger.ZERO); + BigInteger userDepositedQuote = deposit.at(_quoteToken).getOrDefault(user, BigInteger.ZERO); + + // Check deposits are sufficient to cover balances + require(userDepositedBase.compareTo(_baseValue) >= 0, + TAG + ": Insufficient base asset funds deposited"); + require(userDepositedQuote.compareTo(_quoteValue) >= 0, + TAG + ": Insufficient quote asset funds deposited"); + + BigInteger baseToCommit = _baseValue; + BigInteger quoteToCommit = _quoteValue; + + // Initialize pool total variables + BigInteger liquidity; + BigInteger poolBaseAmount = BigInteger.ZERO; + BigInteger poolQuoteAmount = BigInteger.ZERO; + BigInteger poolLpAmount = poolLpTotal.getOrDefault(id, BigInteger.ZERO); + BigInteger userLpAmount = balance.at(id).getOrDefault(user, BigInteger.ZERO); + + // We need to only supply new base and quote in the pool ratio. + // If there isn't a pool yet, we can form one with the supplied ratios. + + if (id == 0) { + // No pool exists for this pair, we should create a new one. + + // Issue the next pool id to this pool + if (!quoteCoins.contains(_quoteToken)) { + Context.revert(TAG + " : QuoteNotAllowed: Supplied quote token not in permitted set."); + } + + Integer nextPoolNonce = nonce.get(); + poolId.at(_baseToken).set(_quoteToken, nextPoolNonce); + poolId.at(_quoteToken).set(_baseToken, nextPoolNonce); + id = nextPoolNonce; + nonce.set(nextPoolNonce + 1); + + active.set(id, true); + poolBase.set(id, _baseToken); + poolQuote.set(id, _quoteToken); + + liquidity = (_baseValue.multiply(_quoteValue)).sqrt(); + require(liquidity.compareTo(MIN_LIQUIDITY) >= 0, + TAG + ": Initial LP tokens must exceed " + MIN_LIQUIDITY); + MarketAdded(BigInteger.valueOf(id), _baseToken, _quoteToken, _baseValue, _quoteValue); + } else { + // Pool already exists, supply in the permitted order. + Address poolBaseAddress = poolBase.get(id); + Address poolQuoteAddress = poolQuote.get(id); + + require((poolBaseAddress.equals(_baseToken)) && (poolQuoteAddress.equals(_quoteToken)), + TAG + ": Must supply " + _baseToken.toString() + " as base and " + _quoteToken.toString() + + " as quote"); + + // We can only commit in the ratio of the pool. We determine this as: + // Min(ratio of quote from base, ratio of base from quote) + // Any assets not used are refunded + + DictDB totalTokensInPool = poolTotal.at(id); + poolBaseAmount = totalTokensInPool.get(_baseToken); + poolQuoteAmount = totalTokensInPool.get(_quoteToken); + + BigInteger poolPrice = poolBaseAmount.multiply(EXA).divide(poolQuoteAmount); + BigInteger priceOfAssetToCommit = baseToCommit.multiply(EXA).divide(quoteToCommit); + + require( + (priceOfAssetToCommit.subtract(poolPrice)).abs().compareTo(_slippagePercentage.multiply(poolPrice).divide(POINTS)) <= 0, + TAG + " : insufficient slippage provided" + ); + + BigInteger baseFromQuote = _quoteValue.multiply(poolBaseAmount).divide(poolQuoteAmount); + BigInteger quoteFromBase = _baseValue.multiply(poolQuoteAmount).divide(poolBaseAmount); + + if (quoteFromBase.compareTo(_quoteValue) <= 0) { + quoteToCommit = quoteFromBase; + } else { + baseToCommit = baseFromQuote; + } + + BigInteger liquidityFromBase = (poolLpAmount.multiply(baseToCommit)).divide(poolBaseAmount); + BigInteger liquidityFromQuote = (poolLpAmount.multiply(quoteToCommit)).divide(poolQuoteAmount); + + liquidity = liquidityFromBase.min(liquidityFromQuote); + require(liquidity.compareTo(BigInteger.ZERO) >= 0, + TAG + ": LP tokens to mint is less than zero"); + } + + // Apply the funds to the pool + poolBaseAmount = poolBaseAmount.add(baseToCommit); + poolQuoteAmount = poolQuoteAmount.add(quoteToCommit); + + DictDB totalTokensInPool = poolTotal.at(id); + totalTokensInPool.set(_baseToken, poolBaseAmount); + totalTokensInPool.set(_quoteToken, poolQuoteAmount); + + // Deduct the user's deposit + userDepositedBase = userDepositedBase.subtract(baseToCommit); + deposit.at(_baseToken).set(user, userDepositedBase); + userDepositedQuote = userDepositedQuote.subtract(quoteToCommit); + deposit.at(_quoteToken).set(user, userDepositedQuote); + + // Credit the user LP Tokens + userLpAmount = userLpAmount.add(liquidity); + poolLpAmount = poolLpAmount.add(liquidity); + + balance.at(id).set(user, userLpAmount); + poolLpTotal.set(id, poolLpAmount); + Add(BigInteger.valueOf(id), user, liquidity, baseToCommit, quoteToCommit); + TransferSingle(user, MINT_ADDRESS, user, BigInteger.valueOf(id), liquidity); + + activeAddresses.get(id).add(user); + + if (userDepositedBase.compareTo(BigInteger.ZERO) > 0) { + withdraw(_baseToken, userDepositedBase); + } + + if (userDepositedQuote.compareTo(BigInteger.ZERO) > 0) { + withdraw(_quoteToken, userDepositedQuote); + } + } + + @External + public void withdrawSicxEarnings(@Optional BigInteger _value) { + isDexOn(); + checkStatus(); + if (_value == null) { + _value = BigInteger.ZERO; + } + Address sender = Context.getCaller(); + BigInteger sicxEarning = getSicxEarnings(sender); + if (_value.equals(BigInteger.ZERO)) { + _value = sicxEarning; + } + + require(_value.compareTo(BigInteger.ZERO) > 0, + TAG + ": InvalidAmountError: Please send a positive amount."); + require(_value.compareTo(sicxEarning) <= 0, TAG + ": Insufficient balance."); + + sicxEarnings.set(sender, sicxEarning.subtract(_value)); + ClaimSicxEarnings(sender, _value); + Address sICX = getSicx(); + BalancedFloorLimits.verifyWithdraw(sICX, _value); + Context.call(sICX, "transfer", sender, _value); + } + + @External + public void onIRC31Received(Address _operator, Address _from, BigInteger _id, BigInteger _value, byte[] _data) { + checkStatus(); + Context.revert(TAG + ": IRC31 Tokens not accepted"); + } + + // TODO remove when dividends no longer use this + @External(readonly = true) + public BigInteger balanceOfAt(Address _account, BigInteger _id, BigInteger _snapshot_id, @Optional boolean _twa) { + + int poolId = _id.intValue(); + BranchDB> snapshot = accountBalanceSnapshot.at(poolId).at(_account); + return snapshotValueAt(_snapshot_id, snapshot); + + } + + @External(readonly = true) + public BigInteger totalSupplyAt(BigInteger _id, BigInteger _snapshot_id, @Optional boolean _twa) { + + int poolId = _id.intValue(); + BranchDB> snapshot = totalSupplySnapshot.at(poolId); + return snapshotValueAt(_snapshot_id, snapshot); + } + + @External(readonly = true) + public BigInteger totalBalnAt(BigInteger _id, BigInteger _snapshot_id, @Optional boolean _twa) { + + int poolId = _id.intValue(); + BranchDB> snapshot = balnSnapshot.at(poolId); + return snapshotValueAt(_snapshot_id, snapshot); + } + +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/LinkedListDB.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/LinkedListDB.java new file mode 100644 index 000000000..b465a75da --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/LinkedListDB.java @@ -0,0 +1,202 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex.db; + +import score.Address; +import score.Context; +import score.UserRevertedException; +import score.VarDB; + +import java.math.BigInteger; + + +public class LinkedListDB { + + private static final String NAME = "_LINKED_LISTDB"; + public static final BigInteger DEFAULT_NODE_ID = BigInteger.ZERO; + + private final VarDB headId; + private final VarDB tailId; + private final VarDB length; + private final String name; + + + public LinkedListDB(String key) { + this.name = key + NAME; + this.headId = Context.newVarDB(this.name + "_head_id", BigInteger.class); + this.tailId = Context.newVarDB(this.name + "_tail_id", BigInteger.class); + this.length = Context.newVarDB(this.name + "_length", BigInteger.class); + } + + public BigInteger getTailId() { + return tailId.getOrDefault(BigInteger.ZERO); + } + + public BigInteger size() { + return length.getOrDefault(BigInteger.ZERO); + } + + public NodeDB createNodeInstance(BigInteger nodeId) { + return new NodeDB(nodeId.toString() + name); + } + + public void updateNode(BigInteger nodeId, BigInteger size, Address user) { + NodeDB node = createNodeInstance(nodeId); + Context.require(node.exists(), "There is no node of the provided node id."); + node.setValues(size, user); + } + + public BigInteger append(BigInteger size, Address user, BigInteger nodeId) { + NodeDB nodeToAppend = createNode(size, user, nodeId); + if (length.getOrDefault(BigInteger.ZERO).equals(BigInteger.ZERO)) { + headId.set(nodeId); + } else { + BigInteger tailId = this.tailId.getOrDefault(DEFAULT_NODE_ID); + NodeDB tail = getNode(tailId); + tail.setNext(nodeId); + nodeToAppend.setPrev(tailId); + } + tailId.set(nodeId); + length.set(length.getOrDefault(BigInteger.ZERO).add(BigInteger.ONE)); + return nodeId; + } + + public NodeDB getNode(BigInteger nodeId) { + if (nodeId == null || nodeId.equals(DEFAULT_NODE_ID)) { + throw new UserRevertedException("Invalid Node Id"); + } + NodeDB node = createNodeInstance(nodeId); + if (!node.exists()) { + LinkedNodeNotFound(name, nodeId); + } + return node; + } + + public NodeDB getTailNode() { + BigInteger tailId = this.tailId.getOrDefault(DEFAULT_NODE_ID); + if (tailId == null || tailId.equals(DEFAULT_NODE_ID)) { + Context.revert("Empty Linked list"); + } + return getNode(tailId); + } + + public NodeDB getHeadNode() { + BigInteger headId = this.headId.getOrDefault(DEFAULT_NODE_ID); + if (headId == null || headId.equals(DEFAULT_NODE_ID)) { + Context.revert("Empty Linked list"); + } + return getNode(headId); + } + + + public NodeDB createNode(BigInteger size, Address user, + BigInteger nodeId) { + NodeDB node = createNodeInstance(nodeId); + if (node.exists()) { + LinkedNodeAlreadyExists(name, nodeId); + } + node.setValues(size, user); + return node; + } + + + public void removeHead() { + BigInteger size = length.getOrDefault(BigInteger.ZERO); + if (size.equals(BigInteger.ZERO)) { + return; + } + if (size.equals(BigInteger.ONE)) { + clear(); + return; + } + + NodeDB oldHead = getNode(headId.getOrDefault(DEFAULT_NODE_ID)); + BigInteger newHead = oldHead.getNext(); + headId.set(newHead); + getNode(newHead).setPrev(DEFAULT_NODE_ID); + oldHead.delete(); + length.set(size.subtract(BigInteger.ONE)); + } + + public void removeTail() { + BigInteger size = length.getOrDefault(BigInteger.ZERO); + if (size.equals(BigInteger.ZERO)) { + return; + } + if (size.equals(BigInteger.ONE)) { + clear(); + return; + } + + NodeDB oldTail = getNode(tailId.getOrDefault(DEFAULT_NODE_ID)); + BigInteger newTail = oldTail.getPrev(); + tailId.set(newTail); + getNode(newTail).setNext(DEFAULT_NODE_ID); + oldTail.delete(); + length.set(size.subtract(BigInteger.ONE)); + } + + public void remove(BigInteger curId) { + BigInteger size = length.getOrDefault(BigInteger.ZERO); + if (size.equals(BigInteger.ZERO)) { + return; + } + if (curId.equals(headId.getOrDefault(DEFAULT_NODE_ID))) { + removeHead(); + } else if (curId.equals(tailId.getOrDefault(DEFAULT_NODE_ID))) { + removeTail(); + } else { + NodeDB nodeToRemove = getNode(curId); + BigInteger nextNodeId = nodeToRemove.getNext(); + NodeDB nextNode = getNode(nextNodeId); + BigInteger previousNodeId = nodeToRemove.getPrev(); + NodeDB previousNode = getNode(previousNodeId); + nextNode.setPrev(previousNodeId); + previousNode.setNext(nextNodeId); + nodeToRemove.delete(); + length.set(size.subtract(BigInteger.ONE)); + } + } + + public void clear() { + BigInteger currentId = headId.getOrDefault(DEFAULT_NODE_ID); + if (currentId == null || currentId.equals(DEFAULT_NODE_ID)) { + return; + } + NodeDB nodeToRemove = getNode(currentId); + BigInteger tailId = this.tailId.getOrDefault(DEFAULT_NODE_ID); + while (!currentId.equals(tailId)) { + currentId = nodeToRemove.getNext(); + nodeToRemove.delete(); + nodeToRemove = getNode(currentId); + } + nodeToRemove.delete(); + + this.tailId.set(null); + this.headId.set(null); + this.length.set(null); + } + + private void LinkedNodeAlreadyExists(String name, BigInteger nodeId) { + Context.revert("Linked List " + name + " already exists of nodeId." + nodeId.toString()); + } + + private void LinkedNodeNotFound(String name, BigInteger nodeId) { + Context.revert("Linked List " + name + " Node not found of nodeId " + nodeId.toString()); + } + +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/NodeDB.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/NodeDB.java new file mode 100644 index 000000000..470536673 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/db/NodeDB.java @@ -0,0 +1,92 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex.db; + +import score.Address; +import score.Context; +import score.VarDB; + +import java.math.BigInteger; + +import static network.balanced.score.core.dex.db.LinkedListDB.DEFAULT_NODE_ID; + +public class NodeDB { + + private static final String NAME = "_NODEDB"; + + private final VarDB size; + private final VarDB
user; + private final VarDB next; + private final VarDB prev; + + public NodeDB(String key) { + String name = key + NAME; + this.size = Context.newVarDB(name + "_value1", BigInteger.class); + this.user = Context.newVarDB(name + "_value2", Address.class); + this.next = Context.newVarDB(name + "_next", BigInteger.class); + this.prev = Context.newVarDB(name + "_prev", BigInteger.class); + } + + public void delete() { + size.set(null); + user.set(null); + prev.set(null); + next.set(null); + } + + public boolean exists() { + return (size.get() != null) && (user.get() != null); + } + + public BigInteger getSize() { + return size.getOrDefault(BigInteger.ZERO); + } + + public void setSize(BigInteger value) { + size.set(value); + } + + public Address getUser() { + return user.get(); + } + + public void setUser(Address user) { + this.user.set(user); + } + + public void setValues(BigInteger size, Address user) { + this.size.set(size); + this.user.set(user); + } + + public BigInteger getNext() { + return next.getOrDefault(DEFAULT_NODE_ID); + } + + public void setNext(BigInteger nextId) { + next.set(nextId); + } + + public BigInteger getPrev() { + return prev.getOrDefault(DEFAULT_NODE_ID); + } + + public void setPrev(BigInteger prev_id) { + prev.set(prev_id); + } + +} 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 index b744e2288..99f71580a 100644 --- 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 @@ -20,9 +20,9 @@ public interface IBalancedMintCallback { /** - * @notice Called to `Context.getCaller()` after minting liquidity to a position from BalancedPool#mint. + * @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 BalancedPool deployed by the canonical BalancedPoolFactory. + * 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 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 index e4497fbfc..4e60b16eb 100644 --- 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 @@ -20,8 +20,8 @@ public interface IBalancedSwapCallback { /** - * Called to `msg.sender` after executing a swap via `BalancedPool::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 BalancedPool deployed by the canonical BalancedPoolFactory. amount0Delta and amount1Delta can both be 0 if no tokens were swapped. + * 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 diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPool.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPool.java similarity index 99% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPool.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPool.java index 5cc459a9d..6fd9f392e 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPool.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPool.java @@ -28,7 +28,7 @@ import score.Address; import score.Context; -public class IBalancedPool { +public class IConcentratedLiquidityPool { // Write methods public static PairAmounts mint ( diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPoolCallee.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPoolCallee.java similarity index 96% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPoolCallee.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPoolCallee.java index 165858421..e88476f48 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IBalancedPoolCallee.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pool/IConcentratedLiquidityPoolCallee.java @@ -20,7 +20,7 @@ import score.Address; import score.Context; -public class IBalancedPoolCallee { +public class IConcentratedLiquidityPoolCallee { // Write methods public static void balancedMintCallback ( diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IBalancedPoolDeployer.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IConcentratedLiquidityPoolDeployer.java similarity index 96% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IBalancedPoolDeployer.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IConcentratedLiquidityPoolDeployer.java index 75a4b7c9a..090fcba3a 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IBalancedPoolDeployer.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/interfaces/pooldeployer/IConcentratedLiquidityPoolDeployer.java @@ -19,7 +19,7 @@ import network.balanced.score.core.dex.structs.factory.Parameters; import score.annotation.External; -public interface IBalancedPoolDeployer { +public interface IConcentratedLiquidityPoolDeployer { // ================================================ // Methods diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/BitMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/BitMath.java similarity index 98% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/BitMath.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/BitMath.java index c81934bcb..616bf2994 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/BitMath.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/BitMath.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import java.math.BigInteger; import network.balanced.score.core.dex.utils.IntUtils; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FixedPoint128.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint128.java similarity index 94% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FixedPoint128.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint128.java index 685a22bb8..8c60221e3 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FixedPoint128.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint128.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import java.math.BigInteger; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FixedPoint96.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint96.java similarity index 94% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FixedPoint96.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint96.java index e0799f572..9742672b1 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FixedPoint96.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FixedPoint96.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import java.math.BigInteger; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FullMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FullMath.java similarity index 99% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FullMath.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FullMath.java index 071fa63f2..47b68aef2 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/FullMath.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/FullMath.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import static network.balanced.score.core.dex.utils.IntUtils.MAX_UINT256; import static java.math.BigInteger.ONE; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/LiquidityMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/LiquidityMath.java similarity index 96% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/LiquidityMath.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/LiquidityMath.java index 80fd1cccb..47f123712 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/LiquidityMath.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/LiquidityMath.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import static network.balanced.score.core.dex.utils.IntUtils.uint128; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/OracleLib.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/OracleLib.java similarity index 96% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/OracleLib.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/OracleLib.java index 8c0b55b24..5e264f774 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/OracleLib.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/OracleLib.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import static java.math.BigInteger.ONE; import static java.math.BigInteger.ZERO; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/PositionLib.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/PositionLib.java similarity index 98% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/PositionLib.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/PositionLib.java index f5b056337..366c8df90 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/PositionLib.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/PositionLib.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import static network.balanced.score.core.dex.utils.IntUtils.uint128; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/SqrtPriceMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SqrtPriceMath.java similarity index 99% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/SqrtPriceMath.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SqrtPriceMath.java index 3466eadec..462c2f4f6 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/SqrtPriceMath.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SqrtPriceMath.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import static java.math.BigInteger.ZERO; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/SwapMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SwapMath.java similarity index 99% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/SwapMath.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SwapMath.java index 874944d9d..4cc0e385a 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/SwapMath.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/SwapMath.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import static java.math.BigInteger.ZERO; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/TickLib.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickLib.java similarity index 95% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/TickLib.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickLib.java index d4a1ac6ab..d38528f4f 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/TickLib.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickLib.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import java.math.BigInteger; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/TickMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickMath.java similarity index 99% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/TickMath.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickMath.java index 8a420aaf8..c5fd7d94a 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/TickMath.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/TickMath.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import static java.math.BigInteger.ONE; import static java.math.BigInteger.ZERO; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/UnsafeMath.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/UnsafeMath.java similarity index 94% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/UnsafeMath.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/UnsafeMath.java index 207c5d152..fc629d4d5 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/librairies/UnsafeMath.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/libs/UnsafeMath.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package network.balanced.score.core.dex.librairies; +package network.balanced.score.core.dex.libs; import java.math.BigInteger; import static network.balanced.score.core.dex.utils.MathUtils.gt; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/BalancedPoolDeployer.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/ConcentratedLiquidityPoolDeployer.java similarity index 95% rename from core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/BalancedPoolDeployer.java rename to core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/ConcentratedLiquidityPoolDeployer.java index fafd4ca1a..d528196ed 100644 --- a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/BalancedPoolDeployer.java +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/models/ConcentratedLiquidityPoolDeployer.java @@ -23,7 +23,7 @@ import score.VarDB; import score.annotation.External; -public class BalancedPoolDeployer { +public class ConcentratedLiquidityPoolDeployer { // ================================================ // Consts 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 index cc6ec3619..07bfb4733 100644 --- 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 @@ -20,7 +20,7 @@ import static java.math.BigInteger.ZERO; import java.math.BigInteger; -import network.balanced.score.core.dex.librairies.OracleLib; +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; 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 index a2f7b2d81..7559f9338 100644 --- 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 @@ -20,7 +20,7 @@ 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.librairies.BitMath; +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; 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 index 502a555ae..0576934e9 100644 --- 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 @@ -19,7 +19,7 @@ import static java.math.BigInteger.ZERO; import java.math.BigInteger; -import network.balanced.score.core.dex.librairies.LiquidityMath; +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; diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Check.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Check.java new file mode 100644 index 000000000..8b5f6758a --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Check.java @@ -0,0 +1,47 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex.utils; + +import score.Context; + +import java.math.BigInteger; + +import static network.balanced.score.core.dex.DexDBVariables.dexOn; +import static network.balanced.score.core.dex.DexDBVariables.nonce; +import static network.balanced.score.core.dex.utils.Const.TAG; +import static network.balanced.score.lib.utils.Constants.POINTS; + +public class Check { + + public static void isDexOn() { + Context.require(dexOn.getOrDefault(false), "NotLaunched: Function cannot be called " + + "before the DEX is turned on"); + } + + public static void isValidPoolId(BigInteger id) { + isValidPoolId(id.intValue()); + } + + public static void isValidPoolId(Integer id) { + Context.require(id > 0 && id <= nonce.get(), TAG + ": Invalid pool ID"); + } + + public static void isValidPercent(Integer percent) { + Context.require(percent >= 0 && percent <= POINTS.intValue(), TAG + ": Invalid percentage"); + } + +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Const.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Const.java new file mode 100644 index 000000000..871efd547 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/Const.java @@ -0,0 +1,41 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex.utils; + +import network.balanced.score.lib.utils.Names; +import score.Address; + +import java.math.BigInteger; + +import static network.balanced.score.lib.utils.Constants.EOA_ZERO; +import static network.balanced.score.lib.utils.Constants.MICRO_SECONDS_IN_A_DAY; + +public class Const { + + public static final String SICXICX_MARKET_NAME = "sICX/ICX"; + + public static final int SICXICX_POOL_ID = 1; + public static final BigInteger MIN_LIQUIDITY = BigInteger.valueOf(1_000); + public static final BigInteger FEE_SCALE = BigInteger.valueOf(10_000); + public static final Integer ICX_QUEUE_FILL_DEPTH = 50; + public static final Address MINT_ADDRESS = EOA_ZERO; + public static final String TAG = Names.DEX; + + public static final String IDS = "ids"; + public static final String VALUES = "values"; + public static final String LENGTH = "length"; +} diff --git a/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/LPMetadataDB.java b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/LPMetadataDB.java new file mode 100644 index 000000000..97f1b9914 --- /dev/null +++ b/core-contracts/Dex/src/main/java/network/balanced/score/core/dex/utils/LPMetadataDB.java @@ -0,0 +1,29 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex.utils; + +import network.balanced.score.lib.utils.EnumerableSetDB; +import score.Address; + +public class LPMetadataDB { + private static final String LP_METADATA_PREFIX = "lp"; + + public EnumerableSetDB
get(Integer id) { + return new EnumerableSetDB<>(LP_METADATA_PREFIX + id, Address.class); + } + +} diff --git a/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestBase.java b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestBase.java new file mode 100644 index 000000000..f3e2e9917 --- /dev/null +++ b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestBase.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2022-2023 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 com.iconloop.score.test.Account; +import com.iconloop.score.test.Score; +import com.iconloop.score.test.ServiceManager; +import network.balanced.score.lib.test.UnitTest; +import network.balanced.score.lib.test.mock.MockBalanced; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import score.Address; +import score.Context; +import score.annotation.Optional; + +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; + +import static network.balanced.score.lib.utils.Constants.EXA; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + + +class DexTestBase extends UnitTest { + protected static final ServiceManager sm = getServiceManager(); + protected static Account ownerAccount = sm.createAccount(); + protected static Account adminAccount = sm.createAccount(); + protected static Account prep_address = sm.createAccount(); + + int scoreCount = 0; + protected MockBalanced mockBalanced; + protected Account governanceScore; + protected Account dividendsScore; + protected Account stakingScore; + protected Account rewardsScore; + protected Account bnusdScore; + protected Account balnScore; + protected Account sicxScore; + protected Account feehandlerScore; + protected Account stakedLPScore; + protected Account balancedOracle; + + public static Score dexScore; + public static DexImpl dexScoreSpy; + + protected final MockedStatic contextMock = Mockito.mockStatic(Context.class, Mockito.CALLS_REAL_METHODS); + + public void setup() throws Exception { + mockBalanced = new MockBalanced(sm, ownerAccount); + governanceScore = mockBalanced.governance.account; + dividendsScore = mockBalanced.dividends.account; + stakingScore = mockBalanced.staking.account; + rewardsScore = mockBalanced.rewards.account; + bnusdScore = mockBalanced.bnUSD.account; + balnScore = mockBalanced.baln.account; + sicxScore = mockBalanced.sicx.account; + feehandlerScore = mockBalanced.feehandler.account; + stakedLPScore = mockBalanced.stakedLp.account; + balancedOracle = mockBalanced.balancedOracle.account; + + contextMock.when(() -> Context.call(eq(governanceScore.getAddress()), eq("checkStatus"), any(String.class))).thenReturn(null); + contextMock.when(() -> Context.call(eq(BigInteger.class), any(Address.class), eq("balanceOf"), any(Address.class))).thenReturn(BigInteger.ZERO); + + dexScore = sm.deploy(ownerAccount, DexImpl.class, governanceScore.getAddress()); + dexScore.invoke(governanceScore, "setTimeOffset", BigInteger.valueOf(Context.getBlockTimestamp())); + dexScoreSpy = (DexImpl) spy(dexScore.getInstance()); + dexScore.setInstance(dexScoreSpy); + + dexScore.invoke(governanceScore, "addQuoteCoin", bnusdScore.getAddress()); + dexScore.invoke(governanceScore, "addQuoteCoin", sicxScore.getAddress()); + } + + + protected void depositToken(Account depositor, Account tokenScore, BigInteger value) { + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + dexScore.invoke(tokenScore, "tokenFallback", depositor.getAddress(), value, tokenData("_deposit", + new HashMap<>())); + } + + protected void xDepositToken(String depositor, Account to, Account tokenScore, BigInteger value) { + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + dexScore.invoke(tokenScore, "xTokenFallback", depositor, value, tokenData("_deposit", + Map.of("address", to.getAddress().toString()))); + } + + protected void supplyLiquidity(Account supplier, Account baseTokenScore, Account quoteTokenScore, + BigInteger baseValue, BigInteger quoteValue, @Optional boolean withdrawUnused) { + // Configure dex. + dexScore.invoke(governanceScore, "addQuoteCoin", quoteTokenScore.getAddress()); + + // Mock these cross-contract calls. + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + + // Deposit tokens and supply liquidity. + dexScore.invoke(baseTokenScore, "tokenFallback", supplier.getAddress(), baseValue, tokenData("_deposit", + new HashMap<>())); + dexScore.invoke(quoteTokenScore, "tokenFallback", supplier.getAddress(), quoteValue, tokenData("_deposit", + new HashMap<>())); + dexScore.invoke(supplier, "add", baseTokenScore.getAddress(), quoteTokenScore.getAddress(), baseValue, + quoteValue, withdrawUnused, BigInteger.valueOf(100)); + } + + protected BigInteger computePrice(BigInteger tokenAValue, BigInteger tokenBValue) { + return (tokenAValue.multiply(EXA)).divide(tokenBValue); + } + + protected void supplyIcxLiquidity(Account supplier, BigInteger value) { + contextMock.when(Context::getValue).thenReturn(value); + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("updateBalanceAndSupply"), + any(String.class), any(BigInteger.class), any(String.class), any(BigInteger.class))).thenReturn(null); + supplier.addBalance("ICX", value); + sm.transfer(supplier, dexScore.getAddress(), value); + } + + // Not done yet. Fails for some reason. + protected void swapSicxToIcx(Account sender, BigInteger value, BigInteger sicxIcxConversionRate) { + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + doReturn(sicxIcxConversionRate).when(dexScoreSpy).getSicxRate(); + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("updateBalanceAndSupplyBatch"), any(), + any(), any())).thenReturn(null); + contextMock.when(() -> Context.call(eq(sicxScore.getAddress()), eq("transfer"), + eq(feehandlerScore.getAddress()), any(BigInteger.class))).thenReturn(true); + contextMock.when(() -> Context.transfer(eq(sender.getAddress()), any(BigInteger.class))).thenAnswer((Answer) invocation -> null); + dexScore.invoke(sicxScore, "tokenFallback", sender.getAddress(), value, tokenData("_swap_icx", + new HashMap<>())); + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestCore.java b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestCore.java new file mode 100644 index 000000000..27fd9f836 --- /dev/null +++ b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestCore.java @@ -0,0 +1,1012 @@ +/* + * Copyright (c) 2022-2023 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 com.eclipsesource.json.JsonObject; +import com.iconloop.score.test.Account; +import network.balanced.score.core.dex.utils.Const; +import network.balanced.score.lib.structs.PrepDelegations; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.mockito.stubbing.Answer; +import score.Address; +import score.Context; + +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; + +import static network.balanced.score.core.dex.utils.Const.*; +import static network.balanced.score.lib.utils.Constants.EXA; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; + + +public class DexTestCore extends DexTestBase { + + @BeforeEach + public void configureContract() throws Exception { + super.setup(); + } + + @SuppressWarnings("unchecked") + @Test + void fallback() { + Account account = sm.createAccount(); + BigInteger icxValue = BigInteger.valueOf(100).multiply(EXA); + contextMock.when(Context::getValue).thenReturn(icxValue); + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("updateBalanceAndSupply"), + any(String.class), any(BigInteger.class), any(String.class), any(BigInteger.class))).thenReturn(null); + contextMock.when(() -> Context.call(any(Address.class), eq("getTodayRate"))).thenReturn(EXA); + dexScore.invoke(ownerAccount, "fallback"); + + BigInteger poolId = BigInteger.valueOf(SICXICX_POOL_ID); + Map poolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger lpBalance = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), + BigInteger.valueOf(SICXICX_POOL_ID)); + assertEquals(icxValue, lpBalance); + assertEquals(lpBalance, poolStats.get("total_supply")); + + BigInteger additionIcxValue = BigInteger.valueOf(50L).multiply(EXA); + contextMock.when(Context::getValue).thenReturn(additionIcxValue); + dexScore.invoke(ownerAccount, "fallback"); + poolStats = (Map) dexScore.call("getPoolStats", poolId); + lpBalance = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), + BigInteger.valueOf(SICXICX_POOL_ID)); + assertEquals(icxValue.add(additionIcxValue), lpBalance); + assertEquals(icxValue.add(additionIcxValue), poolStats.get("total_supply")); + + dexScore.invoke(account, "fallback"); + poolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger balanceOwner = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), poolId); + BigInteger balanceAccount = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + assertEquals(balanceOwner.add(balanceAccount), poolStats.get("total_supply")); + } + + // Test fails in line with: activeAddresses.get(SICXICX_POOL_ID).remove(user); + @Test + void cancelSicxIcxOrder() { + // Arrange. + Account supplier = sm.createAccount(); + BigInteger value = BigInteger.valueOf(1000).multiply(EXA); + + supplyIcxLiquidity(supplier, value); + sm.getBlock().increase(100000); + + // Mock these. + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("updateBalanceAndSupply"), + any(String.class), any(BigInteger.class), any(String.class), any(BigInteger.class))).thenReturn(null); + contextMock.when(() -> Context.transfer(eq(supplier.getAddress()), eq(value))).thenAnswer((Answer) invocation -> null); + + // Act. + dexScore.invoke(supplier, "cancelSicxicxOrder"); + + // Assert. + BigInteger IcxBalance = (BigInteger) dexScore.call("getICXBalance", supplier.getAddress()); + assertEquals(BigInteger.ZERO, IcxBalance); + } + + @Test + void crossChainDeposit() { + Account user = sm.createAccount(); + BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); + xDepositToken("0x1.ETH/0x123", user, bnusdScore, depositValue); + BigInteger retrievedValue = (BigInteger) dexScore.call("getDeposit", bnusdScore.getAddress(), user.getAddress()); + assertEquals(depositValue, retrievedValue); + } + + @Test + void withdrawSicxEarnings() { + Account depositor = sm.createAccount(); + BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); + BigInteger withdrawValue = BigInteger.valueOf(10).multiply(EXA); + depositToken(depositor, balnScore, depositValue); + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(eq(stakingScore.getAddress()), eq("getTodayRate"))).thenReturn(EXA); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + contextMock.when(() -> Context.transfer(any(Address.class), any(BigInteger.class))).then(invocationOnMock -> null); + BigInteger depositBalance = BigInteger.valueOf(100L).multiply(EXA); + supplyIcxLiquidity(depositor, depositBalance); + + contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("transfer"), eq(depositor.getAddress()), + eq(withdrawValue))).thenReturn(null); + + // Act. + dexScore.invoke(depositor, "withdraw", balnScore.getAddress(), withdrawValue); + + // Assert. + BigInteger currentDepositValue = (BigInteger) dexScore.call("getDeposit", balnScore.getAddress(), + depositor.getAddress()); + assertEquals(depositValue.subtract(withdrawValue), currentDepositValue); + + BigInteger swapValue = BigInteger.valueOf(50L).multiply(EXA); + swapSicxToIcx(depositor, swapValue, EXA); + + BigInteger sicxEarning = (BigInteger) dexScore.call("getSicxEarnings", depositor.getAddress()); + dexScore.invoke(depositor, "withdrawSicxEarnings", sicxEarning); + BigInteger newSicxEarning = (BigInteger) dexScore.call("getSicxEarnings", depositor.getAddress()); + assertEquals(BigInteger.ZERO, newSicxEarning); + } + + @Test + void getSicxEarnings() { + Account depositor = sm.createAccount(); + BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); + BigInteger withdrawValue = BigInteger.valueOf(10).multiply(EXA); + depositToken(depositor, balnScore, depositValue); + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(eq(stakingScore.getAddress()), eq("getTodayRate"))).thenReturn(EXA); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + contextMock.when(() -> Context.transfer(any(Address.class), any(BigInteger.class))).then(invocationOnMock -> null); + BigInteger depositBalance = BigInteger.valueOf(100L).multiply(EXA); + supplyIcxLiquidity(depositor, depositBalance); + + contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("transfer"), eq(depositor.getAddress()), + eq(withdrawValue))).thenReturn(null); + + // Act. + dexScore.invoke(depositor, "withdraw", balnScore.getAddress(), withdrawValue); + + // Assert. + BigInteger currentDepositValue = (BigInteger) dexScore.call("getDeposit", balnScore.getAddress(), + depositor.getAddress()); + assertEquals(depositValue.subtract(withdrawValue), currentDepositValue); + + BigInteger swapValue = BigInteger.valueOf(100L).multiply(EXA); + swapSicxToIcx(depositor, swapValue, EXA); + + BigInteger sicxEarning = (BigInteger) dexScore.call("getSicxEarnings", depositor.getAddress()); + } + + + @Test + void tokenFallback_deposit() { + // Arrange. + Account tokenScoreCaller = balnScore; + Account tokenSender = sm.createAccount(); + BigInteger depositValue = BigInteger.valueOf(1000000000); + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + + // Act. + dexScore.invoke(tokenScoreCaller, "tokenFallback", tokenSender.getAddress(), depositValue, tokenData( + "_deposit", new HashMap<>())); + BigInteger retrievedDepositValue = (BigInteger) dexScore.call("getDeposit", tokenScoreCaller.getAddress(), + tokenSender.getAddress()); + + // Assert. + assertEquals(depositValue, retrievedDepositValue); + } + + @Test + void withdrawTokens_negativeAmount() { + // Arrange. + Account depositor = sm.createAccount(); + BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); + BigInteger withdrawValue = BigInteger.valueOf(-1000).multiply(EXA); + String expectedErrorMessage = "Reverted(0): Balanced DEX: Must specify a positive amount"; + depositToken(depositor, balnScore, depositValue); + + // Act & assert. + Executable withdrawalInvocation = () -> dexScore.invoke(depositor, "withdraw", balnScore.getAddress(), + withdrawValue); + expectErrorMessage(withdrawalInvocation, expectedErrorMessage); + } + + @Test + void withdrawToken_insufficientBalance() { + // Arrange. + Account depositor = sm.createAccount(); + BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); + BigInteger withdrawValue = BigInteger.valueOf(1000).multiply(EXA); + String expectedErrorMessage = "Reverted(0): Balanced DEX: Insufficient Balance"; + depositToken(depositor, balnScore, depositValue); + + // Act & assert. + Executable withdrawalInvocation = () -> dexScore.invoke(depositor, "withdraw", balnScore.getAddress(), + withdrawValue); + expectErrorMessage(withdrawalInvocation, expectedErrorMessage); + } + + @Test + void withdrawToken() { + // Arrange. + Account depositor = sm.createAccount(); + BigInteger depositValue = BigInteger.valueOf(100).multiply(EXA); + BigInteger withdrawValue = BigInteger.valueOf(10).multiply(EXA); + depositToken(depositor, balnScore, depositValue); + + contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("transfer"), eq(depositor.getAddress()), + eq(withdrawValue))).thenReturn(null); + + // Act. + dexScore.invoke(depositor, "withdraw", balnScore.getAddress(), withdrawValue); + + // Assert. + BigInteger currentDepositValue = (BigInteger) dexScore.call("getDeposit", balnScore.getAddress(), + depositor.getAddress()); + assertEquals(depositValue.subtract(withdrawValue), currentDepositValue); + } + + @SuppressWarnings("unchecked") + @Test + void addLiquidity() { + Account account = sm.createAccount(); + Account account1 = sm.createAccount(); + + final String data = "{" + + "\"method\": \"_deposit\"" + + "}"; + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + + BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); + //deposit + BigInteger bnusdValue = BigInteger.valueOf(276L).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(100L).multiply(EXA); + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); + dexScore.invoke(bnusdScore, "tokenFallback", account1.getAddress(), bnusdValue, data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account1.getAddress(), bnusdValue, data.getBytes()); + // add liquidity pool + dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); + dexScore.invoke(account1, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); + BigInteger poolId = (BigInteger) dexScore.call("getPoolId", bnusdScore.getAddress(), balnScore.getAddress()); + Map poolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + assertEquals(poolStats.get("base_token"), balnScore.getAddress()); + assertEquals(poolStats.get("quote_token"), bnusdScore.getAddress()); + assertEquals(bnusdValue.multiply(balnValue).sqrt(), balance); + + BigInteger account1_balance = (BigInteger) dexScore.call("balanceOf", account1.getAddress(), poolId); + assertEquals(balance.add(account1_balance), poolStats.get("total_supply")); + } + + @Test + void addLiquidity_higherSlippageFail(){ + // Arrange + Account account = sm.createAccount(); + Account account1 = sm.createAccount(); + + final String data = "{" + + "\"method\": \"_deposit\"" + + "}"; + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + + BigInteger bnusdValue = BigInteger.valueOf(276L).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(100L).multiply(EXA); + BigInteger acc2BnusdValue = BigInteger.valueOf(273L).multiply(EXA); + BigInteger acc2BalnValue = BigInteger.valueOf(100L).multiply(EXA); + + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), balnValue, data.getBytes()); + dexScore.invoke(bnusdScore, "tokenFallback", account1.getAddress(), acc2BnusdValue, data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account1.getAddress(), acc2BalnValue, data.getBytes()); + + + // Act + dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); + Executable addLiquidityInvocation = () -> dexScore.invoke(account1, "add", balnScore.getAddress(), bnusdScore.getAddress(), acc2BalnValue, acc2BnusdValue, false, BigInteger.valueOf(100)); + String expectedErrorMessage = "Reverted(0): Balanced DEX : insufficient slippage provided"; + + // Assert + expectErrorMessage(addLiquidityInvocation, expectedErrorMessage); + } + + @Test + void removeLiquidity() { + // Arrange - remove liquidity arguments. + BigInteger poolId = BigInteger.TWO; + BigInteger lpTokensToRemove = BigInteger.valueOf(1000); + Boolean withdrawTokensOnRemoval = false; + + // Arrange - supply liquidity. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + BigInteger usersLpTokens = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), poolId); + + // Arrange - increase blocks past withdrawal lock. + sm.getBlock().increase(100000000); + + // Act & Assert. + dexScore.invoke(ownerAccount, "remove", poolId, lpTokensToRemove, withdrawTokensOnRemoval); + BigInteger usersLpTokensAfterRemoval = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), + poolId); + assertEquals(usersLpTokens.subtract(lpTokensToRemove), usersLpTokensAfterRemoval); + } + + @SuppressWarnings("unchecked") + @Test + void tokenFallbackSwapFromTokenIs_poolQuote() { + Account account = sm.createAccount(); + + final String data = "{" + + "\"method\": \"_deposit\"" + + "}"; + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + + BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); + //deposit + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), + data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), + data.getBytes()); + // add liquidity pool + dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, + FIFTY.divide(BigInteger.TWO), false, BigInteger.valueOf(100)); + BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); + BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + + Map fees = (Map) dexScore.call("getFees"); + Map poolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger oldFromToken = (BigInteger) poolStats.get("quote"); + BigInteger oldToToken = (BigInteger) poolStats.get("base"); + + BigInteger value = BigInteger.valueOf(100L).multiply(EXA); + BigInteger lp_fee = value.multiply(fees.get("pool_lp_fee")).divide(FEE_SCALE); + BigInteger baln_fee = value.multiply(fees.get("pool_baln_fee")).divide(FEE_SCALE); + BigInteger total_fee = lp_fee.add(baln_fee); + + BigInteger inputWithoutFees = value.subtract(total_fee); + BigInteger newFromToken = oldFromToken.add(inputWithoutFees); + + BigInteger newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); + BigInteger sendAmount = oldToToken.subtract(newToToken); + newFromToken = newFromToken.add(lp_fee); + + // test swap + JsonObject jsonData = new JsonObject(); + JsonObject params = new JsonObject(); + params.add("minimumReceive", BigInteger.valueOf(10L).toString()); + params.add("toToken", balnScore.getAddress().toString()); + jsonData.add("method", "_swap"); + jsonData.add("params", params); + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), value, jsonData.toString().getBytes()); + Map newPoolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger newBalance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + + assertEquals(newFromToken, newPoolStats.get("quote")); + assertEquals(newToToken, newPoolStats.get("base")); + assertEquals(balance, newBalance); + + contextMock.verify(() -> Context.call(eq(bnusdScore.getAddress()), eq("transfer"), + eq(feehandlerScore.getAddress()), eq(baln_fee))); + } + + + @Test + void tokenFallbackSwapFromTokenIs_poolBase() { + Account account = sm.createAccount(); + + final String data = "{" + + "\"method\": \"_deposit\"" + + "}"; + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + + BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); + //deposit + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), + data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), + data.getBytes()); + // add liquidity pool + dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, + FIFTY.divide(BigInteger.TWO), false, BigInteger.valueOf(100)); + BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); + BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + + Map fees = (Map) dexScore.call("getFees"); + Map poolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger oldFromToken = (BigInteger) poolStats.get("base"); + BigInteger oldToToken = (BigInteger) poolStats.get("quote"); + + BigInteger value = BigInteger.valueOf(100L).multiply(EXA); + BigInteger lp_fee = value.multiply(fees.get("pool_lp_fee")).divide(FEE_SCALE); + BigInteger baln_fee = value.multiply(fees.get("pool_baln_fee")).divide(FEE_SCALE); + BigInteger total_fee = lp_fee.add(baln_fee); + + BigInteger inputWithoutFees = value.subtract(total_fee); + BigInteger newFromToken = oldFromToken.add(inputWithoutFees); + + BigInteger newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); + BigInteger sendAmount = oldToToken.subtract(newToToken); + newFromToken = newFromToken.add(lp_fee); + + // swapping fees to quote token + oldFromToken = newFromToken; + oldToToken = newToToken; + BigInteger newFromTokenWithBalnFee = oldFromToken.add(baln_fee); + BigInteger newToTokenAfterFeeSwap = (oldFromToken.multiply(oldToToken)).divide(newFromTokenWithBalnFee); + BigInteger swappedBalnFee = oldToToken.subtract(newToTokenAfterFeeSwap); + + // test swap when from token is pool base token + JsonObject jsonData = new JsonObject(); + JsonObject params = new JsonObject(); + params.add("minimumReceive", BigInteger.valueOf(10L).toString()); + params.add("toToken", bnusdScore.getAddress().toString()); + jsonData.add("method", "_swap"); + jsonData.add("params", params); + dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), value, jsonData.toString().getBytes()); + Map newPoolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger newBalance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + + assertEquals(newFromTokenWithBalnFee, newPoolStats.get("base")); + assertEquals(newToTokenAfterFeeSwap, newPoolStats.get("quote")); + assertEquals(balance, newBalance); + + contextMock.verify(() -> Context.call(eq(bnusdScore.getAddress()), eq("transfer"), + eq(feehandlerScore.getAddress()), eq(swappedBalnFee))); + } + + @SuppressWarnings("unchecked") + @Test + void tokenFallback_donate() { + Account account = sm.createAccount(); + + final String data = "{" + + "\"method\": \"_deposit\"" + + "}"; + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + + BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); + //deposit + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), + data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), + data.getBytes()); + // add liquidity pool + dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, + FIFTY.divide(BigInteger.TWO), false, BigInteger.valueOf(100)); + BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); + Map poolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger initialBase = (BigInteger) poolStats.get("base"); + BigInteger initialQuote = (BigInteger) poolStats.get("quote"); + BigInteger initialSupply = (BigInteger) poolStats.get("total_supply"); + + BigInteger bnusdDonation = BigInteger.valueOf(100L).multiply(EXA); + JsonObject jsonData = new JsonObject(); + JsonObject params = new JsonObject(); + params.add("toToken", balnScore.getAddress().toString()); + jsonData.add("method", "_donate"); + jsonData.add("params", params); + dexScore.invoke(bnusdScore, "tokenFallback", ownerAccount.getAddress(), bnusdDonation, + jsonData.toString().getBytes()); + poolStats = (Map) dexScore.call("getPoolStats", poolId); + + assertEquals(initialBase, poolStats.get("base")); + assertEquals(initialQuote.add(bnusdDonation), poolStats.get("quote")); + + BigInteger balnDonation = BigInteger.valueOf(50L).multiply(EXA); + jsonData = new JsonObject(); + params = new JsonObject(); + params.add("toToken", bnusdScore.getAddress().toString()); + jsonData.add("method", "_donate"); + jsonData.add("params", params); + dexScore.invoke(balnScore, "tokenFallback", ownerAccount.getAddress(), balnDonation, + jsonData.toString().getBytes()); + poolStats = (Map) dexScore.call("getPoolStats", poolId); + + assertEquals(initialBase.add(balnDonation), poolStats.get("base")); + assertEquals(initialQuote.add(bnusdDonation), poolStats.get("quote")); + assertEquals(initialSupply, poolStats.get("total_supply")); + } + + @Test + void tokenfallback_swapSicx() { + Account account = sm.createAccount(); + final String data = "{" + + "\"method\": \"_deposit\"" + + "}"; + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(eq(stakingScore.getAddress()), eq("getTodayRate"))).thenReturn(EXA); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + contextMock.when(() -> Context.transfer(any(Address.class), any(BigInteger.class))).then(invocationOnMock -> null); + + BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); + supplyIcxLiquidity(account, FIFTY.multiply(BigInteger.TEN)); + + // add liquidity pool + BigInteger poolId = BigInteger.valueOf(Const.SICXICX_POOL_ID); + BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + + // test swap + BigInteger swapValue = BigInteger.valueOf(50L).multiply(EXA); + swapSicxToIcx(account, swapValue, EXA); + + BigInteger newBalance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + BigInteger sicxEarning = (BigInteger) dexScore.call("getSicxEarnings", account.getAddress()); + assertEquals(balance.subtract(swapValue.subtract(swapValue.divide(BigInteger.valueOf(100L)))), newBalance); + assertEquals(swapValue.multiply(BigInteger.valueOf(997L)).divide(BigInteger.valueOf(1000L)), sicxEarning); + + } + + @Test + void onIRC31Received() { + // Arrange. + Account irc31Contract = Account.newScoreAccount(1); + Address operator = sm.createAccount().getAddress(); + Address from = sm.createAccount().getAddress(); + BigInteger id = BigInteger.ONE; + BigInteger value = BigInteger.valueOf(100).multiply(EXA); + byte[] data = new byte[0]; + String expectedErrorMessage = "Reverted(0): Balanced DEX: IRC31 Tokens not accepted"; + + // Act and assert. + Executable onIRC31Received = () -> dexScore.invoke(irc31Contract, "onIRC31Received", operator, from, id, + value, data); + expectErrorMessage(onIRC31Received, expectedErrorMessage); + } + + @Test + void transfer() { + Account account = sm.createAccount(); + Account account1 = sm.createAccount(); + + final String data = "{" + + "\"method\": \"_deposit\"" + + "}"; + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + + BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), + data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), + data.getBytes()); + dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, + FIFTY.divide(BigInteger.TWO), false, BigInteger.valueOf(100)); + + BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); + BigInteger transferValue = BigInteger.valueOf(5).multiply(EXA); + BigInteger initialValue = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + dexScore.invoke(account, "transfer", account1.getAddress(), transferValue, poolId, data.getBytes()); + + BigInteger value = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + assertEquals(value, initialValue.subtract(transferValue)); + value = (BigInteger) dexScore.call("balanceOf", account1.getAddress(), poolId); + assertEquals(value, transferValue); + } + + @Test + void transfer_toSelf() { + Account account = sm.createAccount(); + Account account1 = sm.createAccount(); + + final String data = "{" + + "\"method\": \"_deposit\"" + + "}"; + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + + BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), + data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), BigInteger.valueOf(50L).multiply(EXA), + data.getBytes()); + dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, + FIFTY.divide(BigInteger.TWO), false, BigInteger.valueOf(100)); + + BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); + BigInteger transferValue = BigInteger.valueOf(5).multiply(EXA); + BigInteger initialValue = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + dexScore.invoke(account, "transfer", account.getAddress(), transferValue, poolId, data.getBytes()); + + BigInteger value = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + assertEquals(initialValue, value); + } + + // In the process of going through this. + @Test + void swap_sicx_icx() { + // Arrange. + BigInteger value = BigInteger.valueOf(10000000).multiply(EXA); + BigInteger sicxIcxConversionRate = new BigInteger("1100758881004412705"); + BigInteger swapValue = BigInteger.valueOf(100).multiply(EXA); + + // Act. + supplyIcxLiquidity(ownerAccount, value); + supplyIcxLiquidity(sm.createAccount(), value); + supplyIcxLiquidity(sm.createAccount(), value); + swapSicxToIcx(ownerAccount, swapValue, sicxIcxConversionRate); + } + + @Test + void getTotalValue() { + Account account = sm.createAccount(); + + final String data = "{" + + "\"method\": \"_deposit\"" + + "}"; + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + + BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); + //deposit + BigInteger bnusdValue = BigInteger.valueOf(276L).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(100L).multiply(EXA); + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); + + dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); + BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); + + String marketName = "BALN/BNUSD"; + dexScore.invoke(governanceScore, "setMarketName", poolId, marketName); + BigInteger totalValue = (BigInteger) dexScore.call("getTotalValue", marketName, BigInteger.ONE); + BigInteger totalSupply = (BigInteger) dexScore.call("totalSupply", poolId); + assertEquals(totalSupply, totalValue); + } + + @SuppressWarnings("unchecked") + @Test + void getPoolStatsWithPair() { + Account account = sm.createAccount(); + + final String data = "{" + + "\"method\": \"_deposit\"" + + "}"; + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + + //deposit + BigInteger bnusdValue = BigInteger.valueOf(276L).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(100L).multiply(EXA); + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); + + // add liquidity pool + dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); + BigInteger poolId = (BigInteger) dexScore.call("getPoolId", bnusdScore.getAddress(), balnScore.getAddress()); + Map poolStats = (Map) dexScore.call("getPoolStatsForPair", + balnScore.getAddress(), bnusdScore.getAddress()); + + assertEquals(poolId, poolStats.get("id")); + assertEquals(balnScore.getAddress(), poolStats.get("base_token")); + assertEquals(bnusdScore.getAddress(), poolStats.get("quote_token")); + } + + @Test + void delegate() { + PrepDelegations prep = new PrepDelegations(); + prep._address = prep_address.getAddress(); + prep._votes_in_per = BigInteger.valueOf(100); + PrepDelegations[] preps = new PrepDelegations[]{prep}; + + contextMock.when(() -> Context.call(eq(stakingScore.getAddress()), eq("delegate"), any())).thenReturn( + "Staking delegate called"); + dexScore.invoke(governanceScore, "delegate", (Object) preps); + + contextMock.verify(() -> Context.call(eq(stakingScore.getAddress()), eq("delegate"), any())); + } + + @Test + void govWithdraw() { + Account account = sm.createAccount(); + Account account1 = sm.createAccount(); + + final String data = "{" + + "\"method\": \"_deposit\"" + + "}"; + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + + BigInteger FIFTY = BigInteger.valueOf(50L).multiply(EXA); + //deposit + BigInteger bnusdValue = BigInteger.valueOf(276L).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(100L).multiply(EXA); + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), balnValue, data.getBytes()); + dexScore.invoke(bnusdScore, "tokenFallback", account1.getAddress(), bnusdValue, data.getBytes()); + dexScore.invoke(balnScore, "tokenFallback", account1.getAddress(), balnValue, data.getBytes()); + // add liquidity pool + dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); + dexScore.invoke(account1, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false, BigInteger.valueOf(100)); + + BigInteger poolId = (BigInteger) dexScore.call("getPoolId", bnusdScore.getAddress(), balnScore.getAddress()); + Map poolStats = (Map) dexScore.call("getPoolStats", poolId); + assertEquals(poolStats.get("base_token"), balnScore.getAddress()); + assertEquals(poolStats.get("quote_token"), bnusdScore.getAddress()); + assertEquals(poolStats.get("base"), balnValue.add(balnValue)); + assertEquals(poolStats.get("quote"), bnusdValue.add(bnusdValue)); + + BigInteger balnWithdrawAmount = BigInteger.TEN.pow(19); + dexScore.invoke(governanceScore, "govWithdraw", 2, balnScore.getAddress(), balnWithdrawAmount); + + BigInteger bnUSDWithdrawAmount = BigInteger.valueOf(3).multiply(BigInteger.TEN.pow(19)); + dexScore.invoke(governanceScore, "govWithdraw", 2, bnusdScore.getAddress(), bnUSDWithdrawAmount); + + poolStats = (Map) dexScore.call("getPoolStats", poolId); + contextMock.verify(() -> Context.call(eq(bnusdScore.getAddress()), eq("transfer"), eq(mockBalanced.daofund.getAddress()), + eq(bnUSDWithdrawAmount))); + contextMock.verify(() -> Context.call(eq(balnScore.getAddress()), eq("transfer"), eq(mockBalanced.daofund.getAddress()), + eq(balnWithdrawAmount))); + assertEquals(poolStats.get("base"), balnValue.add(balnValue).subtract(balnWithdrawAmount)); + assertEquals(poolStats.get("quote"), bnusdValue.add(bnusdValue).subtract(bnUSDWithdrawAmount)); + } + + // initial price of baln: 25/50 = 0.50 + // oracle protection is 18% that is 0.09 for 0.5 + // price of baln after swap: 27.xx/46.xx = 58.xx + // protection covered up to 0.50+0.09=0.59, should pass + @Test + void swap_ForOracleProtection() { + // Arrange + Account account = sm.createAccount(); + BigInteger points = BigInteger.valueOf(1800); + String symbolBase = "BALN"; + String symbolQuote = "bnUSD"; + supplyLiquidity(account, balnScore, bnusdScore, BigInteger.valueOf(50).multiply(EXA), + BigInteger.valueOf(25).multiply(EXA), true); + + contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); + contextMock.when(() -> Context.call(eq(bnusdScore.getAddress()), eq("symbol"))).thenReturn(symbolQuote); + contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolBase))).thenReturn(EXA.divide(BigInteger.TWO)); + contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolQuote))).thenReturn(EXA); + dexScore.invoke(governanceScore, "setOracleProtection", BigInteger.TWO, points); + + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + + BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), bnusdScore.getAddress()); + BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + + Map fees = (Map) dexScore.call("getFees"); + Map poolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger oldFromToken = (BigInteger) poolStats.get("quote"); + BigInteger oldToToken = (BigInteger) poolStats.get("base"); + + BigInteger value = BigInteger.valueOf(2L).multiply(EXA); + BigInteger lp_fee = value.multiply(fees.get("pool_lp_fee")).divide(FEE_SCALE); + BigInteger baln_fee = value.multiply(fees.get("pool_baln_fee")).divide(FEE_SCALE); + BigInteger total_fee = lp_fee.add(baln_fee); + + BigInteger inputWithoutFees = value.subtract(total_fee); + BigInteger newFromToken = oldFromToken.add(inputWithoutFees); + + BigInteger newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); + BigInteger sendAmount = oldToToken.subtract(newToToken); + newFromToken = newFromToken.add(lp_fee); + + // Act + JsonObject jsonData = new JsonObject(); + JsonObject params = new JsonObject(); + params.add("minimumReceive", sendAmount.toString()); + params.add("toToken", balnScore.getAddress().toString()); + jsonData.add("method", "_swap"); + jsonData.add("params", params); + dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), value, jsonData.toString().getBytes()); + + // Assert + Map newPoolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger newBalance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + assertEquals(newFromToken, newPoolStats.get("quote")); + assertEquals(newToToken, newPoolStats.get("base")); + assertEquals(balance, newBalance); + + contextMock.verify(() -> Context.call(eq(bnusdScore.getAddress()), eq("transfer"), + eq(feehandlerScore.getAddress()), eq(baln_fee))); + } + + // initial price of baln: 25/50 = 0.50 + // oracle protection is 18% that is 0.08 for 0.5 + // price of baln after swap: 27.xx/46.xx = 58.xx + // protection covered up to 0.50+0.08=0.58, should fail + @Test + void swap_FailForOracleProtection() { + // Arrange + Account account = sm.createAccount(); + BigInteger points = BigInteger.valueOf(1600); + String symbolBase = "BALN"; + String symbolQuote = "bnUSD"; + supplyLiquidity(account, balnScore, bnusdScore, BigInteger.valueOf(50).multiply(EXA), + BigInteger.valueOf(25).multiply(EXA), true); + + contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); + contextMock.when(() -> Context.call(eq(bnusdScore.getAddress()), eq("symbol"))).thenReturn(symbolQuote); + contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolBase))).thenReturn(EXA.divide(BigInteger.TWO)); + contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolQuote))).thenReturn(EXA); + dexScore.invoke(governanceScore, "setOracleProtection", BigInteger.TWO, points); + + // Act + JsonObject jsonData = new JsonObject(); + JsonObject params = new JsonObject(); + params.add("minimumReceive", BigInteger.valueOf(2).toString()); + params.add("toToken", balnScore.getAddress().toString()); + jsonData.add("method", "_swap"); + jsonData.add("params", params); + Executable swapToFail = () -> dexScore.invoke(bnusdScore, "tokenFallback", account.getAddress(), BigInteger.TWO.multiply(EXA), jsonData.toString().getBytes()); + + // Assert + expectErrorMessage(swapToFail, TAG + ": oracle protection price violated"); + } + + // initial price of baln: 25/25 = 1 + // oracle protection is 18% that is 0.18 for 1 + // price of baln after swap: 26.xx/23.xx = 1.16xx + // protection covered up to 1+0.18=1.18, should pass + @Test + void swap_ForOracleProtectionForBalnSicx() { + // Arrange + Account account = sm.createAccount(); + BigInteger points = BigInteger.valueOf(1800); + String symbolBase = "BALN"; + String symbolQuote = "sICX"; + supplyLiquidity(account, balnScore, sicxScore, BigInteger.valueOf(25).multiply(EXA), + BigInteger.valueOf(25).multiply(EXA), true); + + contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); + contextMock.when(() -> Context.call(eq(sicxScore.getAddress()), eq("symbol"))).thenReturn(symbolQuote); + contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolBase))).thenReturn(EXA); + contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolQuote))).thenReturn(EXA); + dexScore.invoke(governanceScore, "setOracleProtection", BigInteger.TWO, points); + + + contextMock.when(() -> Context.call(eq(rewardsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(eq(dividendsScore.getAddress()), eq("distribute"))).thenReturn(true); + contextMock.when(() -> Context.call(any(Address.class), eq("decimals"))).thenReturn(BigInteger.valueOf(18)); + contextMock.when(() -> Context.call(any(Address.class), eq("transfer"), any(Address.class), + any(BigInteger.class))).thenReturn(null); + contextMock.when(() -> Context.call(any(Address.class), eq("getTodayRate"))).thenReturn(EXA); + + BigInteger poolId = (BigInteger) dexScore.call("getPoolId", balnScore.getAddress(), sicxScore.getAddress()); + BigInteger balance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + + Map fees = (Map) dexScore.call("getFees"); + Map poolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger oldFromToken = (BigInteger) poolStats.get("quote"); + BigInteger oldToToken = (BigInteger) poolStats.get("base"); + + BigInteger value = BigInteger.valueOf(2L).multiply(EXA); + BigInteger lp_fee = value.multiply(fees.get("pool_lp_fee")).divide(FEE_SCALE); + BigInteger baln_fee = value.multiply(fees.get("pool_baln_fee")).divide(FEE_SCALE); + BigInteger total_fee = lp_fee.add(baln_fee); + + BigInteger inputWithoutFees = value.subtract(total_fee); + BigInteger newFromToken = oldFromToken.add(inputWithoutFees); + + BigInteger newToToken = (oldFromToken.multiply(oldToToken)).divide(newFromToken); + BigInteger sendAmount = oldToToken.subtract(newToToken); + newFromToken = newFromToken.add(lp_fee); + + // Act + JsonObject jsonData = new JsonObject(); + JsonObject params = new JsonObject(); + params.add("minimumReceive", sendAmount.toString()); + params.add("toToken", balnScore.getAddress().toString()); + jsonData.add("method", "_swap"); + jsonData.add("params", params); + dexScore.invoke(sicxScore, "tokenFallback", account.getAddress(), value, jsonData.toString().getBytes()); + + // Assert + Map newPoolStats = (Map) dexScore.call("getPoolStats", poolId); + BigInteger newBalance = (BigInteger) dexScore.call("balanceOf", account.getAddress(), poolId); + assertEquals(newFromToken, newPoolStats.get("quote")); + assertEquals(newToToken, newPoolStats.get("base")); + assertEquals(balance, newBalance); + + contextMock.verify(() -> Context.call(eq(sicxScore.getAddress()), eq("transfer"), + eq(feehandlerScore.getAddress()), eq(baln_fee))); + } + + // initial price of baln: 25/25 = 1 + // oracle protection is 16% that is 0.16 for 1 + // price of baln after swap: 26.xx/23.xx = 1.16xx + // protection covered up to 1+0.16=1.16, should fail + @Test + void swap_FailForOracleProtectionForBalnSicx() { + // Arrange + Account account = sm.createAccount(); + BigInteger points = BigInteger.valueOf(1600); + String symbolBase = "BALN"; + String symbolQuote = "sICX"; + supplyLiquidity(account, balnScore, sicxScore, BigInteger.valueOf(25).multiply(EXA), + BigInteger.valueOf(25).multiply(EXA), true); + + contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); + contextMock.when(() -> Context.call(eq(sicxScore.getAddress()), eq("symbol"))).thenReturn(symbolQuote); + contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolBase))).thenReturn(EXA); + contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolQuote))).thenReturn(EXA); + dexScore.invoke(governanceScore, "setOracleProtection", BigInteger.TWO, points); + contextMock.when(() -> Context.call(any(Address.class), eq("getTodayRate"))).thenReturn(EXA); + + // Act + JsonObject jsonData = new JsonObject(); + JsonObject params = new JsonObject(); + params.add("minimumReceive", BigInteger.valueOf(2L).toString()); + params.add("toToken", balnScore.getAddress().toString()); + jsonData.add("method", "_swap"); + jsonData.add("params", params); + Executable swapToFail = () -> dexScore.invoke(sicxScore, "tokenFallback", account.getAddress(), BigInteger.TWO.multiply(EXA), jsonData.toString().getBytes()); + + // Assert + expectErrorMessage(swapToFail, TAG + ": oracle protection price violated"); + } + + @AfterEach + void closeMock() { + contextMock.close(); + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestSettersAndGetters.java b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestSettersAndGetters.java new file mode 100644 index 000000000..4e4aede91 --- /dev/null +++ b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/DexTestSettersAndGetters.java @@ -0,0 +1,658 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex; + +import com.iconloop.score.test.Account; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import score.Address; +import score.Context; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static network.balanced.score.lib.utils.BalancedAddressManager.getBalancedOracle; +import static network.balanced.score.lib.utils.Constants.EXA; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; + + +public class DexTestSettersAndGetters extends DexTestBase { + + @BeforeEach + public void configureContract() throws Exception { + super.setup(); + } + + @Test + void testName() { + assertEquals(dexScore.call("name"), "Balanced DEX"); + } + + @Test + @SuppressWarnings("unchecked") + void setGetFees() { + // Arrange - fees to be set. + BigInteger poolLpFee = BigInteger.valueOf(100); + BigInteger poolBalnFee = BigInteger.valueOf(200); + BigInteger icxConversionFee = BigInteger.valueOf(300); + BigInteger icxBalnFee = BigInteger.valueOf(400); + + // Arrange - methods to be called and set specified fee. + Map fees = Map.of( + "setPoolLpFee", poolLpFee, + "setPoolBalnFee", poolBalnFee, + "setIcxConversionFee", icxConversionFee, + "setIcxBalnFee", icxBalnFee + ); + + // Arrange - expected result when retrieving fees". + Map expectedResult = Map.of( + "icx_total", icxBalnFee.add(icxConversionFee), + "pool_total", poolBalnFee.add(poolLpFee), + "pool_lp_fee", poolLpFee, + "pool_baln_fee", poolBalnFee, + "icx_conversion_fee", icxConversionFee, + "icx_baln_fee", icxBalnFee + ); + Map returnedFees; + + // Act & assert - set all fees and assert that all fee methods are only settable by governance. + for (Map.Entry fee : fees.entrySet()) { + dexScore.invoke(governanceScore, fee.getKey(), fee.getValue()); + assertOnlyCallableBy(governanceScore.getAddress(), dexScore, fee.getKey(), fee.getValue()); + } + + // Assert - retrieve all fees and check validity. + returnedFees = (Map) dexScore.call("getFees"); + assertEquals(expectedResult, returnedFees); + } + + @Test + @SuppressWarnings("unchecked") + void getBalanceAndSupply_normalPool() { + // Arrange - variables. + String poolName = "bnUSD/BALN"; + BigInteger poolId = BigInteger.TWO; + BigInteger mockBalance = BigInteger.valueOf(100); + BigInteger mockTotalSupply = BigInteger.valueOf(200); + Map expectedData = Map.of( + "_balance", mockBalance, + "_totalSupply", mockTotalSupply + ); + + // Arrange - Setup dex contract. + dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); + + // Arrange - Mock these calls to stakedLP contract. + contextMock.when(() -> Context.call(eq(stakedLPScore.getAddress()), eq("balanceOf"), + eq(ownerAccount.getAddress()), eq(poolId))).thenReturn(mockBalance); + contextMock.when(() -> Context.call(eq(stakedLPScore.getAddress()), eq("totalStaked"), eq(poolId))).thenReturn(mockTotalSupply); + + // Assert. + Map returnedData = (Map) dexScore.call("getBalanceAndSupply", + poolName, ownerAccount.getAddress().toString()); + assertEquals(expectedData, returnedData); + } + + @Test + @SuppressWarnings("unchecked") + void getBalanceAndSupply_sicxIcxPool() { + // Arrange. + Account supplier = sm.createAccount(); + BigInteger icxValue = BigInteger.valueOf(100).multiply(EXA); + String poolName = "sICX/ICX"; + + Map expectedData = Map.of( + "_balance", icxValue, + "_totalSupply", icxValue + ); + + // Act. + supplyIcxLiquidity(supplier, icxValue); + + // Assert. + Map returnedData = (Map) dexScore.call("getBalanceAndSupply", + poolName, supplier.getAddress().toString()); + assertEquals(expectedData, returnedData); + } + + @Test + void turnDexOnAndGetDexOn() { + dexScore.invoke(governanceScore, "turnDexOn"); + assertEquals(true, dexScore.call("getDexOn")); + assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "turnDexOn"); + } + + @Test + void addQuoteCoinAndCheckIfAllowed() { + // Arrange. + Address quoteCoin = Account.newScoreAccount(1).getAddress(); + + // Act. + dexScore.invoke(governanceScore, "addQuoteCoin", quoteCoin); + + // Assert. + Boolean quoteCoinAllowed = (Boolean) dexScore.call("isQuoteCoinAllowed", quoteCoin); + assertEquals(true, quoteCoinAllowed); + assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "addQuoteCoin", quoteCoin); + } + + @Test + void setGetTimeOffSet() { + // Arrange. + BigInteger timeOffset = BigInteger.valueOf(100); + + // Act. + dexScore.invoke(governanceScore, "setTimeOffset", timeOffset); + + // Assert. + BigInteger retrievedTimeOffset = (BigInteger) dexScore.call("getTimeOffset"); + assertEquals(timeOffset, retrievedTimeOffset); + assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "setTimeOffset", timeOffset); + } + + @Test + void setOracleProtection() { + // Arrange + BigInteger points = BigInteger.valueOf(30); + String symbolBase = "BALN"; + String symbolQuote = "bnUSD"; + + supplyLiquidity(sm.createAccount(), balnScore, bnusdScore, BigInteger.valueOf(30000).multiply(EXA), + BigInteger.valueOf(10000).multiply(EXA), true); + + contextMock.when(() -> Context.call(eq(balnScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); + contextMock.when(() -> Context.call(eq(bnusdScore.getAddress()), eq("symbol"))).thenReturn(symbolBase); + contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolBase))).thenReturn(EXA.divide(BigInteger.TWO)); + contextMock.when(() -> Context.call(eq(balancedOracle.getAddress()), eq("getPriceInUSD"), eq(symbolQuote))).thenReturn(EXA); + + // Act + dexScore.invoke(governanceScore, "setOracleProtection", BigInteger.TWO, points); + + // Asset + BigInteger oracleProtection = (BigInteger) dexScore.call("getOracleProtection", BigInteger.TWO); + assertEquals(oracleProtection, points); + } + + @Test + void getIcxBalance() { + // Arrange. + Account supplier = sm.createAccount(); + BigInteger value = BigInteger.valueOf(1000).multiply(EXA); + + // Act. + supplyIcxLiquidity(supplier, value); + + // Assert. + BigInteger IcxBalance = (BigInteger) dexScore.call("getICXBalance", supplier.getAddress()); + assertEquals(IcxBalance, value); + } + + @Test + void getSicxEarnings() { + // Supply liquidity to sicx/icx pool. + // Swap some sicx to icx. + // Get and verify earnings. + } + + @Test + void getDeposit() { + // Arrange. + Account depositor = sm.createAccount(); + BigInteger value = BigInteger.valueOf(100).multiply(EXA); + + // Act. + depositToken(depositor, bnusdScore, value); + + // Assert. + BigInteger retrievedValue = (BigInteger) dexScore.call("getDeposit", bnusdScore.getAddress(), + depositor.getAddress()); + assertEquals(value, retrievedValue); + + } + + @Test + void getNonce() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + BigInteger expectedNonce = BigInteger.valueOf(3); + + // Act. + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert. + BigInteger retrievedNonce = (BigInteger) dexScore.call("getNonce"); + assertEquals(expectedNonce, retrievedNonce); + } + + @Test + void getPoolId() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + BigInteger expectedPoolValue = BigInteger.TWO; + + // Act. + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert. + BigInteger retrievedPoolId = (BigInteger) dexScore.call("getPoolId", bnusdScore.getAddress(), + balnScore.getAddress()); + assertEquals(expectedPoolValue, retrievedPoolId); + } + + @Test + void lookupId() { + // Arrange. + String namedMarket = "sICX/ICX"; + BigInteger expectedId = BigInteger.ONE; + + // Assert. + BigInteger retrievedId = (BigInteger) dexScore.call("lookupPid", namedMarket); + assertEquals(expectedId, retrievedId); + } + + @Test + void getPoolTotal() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(10).pow(19); + BigInteger balnValue = BigInteger.valueOf(10).pow(19); + BigInteger poolId = BigInteger.TWO; + + // Act. + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert. + BigInteger retrievedBnusdValue = (BigInteger) dexScore.call("getPoolTotal", poolId, bnusdScore.getAddress()); + BigInteger retrievedBalnValue = (BigInteger) dexScore.call("getPoolTotal", poolId, balnScore.getAddress()); + assertEquals(bnusdValue, retrievedBnusdValue); + assertEquals(balnValue, retrievedBalnValue); + } + + @Test + void balanceOf_normalPool() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(10).pow(19); + BigInteger balnValue = BigInteger.valueOf(10).pow(19); + BigInteger userLpTokenValue = (bnusdValue.multiply(balnValue)).sqrt(); + BigInteger poolId = BigInteger.TWO; + + // Act. + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert. + BigInteger retrievedUserLpTokenValue = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), + poolId); + assertEquals(userLpTokenValue, retrievedUserLpTokenValue); + } + + @Test + void balanceOf_icxSicxPool() { + // Arrange. + BigInteger value = BigInteger.valueOf(100).multiply(EXA); + BigInteger poolId = BigInteger.ONE; + + // Act. + supplyIcxLiquidity(ownerAccount, value); + + // Assert. + BigInteger retrievedBalance = (BigInteger) dexScore.call("balanceOf", ownerAccount.getAddress(), poolId); + assertEquals(value, retrievedBalance); + } + + @Test + void totalSupply_SicxIcxPool() { + // Arrange. + BigInteger value = BigInteger.valueOf(100).multiply(EXA); + BigInteger poolId = BigInteger.ONE; + + // Act. + supplyIcxLiquidity(ownerAccount, value); + + // Assert. + BigInteger totalIcxSupply = (BigInteger) dexScore.call("totalSupply", poolId); + assertEquals(value, totalIcxSupply); + } + + @Test + void totalSupply_normalPool() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(10).pow(19); + BigInteger balnValue = BigInteger.valueOf(10).pow(19); + BigInteger totalLpTokens = (bnusdValue.multiply(balnValue)).sqrt(); + BigInteger poolId = BigInteger.TWO; + + // Act. + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert. + BigInteger retrievedTotalLpTokens = (BigInteger) dexScore.call("totalSupply", poolId); + assertEquals(totalLpTokens, retrievedTotalLpTokens); + } + + @Test + @SuppressWarnings("unchecked") + void setGetMarketNames() { + // Arrange. + String poolName = "bnUSD/BALN"; + BigInteger poolId = BigInteger.TWO; + List expectedMarketNames = Arrays.asList("sICX/ICX", poolName); + + // Act. + dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); + + // Assert. + List namedPools = (List) dexScore.call("getNamedPools"); + assertEquals(expectedMarketNames, namedPools); + assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "setMarketName", poolId, poolName); + } + + @Test + void getPoolBaseAndGetPoolQuote() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(10).pow(19); + BigInteger balnValue = BigInteger.valueOf(10).pow(19); + + // Act. + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert. + Address poolBase = (Address) dexScore.call("getPoolBase", BigInteger.TWO); + Address poolQuote = (Address) dexScore.call("getPoolQuote", BigInteger.TWO); + assertEquals(poolBase, bnusdScore.getAddress()); + assertEquals(poolQuote, balnScore.getAddress()); + } + + @Test + void getQuotePriceInBase() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + BigInteger expectedPrice = computePrice(bnusdValue, balnValue); + BigInteger poolId = BigInteger.TWO; + + // Act. + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert. + BigInteger price = (BigInteger) dexScore.call("getQuotePriceInBase", poolId); + assertEquals(expectedPrice, price); + } + + @Test + void getBasePriceInQuote() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + BigInteger expectedPrice = computePrice(balnValue, bnusdValue); + BigInteger poolId = BigInteger.TWO; + + // Act. + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert. + BigInteger price = (BigInteger) dexScore.call("getBasePriceInQuote", poolId); + assertEquals(expectedPrice, price); + } + + @Test + void getPrice() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + BigInteger expectedPrice = computePrice(balnValue, bnusdValue); + BigInteger poolId = BigInteger.TWO; + + // Act. + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert. + BigInteger price = (BigInteger) dexScore.call("getPrice", poolId); + assertEquals(expectedPrice, price); + } + + @Test + void getBalnPrice() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + BigInteger expectedPrice = computePrice(balnValue, bnusdValue); + + // Act. + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert. + BigInteger price = (BigInteger) dexScore.call("getBalnPrice"); + assertEquals(expectedPrice, price); + } + + @Test + void getSicxBnusdPrice() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger sicxValue = BigInteger.valueOf(350).multiply(EXA); + BigInteger expectedPrice = computePrice(bnusdValue, sicxValue); + + // Act. + supplyLiquidity(ownerAccount, sicxScore, bnusdScore, sicxValue, bnusdValue, false); + + // Assert. + BigInteger price = (BigInteger) dexScore.call("getSicxBnusdPrice"); + assertEquals(expectedPrice, price); + } + + @Test + void getBnusdValue_sicxIcxPool() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger sicxValue = BigInteger.valueOf(350).multiply(EXA); + BigInteger icxValue = BigInteger.valueOf(10).multiply(EXA); + BigInteger sicxIcxConversionRate = BigInteger.valueOf(10).multiply(EXA); + String poolName = "sICX/ICX"; + BigInteger expectedPoolValue = + (computePrice(bnusdValue, sicxValue).multiply(icxValue)).divide(sicxIcxConversionRate); + doReturn(sicxIcxConversionRate).when(dexScoreSpy).getSicxRate(); + + // Act. + supplyLiquidity(ownerAccount, sicxScore, bnusdScore, sicxValue, bnusdValue, false); + supplyIcxLiquidity(ownerAccount, icxValue); + + // Assert. + BigInteger poolValue = (BigInteger) dexScore.call("getBnusdValue", poolName); + assertEquals(expectedPoolValue, poolValue); + } + + // @Test + // void getBnusdValue_sicxIsQuote() { + // // Arrange. + // BigInteger balnValue = BigInteger.valueOf(195).multiply(EXA); + // BigInteger sicxValue = BigInteger.valueOf(350).multiply(EXA); + // String poolName = "bnUSD/sICX"; + // BigInteger poolId = BigInteger.TWO; + // BigInteger sicxBnusdPrice = BigInteger.valueOf(10).multiply(EXA); + // BigInteger expectedValue = (sicxValue.multiply(BigInteger.TWO).multiply(sicxBnusdPrice)).divide(EXA); + // doReturn(sicxBnusdPrice).when(dexScoreSpy).getSicxBnusdPrice(); + + // // Act. Why can I not supply with sicx as quote currency? Fails. + // dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); + // supplyLiquidity(ownerAccount, bnusdScore, sicxScore, balnValue, sicxValue, false); + + // // Assert. + // //BigInteger poolValue = (BigInteger) dexScore.call( "getBnusdValue", poolName); + // //assertEquals(expectedValue, poolValue); + // } + + @Test + void getBnusdValue_bnusdIsQuote() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + BigInteger expectedValue = BigInteger.valueOf(195).multiply(EXA).multiply(BigInteger.TWO); + String poolName = "bnUSD/BALN"; + BigInteger poolId = BigInteger.TWO; + + // Act. + dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); + supplyLiquidity(ownerAccount, balnScore, bnusdScore, balnValue, bnusdValue, false); + + // Assert. + BigInteger poolValue = (BigInteger) dexScore.call("getBnusdValue", poolName); + assertEquals(expectedValue, poolValue); + } + + @Test + void getBnusdValue_QuoteNotSupported() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + String poolName = "bnUSD/BALN"; + BigInteger poolId = BigInteger.TWO; + + // Act. + dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); + supplyLiquidity(ownerAccount, bnusdScore, balnScore, balnValue, bnusdValue, false); + + // Assert + BigInteger poolValue = (BigInteger) dexScore.call("getBnusdValue", "bnUSD/BALN"); + assertEquals(BigInteger.ZERO, poolValue); + } + + @Test + void getPriceByName() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + String poolName = "bnUSD/BALN"; + BigInteger poolId = BigInteger.TWO; + BigInteger expectedPrice = computePrice(balnValue, bnusdValue); + + // Act. + dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert + BigInteger price = (BigInteger) dexScore.call("getPriceByName", "bnUSD/BALN"); + assertEquals(expectedPrice, price); + } + + @Test + void getPoolName() { + // Arrange. + String poolName = "bnUSD/BALN"; + BigInteger poolId = BigInteger.TWO; + + // Act. + dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); + + // Assert. + String retrievedPoolName = (String) dexScore.call("getPoolName", poolId); + assertEquals(poolName, retrievedPoolName); + } + + @Test + @SuppressWarnings("unchecked") + void getPoolStats_notSicxIcxPool() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + BigInteger expectedPrice = computePrice(balnValue, bnusdValue); + String poolName = "bnUSD/BALN"; + BigInteger poolId = BigInteger.TWO; + BigInteger tokenDecimals = BigInteger.valueOf(18); + BigInteger minQuote = BigInteger.ZERO; + BigInteger totalLpTokens = new BigInteger("261247009552262626468"); + + Map expectedPoolStats = Map.of( + "base", bnusdValue, + "quote", balnValue, + "base_token", bnusdScore.getAddress(), + "quote_token", balnScore.getAddress(), + "total_supply", totalLpTokens, + "price", expectedPrice, + "name", poolName, + "base_decimals", tokenDecimals, + "quote_decimals", tokenDecimals, + "min_quote", minQuote + ); + + // Act. + dexScore.invoke(governanceScore, "setMarketName", poolId, poolName); + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert. + Map poolStats = (Map) dexScore.call("getPoolStats", poolId); + assertEquals(expectedPoolStats, poolStats); + } + + @Test + void getTotalDexAddresses() { + // Arrange. + BigInteger bnusdValue = BigInteger.valueOf(195).multiply(EXA); + BigInteger balnValue = BigInteger.valueOf(350).multiply(EXA); + BigInteger poolId = BigInteger.TWO; + + // Act. + supplyLiquidity(governanceScore, bnusdScore, balnScore, bnusdValue, balnValue, false); + supplyLiquidity(ownerAccount, bnusdScore, balnScore, bnusdValue, balnValue, false); + + // Assert + BigInteger totalDexAddresses = (BigInteger) dexScore.call("totalDexAddresses", BigInteger.TWO); + assertEquals(BigInteger.TWO, totalDexAddresses); + } + + @Test + void permit_OnlyGovernance() { + // Arrange. + BigInteger poolId = BigInteger.ONE; + Boolean permission = true; + + // Assert. + assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "permit", poolId, permission); + } + + @Test + void govWithdraw() { + assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "govWithdraw", 1, dexScore.getAddress(), BigInteger.ZERO); + } + + @Test + void govSetPoolTotal() { + assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "govSetPoolTotal", 1, BigInteger.ZERO); + + } + + @Test + void govSetUserPoolTotal() { + assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "govSetUserPoolTotal", 1, dexScore.getAddress(), BigInteger.ZERO); + } + + @Test + void govSetOracleProtection() { + assertOnlyCallableBy(governanceScore.getAddress(), dexScore, "setOracleProtection", BigInteger.ZERO, BigInteger.TWO); + } + + @AfterEach + void closeMock() { + contextMock.close(); + } +} \ No newline at end of file diff --git a/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/db/LinkedListDBTest.java b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/db/LinkedListDBTest.java new file mode 100644 index 000000000..3e04035e8 --- /dev/null +++ b/core-contracts/Dex/src/test/java/network/balanced/score/core/dex/db/LinkedListDBTest.java @@ -0,0 +1,167 @@ +/* + * 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. + */ + +package network.balanced.score.core.dex.db; + +import com.iconloop.score.test.Account; +import com.iconloop.score.test.Score; +import com.iconloop.score.test.ServiceManager; +import com.iconloop.score.test.TestBase; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import score.Address; +import scorex.util.ArrayList; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class LinkedListDBTest extends TestBase { + + private static final ServiceManager sm = getServiceManager(); + private static final Account owner = sm.createAccount(); + private static final Address user1 = sm.createAccount().getAddress(); + private static final Address user2 = sm.createAccount().getAddress(); + private static final Address user3 = sm.createAccount().getAddress(); + private static final Address user4 = sm.createAccount().getAddress(); + private static final Address user5 = sm.createAccount().getAddress(); + private static final Address user6 = sm.createAccount().getAddress(); + private static final Address user7 = sm.createAccount().getAddress(); + + private static Score dummyScore; + + public static class DummyScore { + + LinkedListDB linkedListDB = new LinkedListDB("linked_list_db"); + + public DummyScore() { + + } + + public void appendUsers(ArrayList
addresses) { + int id = 1; + for (Address address : addresses) { + linkedListDB.append(BigInteger.valueOf(3), address, BigInteger.valueOf(id++)); + } + } + + public void appendUserWithId(Address address, BigInteger id) { + linkedListDB.append(BigInteger.valueOf(3), address, id); + } + + public BigInteger size() { + return linkedListDB.size(); + } + + public Map headNode() { + NodeDB node = linkedListDB.getHeadNode(); + return Map.of("user", node.getUser(), "size", node.getSize(), "prev", node.getPrev(), "next", + node.getNext()); + } + + public Map tailNode() { + NodeDB node = linkedListDB.getTailNode(); + return Map.of("user", node.getUser(), "size", node.getSize(), "prev", node.getPrev(), "next", + node.getNext()); + } + + public void updateNode(BigInteger nodeId, BigInteger size, Address user) { + linkedListDB.updateNode(nodeId, size, user); + } + + public Map getNode(BigInteger nodeId) { + NodeDB node = linkedListDB.getNode(nodeId); + return Map.of("user", node.getUser(), "size", node.getSize(), "prev", node.getPrev(), "next", + node.getNext()); + } + + public void removeHead() { + linkedListDB.removeHead(); + } + + public void removeTail() { + linkedListDB.removeTail(); + } + + public void remove(BigInteger nodeId) { + linkedListDB.remove(nodeId); + } + + public void clear() { + linkedListDB.clear(); + } + + } + + @BeforeAll + public static void setup() throws Exception { + dummyScore = sm.deploy(owner, DummyScore.class); + } + + @SuppressWarnings("unchecked") + @Test + public void testUnorderedAddressBag() { + List
users = new ArrayList<>(); + users.add(user1); + users.add(user2); + users.add(user3); + users.add(user4); + users.add(user5); + dummyScore.invoke(owner, "appendUsers", users); + assertEquals(BigInteger.valueOf(5), dummyScore.call("size")); + dummyScore.invoke(owner, "removeHead"); + assertEquals(BigInteger.valueOf(4), dummyScore.call("size")); + dummyScore.invoke(owner, "removeTail"); + assertEquals(BigInteger.valueOf(3), dummyScore.call("size")); + + dummyScore.invoke(owner, "remove", BigInteger.valueOf(3)); + assertEquals(BigInteger.valueOf(2), dummyScore.call("size")); + Map nodeDB = (Map) dummyScore.call("getNode", BigInteger.TWO); + assertEquals(nodeDB.get("size"), BigInteger.valueOf(3)); + assertEquals(nodeDB.get("user"), user2); + assertEquals(nodeDB.get("prev"), BigInteger.ZERO); + assertEquals(nodeDB.get("next"), BigInteger.valueOf(4)); + + dummyScore.invoke(owner, "updateNode", BigInteger.TWO, BigInteger.valueOf(5), user6); + Map nodeTwo = (Map) dummyScore.call("getNode", BigInteger.TWO); + assertEquals(nodeTwo.get("size"), BigInteger.valueOf(5)); + assertEquals(nodeTwo.get("user"), user6); + assertEquals(nodeDB.get("prev"), BigInteger.ZERO); + assertEquals(nodeTwo.get("next"), BigInteger.valueOf(4)); + + Map headNode = (Map) dummyScore.call("headNode"); + Map tailNode = (Map) dummyScore.call("tailNode"); + assertEquals(headNode.get("user"), user6); + assertEquals(headNode.get("next"), BigInteger.valueOf(4)); + + + assertEquals(tailNode.get("user"), user4); + assertEquals(tailNode.get("prev"), BigInteger.valueOf(2)); + + AssertionError e = Assertions.assertThrows(AssertionError.class, () -> dummyScore.call("getNode", BigInteger.valueOf(7))); + assertEquals(e.getMessage(), "Reverted(0): Linked List linked_list_db_LINKED_LISTDB Node not found of nodeId " + + "7"); + + e = Assertions.assertThrows(AssertionError.class, () -> dummyScore.invoke(owner, "appendUserWithId", user7, BigInteger.TWO)); + assertEquals(e.getMessage(), "Reverted(0): Linked List linked_list_db_LINKED_LISTDB already exists of nodeId" + + "." + "2"); + } + + +} From 959e6413357e4e43371b3fc11245c0f75590deb4 Mon Sep 17 00:00:00 2001 From: Spl3en Date: Wed, 7 Aug 2024 17:14:39 +0200 Subject: [PATCH 3/3] Implement Non Fungible Token Position Manager contract --- .../NonFungiblePositionManager/build.gradle | 141 ++++ .../NonFungiblePositionManager.java | 652 ++++++++++++++++++ .../BalancedLiquidityManagement.java | 277 ++++++++ .../IBalancedLiquidityManagement.java | 64 ++ ...lancedLiquidityManagementAddLiquidity.java | 48 ++ .../INonfungibleTokenPositionDescriptor.java | 34 + .../positionmgr/libs/CallbackValidation.java | 48 ++ .../positionmgr/libs/LiquidityAmounts.java | 107 +++ .../positionmgr/libs/PeripheryPayments.java | 37 + .../core/positionmgr/libs/PoolAddressLib.java | 47 ++ .../structs/AddLiquidityParams.java | 60 ++ .../structs/AddLiquidityResult.java | 40 ++ .../positionmgr/structs/CollectParams.java | 46 ++ .../structs/DecreaseLiquidityParams.java | 48 ++ .../structs/IncreaseLiquidityParams.java | 52 ++ .../structs/IncreaseLiquidityResult.java | 38 + .../core/positionmgr/structs/MintParams.java | 90 +++ .../core/positionmgr/structs/MintResult.java | 53 ++ .../core/positionmgr/structs/NFTPosition.java | 113 +++ .../structs/PositionInformation.java | 96 +++ settings.gradle | 6 + token-contracts/irc721/build.gradle | 11 + .../token/irc721/IIRC721Receiver.java | 17 + .../standards/token/irc721/IRC721.java | 393 +++++++++++ .../token/irc721/IRC721Enumerable.java | 204 ++++++ .../token/irc721/IRC721Receiver.java | 24 + 26 files changed, 2746 insertions(+) create mode 100644 core-contracts/NonFungiblePositionManager/build.gradle create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/NonFungiblePositionManager.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/implementation/BalancedLiquidityManagement.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/IBalancedLiquidityManagement.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/IBalancedLiquidityManagementAddLiquidity.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/interfaces/INonfungibleTokenPositionDescriptor.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/CallbackValidation.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/LiquidityAmounts.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/PeripheryPayments.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/libs/PoolAddressLib.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/AddLiquidityParams.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/AddLiquidityResult.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/CollectParams.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/DecreaseLiquidityParams.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/IncreaseLiquidityParams.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/IncreaseLiquidityResult.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/MintParams.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/MintResult.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/NFTPosition.java create mode 100644 core-contracts/NonFungiblePositionManager/src/main/java/network/balanced/score/core/positionmgr/structs/PositionInformation.java create mode 100644 token-contracts/irc721/build.gradle create mode 100644 token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IIRC721Receiver.java create mode 100644 token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721.java create mode 100644 token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721Enumerable.java create mode 100644 token-contracts/irc721/src/main/java/team/iconation/standards/token/irc721/IRC721Receiver.java 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/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); +}