diff --git a/.gitmodules b/.gitmodules index e7560086f..b449fa797 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,7 +2,3 @@ path = gochain-local url = git@github.com:nightowl121/gochain-local.git branch = master -[submodule "xcall-lib"] - path = xcall-lib - url = git@github.com:AntonAndell/XCall-lib.git - branch = master diff --git a/core-contracts/AssetManager/src/main/java/network/balanced/score/core/asset/manager/AssetManagerImpl.java b/core-contracts/AssetManager/src/main/java/network/balanced/score/core/asset/manager/AssetManagerImpl.java index 8afaf6031..02d2e3f1a 100644 --- a/core-contracts/AssetManager/src/main/java/network/balanced/score/core/asset/manager/AssetManagerImpl.java +++ b/core-contracts/AssetManager/src/main/java/network/balanced/score/core/asset/manager/AssetManagerImpl.java @@ -34,6 +34,7 @@ import java.util.Map; import static network.balanced.score.lib.utils.Check.*; +import static network.balanced.score.lib.utils.Math.pow; public class AssetManagerImpl implements AssetManager { @@ -42,9 +43,9 @@ public class AssetManagerImpl implements AssetManager { public static final String ASSETS = "assets"; public static final String NATIVE_ASSET_ADDRESS = "native_asset_address"; public static final String NATIVE_ASSET_ADDRESSES = "native_asset_addresses"; - public static final String DATA_MIGRATED = "data_migrated"; public static final String ASSET_DEPOSIT_CHAIN_LIMIT = "asset_deposit_chain_limit"; public static final String ASSET_DEPOSITS = "asset_deposits"; + public static final String DECIMAL_TRANSFORMATIONS = "decimal_transformations"; public static String NATIVE_NID; public static byte[] tokenBytes; @@ -58,7 +59,7 @@ public class AssetManagerImpl implements AssetManager { private final BranchDB> assetNativeAddresses = Context.newBranchDB(NATIVE_ASSET_ADDRESSES, String.class); private final DictDB assetChainDepositLimit = Context.newDictDB(ASSET_DEPOSIT_CHAIN_LIMIT, BigInteger.class); private final DictDB assetDeposits = Context.newDictDB(ASSET_DEPOSITS, BigInteger.class); - private final VarDB dataMigrated = Context.newVarDB(DATA_MIGRATED, Boolean.class); + private final DictDB decimalTransformations = Context.newDictDB(DECIMAL_TRANSFORMATIONS, BigInteger.class); public AssetManagerImpl(Address _governance, byte[] tokenBytes) { AssetManagerImpl.tokenBytes = tokenBytes; @@ -73,9 +74,6 @@ public AssetManagerImpl(Address _governance, byte[] tokenBytes) { } currentVersion.set(Versions.BALANCED_ASSET_MANAGER); - if (dataMigrated.get() == null) { - migrateTokenNativeAddressAndAssetDeposits(); - } } @External(readonly = true) @@ -118,44 +116,52 @@ public void deployAsset(String tokenNetworkAddress, String name, String symbol, Address token = Context.deploy(tokenBytes, BalancedAddressManager.getGovernance(), name, symbol, decimals); assets.set(tokenNetworkAddress, token); assetNativeAddresses.at(token).set(nativeAddress.net(), nativeAddress.account()); + if (assetNativeAddress.get(token) == null) { + assetNativeAddress.set(token, tokenNetworkAddress); + } + Address SYSTEM_SCORE_ADDRESS = getSystemScoreAddress(); Context.call(SYSTEM_SCORE_ADDRESS, "setScoreOwner", token, BalancedAddressManager.getGovernance()); } - private void migrateTokenNativeAddressAndAssetDeposits() { - List nativeAddresses = assets.keys(); - for (String na : nativeAddresses) { - Address address = assets.get(na); - if (assetNativeAddress.get(address) != null) { - NetworkAddress networkAddress = NetworkAddress.valueOf(na); - assetNativeAddresses.at(address).set(networkAddress.net(), networkAddress.account()); - //delete once migrated - //assetNativeAddress.set(address, null); - } - - assetDeposits.set(na, getTotalSupply(address)); - } - - dataMigrated.set(true); - } @External - public void linkToken(String tokenNetworkAddress, Address token) { + public void linkToken(String tokenNetworkAddress, Address token, @Optional BigInteger decimals) { onlyGovernance(); + BigInteger tokenDecimals = Context.call(BigInteger.class, token, "decimals"); + if (decimals != null && !decimals.equals(BigInteger.ZERO) && !decimals.equals(tokenDecimals) ) { + BigInteger diff = decimals.subtract(tokenDecimals); + BigInteger transformation = pow(BigInteger.TEN, diff.abs().intValue()).multiply(BigInteger.valueOf(diff.signum())); + decimalTransformations.set(tokenNetworkAddress, transformation); + } + NetworkAddress networkAddress = NetworkAddress.valueOf(tokenNetworkAddress); Context.require(spokes.get(networkAddress.net()) != null, "Add the spoke spoke manager first"); Context.require(assets.get(tokenNetworkAddress) == null, "Token is already available"); assets.set(tokenNetworkAddress, token); assetNativeAddresses.at(token).set(networkAddress.net(), networkAddress.account()); + if (assetNativeAddress.get(token) == null) { + assetNativeAddress.set(token, tokenNetworkAddress); + } } @External public void removeToken(Address token, String nid) { onlyGovernance(); String nativeAddress = assetNativeAddresses.at(token).get(nid); + String networkAddress = new NetworkAddress(nid, nativeAddress).toString(); Context.require(nativeAddress != null, "Token is not available"); assetNativeAddresses.at(token).set(nid, null); - assets.set(new NetworkAddress(nid, nativeAddress).toString(), null); + decimalTransformations.set(networkAddress, null); + assets.set(networkAddress, null); + } + + @External + public void overrideChainDeposits(String tokenNetworkAddress, BigInteger addedAmount) { + onlyGovernance(); + BigInteger remainingDeposit = getAssetDeposit(tokenNetworkAddress).add(addedAmount); + Context.require(remainingDeposit.signum() >= 0, "Remaining deposit can't be negative"); + assetDeposits.set(tokenNetworkAddress, remainingDeposit); } @External @@ -268,6 +274,7 @@ public void deposit(String from, String tokenAddress, String fromAddress, String Address assetAddress = assets.get(spokeTokenAddress); Context.require(assetAddress != null, "Token is not yet deployed"); + _amount = translateIncomingDecimals(spokeTokenAddress, _amount); BigInteger tokenAddressDepositLimit = assetChainDepositLimit.get(spokeTokenAddress); Context.require(tokenAddressDepositLimit == null || getAssetDeposit(spokeTokenAddress).add(_amount).compareTo(tokenAddressDepositLimit) <= 0, "Max deposit limit exceeded"); @@ -305,18 +312,42 @@ private void _withdrawTo(Address asset, String from, String to, BigInteger amoun byte[] msg; byte[] rollback = AssetManagerMessages.withdrawRollback(tokenAddress.toString(), to, amount); + BigInteger sendAmount = translateOutgoingDecimals(tokenAddress.toString(), amount); + Context.require(sendAmount.compareTo(BigInteger.ZERO) > 0, "Amount needs to be greater than 0 on the destination chain"); if (toNative) { - msg = SpokeAssetManagerMessages.WithdrawNativeTo(tokenAddress.account(), targetAddress.account(), amount); + msg = SpokeAssetManagerMessages.WithdrawNativeTo(tokenAddress.account(), targetAddress.account(), sendAmount); } else { - msg = SpokeAssetManagerMessages.WithdrawTo(tokenAddress.account(), targetAddress.account(), amount); + msg = SpokeAssetManagerMessages.WithdrawTo(tokenAddress.account(), targetAddress.account(), sendAmount); } - - assetDeposits.set(tokenAddress.toString(), getAssetDeposit(tokenAddress.toString()).subtract(amount)); + BigInteger remainingDeposit = getAssetDeposit(tokenAddress.toString()).subtract(amount); + Context.require(remainingDeposit.signum() >= 0, "Remaining deposit can't be negative"); + assetDeposits.set(tokenAddress.toString(), remainingDeposit); XCallUtils.sendCall(fee, spoke, msg, rollback); } + private BigInteger translateOutgoingDecimals(String token, BigInteger amount) { + return translateDecimals(token, amount, 1); + } + + private BigInteger translateIncomingDecimals(String token, BigInteger amount) { + return translateDecimals(token, amount, -1); + } + + private BigInteger translateDecimals(String token, BigInteger amount, int sign) { + BigInteger translation = decimalTransformations.get(token); + if (translation == null) { + return amount; + } + + if (translation.signum() == sign) { + return amount.multiply(translation).abs(); + } else { + return amount.divide(translation).abs(); + } + } + private BigInteger getTotalSupply(Address assetAddress) { return Context.call(BigInteger.class, assetAddress, "totalSupply"); } diff --git a/core-contracts/AssetManager/src/test/java/network/balanced/score/core/asset/manager/AssetManagerTest.java b/core-contracts/AssetManager/src/test/java/network/balanced/score/core/asset/manager/AssetManagerTest.java index 23feab585..f700f1909 100644 --- a/core-contracts/AssetManager/src/test/java/network/balanced/score/core/asset/manager/AssetManagerTest.java +++ b/core-contracts/AssetManager/src/test/java/network/balanced/score/core/asset/manager/AssetManagerTest.java @@ -94,6 +94,7 @@ void setup() throws Exception { when(mockBalanced.xCallManager.mock.getProtocols(ETH_NID)).thenReturn(Map.of("sources", defaultProtocols, "destinations", defaultDestinationsProtocols)); when(mockBalanced.xCallManager.mock.getProtocols(BSC_NID)).thenReturn(Map.of("sources", defaultProtocols, "destinations", defaultDestinationsProtocols)); + when(mockBalanced.xCallManager.mock.getProtocols(INJ_NID)).thenReturn(Map.of("sources", defaultProtocols, "destinations", defaultDestinationsProtocols)); assetManager.invoke(governance.account, "addSpokeManager", ethSpoke.toString()); assetManagerSpy = (AssetManagerImpl) spy(assetManager.getInstance()); @@ -153,13 +154,12 @@ void withdrawTo() { NetworkAddress ethAccount = new NetworkAddress(ETH_NID, "0xTest"); BigInteger amount = BigInteger.TEN; NetworkAddress tokenAddress = new NetworkAddress(ETH_NID, ethAsset1Address); + doReturn(amount).when(assetManagerSpy).getAssetDeposit(tokenAddress.toString()); // Act assetManager.invoke(user, "withdrawTo", ethAsset1.getAddress(), ethAccount.toString(), amount); // Assert - BigInteger assetDeposit = (BigInteger) assetManager.call("getAssetDeposit", tokenAddress.toString()); - assertEquals(assetDeposit, amount.negate()); byte[] expectedMsg = SpokeAssetManagerMessages.WithdrawTo(tokenAddress.account(), ethAccount.account(), amount); byte[] expectedRollback = AssetManagerMessages.withdrawRollback(tokenAddress.toString(), ethAccount.toString(), amount); @@ -174,13 +174,12 @@ void withdrawNativeTo() { NetworkAddress ethAccount = new NetworkAddress(ETH_NID, "0xTest"); BigInteger amount = BigInteger.TEN; NetworkAddress tokenAddress = new NetworkAddress(ETH_NID, ethAsset1Address); + doReturn(amount).when(assetManagerSpy).getAssetDeposit(tokenAddress.toString()); // Act assetManager.invoke(user, "withdrawNativeTo", ethAsset1.getAddress(), ethAccount.toString(), amount); // Assert - BigInteger assetDeposit = (BigInteger) assetManager.call("getAssetDeposit", tokenAddress.toString()); - assertEquals(assetDeposit, amount.negate()); byte[] expectedMsg = SpokeAssetManagerMessages.WithdrawNativeTo(tokenAddress.account(), ethAccount.account(), amount); byte[] expectedRollback = AssetManagerMessages.withdrawRollback(tokenAddress.toString(), ethAccount.toString(), amount); @@ -226,13 +225,12 @@ void xCallWithdraw() { BigInteger amount = BigInteger.TEN; NetworkAddress tokenAddress = new NetworkAddress(ETH_NID, ethAsset1Address); byte[] withdraw = AssetManagerMessages.xWithdraw(ethAsset1.getAddress(), amount); + doReturn(amount).when(assetManagerSpy).getAssetDeposit(tokenAddress.toString()); // Act assetManager.invoke(mockBalanced.xCall.account, "handleCallMessage", ethAccount.toString(), withdraw, defaultProtocols); // Assert - BigInteger assetDeposit = (BigInteger) assetManager.call("getAssetDeposit", tokenAddress.toString()); - assertEquals(assetDeposit, amount.negate()); byte[] expectedMsg = SpokeAssetManagerMessages.WithdrawTo(tokenAddress.account(), ethAccount.account(), amount); byte[] expectedRollback = AssetManagerMessages.withdrawRollback(tokenAddress.toString(), ethAccount.toString(), amount); @@ -330,13 +328,13 @@ void linkToken() { NetworkAddress injAccount = new NetworkAddress(INJ_NID, "inj1x32"); // Act - Executable noSpokeManager = () -> assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress()); + Executable noSpokeManager = () -> assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress(), BigInteger.ZERO); expectErrorMessage(noSpokeManager, "Reverted(0): Add the spoke spoke manager first"); assetManager.invoke(governance.account, "addSpokeManager", injSpoke.toString()); - assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress()); + assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress(), BigInteger.ZERO); - Executable alreadyLinked = () -> assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress()); + Executable alreadyLinked = () -> assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress(), BigInteger.ZERO); expectErrorMessage(alreadyLinked, "Reverted(0): Token is already available"); byte[] deposit = AssetManagerMessages.deposit(injAsset1Address, injAccount.account(), "", amount, new byte[0]); @@ -356,8 +354,8 @@ void linkToken_multipleNetworkAddresses() { NetworkAddress link2Account = new NetworkAddress(LINK2_NID, "link1x32"); assetManager.invoke(governance.account, "addSpokeManager", injSpoke.toString()); assetManager.invoke(governance.account, "addSpokeManager", link2Spoke.toString()); - assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress()); - assetManager.invoke(mockBalanced.governance.account, "linkToken", link2TokenNetworkAddress.toString(), injLinkAsset.getAddress()); + assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress(), BigInteger.ZERO); + assetManager.invoke(mockBalanced.governance.account, "linkToken", link2TokenNetworkAddress.toString(), injLinkAsset.getAddress(), BigInteger.ZERO); // Act byte[] deposit = AssetManagerMessages.deposit(injAsset1Address, injAccount.account(), "", amount, new byte[0]); @@ -370,6 +368,123 @@ void linkToken_multipleNetworkAddresses() { verify(injLinkAsset.mock).mintAndTransfer(link2Account.toString(), injAccount.toString(), amount, new byte[0]); } + @Test + void linkToken_lowerDecimals_deposit() { + // Arrange + NetworkAddress tokenNetworkAddress = new NetworkAddress(INJ_NID, injAsset1Address); + NetworkAddress injAccount = new NetworkAddress(INJ_NID, "inj1x32"); + BigInteger decimals = BigInteger.valueOf(18); + BigInteger tokenDecimals = BigInteger.valueOf(6); + BigInteger amount = BigInteger.TEN; + BigInteger depositAmount = amount.pow(tokenDecimals.intValue()); + BigInteger mintAmount = amount.pow(decimals.intValue()); + + when(injLinkAsset.mock.decimals()).thenReturn(decimals); + assetManager.invoke(governance.account, "addSpokeManager", injSpoke.toString()); + assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress(), tokenDecimals); + + // Act + byte[] deposit = AssetManagerMessages.deposit(injAsset1Address, injAccount.account(), "", depositAmount, new byte[0]); + assetManager.invoke(mockBalanced.xCall.account, "handleCallMessage", injSpoke.toString(), deposit, defaultProtocols); + + // Assert + verify(injLinkAsset.mock).mintAndTransfer(injAccount.toString(), injAccount.toString(), mintAmount, new byte[0]); + BigInteger assetDeposit = (BigInteger) assetManager.call("getAssetDeposit", tokenNetworkAddress.toString()); + assertEquals(assetDeposit, mintAmount); + } + + @Test + void linkToken_lowerDecimals_withdraw() { + // Arrange + NetworkAddress tokenNetworkAddress = new NetworkAddress(INJ_NID, injAsset1Address); + Account user = sm.createAccount(); + NetworkAddress injAccount = new NetworkAddress(INJ_NID, "inj1x32"); + BigInteger decimals = BigInteger.valueOf(18); + BigInteger tokenDecimals = BigInteger.valueOf(6); + BigInteger amount = BigInteger.TEN; + BigInteger withdrawAmount = amount.pow(tokenDecimals.intValue()); + BigInteger burnAmount = amount.pow(decimals.intValue()); + when(injLinkAsset.mock.decimals()).thenReturn(decimals); + + assetManager.invoke(governance.account, "addSpokeManager", injSpoke.toString()); + assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress(), tokenDecimals); + + byte[] deposit = AssetManagerMessages.deposit(injAsset1Address, injAccount.account(), "", withdrawAmount, new byte[0]); + assetManager.invoke(mockBalanced.xCall.account, "handleCallMessage", injSpoke.toString(), deposit, defaultProtocols); + + // Act + assetManager.invoke(user, "withdrawTo", injLinkAsset.getAddress(), injAccount.toString(), burnAmount); + + // Assert + byte[] expectedMsg = SpokeAssetManagerMessages.WithdrawTo(tokenNetworkAddress.account(), injAccount.account(), withdrawAmount); + byte[] expectedRollback = AssetManagerMessages.withdrawRollback(tokenNetworkAddress.toString(), injAccount.toString(), burnAmount); + + verify(mockBalanced.xCall.mock).sendCallMessage(injSpoke.toString(), expectedMsg, expectedRollback, defaultProtocols, defaultDestinationsProtocols); + verify(injLinkAsset.mock).burnFrom(user.getAddress().toString(), burnAmount); + BigInteger assetDeposit = (BigInteger) assetManager.call("getAssetDeposit", tokenNetworkAddress.toString()); + assertEquals(assetDeposit, BigInteger.ZERO); + } + + @Test + void linkToken_higherDecimals_deposit() { + // Arrange + NetworkAddress tokenNetworkAddress = new NetworkAddress(INJ_NID, injAsset1Address); + NetworkAddress injAccount = new NetworkAddress(INJ_NID, "inj1x32"); + BigInteger decimals = BigInteger.valueOf(15); + BigInteger tokenDecimals = BigInteger.valueOf(18); + BigInteger amount = BigInteger.TEN; + BigInteger depositAmount = amount.pow(tokenDecimals.intValue()); + BigInteger mintAmount = amount.pow(decimals.intValue()); + + when(injLinkAsset.mock.decimals()).thenReturn(decimals); + assetManager.invoke(governance.account, "addSpokeManager", injSpoke.toString()); + assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress(), tokenDecimals); + + // Act + byte[] deposit = AssetManagerMessages.deposit(injAsset1Address, injAccount.account(), "", depositAmount, new byte[0]); + assetManager.invoke(mockBalanced.xCall.account, "handleCallMessage", injSpoke.toString(), deposit, defaultProtocols); + + // Assert + verify(injLinkAsset.mock).mintAndTransfer(injAccount.toString(), injAccount.toString(), mintAmount, new byte[0]); + BigInteger assetDeposit = (BigInteger) assetManager.call("getAssetDeposit", tokenNetworkAddress.toString()); + assertEquals(assetDeposit, mintAmount); + } + + @Test + void linkToken_higherDecimals_withdraw() { + // Arrange + NetworkAddress tokenNetworkAddress = new NetworkAddress(INJ_NID, injAsset1Address); + Account user = sm.createAccount(); + NetworkAddress injAccount = new NetworkAddress(INJ_NID, "inj1x32"); + BigInteger decimals = BigInteger.valueOf(15); + BigInteger tokenDecimals = BigInteger.valueOf(8); + BigInteger amount = BigInteger.TEN; + BigInteger withdrawAmount = amount.pow(tokenDecimals.intValue()); + BigInteger burnAmount = amount.pow(decimals.intValue()); + when(injLinkAsset.mock.decimals()).thenReturn(decimals); + + assetManager.invoke(governance.account, "addSpokeManager", injSpoke.toString()); + assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress(), tokenDecimals); + + byte[] deposit = AssetManagerMessages.deposit(injAsset1Address, injAccount.account(), "", withdrawAmount, new byte[0]); + assetManager.invoke(mockBalanced.xCall.account, "handleCallMessage", injSpoke.toString(), deposit, defaultProtocols); + + // Act + assetManager.invoke(user, "withdrawTo", injLinkAsset.getAddress(), injAccount.toString(), burnAmount); + + // Assert + byte[] expectedMsg = SpokeAssetManagerMessages.WithdrawTo(tokenNetworkAddress.account(), injAccount.account(), withdrawAmount); + byte[] expectedRollback = AssetManagerMessages.withdrawRollback(tokenNetworkAddress.toString(), injAccount.toString(), burnAmount); + + verify(mockBalanced.xCall.mock).sendCallMessage(injSpoke.toString(), expectedMsg, expectedRollback, defaultProtocols, defaultDestinationsProtocols); + verify(injLinkAsset.mock).burnFrom(user.getAddress().toString(), burnAmount); + BigInteger assetDeposit = (BigInteger) assetManager.call("getAssetDeposit", tokenNetworkAddress.toString()); + assertEquals(assetDeposit, BigInteger.ZERO); + } + + + + @Test void removeToken() { // Arrange @@ -382,7 +497,7 @@ void removeToken() { expectErrorMessage(noSpokeManager, "Reverted(0): Token is not available"); assetManager.invoke(governance.account, "addSpokeManager", injSpoke.toString()); - assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress()); + assetManager.invoke(mockBalanced.governance.account, "linkToken", tokenNetworkAddress.toString(), injLinkAsset.getAddress(), BigInteger.ZERO); assetManager.invoke(mockBalanced.governance.account, "removeToken", injLinkAsset.getAddress(), INJ_NID); // Assert diff --git a/core-contracts/DAOfund/src/intTest/java/network/balanced/score/core/daofund/DaofundIntegrationTest.java b/core-contracts/DAOfund/src/intTest/java/network/balanced/score/core/daofund/DaofundIntegrationTest.java index 69759987a..53f64fff1 100644 --- a/core-contracts/DAOfund/src/intTest/java/network/balanced/score/core/daofund/DaofundIntegrationTest.java +++ b/core-contracts/DAOfund/src/intTest/java/network/balanced/score/core/daofund/DaofundIntegrationTest.java @@ -232,7 +232,7 @@ void protocolOwnedLiquidity_stakeLpTokens() throws Exception { client.staking.stakeICX(lpAmount.multiply(BigInteger.TWO), null, null); client.bnUSD.transfer(balanced.dex._address(), lpAmount, tokenDepositData); client.sicx.transfer(balanced.dex._address(), lpAmount, tokenDepositData); - client.dex.add(balanced.sicx._address(), balanced.bnusd._address(), lpAmount, lpAmount, true); + client.dex.add(balanced.sicx._address(), balanced.bnusd._address(), lpAmount, lpAmount, true, BigInteger.valueOf(10000)); BigInteger lpBalance = client.dex.balanceOf(client.getAddress(), pid); // Act diff --git a/core-contracts/DAOfund/src/main/java/network/balanced/score/core/daofund/DAOfundImpl.java b/core-contracts/DAOfund/src/main/java/network/balanced/score/core/daofund/DAOfundImpl.java index a806f54ee..1d565349e 100644 --- a/core-contracts/DAOfund/src/main/java/network/balanced/score/core/daofund/DAOfundImpl.java +++ b/core-contracts/DAOfund/src/main/java/network/balanced/score/core/daofund/DAOfundImpl.java @@ -18,10 +18,12 @@ import network.balanced.score.lib.interfaces.DAOfund; import network.balanced.score.lib.structs.PrepDelegations; +import network.balanced.score.lib.structs.ProtocolConfig; import network.balanced.score.lib.utils.BalancedAddressManager; import network.balanced.score.lib.utils.EnumerableSetDB; import network.balanced.score.lib.utils.Names; import network.balanced.score.lib.utils.Versions; +import network.balanced.score.lib.utils.XCallUtils; import score.*; import score.annotation.EventLog; import score.annotation.External; @@ -246,7 +248,8 @@ public boolean getXCallFeePermission(Address contract, String net) { public BigInteger claimXCallFee(String net, boolean response) { Address contract = Context.getCaller(); Context.require(xCallFeePermissions.at(contract).getOrDefault(net, false), contract + " is not allowed to use fees from daofund"); - BigInteger fee = Context.call(BigInteger.class, BalancedAddressManager.getXCall(), "getFee", net, response); + Map protocol = XCallUtils.getProtocols(net); + BigInteger fee = Context.call(BigInteger.class, BalancedAddressManager.getXCall(), "getFee", net, response, protocol.get(ProtocolConfig.sourcesKey)); Context.require(fee.compareTo(Context.getBalance(Context.getAddress())) <= 0, "Daofund out of Balance" ); if (fee.equals(BigInteger.ZERO)) { return BigInteger.ZERO; diff --git a/core-contracts/DAOfund/src/main/java/network/balanced/score/core/daofund/POLManager.java b/core-contracts/DAOfund/src/main/java/network/balanced/score/core/daofund/POLManager.java index 53f16bb63..f8bb2c5e7 100644 --- a/core-contracts/DAOfund/src/main/java/network/balanced/score/core/daofund/POLManager.java +++ b/core-contracts/DAOfund/src/main/java/network/balanced/score/core/daofund/POLManager.java @@ -61,19 +61,9 @@ public static void claimNetworkFees() { public static void supplyLiquidity(Address baseAddress, BigInteger baseAmount, Address quoteAddress, BigInteger quoteAmount) { Address dex = getDex(); - BigInteger pid = Context.call(BigInteger.class, dex, "getPoolId", baseAddress, quoteAddress); - - BigInteger supplyPrice = quoteAmount.multiply(EXA).divide(baseAmount); - BigInteger dexPrice = Context.call(BigInteger.class, dex, "getPrice", pid); - BigInteger allowedDiff = supplyPrice.multiply(polSupplySlippage.get()).divide(POINTS); - Context.require(supplyPrice.subtract(allowedDiff).compareTo(dexPrice) < 0, "Price on dex was below allowed " + - "threshold"); - Context.require(supplyPrice.add(allowedDiff).compareTo(dexPrice) > 0, "Price on dex was above allowed " + - "threshold"); - Context.call(baseAddress, "transfer", dex, baseAmount, tokenDepositData); Context.call(quoteAddress, "transfer", dex, quoteAmount, tokenDepositData); - Context.call(dex, "add", baseAddress, quoteAddress, baseAmount, quoteAmount, true); + Context.call(dex, "add", baseAddress, quoteAddress, baseAmount, quoteAmount, true, getPOLSupplySlippage()); } public static void stake(BigInteger pid, BigInteger amount) { diff --git a/core-contracts/DAOfund/src/test/java/network/balanced/score/core/daofund/DAOfundImplTest.java b/core-contracts/DAOfund/src/test/java/network/balanced/score/core/daofund/DAOfundImplTest.java index 6ab04996c..4a6405467 100644 --- a/core-contracts/DAOfund/src/test/java/network/balanced/score/core/daofund/DAOfundImplTest.java +++ b/core-contracts/DAOfund/src/test/java/network/balanced/score/core/daofund/DAOfundImplTest.java @@ -206,7 +206,7 @@ void supplyLiquidity() { assertOnlyCallableBy(mockBalanced.governance.getAddress(), daofundScore, "stakeLpTokens", pid, lpBalance); daofundScore.invoke(mockBalanced.governance.account, "stakeLpTokens", pid, lpBalance); - verify(mockBalanced.dex.mock).add(baseToken, quoteToken, baseAmount, quoteAmount, true); + verify(mockBalanced.dex.mock).add(baseToken, quoteToken, baseAmount, quoteAmount, true, BigInteger.valueOf(1000)); verify(mockBalanced.dex.mock).transfer(mockBalanced.stakedLp.getAddress(), lpBalance, pid, new byte[0]); } @@ -219,34 +219,7 @@ void supplyLiquidity_ToLargePriceChange() { Address quoteToken = mockBalanced.bnUSD.getAddress(); BigInteger quoteAmount = EXA.multiply(BigInteger.TWO); - BigInteger pid = BigInteger.TWO; - BigInteger lpBalance = BigInteger.TEN; - when(mockBalanced.dex.mock.getPoolId(baseToken, quoteToken)).thenReturn(pid); - when(mockBalanced.dex.mock.balanceOf(daofundScore.getAddress(), pid)).thenReturn(lpBalance); - - BigInteger price = quoteAmount.multiply(EXA).divide(baseAmount); - BigInteger priceChangeThreshold = (BigInteger) daofundScore.call("getPOLSupplySlippage"); - BigInteger maxDiff = price.multiply(priceChangeThreshold).divide(POINTS); - // Act & Assert - String expectedErrorMessage = "Price on dex was above allowed threshold"; - when(mockBalanced.dex.mock.getPrice(pid)).thenReturn(price.add(maxDiff)); - Executable aboveThreshold = () -> daofundScore.invoke(mockBalanced.governance.account, "supplyLiquidity", - baseToken, baseAmount, quoteToken, quoteAmount); - expectErrorMessage(aboveThreshold, expectedErrorMessage); - - when(mockBalanced.dex.mock.getPrice(pid)).thenReturn(price.add(maxDiff).subtract(BigInteger.ONE)); - daofundScore.invoke(mockBalanced.governance.account, "supplyLiquidity", baseToken, baseAmount, quoteToken, - quoteAmount); - - // Act & Assert - expectedErrorMessage = "Price on dex was below allowed threshold"; - when(mockBalanced.dex.mock.getPrice(pid)).thenReturn(price.subtract(maxDiff)); - Executable belowThreshold = () -> daofundScore.invoke(mockBalanced.governance.account, "supplyLiquidity", - baseToken, baseAmount, quoteToken, quoteAmount); - expectErrorMessage(belowThreshold, expectedErrorMessage); - - when(mockBalanced.dex.mock.getPrice(pid)).thenReturn(price.subtract(maxDiff).add(BigInteger.ONE)); daofundScore.invoke(mockBalanced.governance.account, "supplyLiquidity", baseToken, baseAmount, quoteToken, quoteAmount); } 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 index 639f243f9..9238ee571 100644 --- 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 @@ -261,23 +261,23 @@ void testLpTokensAndTransfer() { mintAndTransferTestTokens(tokenDeposit); transferSicxToken(); dexUserScoreClient.add(tokenBAddress, Address.fromString(sIcxScoreClient._address().toString()), - BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(25).multiply(EXA), false); + 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(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(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(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(25).multiply(EXA), false, BigInteger.valueOf(100)); waitForADay(); 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 index 9bb3fe92e..07e861029 100644 --- 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 @@ -108,7 +108,7 @@ void testBalnPoolTokenTransferableOnContinuousRewards() { 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(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 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 index f3a861b99..a2f095d38 100644 --- 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 @@ -20,13 +20,18 @@ 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; @@ -36,18 +41,10 @@ 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.assertNull; +import static org.junit.jupiter.api.Assertions.*; +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class MultipleAddTest { - - static StakingScoreClient staking; - static LoansScoreClient loans; - static RewardsScoreClient rewards; - static SicxScoreClient sicx; - static StakedLPScoreClient stakedLp; - static BalancedTokenScoreClient baln; - private static final Env.Chain chain = Env.getDefaultChain(); private static Wallet userWallet; private static Wallet testOwnerWallet; @@ -67,7 +64,6 @@ public class MultipleAddTest { balanced = new Balanced(); testOwnerWallet = balanced.owner; userWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(800).multiply(EXA)); - Wallet tUserWallet = ScoreIntegrationTest.createWalletWithBalance(BigInteger.valueOf(500).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, @@ -117,7 +113,7 @@ void testMultipleAdd() { //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(50).multiply(EXA), true, BigInteger.valueOf(100)); BigInteger poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestThirdScoreAddress), Address.fromString(dexTestFourthScoreAddress)); Map poolStats = dexUserScoreClient.getPoolStats(poolId); @@ -141,12 +137,12 @@ void testMultipleAdd() { this.mintAndTransferTestTokens(tokenDeposit); dexUserScoreClient.add(Address.fromString(dexTestThirdScoreAddress), - Address.fromString(dexTestFourthScoreAddress), BigInteger.valueOf(80).multiply(EXA), - BigInteger.valueOf(60).multiply(EXA), true); + 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(290).multiply(EXA), ownerDexTestFourthScoreClient.balanceOf(userAddress)); - assertEquals(BigInteger.valueOf(290).multiply(EXA), ownerDexTestThirdScoreClient.balanceOf(userAddress)); + 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)); @@ -154,9 +150,9 @@ void testMultipleAdd() { 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(110).multiply(EXA)); - assertEquals(hexToBigInteger(poolStats.get("quote").toString()), BigInteger.valueOf(110).multiply(EXA)); - assertEquals(hexToBigInteger(poolStats.get("total_supply").toString()), BigInteger.valueOf(110).multiply(EXA)); + 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)); @@ -168,6 +164,26 @@ void testMultipleAdd() { 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)); 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 index e367560e9..6167fc600 100644 --- 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 @@ -126,7 +126,7 @@ void testNonStakedLpRewards() { //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).multiply(EXA), BigInteger.valueOf(100).multiply(EXA), false, BigInteger.valueOf(100)); waitForADay(); balanced.syncDistributions(); 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 index 93411db3b..36c2d5f3e 100644 --- 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 @@ -117,7 +117,7 @@ void testSwapTokensVerifySendsFeeAndRemove() { this.mintAndTransferTestTokens(tokenDeposit); dexUserScoreClient.add(Address.fromString(dexTestBaseScoreAddress), Address.fromString(dexTestScoreAddress), - BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(50).multiply(EXA), true); + BigInteger.valueOf(50).multiply(EXA), BigInteger.valueOf(50).multiply(EXA), true, BigInteger.valueOf(100)); BigInteger poolId = dexUserScoreClient.getPoolId(Address.fromString(dexTestBaseScoreAddress), Address.fromString(dexTestScoreAddress)); 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 index f607cc3fb..44a27835a 100644 --- 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 @@ -20,9 +20,8 @@ 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.FloorLimited; import network.balanced.score.lib.utils.BalancedFloorLimits; - +import network.balanced.score.lib.utils.FloorLimited; import score.Address; import score.BranchDB; import score.Context; @@ -37,11 +36,11 @@ 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.Check.checkStatus; import static network.balanced.score.lib.utils.Constants.*; import static network.balanced.score.lib.utils.Math.pow; @@ -176,6 +175,46 @@ public void setMarketName(BigInteger _id, String _name) { 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()); @@ -262,6 +301,11 @@ 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); @@ -469,6 +513,10 @@ public void delegate(PrepDelegations[] prepDelegations) { 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"); } @@ -567,6 +615,9 @@ void exchange(Address fromToken, Address toToken, Address sender, 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); @@ -576,7 +627,6 @@ void exchange(Address fromToken, Address toToken, Address sender, // Broadcast pool ending price BigInteger effectiveFillPrice = (value.multiply(EXA)).divide(sendAmount); - BigInteger endingPrice = totalQuote.multiply(EXA).divide(totalBase); if (!isSell) { effectiveFillPrice = (sendAmount.multiply(EXA)).divide(value); 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 index df7b9041d..179670678 100644 --- 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 @@ -60,6 +60,7 @@ public class DexDBVariables { 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); @@ -144,4 +145,7 @@ public class DexDBVariables { 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 index d4d6007ef..2726cc6e3 100644 --- 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 @@ -21,8 +21,8 @@ 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.Versions; import network.balanced.score.lib.utils.BalancedFloorLimits; +import network.balanced.score.lib.utils.Versions; import score.Address; import score.BranchDB; import score.Context; @@ -35,14 +35,16 @@ import java.math.BigInteger; import java.util.List; -import static network.balanced.score.core.dex.utils.Check.isDexOn; 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 network.balanced.score.lib.utils.Check.checkStatus; import static score.Context.require; public class DexImpl extends AbstractDex { @@ -117,6 +119,8 @@ public void cancelSicxicxOrder() { activeAddresses.get(SICXICX_POOL_ID).remove(user); sendRewardsData(user, BigInteger.ZERO, currentIcxTotal); + + BalancedFloorLimits.verifyNativeWithdraw(withdrawAmount); Context.transfer(user, withdrawAmount); } @@ -173,14 +177,12 @@ public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { 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": { @@ -208,16 +210,13 @@ public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { // 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: @@ -323,9 +322,10 @@ public void remove(BigInteger _id, BigInteger _value, @Optional boolean _withdra @External public void add(Address _baseToken, Address _quoteToken, BigInteger _baseValue, BigInteger _quoteValue, - @Optional boolean _withdraw_unused) { + @Optional boolean _withdraw_unused, @Optional BigInteger _slippagePercentage) { isDexOn(); checkStatus(); + isValidPercent(_slippagePercentage.intValue()); Address user = Context.getCaller(); @@ -401,6 +401,13 @@ public void add(Address _baseToken, Address _quoteToken, BigInteger _baseValue, 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); @@ -420,7 +427,6 @@ public void add(Address _baseToken, Address _quoteToken, BigInteger _baseValue, } // Apply the funds to the pool - poolBaseAmount = poolBaseAmount.add(baseToCommit); poolQuoteAmount = poolQuoteAmount.add(quoteToCommit); 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 index 4af1779bd..8b5f6758a 100644 --- 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 @@ -23,6 +23,7 @@ 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 { @@ -38,4 +39,9 @@ public static void isValidPoolId(BigInteger id) { 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 index b0d53d4ff..871efd547 100644 --- 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 @@ -31,12 +31,7 @@ public class Const { 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 int FIRST_NON_BALANCED_POOL = 6; public static final Integer ICX_QUEUE_FILL_DEPTH = 50; - - public static final int USDS_BNUSD_ID = 10; - public static final int IUSDT_BNUSD_ID = 15; - public static final BigInteger WITHDRAW_LOCK_TIMEOUT = MICRO_SECONDS_IN_A_DAY; public static final Address MINT_ADDRESS = EOA_ZERO; public static final String TAG = Names.DEX; 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 index bec23c510..f3e2e9917 100644 --- 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 @@ -56,6 +56,7 @@ class DexTestBase extends UnitTest { protected Account sicxScore; protected Account feehandlerScore; protected Account stakedLPScore; + protected Account balancedOracle; public static Score dexScore; public static DexImpl dexScoreSpy; @@ -73,6 +74,7 @@ public void setup() throws Exception { 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); @@ -115,7 +117,7 @@ protected void supplyLiquidity(Account supplier, Account baseTokenScore, Account dexScore.invoke(quoteTokenScore, "tokenFallback", supplier.getAddress(), quoteValue, tokenData("_deposit", new HashMap<>())); dexScore.invoke(supplier, "add", baseTokenScore.getAddress(), quoteTokenScore.getAddress(), baseValue, - quoteValue, withdrawUnused); + quoteValue, withdrawUnused, BigInteger.valueOf(100)); } protected BigInteger computePrice(BigInteger tokenAValue, BigInteger tokenBValue) { 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 index c5584f3a4..27fd9f836 100644 --- 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 @@ -32,8 +32,7 @@ import java.util.HashMap; import java.util.Map; -import static network.balanced.score.core.dex.utils.Const.FEE_SCALE; -import static network.balanced.score.core.dex.utils.Const.SICXICX_POOL_ID; +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; @@ -285,8 +284,8 @@ void addLiquidity() { 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); - dexScore.invoke(account1, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false); + 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); @@ -298,6 +297,42 @@ void addLiquidity() { 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. @@ -344,7 +379,7 @@ void tokenFallbackSwapFromTokenIs_poolQuote() { data.getBytes()); // add liquidity pool dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, - FIFTY.divide(BigInteger.TWO), false); + 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); @@ -407,7 +442,7 @@ void tokenFallbackSwapFromTokenIs_poolBase() { data.getBytes()); // add liquidity pool dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, - FIFTY.divide(BigInteger.TWO), false); + 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); @@ -477,7 +512,7 @@ void tokenFallback_donate() { data.getBytes()); // add liquidity pool dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), FIFTY, - FIFTY.divide(BigInteger.TWO), false); + 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"); @@ -582,7 +617,7 @@ void transfer() { 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); + 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); @@ -616,7 +651,7 @@ void transfer_toSelf() { 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); + 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); @@ -663,7 +698,7 @@ void getTotalValue() { 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); + 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"; @@ -695,7 +730,7 @@ void getPoolStatsWithPair() { dexScore.invoke(balnScore, "tokenFallback", account.getAddress(), bnusdValue, data.getBytes()); // add liquidity pool - dexScore.invoke(account, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false); + 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()); @@ -743,8 +778,8 @@ void govWithdraw() { 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); - dexScore.invoke(account1, "add", balnScore.getAddress(), bnusdScore.getAddress(), balnValue, bnusdValue, false); + 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); @@ -761,13 +796,215 @@ void govWithdraw() { poolStats = (Map) dexScore.call("getPoolStats", poolId); contextMock.verify(() -> Context.call(eq(bnusdScore.getAddress()), eq("transfer"), eq(mockBalanced.daofund.getAddress()), - eq(bnUSDWithdrawAmount))); + eq(bnUSDWithdrawAmount))); contextMock.verify(() -> Context.call(eq(balnScore.getAddress()), eq("transfer"), eq(mockBalanced.daofund.getAddress()), - eq(balnWithdrawAmount))); + 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(); 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 index 432afb16f..4e4aede91 100644 --- 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 @@ -28,6 +28,7 @@ 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; @@ -169,6 +170,29 @@ void setGetTimeOffSet() { 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. @@ -622,6 +646,11 @@ 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(); diff --git a/core-contracts/Dividends/src/intTest/java/network/balanced/score/core/dividends/DividendsIntegrationTest.java b/core-contracts/Dividends/src/intTest/java/network/balanced/score/core/dividends/DividendsIntegrationTest.java index 62243b4ce..528995c61 100644 --- a/core-contracts/Dividends/src/intTest/java/network/balanced/score/core/dividends/DividendsIntegrationTest.java +++ b/core-contracts/Dividends/src/intTest/java/network/balanced/score/core/dividends/DividendsIntegrationTest.java @@ -104,7 +104,7 @@ void setupBalnEarnings() { // provides liquidity to baln/Sicx pool by owner owner.baln.transfer(balanced.dex._address(), lpAmount, data.toString().getBytes()); owner.sicx.transfer(balanced.dex._address(), lpAmount, data.toString().getBytes()); - owner.dex.add(balanced.baln._address(), balanced.sicx._address(), lpAmount, lpAmount, true); + owner.dex.add(balanced.baln._address(), balanced.sicx._address(), lpAmount, lpAmount, true, BigInteger.valueOf(100)); owner.baln.transfer(Dave.getAddress(), BigInteger.valueOf(50).multiply(BigInteger.TEN.pow(18)), null); } diff --git a/core-contracts/Governance/src/intTest/java/network/balanced/score/core/governance/GovernanceIntegrationTest.java b/core-contracts/Governance/src/intTest/java/network/balanced/score/core/governance/GovernanceIntegrationTest.java index ab6d0f8ee..a9c717a7c 100644 --- a/core-contracts/Governance/src/intTest/java/network/balanced/score/core/governance/GovernanceIntegrationTest.java +++ b/core-contracts/Governance/src/intTest/java/network/balanced/score/core/governance/GovernanceIntegrationTest.java @@ -20,6 +20,7 @@ import com.eclipsesource.json.JsonObject; import foundation.icon.icx.KeyWallet; import foundation.icon.score.client.DefaultScoreClient; +import network.balanced.score.lib.utils.Names; import network.balanced.score.lib.test.integration.Balanced; import network.balanced.score.lib.test.integration.BalancedClient; import network.balanced.score.lib.test.integration.ScoreIntegrationTest; @@ -28,6 +29,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import score.Address; +import score.UserRevertedException; import java.io.IOException; import java.math.BigInteger; @@ -232,6 +234,36 @@ void updateContractViaStoreAndSetNewValue() throws IOException { assertEquals(deploymentValueParameter, getValue(contractAddress)); } + @Test + @Order(14) + void deployContractWithSameName() throws IOException { + // Arrange + String deploymentValueParameter = "first deployment"; + byte[] contractData = getContractBytesFromResources(this.getClass(), deploymentTesterJar1); + JsonArray params = new JsonArray() + .add(createParameter(deploymentValueParameter)); + + // Act & Assert + Executable contractAlreadyExists = () -> owner.governance.deploy(contractData, params.toString()); + assertThrows(UserRevertedException.class, contractAlreadyExists); + Executable addContractAlreadyExists = () -> owner.governance.addExternalContract(Names.LOANS, owner.staking._address()); + assertThrows(UserRevertedException.class, contractAlreadyExists); + } + + @Test + @Order(15) + void deployToWrongContract() throws IOException { + // Arrange + String deploymentValueParameter = "first deployment"; + byte[] contractData = getContractBytesFromResources(this.getClass(), deploymentTesterJar1); + JsonArray params = new JsonArray() + .add(createParameter(deploymentValueParameter)); + + // Act & Assert + Executable wrongContractTarget = () -> owner.governance.deployTo(owner.loans._address(), contractData, params.toString()); + assertThrows(UserRevertedException.class, wrongContractTarget); + } + @Test @Order(99) void updateContractFromVote() throws IOException { @@ -292,7 +324,6 @@ void updateSmallContractFromVote() throws IOException { byte[] contractData = getContractBytesFromResources(this.getClass(), deploymentTesterJar1); JsonArray params = new JsonArray() .add(createParameter(deploymentValueParameter)); - owner.governance.deploy(contractData, params.toString()); Address contractAddress = owner.governance.getAddress(deploymentTesterName); @@ -426,8 +457,8 @@ void emergency_disable_enable() throws Throwable { balanced.increaseDay(1); user.rewards.claimRewards(new String[]{"Loans"}); - owner.governance.addAuthorizedCallerShutdown(trustedUser1.getAddress()); - owner.governance.addAuthorizedCallerShutdown(trustedUser2.getAddress()); + owner.governance.addAuthorizedCallerShutdown(trustedUser1.getAddress(), false); + owner.governance.addAuthorizedCallerShutdown(trustedUser2.getAddress(), false); // Act & Assert trustedUser1.governance.disable(); diff --git a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/GovernanceImpl.java b/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/GovernanceImpl.java index aa6b27b8d..0389f53a8 100644 --- a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/GovernanceImpl.java +++ b/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/GovernanceImpl.java @@ -18,7 +18,6 @@ import network.balanced.score.core.governance.proposal.ProposalDB; import network.balanced.score.core.governance.proposal.ProposalManager; -import network.balanced.score.core.governance.utils.ArbitraryCallManager; import network.balanced.score.core.governance.utils.ContractManager; import network.balanced.score.core.governance.utils.EmergencyManager; import network.balanced.score.core.governance.utils.SetupManager; @@ -26,6 +25,7 @@ import network.balanced.score.lib.structs.PrepDelegations; import network.balanced.score.lib.utils.Names; import network.balanced.score.lib.utils.Versions; +import network.balanced.score.lib.utils.ArbitraryCallManager; import score.Address; import score.Context; import score.VarDB; @@ -61,7 +61,7 @@ public GovernanceImpl() { setVoteDurationLimits(BigInteger.ONE, BigInteger.valueOf(14)); return; } -// ContractManager.migrateAddresses(); + if (currentVersion.getOrDefault("").equals(Versions.GOVERNANCE)) { Context.revert("Can't Update same version of code"); } @@ -95,6 +95,7 @@ public void changeScoreOwner(Address score, Address newOwner) { Context.call(SYSTEM_SCORE_ADDRESS, "setScoreOwner", score, newOwner); } + @External public void setVoteDurationLimits(BigInteger min, BigInteger max) { onlyOwnerOrContract(); Context.require(min.compareTo(BigInteger.ONE) >= 0, "Minimum vote duration has to be above 1"); @@ -347,6 +348,7 @@ public void execute(String transactions) { @External public void enable() { EmergencyManager.authorizeEnableAndDisable(); + Context.require(!EmergencyManager.canOnlyDisable(Context.getCaller()), "This address does not have permission to enable balanced"); EmergencyManager.enable(); } @@ -389,9 +391,9 @@ public void checkStatus(String address) { } @External - public void addAuthorizedCallerShutdown(Address address) { + public void addAuthorizedCallerShutdown(Address address, @Optional boolean disableOnly) { onlyOwnerOrContract(); - EmergencyManager.addAuthorizedCallerShutdown(address); + EmergencyManager.addAuthorizedCallerShutdown(address, disableOnly); } @External diff --git a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/proposal/ProposalManager.java b/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/proposal/ProposalManager.java index 22ade2a6a..53ea398b6 100644 --- a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/proposal/ProposalManager.java +++ b/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/proposal/ProposalManager.java @@ -16,8 +16,8 @@ package network.balanced.score.core.governance.proposal; -import network.balanced.score.core.governance.utils.ArbitraryCallManager; import network.balanced.score.core.governance.utils.ContractManager; +import network.balanced.score.lib.utils.ArbitraryCallManager; import network.balanced.score.lib.utils.Names; import score.Address; import score.Context; diff --git a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/ContractManager.java b/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/ContractManager.java index f5b46c578..8c5a33cce 100644 --- a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/ContractManager.java +++ b/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/ContractManager.java @@ -18,6 +18,7 @@ import network.balanced.score.core.governance.GovernanceImpl; import network.balanced.score.lib.utils.Names; +import network.balanced.score.lib.utils.ArbitraryCallManager; import score.*; import scorex.util.HashMap; @@ -74,98 +75,6 @@ public static Address get(String key) { return getAddress(oldNamesMap.get(key)); } - public static void migrateAddresses() { - Address loansAddress = loans.get(); - String loansName = getName(loansAddress); - balancedContractNames.add(loansName); - contractAddresses.set(loansName, loansAddress); - - Address dexAddress = dex.get(); - String dexName = getName(dexAddress); - balancedContractNames.add(dexName); - contractAddresses.set(dexName, dexAddress); - - Address stakingAddress = staking.get(); - String stakingName = getName(stakingAddress); - balancedContractNames.add(stakingName); - contractAddresses.set(stakingName, stakingAddress); - - Address rewardsAddress = rewards.get(); - String rewardsName = getName(rewardsAddress); - balancedContractNames.add(rewardsName); - contractAddresses.set(rewardsName, rewardsAddress); - - Address reserveAddress = reserve.get(); - String reserveName = getName(reserveAddress); - balancedContractNames.add(reserveName); - contractAddresses.set(reserveName, reserveAddress); - - Address dividendsAddress = dividends.get(); - String dividendsName = getName(dividendsAddress); - balancedContractNames.add(dividendsName); - contractAddresses.set(dividendsName, dividendsAddress); - - Address daofundAddress = daofund.get(); - String daofundName = getName(daofundAddress); - balancedContractNames.add(daofundName); - contractAddresses.set(daofundName, daofundAddress); - - Address oracleAddress = oracle.get(); - String oracleName = Names.ORACLE; - balancedContractNames.add(oracleName); - contractAddresses.set(oracleName, oracleAddress); - - Address sicxAddress = sicx.get(); - String sicxName = getName(sicxAddress); - balancedContractNames.add(sicxName); - contractAddresses.set(sicxName, sicxAddress); - - Address bnUSDAddress = bnUSD.get(); - String bnUSDName = getName(bnUSDAddress); - balancedContractNames.add(bnUSDName); - contractAddresses.set(bnUSDName, bnUSDAddress); - - Address balnAddress = baln.get(); - String balnName = getName(balnAddress); - balancedContractNames.add(balnName); - contractAddresses.set(balnName, balnAddress); - - Address bwtAddress = bwt.get(); - String bwtName = getName(bwtAddress); - balancedContractNames.add(bwtName); - contractAddresses.set(bwtName, bwtAddress); - - Address rebalancingAddress = rebalancing.get(); - String rebalancingName = getName(rebalancingAddress); - balancedContractNames.add(rebalancingName); - contractAddresses.set(rebalancingName, rebalancingAddress); - - Address routerAddress = router.get(); - String routerName = getName(routerAddress); - balancedContractNames.add(routerName); - contractAddresses.set(routerName, routerAddress); - - Address feehandlerAddress = feehandler.get(); - String feehandlerName = getName(feehandlerAddress); - balancedContractNames.add(feehandlerName); - contractAddresses.set(feehandlerName, feehandlerAddress); - - Address stakedLpAddress = stakedLp.get(); - String stakedLpName = getName(stakedLpAddress); - balancedContractNames.add(stakedLpName); - contractAddresses.set(stakedLpName, stakedLpAddress); - - Address balancedOracleAddress = balancedOracle.get(); - String balancedOracleName = getName(balancedOracleAddress); - balancedContractNames.add(balancedOracleName); - contractAddresses.set(balancedOracleName, balancedOracleAddress); - - Address bBalnAddress = bBaln.get(); - String bBalnName = getName(bBalnAddress); - balancedContractNames.add(bBalnName); - contractAddresses.set(bBalnName, bBalnAddress); - } - public static Map getAddresses() { Map addressData = new HashMap<>(); int numberOfContracts = balancedContractNames.size(); @@ -224,13 +133,16 @@ public static void setAdmins() { } public static void addContract(String name, Address address) { + Context.require(contractAddresses.get(name) == null, "Contract already exists"); balancedContractNames.add(name); contractAddresses.set(name, address); } public static void updateContract(Address targetContract, byte[] contractData, String params) { Object[] parsedParams = ArbitraryCallManager.getConvertedParameters(params); + String name = getName(targetContract); Context.deploy(targetContract, contractData, parsedParams); + Context.require(name.equals(getName(targetContract)), "Invalid contract upgrade"); } public static String deployStoredContract(String name, byte[] contractData) { @@ -257,6 +169,7 @@ public static void newContract(byte[] contractData, String params) { Object[] parsedParams = ArbitraryCallManager.getConvertedParameters(params); Address contractAddress = Context.deploy(contractData, parsedParams); String name = getName(contractAddress); + Context.require(contractAddresses.get(name) == null, "Contract already exists"); balancedContractNames.add(name); contractAddresses.set(name, contractAddress); } diff --git a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/EmergencyManager.java b/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/EmergencyManager.java index 9c939b019..832ccf444 100644 --- a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/EmergencyManager.java +++ b/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/EmergencyManager.java @@ -20,6 +20,7 @@ import score.Address; import score.Context; import score.VarDB; +import score.DictDB; import scorex.util.HashMap; import java.math.BigInteger; @@ -30,6 +31,7 @@ public class EmergencyManager { private static final IterableDictDB authorizedCallersShutdown = new IterableDictDB<>( "authorized_shutdown_callers", BigInteger.class, Address.class, false); + private static final DictDB disableOnly = Context.newDictDB("disable_only", Boolean.class); private static final IterableDictDB blacklist = new IterableDictDB<>("balanced_black_list", Boolean.class, String.class, false); private static final VarDB enableDisableTimeLock = Context.newVarDB("enable_disable_time_lock", @@ -37,12 +39,18 @@ public class EmergencyManager { private static final VarDB enabled = Context.newVarDB("balanced_status", Boolean.class); - public static void addAuthorizedCallerShutdown(Address address) { + public static void addAuthorizedCallerShutdown(Address address, boolean onlyDisable) { authorizedCallersShutdown.set(address, BigInteger.ZERO); + disableOnly.set(address, onlyDisable); } public static void removeAuthorizedCallerShutdown(Address address) { authorizedCallersShutdown.remove(address); + disableOnly.set(address, null); + } + + public static boolean canOnlyDisable(Address address) { + return disableOnly.getOrDefault(address, false); } public static Map getShutdownCallers() { diff --git a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/SetupManager.java b/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/SetupManager.java index 08a61c36a..b92e3d3a3 100644 --- a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/SetupManager.java +++ b/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/SetupManager.java @@ -112,7 +112,7 @@ public static void createBnusdMarket() { call(bnUSDAddress, "transfer", dexAddress, bnUSDValue, depositData.toString().getBytes()); call(sICXAddress, "transfer", dexAddress, sICXValue, depositData.toString().getBytes()); - call(dexAddress, "add", sICXAddress, bnUSDAddress, sICXValue, bnUSDValue, false); + call(dexAddress, "add", sICXAddress, bnUSDAddress, sICXValue, bnUSDValue, false, BigInteger.ZERO); String name = "sICX/bnUSD"; BigInteger pid = call(BigInteger.class, dexAddress, "getPoolId", sICXAddress, bnUSDAddress); call(dexAddress, "setMarketName", pid, name); @@ -140,7 +140,7 @@ public static void createBalnMarket(BigInteger _bnUSD_amount, BigInteger _baln_a call(bnUSDAddress, "transfer", dexAddress, _bnUSD_amount, depositData.toString().getBytes()); call(balnAddress, "transfer", dexAddress, _baln_amount, depositData.toString().getBytes()); - call(dexAddress, "add", balnAddress, bnUSDAddress, _baln_amount, _bnUSD_amount, false); + call(dexAddress, "add", balnAddress, bnUSDAddress, _baln_amount, _bnUSD_amount, false, BigInteger.ZERO); String name = "BALN/bnUSD"; BigInteger pid = call(BigInteger.class, dexAddress, "getPoolId", balnAddress, bnUSDAddress); call(dexAddress, "setMarketName", pid, name); @@ -163,7 +163,7 @@ public static void createBalnSicxMarket(BigInteger _sicx_amount, BigInteger _bal call(sICXAddress, "transfer", dexAddress, _sicx_amount, depositData.toString().getBytes()); call(balnAddress, "transfer", dexAddress, _baln_amount, depositData.toString().getBytes()); - call(dexAddress, "add", balnAddress, sICXAddress, _baln_amount, _sicx_amount, false); + call(dexAddress, "add", balnAddress, sICXAddress, _baln_amount, _sicx_amount, false, BigInteger.ZERO); String name = "BALN/sICX"; BigInteger pid = call(BigInteger.class, dexAddress, "getPoolId", balnAddress, sICXAddress); call(dexAddress, "setMarketName", pid, name); diff --git a/core-contracts/Governance/src/test/java/network/balanced/score/core/governance/GovernanceTest.java b/core-contracts/Governance/src/test/java/network/balanced/score/core/governance/GovernanceTest.java index 610db72bf..1457fd852 100644 --- a/core-contracts/Governance/src/test/java/network/balanced/score/core/governance/GovernanceTest.java +++ b/core-contracts/Governance/src/test/java/network/balanced/score/core/governance/GovernanceTest.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; +import org.mockito.Mockito; import score.Address; import score.Context; @@ -231,6 +232,7 @@ void createBnusdMarket() { when(sicx.mock.balanceOf(governance.getAddress())).thenReturn(sICXValue); when(dex.mock.getPoolId(sicx.getAddress(), bnUSD.getAddress())).thenReturn(sicxBnusdPid); + // Act sm.call(owner, initialICX, governance.getAddress(), "createBnusdMarket"); @@ -245,7 +247,7 @@ void createBnusdMarket() { verify(bnUSD.mock).transfer(dex.getAddress(), bnUSDValue, depositData.toString().getBytes()); verify(sicx.mock).transfer(dex.getAddress(), sICXValue, depositData.toString().getBytes()); - verify(dex.mock).add(sicx.getAddress(), bnUSD.getAddress(), sICXValue, bnUSDValue, false); + verify(dex.mock).add(sicx.getAddress(), bnUSD.getAddress(), sICXValue, bnUSDValue, false, BigInteger.ZERO); verify(dex.mock).setMarketName(sicxBnusdPid, "sICX/bnUSD"); verify(stakedLp.mock).addDataSource(sicxBnusdPid, "sICX/bnUSD"); @@ -275,7 +277,7 @@ void createBalnMarket() { verify(bnUSD.mock, times(2)).transfer(dex.getAddress(), bnUSDValue, depositData.toString().getBytes()); verify(baln.mock).transfer(dex.getAddress(), balnValue, depositData.toString().getBytes()); - verify(dex.mock).add(baln.getAddress(), bnUSD.getAddress(), balnValue, bnUSDValue, false); + verify(dex.mock).add(baln.getAddress(), bnUSD.getAddress(), balnValue, bnUSDValue, false, BigInteger.ZERO); verify(dex.mock).setMarketName(balnBnusdPid, "BALN/bnUSD"); verify(stakedLp.mock).addDataSource(balnBnusdPid, "BALN/bnUSD"); @@ -307,7 +309,7 @@ void createBalnSicxMarket() { verify(sicx.mock, times(2)).transfer(dex.getAddress(), sicxValue, depositData.toString().getBytes()); verify(baln.mock, times(2)).transfer(dex.getAddress(), balnValue, depositData.toString().getBytes()); - verify(dex.mock).add(baln.getAddress(), sicx.getAddress(), balnValue, sicxValue, false); + verify(dex.mock).add(baln.getAddress(), sicx.getAddress(), balnValue, sicxValue, false, BigInteger.ZERO); verify(dex.mock).setMarketName(balnSicxPid, "BALN/sICX"); verify(stakedLp.mock).addDataSource(balnSicxPid, "BALN/sICX"); @@ -333,20 +335,26 @@ void disable_enable_permission() { BigInteger timeLockDays = BigInteger.TEN; String expectedErrorMessageAuth = "Not authorized"; String expectedErrorMessageTime = "Your privileges are disabled until "; + String expectedErrorMessageDisableOnly = "This address does not have permission to enable balanced"; governance.invoke(owner, "setShutdownPrivilegeTimeLock", timeLockDays); // Act Executable beforeAuth = () -> governance.invoke(trustedUser1, "disable"); expectErrorMessage(beforeAuth, expectedErrorMessageAuth); - governance.invoke(owner, "addAuthorizedCallerShutdown", trustedUser1.getAddress()); - governance.invoke(owner, "addAuthorizedCallerShutdown", trustedUser2.getAddress()); + governance.invoke(owner, "addAuthorizedCallerShutdown", trustedUser1.getAddress(), true); + governance.invoke(owner, "addAuthorizedCallerShutdown", trustedUser2.getAddress(), false); governance.invoke(trustedUser1, "disable"); // Assert Executable onTimeLock = () -> governance.invoke(trustedUser1, "enable"); expectErrorMessage(onTimeLock, expectedErrorMessageTime); + sm.getBlock().increase(DAY * timeLockDays.longValue()); + + Executable disableOnly = () -> governance.invoke(trustedUser1, "enable"); + expectErrorMessage(onTimeLock, expectedErrorMessageDisableOnly); + governance.invoke(trustedUser2, "enable"); sm.getBlock().increase(DAY * timeLockDays.longValue()); governance.invoke(trustedUser1, "disable"); @@ -387,12 +395,12 @@ void addRemoveTrustedUsersPermissions() { // Arrange Account trustedUser1 = sm.createAccount(); Account trustedUser2 = sm.createAccount(); - assertOnlyCallableByContractOrOwner("addAuthorizedCallerShutdown", trustedUser1.getAddress()); + assertOnlyCallableByContractOrOwner("addAuthorizedCallerShutdown", trustedUser1.getAddress(), false); assertOnlyCallableByContractOrOwner("removeAuthorizedCallerShutdown", trustedUser2.getAddress()); // Act - governance.invoke(owner, "addAuthorizedCallerShutdown", trustedUser1.getAddress()); - governance.invoke(owner, "addAuthorizedCallerShutdown", trustedUser2.getAddress()); + governance.invoke(owner, "addAuthorizedCallerShutdown", trustedUser1.getAddress(), false); + governance.invoke(owner, "addAuthorizedCallerShutdown", trustedUser2.getAddress(), false); // Assert Map authorizedCallers = (Map) governance.call( diff --git a/core-contracts/Loans/src/intTest/java/network/balanced/score/core/loans/LoansIntegrationTest.java b/core-contracts/Loans/src/intTest/java/network/balanced/score/core/loans/LoansIntegrationTest.java index 0808df50b..353a25f09 100644 --- a/core-contracts/Loans/src/intTest/java/network/balanced/score/core/loans/LoansIntegrationTest.java +++ b/core-contracts/Loans/src/intTest/java/network/balanced/score/core/loans/LoansIntegrationTest.java @@ -466,26 +466,19 @@ void rateLimits() throws Exception { loanTaker.stakeDepositAndBorrow(collateral, BigInteger.ZERO); JsonArray setPercentageParameters = new JsonArray() + .add(createParameter(balanced.sicx._address())) .add(createParameter(BigInteger.valueOf(500)));//5% JsonArray actions = new JsonArray() .add(createTransaction(balanced.loans._address(), "setFloorPercentage", setPercentageParameters)); owner.governance.execute(actions.toString()); JsonArray setTimeDelay = new JsonArray() - .add(createParameter(MICRO_SECONDS_IN_A_DAY)); // 1 day delay + .add(createParameter(balanced.sicx._address())) + .add(createParameter(MICRO_SECONDS_IN_A_DAY)); // 1 day delay actions = new JsonArray() .add(createTransaction(balanced.loans._address(), "setTimeDelayMicroSeconds", setTimeDelay)); owner.governance.execute(actions.toString()); - JsonObject param = new JsonObject() - .add("type", "Address[]") - .add("value", new JsonArray().add(balanced.sicx._address().toString())); - JsonArray enableFloors = new JsonArray() - .add(param); - actions = new JsonArray() - .add(createTransaction(balanced.loans._address(), "enableFloors", enableFloors)); - owner.governance.execute(actions.toString()); - // Assert assertThrows(UserRevertedException.class, () -> loanTaker.loans.withdrawCollateral(collateral, "sICX")); @@ -503,7 +496,8 @@ void rateLimits() throws Exception { setPercentageParameters = new JsonArray() - .add(createParameter(POINTS)); + .add(createParameter(balanced.sicx._address())) + .add(createParameter(BigInteger.ZERO)); actions = new JsonArray() .add(createTransaction(balanced.loans._address(), "setFloorPercentage", setPercentageParameters)); owner.governance.execute(actions.toString()); @@ -936,7 +930,7 @@ private void addCollateralAndLiquidity(BalancedClient minter, Address collateral owner.irc2(collateralAddress).transfer(balanced.dex._address(), tokenAmount, depositData.toString().getBytes()); owner.bnUSD.transfer(balanced.dex._address(), bnUSDAmount, depositData.toString().getBytes()); - owner.dex.add(collateralAddress, balanced.bnusd._address(), tokenAmount, bnUSDAmount, false); + owner.dex.add(collateralAddress, balanced.bnusd._address(), tokenAmount, bnUSDAmount, false, BigInteger.valueOf(100)); addCollateral(collateralAddress, peg); } @@ -973,7 +967,7 @@ private void addDexCollateralType(BalancedClient minter, Address collateralAddre owner.irc2(collateralAddress).transfer(balanced.dex._address(), tokenAmount, depositData.toString().getBytes()); owner.bnUSD.transfer(balanced.dex._address(), bnUSDAmount, depositData.toString().getBytes()); - owner.dex.add(collateralAddress, balanced.bnusd._address(), tokenAmount, bnUSDAmount, false); + owner.dex.add(collateralAddress, balanced.bnusd._address(), tokenAmount, bnUSDAmount, false, BigInteger.valueOf(100)); BigInteger lockingRatio = BigInteger.valueOf(40_000); BigInteger liquidationRatio = BigInteger.valueOf(15_000); BigInteger debtCeiling = BigInteger.TEN.pow(30); diff --git a/core-contracts/Reserve/src/intTest/java/network/balanced/score/core/reserve/ReserveIntegrationTest.java b/core-contracts/Reserve/src/intTest/java/network/balanced/score/core/reserve/ReserveIntegrationTest.java index a3c914831..258075238 100644 --- a/core-contracts/Reserve/src/intTest/java/network/balanced/score/core/reserve/ReserveIntegrationTest.java +++ b/core-contracts/Reserve/src/intTest/java/network/balanced/score/core/reserve/ReserveIntegrationTest.java @@ -418,7 +418,7 @@ private void addCollateralType(BalancedClient minter, Address collateralAddress, BigInteger bnusdDeposit = owner.bnUSD.balanceOf(owner.getAddress()); owner.bnUSD.transfer(balanced.dex._address(), bnusdDeposit, depositData.toString().getBytes()); - owner.dex.add(collateralAddress, balanced.bnusd._address(), tokenAmount, bnusdDeposit, false); + owner.dex.add(collateralAddress, balanced.bnusd._address(), tokenAmount, bnusdDeposit, false, BigInteger.valueOf(100)); BigInteger lockingRatio = BigInteger.valueOf(40_000); BigInteger liquidationRatio = BigInteger.valueOf(15_000); diff --git a/core-contracts/Rewards/src/intTest/java/network/balanced/score/core/rewards/RewardsIntegrationTest.java b/core-contracts/Rewards/src/intTest/java/network/balanced/score/core/rewards/RewardsIntegrationTest.java index a6f87f035..38a4d7ef1 100644 --- a/core-contracts/Rewards/src/intTest/java/network/balanced/score/core/rewards/RewardsIntegrationTest.java +++ b/core-contracts/Rewards/src/intTest/java/network/balanced/score/core/rewards/RewardsIntegrationTest.java @@ -421,7 +421,7 @@ private void joinsICXBnusdLP(BalancedClient client, BigInteger icxAmount, BigInt BigInteger sicxDeposit = client.sicx.balanceOf(client.getAddress()); client.sicx.transfer(balanced.dex._address(), sicxDeposit, depositData.toString().getBytes()); - client.dex.add(balanced.sicx._address(), balanced.bnusd._address(), sicxDeposit, bnusdAmount, true); + client.dex.add(balanced.sicx._address(), balanced.bnusd._address(), sicxDeposit, bnusdAmount, true, BigInteger.valueOf(10000)); } private void leaveICXBnusdLP(BalancedClient client) { diff --git a/core-contracts/Router/src/main/java/network/balanced/score/core/router/RouterImpl.java b/core-contracts/Router/src/main/java/network/balanced/score/core/router/RouterImpl.java index dfa8fbfea..9a3ee74d9 100644 --- a/core-contracts/Router/src/main/java/network/balanced/score/core/router/RouterImpl.java +++ b/core-contracts/Router/src/main/java/network/balanced/score/core/router/RouterImpl.java @@ -20,11 +20,15 @@ import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; +import foundation.icon.xcall.NetworkAddress; import network.balanced.score.lib.interfaces.Router; +import network.balanced.score.lib.structs.Route; +import network.balanced.score.lib.structs.RouteAction; +import network.balanced.score.lib.structs.RouteData; import network.balanced.score.lib.utils.BalancedAddressManager; -import network.balanced.score.lib.utils.XCallUtils; import network.balanced.score.lib.utils.Names; import network.balanced.score.lib.utils.Versions; +import network.balanced.score.lib.utils.XCallUtils; import score.Address; import score.Context; import score.UserRevertException; @@ -33,19 +37,15 @@ import score.annotation.External; import score.annotation.Optional; import score.annotation.Payable; -import foundation.icon.xcall.NetworkAddress; +import scorex.util.ArrayList; import java.math.BigInteger; +import java.util.List; -import static network.balanced.score.lib.utils.Check.*; +import static network.balanced.score.lib.utils.BalancedAddressManager.*; +import static network.balanced.score.lib.utils.Check.isContract; import static network.balanced.score.lib.utils.Constants.EOA_ZERO; import static network.balanced.score.lib.utils.StringUtils.convertStringToBigInteger; -import static network.balanced.score.lib.utils.BalancedAddressManager.getSicx; -import static network.balanced.score.lib.utils.BalancedAddressManager.getStaking; -import static network.balanced.score.lib.utils.BalancedAddressManager.getDex; -import static network.balanced.score.lib.utils.BalancedAddressManager.getDaofund; -import static network.balanced.score.lib.utils.BalancedAddressManager.getAssetManager; -import static network.balanced.score.lib.utils.BalancedAddressManager.getBnusd; public class RouterImpl implements Router { private static final String GOVERNANCE_ADDRESS = "governance_address"; @@ -59,6 +59,11 @@ public class RouterImpl implements Router { private final VarDB
governance = Context.newVarDB(GOVERNANCE_ADDRESS, Address.class); private final VarDB currentVersion = Context.newVarDB(VERSION, String.class); + // ENUM of actions + static final int SWAP = 1; + static final int STABILITY_SWAP = 2; + public boolean inRoute = false; + public RouterImpl(Address _governance) { if (governance.get() == null) { isContract(_governance); @@ -91,7 +96,20 @@ public Address getAddress(String name) { return BalancedAddressManager.getAddressByName(name); } - private void swap(Address fromToken, Address toToken) { + private void swap(Address fromToken, Address toToken, int action) { + if (action == SWAP) { + swapDefault(fromToken, toToken); + } else if (action == STABILITY_SWAP) { + swapStable(fromToken, toToken); + } + } + + private void swapStable(Address fromToken, Address toToken) { + BigInteger balance = (BigInteger) Context.call(fromToken, "balanceOf", Context.getAddress()); + Context.call(fromToken, "transfer", getStabilityFund(), balance, toToken.toString().getBytes()); + } + + private void swapDefault(Address fromToken, Address toToken) { if (fromToken == null) { Context.require(toToken.equals(getSicx()), TAG + ": ICX can only be traded for sICX"); BigInteger balance = Context.getBalance(Context.getAddress()); @@ -116,7 +134,7 @@ private void swap(Address fromToken, Address toToken) { } } - private void route(String from, Address startToken, Address[] _path, BigInteger _minReceive) { + private void route(String from, Address startToken, List _path, BigInteger _minReceive) { Address prevToken = null; Address currentToken = startToken; BigInteger fromAmount; @@ -129,11 +147,13 @@ private void route(String from, Address startToken, Address[] _path, BigInteger fromAddress = startToken; } - for (Address token : _path) { - swap(currentToken, token); + inRoute = true; + for (RouteAction action : _path) { + swap(currentToken, action.toAddress, action.action); prevToken = currentToken; - currentToken = token; + currentToken = action.toAddress; } + inRoute = false; String nativeNid = XCallUtils.getNativeNid(); NetworkAddress networkAddress = NetworkAddress.valueOf(from, nativeNid); @@ -163,7 +183,6 @@ private void route(String from, Address startToken, Address[] _path, BigInteger transferCrossChainResult(currentToken, networkAddress, balance, toNative); } - Route(fromAddress, fromAmount, currentToken, balance); } @@ -212,18 +231,41 @@ private boolean canWithdraw(String net) { @Payable @External public void route(Address[] _path, @Optional BigInteger _minReceive, @Optional String _receiver) { - if (_minReceive == null) { - _minReceive = BigInteger.ZERO; + Context.require(!inRoute); + validateRoutePayload(_path.length, _minReceive); + + if (_receiver == null || _receiver.equals("")) { + _receiver = Context.getCaller().toString(); + } + List routeActions = new ArrayList<>(); + for (Address path : _path) { + routeActions.add(new RouteAction(1, path)); } + route(_receiver, null, routeActions, _minReceive); + } + + @Payable + @External + public void routeV2(byte[] _path, @Optional BigInteger _minReceive, @Optional String _receiver) { + Context.require(!inRoute); + List actions = Route.fromBytes(_path).actions; + validateRoutePayload(actions.size(), _minReceive); if (_receiver == null || _receiver.equals("")) { _receiver = Context.getCaller().toString(); } + route(_receiver, null, actions, _minReceive); + } + + private void validateRoutePayload(int _pathLength, BigInteger _minReceive) { + if (_minReceive == null) { + _minReceive = BigInteger.ZERO; + } + Context.require(_minReceive.signum() >= 0, TAG + ": Must specify a positive number for minimum to receive"); - Context.require(_path.length <= MAX_NUMBER_OF_ITERATIONS, - TAG + ": Passed max swaps of " + MAX_NUMBER_OF_ITERATIONS); - route(_receiver, null, _path, _minReceive); + Context.require(_pathLength <= MAX_NUMBER_OF_ITERATIONS, + TAG + ": Passed max swaps of " + MAX_NUMBER_OF_ITERATIONS); } /** @@ -242,15 +284,44 @@ public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { xTokenFallback(_from.toString(), _value, _data); } + @External public void xTokenFallback(String _from, BigInteger _value, byte[] _data) { - // Receive token transfers from Balanced DEX and staking while in mid-route - if (_from.equals(getDex().toString()) || _from.equals(MINT_ADDRESS.toString())) { + if (inRoute) { return; } + Context.require(_data.length > 0, "Token Fallback: Data can't be empty"); - String unpackedData = new String(_data); - Context.require(!unpackedData.equals(""), "Token Fallback: Data can't be empty"); + // "{" is 123 as byte + if (_data[0] == 123) { + jsonRoute(_from, _data); + return; + } + executeRoute(_from, _data); + } + + private void executeRoute(String _from, byte[] data) { + RouteData routeData = RouteData.fromBytes(data); + Context.require(routeData.method.contains("_swap"), TAG + ": Fallback directly not allowed."); + + Address fromToken = Context.getCaller(); + BigInteger minimumReceive = BigInteger.ZERO; + if (routeData.minimumReceive != null) { + minimumReceive = routeData.minimumReceive; + } + + String receiver; + if (routeData.receiver != null) { + receiver = routeData.receiver; + } else { + receiver = _from; + } + + route(receiver, fromToken, routeData.actions, minimumReceive); + } + + private void jsonRoute(String _from, byte[] data) { + String unpackedData = new String(data); JsonObject json = Json.parse(unpackedData).asObject(); String method = json.get("method").asString(); JsonObject params = json.get("params").asObject(); @@ -276,23 +347,22 @@ public void xTokenFallback(String _from, BigInteger _value, byte[] _data) { receiver = _from; } + List actions = new ArrayList<>(); JsonArray pathArray = params.get("path").asArray(); Context.require(pathArray.size() <= MAX_NUMBER_OF_ITERATIONS, TAG + ": Passed max swaps of " + MAX_NUMBER_OF_ITERATIONS); - Address[] path = new Address[pathArray.size()]; - for (int i = 0; i < pathArray.size(); i++) { JsonValue addressJsonValue = pathArray.get(i); if (addressJsonValue == null || addressJsonValue.toString().equals("null")) { - path[i] = null; + actions.add(new RouteAction(1, null)); } else { - path[i] = Address.fromString(addressJsonValue.asString()); + actions.add(new RouteAction(1, Address.fromString(addressJsonValue.asString()))); } } Address fromToken = Context.getCaller(); - route(receiver, fromToken, path, minimumReceive); + route(receiver, fromToken, actions, minimumReceive); } @Payable diff --git a/core-contracts/Router/src/test/java/network/balanced/score/core/router/RouterTest.java b/core-contracts/Router/src/test/java/network/balanced/score/core/router/RouterTest.java index 73d5c43dc..0a4be782b 100644 --- a/core-contracts/Router/src/test/java/network/balanced/score/core/router/RouterTest.java +++ b/core-contracts/Router/src/test/java/network/balanced/score/core/router/RouterTest.java @@ -20,36 +20,31 @@ import com.iconloop.score.test.Score; import com.iconloop.score.test.ServiceManager; import com.iconloop.score.test.TestBase; - +import foundation.icon.xcall.NetworkAddress; import network.balanced.score.lib.interfaces.Sicx; import network.balanced.score.lib.interfaces.tokens.IRC2; import network.balanced.score.lib.interfaces.tokens.IRC2ScoreInterface; import network.balanced.score.lib.interfaces.tokens.SpokeToken; import network.balanced.score.lib.interfaces.tokens.SpokeTokenScoreInterface; +import network.balanced.score.lib.structs.Route; +import network.balanced.score.lib.structs.RouteAction; +import network.balanced.score.lib.structs.RouteData; import network.balanced.score.lib.test.mock.MockBalanced; import network.balanced.score.lib.test.mock.MockContract; - -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.MockedStatic; -import org.mockito.Mockito; import score.Address; -import score.Context; -import foundation.icon.xcall.NetworkAddress; +import scorex.util.ArrayList; import java.math.BigInteger; +import java.util.List; import java.util.Map; -import static network.balanced.score.core.router.RouterImpl.MAX_NUMBER_OF_ITERATIONS; -import static network.balanced.score.core.router.RouterImpl.TAG; -import static network.balanced.score.core.router.RouterImpl.EMPTY_DATA; +import static network.balanced.score.core.router.RouterImpl.*; import static network.balanced.score.lib.test.UnitTest.*; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -79,7 +74,7 @@ void route() { BigInteger icxToTrade = BigInteger.TEN.multiply(ICX); Account balnToken = Account.newScoreAccount(scoreCount++); - Address[] pathWithNonSicx = new Address[] { balnToken.getAddress() }; + Address[] pathWithNonSicx = new Address[]{balnToken.getAddress()}; Executable nonSicxTrade = () -> sm.call(owner, icxToTrade, routerScore.getAddress(), "route", pathWithNonSicx, BigInteger.ZERO, ""); String expectedErrorMessage = "Reverted(0): " + TAG + ": ICX can only be traded for sICX"; @@ -93,19 +88,24 @@ void route() { Executable maxTradeHops = () -> sm.call(owner, icxToTrade, routerScore.getAddress(), "route", pathWithMoreHops, BigInteger.ZERO, ""); expectedErrorMessage = "Reverted(0): " + TAG + ": Passed max swaps of " + MAX_NUMBER_OF_ITERATIONS; + + resetInRoute(); expectErrorMessage(maxTradeHops, expectedErrorMessage); when(balanced.sicx.mock.balanceOf(routerScore.getAddress())).thenReturn(icxToTrade); - Address[] path = new Address[] { sicxScore.getAddress() }; + Address[] path = new Address[]{sicxScore.getAddress()}; routerScore.getAccount().addBalance("ICX", icxToTrade); Executable lessThanMinimumReceivable = () -> sm.call(owner, icxToTrade, routerScore.getAddress(), "route", path, icxToTrade.multiply(BigInteger.TWO), ""); expectedErrorMessage = "Reverted(0): " + TAG + ": Below minimum receive amount of " + icxToTrade.multiply(BigInteger.TWO); + + resetInRoute(); expectErrorMessage(lessThanMinimumReceivable, expectedErrorMessage); routerScore.getAccount().addBalance("ICX", icxToTrade); + resetInRoute(); sm.call(owner, icxToTrade, routerScore.getAddress(), "route", path, BigInteger.ZERO, owner.getAddress().toString()); verify(sicxScore.mock).transfer(owner.getAddress(), icxToTrade, EMPTY_DATA); @@ -179,15 +179,16 @@ void tokenFallback() throws Exception { when(balanced.sicx.mock.balanceOf(routerScore.getAddress())).thenReturn(BigInteger.TEN); byte[] invalidPathWithSicxTerminalToken = tokenData("_swap", Map.of("path", - new Object[] { balanced.baln.getAddress().toString(), null })); + new Object[]{balanced.baln.getAddress().toString(), null})); Executable nonSicxIcxTrade = () -> routerScore.invoke(sicxScore.account, "tokenFallback", owner.getAddress(), BigInteger.TEN, invalidPathWithSicxTerminalToken); expectedErrorMessage = "Reverted(0): " + TAG + ": Native swaps not available to icon from " + balanced.baln.getAddress(); expectErrorMessage(nonSicxIcxTrade, expectedErrorMessage); + resetInRoute(); Account newReceiver = sm.createAccount(); byte[] pathWithSicxTerminalToken = tokenData("_swap", Map.of("path", - new Object[] { sicxScore.getAddress().toString(), null }, "receiver", + new Object[]{sicxScore.getAddress().toString(), null}, "receiver", newReceiver.getAddress().toString())); routerScore.invoke(balanced.baln.account, "tokenFallback", owner.getAddress(), BigInteger.TEN, pathWithSicxTerminalToken); @@ -212,7 +213,7 @@ void xTrade_WithdrawToken() throws Exception { // Act byte[] path = tokenData("_swap", Map.of("path", - new Object[] { token.getAddress().toString() }, "receiver", receiver.toString())); + new Object[]{token.getAddress().toString()}, "receiver", receiver.toString())); routerScore.invoke(balanced.sicx.account, "tokenFallback", owner.getAddress(), amount, path); // Assert @@ -234,7 +235,7 @@ void xTrade_WithdrawNotAllowed() throws Exception { // Act byte[] path = tokenData("_swap", Map.of("path", - new Object[] { token.getAddress().toString() }, "receiver", receiver.toString())); + new Object[]{token.getAddress().toString()}, "receiver", receiver.toString())); routerScore.invoke(balanced.sicx.account, "tokenFallback", owner.getAddress(), amount, path); // Assert @@ -259,7 +260,7 @@ void xTrade_ToNetworkAddressWithDifferentNet() throws Exception { // Act byte[] path = tokenData("_swap", Map.of("path", - new Object[] { token.getAddress().toString() }, "receiver", receiver.toString())); + new Object[]{token.getAddress().toString()}, "receiver", receiver.toString())); routerScore.invoke(balanced.sicx.account, "tokenFallback", owner.getAddress(), amount, path); // Assert @@ -284,7 +285,7 @@ void xTradeNative_ToNetworkAddressWithDifferentNet() throws Exception { // Act & Assert byte[] path = tokenData("_swap", Map.of("path", - new Object[] { token.getAddress().toString(), null }, "receiver", receiver.toString())); + new Object[]{token.getAddress().toString(), null}, "receiver", receiver.toString())); Executable nativeToWrongNet = () -> routerScore.invoke(balanced.sicx.account, "tokenFallback", owner.getAddress(), amount, path); String expectedErrorMessage = "Reverted(0): " + TAG + ": Native swaps are not supported to other networks"; expectErrorMessage(nativeToWrongNet, expectedErrorMessage); @@ -307,7 +308,7 @@ void xTradeNative_cannotWithdraw() throws Exception { // Act & Assert byte[] path = tokenData("_swap", Map.of("path", - new Object[] { token.getAddress().toString(), null }, "receiver", receiver.toString())); + new Object[]{token.getAddress().toString(), null}, "receiver", receiver.toString())); Executable nativeToWrongNet = () -> routerScore.invoke(balanced.sicx.account, "tokenFallback", owner.getAddress(), amount, path); String expectedErrorMessage = "Reverted(0): " + TAG + ": Native swaps are not supported for this network"; expectErrorMessage(nativeToWrongNet, expectedErrorMessage); @@ -330,7 +331,7 @@ void xTrade_toNative() throws Exception { // Act byte[] path = tokenData("_swap", Map.of("path", - new Object[] { token.getAddress().toString(), null }, "receiver", receiver.toString())); + new Object[]{token.getAddress().toString(), null}, "receiver", receiver.toString())); routerScore.invoke(balanced.sicx.account, "tokenFallback", owner.getAddress(), amount, path); // Assert @@ -351,7 +352,7 @@ void xTrade_ToBnUSD() throws Exception { // Act byte[] path = tokenData("_swap", Map.of("path", - new Object[] { balanced.bnUSD.getAddress().toString() }, "receiver", receiver.toString())); + new Object[]{balanced.bnUSD.getAddress().toString()}, "receiver", receiver.toString())); routerScore.invoke(balanced.sicx.account, "tokenFallback", owner.getAddress(), amount, path); // Assert @@ -371,7 +372,7 @@ void xTrade_toIRC20() throws Exception { // Act & Assert byte[] path = tokenData("_swap", Map.of("path", - new Object[] { balanced.sicx.getAddress().toString() })); + new Object[]{balanced.sicx.getAddress().toString()})); Executable tradeToIRC2WithNetworkAddress = () -> routerScore.invoke(balanced.bnUSD.account, "xTokenFallback", user.toString(), amount, path); expectErrorMessage(tradeToIRC2WithNetworkAddress, "hubTransfer"); @@ -388,7 +389,7 @@ void xTrade_toICX() throws Exception { // Act & Assert byte[] path = tokenData("_swap", Map.of("path", - new Object[] { balanced.sicx.getAddress().toString(), null })); + new Object[]{balanced.sicx.getAddress().toString(), null})); assertThrows(AssertionError.class, () -> routerScore.invoke(balanced.bnUSD.account, "xTokenFallback", user.toString(), amount, path)); } @@ -405,7 +406,7 @@ void xTrade_toNativeICX() throws Exception { // Act byte[] path = tokenData("_swap", Map.of("path", - new Object[] { balanced.sicx.getAddress().toString(), null }, "receiver", + new Object[]{balanced.sicx.getAddress().toString(), null}, "receiver", receiver.getAddress().toString())); routerScore.getAccount().addBalance("ICX", amount); routerScore.invoke(balanced.bnUSD.account, "xTokenFallback", user.toString(), amount, path); @@ -427,7 +428,7 @@ void xTrade_toNativeIRC20() throws Exception { // Act byte[] path = tokenData("_swap", Map.of("path", - new Object[] { balanced.sicx.getAddress().toString() }, "receiver", receiver.getAddress().toString())); + new Object[]{balanced.sicx.getAddress().toString()}, "receiver", receiver.getAddress().toString())); routerScore.getAccount().addBalance("ICX", amount); routerScore.invoke(balanced.bnUSD.account, "xTokenFallback", user.toString(), amount, path); @@ -435,4 +436,197 @@ void xTrade_toNativeIRC20() throws Exception { verify(balanced.sicx.mock).transfer(receiver.getAddress(), amount, EMPTY_DATA); } + @Test + void routeV2_swap_icxSicx() { + // Arrange + BigInteger icxToTrade = BigInteger.TEN.multiply(ICX); + Account balnToken = Account.newScoreAccount(scoreCount++); + List actions = new ArrayList<>(1); + actions.add(new RouteAction(SWAP, balnToken.getAddress())); + Route route = new Route(actions); + byte[] pathWithNonSicx = route.toBytes(); + + // Act + Executable nonSicxTrade = () -> sm.call(owner, icxToTrade, routerScore.getAddress(), "routeV2", pathWithNonSicx, + BigInteger.ZERO, ""); + + // Assert + String expectedErrorMessage = "Reverted(0): " + TAG + ": ICX can only be traded for sICX"; + expectErrorMessage(nonSicxTrade, expectedErrorMessage); + } + + @Test + void routeV2_swap_pathWithMoreHops() { + // Arrange + BigInteger icxToTrade = BigInteger.TEN.multiply(ICX); + List actions = new ArrayList<>(MAX_NUMBER_OF_ITERATIONS + 1); + for (int i = 0; i < MAX_NUMBER_OF_ITERATIONS + 1; i++) { + actions.add(new RouteAction(SWAP, Account.newScoreAccount(scoreCount++).getAddress())); + } + Route route = new Route(actions); + byte[] pathWithMoreHops = route.toBytes(); + + // Act + Executable maxTradeHops = () -> sm.call(owner, icxToTrade, routerScore.getAddress(), "routeV2", + pathWithMoreHops, BigInteger.ZERO, ""); + + // Assert + String expectedErrorMessage = "Reverted(0): " + TAG + ": Passed max swaps of " + MAX_NUMBER_OF_ITERATIONS; + expectErrorMessage(maxTradeHops, expectedErrorMessage); + } + + @Test + void routeV2_swap_belowMinimumReceive() { + // Arrange + BigInteger icxToTrade = BigInteger.TEN.multiply(ICX); + when(balanced.sicx.mock.balanceOf(routerScore.getAddress())).thenReturn(icxToTrade); + routerScore.getAccount().addBalance("ICX", icxToTrade); + List actions = new ArrayList<>(1); + actions.add(new RouteAction(SWAP, sicxScore.getAddress())); + Route route = new Route(actions); + byte[] path = route.toBytes(); + + // Act + Executable lessThanMinimumReceivable = () -> sm.call(owner, icxToTrade, routerScore.getAddress(), "routeV2", + path, icxToTrade.multiply(BigInteger.TWO), ""); + + //Assert + String expectedErrorMessage = "Reverted(0): " + TAG + ": Below minimum receive amount of " + + icxToTrade.multiply(BigInteger.TWO); + expectErrorMessage(lessThanMinimumReceivable, expectedErrorMessage); + } + + @Test + void routeV2_swap_positiveMinimumReceive() { + // Arrange + BigInteger icxToTrade = BigInteger.TEN.multiply(ICX); + List actions = new ArrayList<>(1); + actions.add(new RouteAction(SWAP, sicxScore.getAddress())); + Route route = new Route(actions); + byte[] path = route.toBytes(); + + // Act + Executable negativeMinimumBalance = () -> sm.call(owner, icxToTrade, routerScore.getAddress(), + "routeV2", path, icxToTrade.negate(), ""); + + // Assert + String expectedErrorMessage = "Reverted(0): " + TAG + ": Must specify a positive number for minimum to receive"; + expectErrorMessage(negativeMinimumBalance, expectedErrorMessage); + } + + @Test + void routeV2_swapStable() throws Exception { + // Arrange + BigInteger icxToTrade = BigInteger.TEN.multiply(ICX); + List actions = new ArrayList<>(MAX_NUMBER_OF_ITERATIONS); + routerScore.getAccount().addBalance("ICX", icxToTrade); + List> tokens = new ArrayList<>(MAX_NUMBER_OF_ITERATIONS - 1); + for (int i = 0; i < MAX_NUMBER_OF_ITERATIONS; i++) { + if (i == 0) { + actions.add(new RouteAction(SWAP, balanced.sicx.getAddress())); + continue; + } + MockContract token = new MockContract<>(IRC2ScoreInterface.class, IRC2.class, sm, owner); + when(token.mock.balanceOf(routerScore.getAddress())).thenReturn(icxToTrade); + actions.add(new RouteAction(STABILITY_SWAP, token.getAddress())); + tokens.add(token); + } + Route route = new Route(actions); + byte[] pathWithMoreHops = route.toBytes(); + + // Act + sm.call(owner, icxToTrade, routerScore.getAddress(), "routeV2", + pathWithMoreHops, BigInteger.ZERO, ""); + + // Assert + int i = 0; + for (MockContract token : tokens) { + if (i < tokens.size() - 1) { + byte[] data = tokens.get(i + 1).getAddress().toString().getBytes(); + verify(token.mock).transfer(balanced.stability.getAddress(), icxToTrade, data); + } + i++; + } + } + + @Test + void tokenFallback_swapStable() throws Exception { + // Arrange + BigInteger balnToSwap = BigInteger.TEN.multiply(ICX); + List actions = new ArrayList<>(MAX_NUMBER_OF_ITERATIONS); + List> tokens = new ArrayList<>(MAX_NUMBER_OF_ITERATIONS - 1); + for (int i = 0; i < MAX_NUMBER_OF_ITERATIONS; i++) { + if (i == 0) { + actions.add(new RouteAction(SWAP, balanced.sicx.getAddress())); + continue; + } + MockContract token = new MockContract<>(IRC2ScoreInterface.class, IRC2.class, sm, owner); + when(token.mock.balanceOf(routerScore.getAddress())).thenReturn(balnToSwap); + actions.add(new RouteAction(STABILITY_SWAP, token.getAddress())); + tokens.add(token); + } + + Account newReceiver = sm.createAccount(); + byte[] data = new RouteData("_swap", newReceiver.getAddress().toString(), BigInteger.ZERO, actions).toBytes(); + + // Act + routerScore.invoke(balanced.baln.account, "tokenFallback", owner.getAddress(), balnToSwap, + data); + + // Assert + int i = 0; + for (MockContract token : tokens) { + if (i < tokens.size() - 1) { + byte[] d = tokens.get(i + 1).getAddress().toString().getBytes(); + verify(token.mock).transfer(balanced.stability.getAddress(), balnToSwap, d); + } + i++; + } + } + + @Test + void tokenFallback_swapStableWithoutOptField_AndMixedSwapTypes() throws Exception { + // Arrange + BigInteger balnToSwap = BigInteger.TEN.multiply(ICX); + List actions = new ArrayList<>(MAX_NUMBER_OF_ITERATIONS); + List> tokens = new ArrayList<>(MAX_NUMBER_OF_ITERATIONS - 1); + for (int i = 0; i < MAX_NUMBER_OF_ITERATIONS; i++) { + if (i == 0) { + actions.add(new RouteAction(SWAP, balanced.sicx.getAddress())); + continue; + } + MockContract token = new MockContract<>(IRC2ScoreInterface.class, IRC2.class, sm, owner); + when(token.mock.balanceOf(routerScore.getAddress())).thenReturn(balnToSwap); + if(i%2==0) { + actions.add(new RouteAction(STABILITY_SWAP, token.getAddress())); + }else{ + actions.add(new RouteAction(SWAP, token.getAddress())); + } + tokens.add(token); + } + + Account newReceiver = sm.createAccount(); + byte[] data = new RouteData("_swap", actions).toBytes(); + + // Act + routerScore.invoke(balanced.baln.account, "tokenFallback", owner.getAddress(), balnToSwap, + data); + + // Assert + int i = 0; + for (MockContract token : tokens) { + if (i < tokens.size() - 1) { + byte[] d = tokens.get(i + 1).getAddress().toString().getBytes(); + if(i%2==0) { + verify(token.mock).transfer(balanced.stability.getAddress(), balnToSwap, d); + } + } + i++; + } + } + + private void resetInRoute() { + // in Production this happens between each tx + ((RouterImpl)routerScore.getInstance()).inRoute = false; + } } diff --git a/core-contracts/StakedLP/src/intTest/java/network/balanced/score/core/stakedlp/StakedlpIntegrationTest.java b/core-contracts/StakedLP/src/intTest/java/network/balanced/score/core/stakedlp/StakedlpIntegrationTest.java index b840bc8c3..f6e6903d8 100644 --- a/core-contracts/StakedLP/src/intTest/java/network/balanced/score/core/stakedlp/StakedlpIntegrationTest.java +++ b/core-contracts/StakedLP/src/intTest/java/network/balanced/score/core/stakedlp/StakedlpIntegrationTest.java @@ -118,7 +118,7 @@ void testStakeAndUnstake() { // add tokens on dex and receive lp testerScoreDex.add(tokenAClient._address(), tokenBClient._address(), BigInteger.valueOf(190).multiply(EXA), - BigInteger.valueOf(190).multiply(EXA), false); + BigInteger.valueOf(190).multiply(EXA), false, BigInteger.valueOf(100)); BigInteger poolId = dex.getPoolId(tokenAClient._address(), tokenBClient._address()); BigInteger balance = dex.balanceOf(userAddress, poolId); diff --git a/score-lib/build.gradle b/score-lib/build.gradle index 863316ad0..79b56c840 100644 --- a/score-lib/build.gradle +++ b/score-lib/build.gradle @@ -32,14 +32,13 @@ dependencies { compileOnly Dependencies.javaeeApi implementation Dependencies.javaeeScorex implementation Dependencies.minimalJson - implementation 'xcall-lib:score-lib' implementation 'xyz.venture23:xcall-lib:0.1.1' compileOnly Dependencies.javaeeScoreClient annotationProcessor Dependencies.javaeeScoreClient - compileOnly 'xcall-lib:xcall-lib' - annotationProcessor 'xcall-lib:xcall-lib' + compileOnly project(':xcall-annotations') + annotationProcessor project(':xcall-annotations') implementation Dependencies.jacksonDatabind implementation Dependencies.iconSdk diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/AssetManager.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/AssetManager.java index 6e936ce4b..f2b72cfd8 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/interfaces/AssetManager.java +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/AssetManager.java @@ -18,7 +18,7 @@ import foundation.icon.score.client.ScoreClient; import foundation.icon.score.client.ScoreInterface; -import icon.xcall.lib.annotation.XCall; +import network.balanced.score.lib.annotations.XCall; import network.balanced.score.lib.interfaces.addresses.AddressManager; import network.balanced.score.lib.interfaces.base.Fallback; import network.balanced.score.lib.interfaces.base.Version; @@ -121,7 +121,7 @@ public interface AssetManager extends AddressManager, Version, Fallback { @XCall void withdrawRollback(String from, String tokenAddress, String _to, BigInteger _amount); - void linkToken(String tokenNetworkAddress, Address token); + void linkToken(String tokenNetworkAddress, Address token, @Optional BigInteger decimals); void removeToken(Address token, String nid); } diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/BalancedOracle.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/BalancedOracle.java index eb023884d..ce8e7e593 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/interfaces/BalancedOracle.java +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/BalancedOracle.java @@ -25,7 +25,7 @@ import network.balanced.score.lib.structs.PriceProtectionParameter; import score.annotation.External; import score.annotation.Optional; -import icon.xcall.lib.annotation.XCall; +import network.balanced.score.lib.annotations.XCall; import java.math.BigInteger; import java.util.Map; diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/Dex.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/Dex.java index cd2608a7f..bed093ed8 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/interfaces/Dex.java +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/Dex.java @@ -49,6 +49,9 @@ public interface Dex extends Name, AddressManager, Fallback, TokenFallback, @External void setMarketName(BigInteger _id, String _name); + @External + void setOracleProtection(BigInteger pid, BigInteger percentage); + @External void addQuoteCoin(Address _address); @@ -171,7 +174,7 @@ public interface Dex extends Name, AddressManager, Fallback, TokenFallback, @External void add(Address _baseToken, Address _quoteToken, BigInteger _baseValue, BigInteger _quoteValue, - @Optional boolean _withdraw_unused); + @Optional boolean _withdraw_unused, @Optional BigInteger _slippagePercentage); @External void withdrawSicxEarnings(@Optional BigInteger _value); diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/FloorLimitedInterface.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/FloorLimitedInterface.java index 55a9f50b7..3f8f14c8b 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/interfaces/FloorLimitedInterface.java +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/FloorLimitedInterface.java @@ -7,22 +7,22 @@ public interface FloorLimitedInterface { @External - void setFloorPercentage(BigInteger points); + void setFloorPercentage(Address token, BigInteger points); @External(readonly = true) - BigInteger getFloorPercentage(); + BigInteger getFloorPercentage(Address token); @External - void setTimeDelayMicroSeconds(BigInteger ms); + void setTimeDelayMicroSeconds(Address token, BigInteger us); @External(readonly = true) - BigInteger getTimeDelayMicroSeconds(); + BigInteger getTimeDelayMicroSeconds(Address token); @External - void setDisabled(Address token, boolean disabled); + void setMinimumFloor(Address token, BigInteger minFloor); @External(readonly = true) - boolean isDisabled(Address token); + BigInteger getMinimumFloor(Address token); @External(readonly = true) BigInteger getCurrentFloor(Address tokenAddress); diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/Governance.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/Governance.java index 1eddaf646..0f54213b7 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/interfaces/Governance.java +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/Governance.java @@ -138,7 +138,7 @@ void defineVote(String name, String description, BigInteger vote_start, BigInteg Map getBlacklist(); @External - void addAuthorizedCallerShutdown(Address address); + void addAuthorizedCallerShutdown(Address address, @Optional boolean disableOnly); @External void removeAuthorizedCallerShutdown(Address address); diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/Loans.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/Loans.java index d34b93d2e..1ae558b25 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/interfaces/Loans.java +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/Loans.java @@ -18,7 +18,7 @@ import foundation.icon.score.client.ScoreClient; import foundation.icon.score.client.ScoreInterface; -import icon.xcall.lib.annotation.XCall; +import network.balanced.score.lib.annotations.XCall; import network.balanced.score.lib.interfaces.addresses.AddressManager; import network.balanced.score.lib.interfaces.base.Name; import network.balanced.score.lib.interfaces.base.Version; diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/SpokeAssetManager.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/SpokeAssetManager.java index f111175a3..396616ca4 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/interfaces/SpokeAssetManager.java +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/SpokeAssetManager.java @@ -18,13 +18,19 @@ import foundation.icon.score.client.ScoreClient; import foundation.icon.score.client.ScoreInterface; -import icon.xcall.lib.annotation.XCall; +import network.balanced.score.lib.annotations.XCall; +import network.balanced.score.lib.interfaces.base.Version; +import network.balanced.score.lib.interfaces.base.Name; +import score.Address; +import score.annotation.External; +import score.annotation.Payable; +import score.annotation.Optional; import java.math.BigInteger; @ScoreClient @ScoreInterface -public interface SpokeAssetManager { +public interface SpokeAssetManager extends Version, Name { /** * Burns tokens from user and unlocks on source @@ -37,8 +43,9 @@ public interface SpokeAssetManager { @XCall void WithdrawTo(String from, String tokenAddress, String toAddress, BigInteger amount); - /** - * Burns tokens from user and unlocks on source and tries to acquire the native token + /** + * Burns tokens from user and unlocks on source and tries to acquire the native + * token * * @param from xCall caller. * @param tokenAddress native token address as string. @@ -47,4 +54,37 @@ public interface SpokeAssetManager { */ @XCall void WithdrawNativeTo(String from, String tokenAddress, String toAddress, BigInteger amount); + + /** + * Reverts a deposit message + * + * @param from xCall caller. + * @param token token to be returned + * @param to the user who originally sent the funds + * @param amount amount to return + */ + @XCall + void DepositRevert(String from, Address token, Address to, BigInteger amount); + + @External + void setXCallManager(Address address); + + @External(readonly = true) + Address getXCallManager(); + + @External + void setICONAssetManager(String address); + + @External(readonly = true) + String getICONAssetManager(); + + @External + void setXCall(Address address); + + @External(readonly = true) + Address getXCall(); + + @External + @Payable + void deposit(@Optional String _to, @Optional byte[] _data); } diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/SpokeBalancedDollar.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/SpokeBalancedDollar.java new file mode 100644 index 000000000..b03b8ac9f --- /dev/null +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/SpokeBalancedDollar.java @@ -0,0 +1,57 @@ +/* + * 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.lib.interfaces; + +import network.balanced.score.lib.annotations.XCall; +import network.balanced.score.lib.interfaces.base.Name; +import network.balanced.score.lib.interfaces.base.Version; +import score.annotation.External; +import score.annotation.Payable; +import score.Address; + +import java.math.BigInteger; + +public interface SpokeBalancedDollar extends Name, Version { + @External + @Payable + void crossTransfer(String _to, BigInteger _value, byte[] _data); + + @XCall + void xCrossTransfer(String from, String _from, String _to, BigInteger _value, byte[] _data); + + @XCall + void xCrossTransferRevert(String from, Address _to, BigInteger _value); + + @External + void setXCallManager(Address address); + + @External(readonly = true) + Address getXCallManager(); + + @External + void setICONBnUSD(String address); + + @External(readonly = true) + String getICONBnUSD(); + + @External + void setXCall(Address address); + + @External(readonly = true) + Address getXCall(); +} + diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/SpokeXCallManager.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/SpokeXCallManager.java new file mode 100644 index 000000000..05788a7a1 --- /dev/null +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/SpokeXCallManager.java @@ -0,0 +1,61 @@ +/* + * 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.lib.interfaces; + +import foundation.icon.score.client.ScoreClient; +import foundation.icon.score.client.ScoreInterface; +import network.balanced.score.lib.annotations.XCall; +import network.balanced.score.lib.interfaces.base.Version; +import network.balanced.score.lib.interfaces.base.Name; +import score.Address; +import score.annotation.External; + +import java.util.Map; + +@ScoreClient +@ScoreInterface +public interface SpokeXCallManager extends Version, Name { + @XCall + void execute(String from, String transactions); + + @XCall + void configureProtocols(String from, String[] sources, String[] destinations); + + @External + void proposeRemoval(String address); + + @External(readonly = true) + String getProposedRemoval(); + + @External(readonly = true) + Map getProtocols(); + + @External(readonly = true) + void verifyProtocols(String[] protocols); + + @External + void setAdmin(Address address); + + @External(readonly = true) + Address getAdmin(); + + @External + void setXCall(Address address); + + @External(readonly = true) + Address getXCall(); +} diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/XCallManager.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/XCallManager.java index 5374b88b1..a14fd0e30 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/interfaces/XCallManager.java +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/XCallManager.java @@ -16,7 +16,6 @@ package network.balanced.score.lib.interfaces; -import network.balanced.score.lib.structs.ProtocolConfig; import foundation.icon.score.client.ScoreClient; import foundation.icon.score.client.ScoreInterface; import network.balanced.score.lib.interfaces.addresses.AddressManager; diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/tokens/HubToken.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/tokens/HubToken.java index 6590daa16..6d715bcbe 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/interfaces/tokens/HubToken.java +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/tokens/HubToken.java @@ -16,7 +16,7 @@ package network.balanced.score.lib.interfaces.tokens; -import icon.xcall.lib.annotation.XCall; +import network.balanced.score.lib.annotations.XCall; import score.annotation.EventLog; import score.annotation.External; import score.annotation.Payable; @@ -42,9 +42,15 @@ public interface HubToken extends SpokeToken { @External(readonly = true) String[] getConnectedChains(); + /** + * @param _to NetworkAddress to send to. + * @param _value amount to send. + * @param _data _data used in tokenFallbacks. + */ + /** * If {@code _to} is a ICON address, use IRC2 transfer - * If {@code _to} is a BTPAddress, then the transaction must + * If {@code _to} is a NetworkAddress, then the transaction must * trigger xTransfer via XCall on corresponding spoke chain * and MUST fire the {@code XTransfer} event. * {@code _data} can be attached to this token transaction. @@ -55,6 +61,13 @@ public interface HubToken extends SpokeToken { @Payable void crossTransfer(String _to, BigInteger _value, byte[] _data); + /** + * @param _from from NetworkAddress + * @param _to NetworkAddress to send to. + * @param _value amount to send. + * @param _data _data used in tokenFallbacks. + */ + /** * Method for processing cross chain transfers from spokes * If {@code _to} is a contract trigger xTokenFallback(String, int, byte[]) diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/tokens/SpokeToken.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/tokens/SpokeToken.java index c1f0e1f62..31e430d79 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/interfaces/tokens/SpokeToken.java +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/tokens/SpokeToken.java @@ -23,7 +23,7 @@ import foundation.icon.score.client.ScoreClient; import foundation.icon.score.client.ScoreInterface; -import icon.xcall.lib.annotation.XCall; +import network.balanced.score.lib.annotations.XCall; @ScoreInterface @ScoreClient diff --git a/score-lib/src/main/java/network/balanced/score/lib/structs/ProtocolConfig.java b/score-lib/src/main/java/network/balanced/score/lib/structs/ProtocolConfig.java index a3f2a6812..bf5bf8c07 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/structs/ProtocolConfig.java +++ b/score-lib/src/main/java/network/balanced/score/lib/structs/ProtocolConfig.java @@ -27,6 +27,8 @@ public class ProtocolConfig { public final String[] sources; public final String[] destinations; + public static final String sourcesKey = "sources"; + public static final String destinationsKey = "destinations"; public ProtocolConfig(String[] sources, String[] destinations) { this.sources = sources; diff --git a/score-lib/src/main/java/network/balanced/score/lib/structs/Route.java b/score-lib/src/main/java/network/balanced/score/lib/structs/Route.java new file mode 100644 index 000000000..680d26d1e --- /dev/null +++ b/score-lib/src/main/java/network/balanced/score/lib/structs/Route.java @@ -0,0 +1,55 @@ +package network.balanced.score.lib.structs; + +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; +import scorex.util.ArrayList; + +import java.util.List; + +public class Route { + + public List actions; + + public Route() { + this.actions = new ArrayList<>(); + } + + public Route(List actions) { + this.actions = actions; + } + + public static Route readObject(ObjectReader reader) { + Route obj = new Route(); + reader.beginList(); + List actions = new ArrayList<>(); + while (reader.hasNext()) { + RouteAction data = reader.read(RouteAction.class); + actions.add(data); + } + obj.actions = actions; + reader.end(); + return obj; + } + + public static void writeObject(ObjectWriter w, Route obj) { + w.beginList(obj.actions.size()); + for (RouteAction action : obj.actions) { + w.write(action); + } + w.end(); + } + + public static Route fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); + return Route.readObject(reader); + } + + public byte[] toBytes() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + Route.writeObject(writer, this); + return writer.toByteArray(); + } + +} diff --git a/score-lib/src/main/java/network/balanced/score/lib/structs/RouteAction.java b/score-lib/src/main/java/network/balanced/score/lib/structs/RouteAction.java new file mode 100644 index 000000000..fa40e3b4f --- /dev/null +++ b/score-lib/src/main/java/network/balanced/score/lib/structs/RouteAction.java @@ -0,0 +1,40 @@ +package network.balanced.score.lib.structs; + +import score.Address; +import score.ObjectReader; +import score.ObjectWriter; + +public class RouteAction { + + public Integer action; + public Address toAddress; + + public RouteAction() { + } + + public RouteAction(Integer action, Address toAddress) { + this.action = action; + this.toAddress = toAddress; + } + + public static void writeObject(ObjectWriter writer, RouteAction obj) { + obj.writeObject(writer); + } + + public static RouteAction readObject(ObjectReader reader) { + RouteAction obj = new RouteAction(); + reader.beginList(); + obj.action = reader.readInt(); + obj.toAddress = reader.readAddress(); + reader.end(); + return obj; + } + + public void writeObject(ObjectWriter writer) { + writer.beginList(2); + writer.write(this.action); + writer.write(this.toAddress); + writer.end(); + } + +} diff --git a/score-lib/src/main/java/network/balanced/score/lib/structs/RouteData.java b/score-lib/src/main/java/network/balanced/score/lib/structs/RouteData.java new file mode 100644 index 000000000..8b3194b2d --- /dev/null +++ b/score-lib/src/main/java/network/balanced/score/lib/structs/RouteData.java @@ -0,0 +1,68 @@ +package network.balanced.score.lib.structs; + +import score.*; +import scorex.util.ArrayList; + +import java.math.BigInteger; +import java.util.List; + +public class RouteData { + + + public String method; + public String receiver; + public BigInteger minimumReceive; + public List actions; + public RouteData(){} + + public RouteData(String method, String receiver, BigInteger minimumReceive, List actions) { + this.method = method; + this.receiver = receiver; + this.minimumReceive = minimumReceive; + this.actions = actions; + } + + public RouteData(String method, List actions) { + this.method = method; + this.actions = actions; + } + + public static RouteData readObject(ObjectReader reader) { + RouteData obj = new RouteData(); + reader.beginList(); + List actions = new ArrayList<>(); + obj.method = reader.readString(); + obj.receiver = reader.readNullable(String.class); + obj.minimumReceive = reader.readNullable((BigInteger.class)); + while (reader.hasNext()) { + RouteAction data = reader.read(RouteAction.class); + actions.add(data); + } + obj.actions = actions; + reader.end(); + return obj; + } + + public static void writeObject(ObjectWriter w, RouteData obj) { + w.beginList(obj.actions.size()+3); + w.write(obj.method); + w.writeNullable(obj.receiver); + w.writeNullable(obj.minimumReceive); + for (RouteAction action : obj.actions) { + w.write(action); + } + w.end(); + } + + public static RouteData fromBytes(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); + return RouteData.readObject(reader); + } + + public byte[] toBytes() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + RouteData.writeObject(writer, this); + return writer.toByteArray(); + } + +} diff --git a/score-lib/src/main/java/network/balanced/score/lib/tokens/IRC2Base.java b/score-lib/src/main/java/network/balanced/score/lib/tokens/IRC2Base.java index 1eb6f39c1..203c1d5ee 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/tokens/IRC2Base.java +++ b/score-lib/src/main/java/network/balanced/score/lib/tokens/IRC2Base.java @@ -43,7 +43,7 @@ public class IRC2Base implements IRC2 { private final VarDB totalSupply = Context.newVarDB(TOTAL_SUPPLY, BigInteger.class); protected final DictDB balances = Context.newDictDB(BALANCES, BigInteger.class); - IRC2Base(String _tokenName, String _symbolName, @Optional BigInteger _decimals) { + protected IRC2Base(String _tokenName, String _symbolName, @Optional BigInteger _decimals) { if (this.name.get() == null) { _decimals = _decimals == null ? BigInteger.valueOf(18L) : _decimals; Context.require(_decimals.compareTo(BigInteger.ZERO) >= 0, "Decimals cannot be less than zero"); diff --git a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/ArbitraryCallManager.java b/score-lib/src/main/java/network/balanced/score/lib/utils/ArbitraryCallManager.java similarity index 83% rename from core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/ArbitraryCallManager.java rename to score-lib/src/main/java/network/balanced/score/lib/utils/ArbitraryCallManager.java index 811f10fd5..936c4d385 100644 --- a/core-contracts/Governance/src/main/java/network/balanced/score/core/governance/utils/ArbitraryCallManager.java +++ b/score-lib/src/main/java/network/balanced/score/lib/utils/ArbitraryCallManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2022 Balanced.network. + * Copyright (c) 2023-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. @@ -14,20 +14,20 @@ * limitations under the License. */ -package network.balanced.score.core.governance.utils; +package network.balanced.score.lib.utils; import com.eclipsesource.json.Json; import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.eclipsesource.json.JsonValue; -import network.balanced.score.core.governance.GovernanceImpl; -import network.balanced.score.lib.utils.Math; import score.Address; import score.Context; import scorex.util.HashMap; +import score.UserRevertedException; import java.math.BigInteger; import java.util.Map; + import java.util.function.Function; public class ArbitraryCallManager { @@ -50,7 +50,7 @@ public static void executeTransaction(JsonObject transaction) { JsonArray jsonParams = transaction.get(PARAMS).asArray(); BigInteger value = Math.convertToNumber(transaction.get(VALUE), BigInteger.ZERO); Object[] params = getConvertedParameters(jsonParams); - GovernanceImpl.call(value, address, method, params); + Context.call(value, address, method, params); } public static Object[] getConvertedParameters(String params) { @@ -89,7 +89,7 @@ private static Object convertParam(String type, JsonValue value, boolean isArray case "BigInteger": case "Long": case "Short": - return parse(value, isArray, Math::convertToNumber); + return parse(value, isArray, ArbitraryCallManager::convertToNumber); case "boolean": case "Boolean": return parse(value, isArray, JsonValue::asBoolean); @@ -140,6 +140,27 @@ private static Object convertBytesParam(JsonValue value) { return bytes; } + private static BigInteger convertToNumber(JsonValue value) { + if (value == null) { + return null; + } + if (value.isString()) { + String number = value.asString(); + try { + if (number.startsWith("0x") || number.startsWith("-0x")) { + return new BigInteger(number.replace("0x", ""), 16); + } else { + return new BigInteger(number); + } + } catch (NumberFormatException e) { + throw new UserRevertedException("Invalid numeric value: " + number); + } + } else if (value.isNumber()) { + return new BigInteger(value.toString()); + } + throw new UserRevertedException("Invalid value format for number in json: " + value); + } + private static Object parseStruct(JsonObject jsonStruct) { Map struct = new HashMap<>(); for (JsonObject.Member member : jsonStruct) { diff --git a/score-lib/src/main/java/network/balanced/score/lib/utils/BalancedFloorLimits.java b/score-lib/src/main/java/network/balanced/score/lib/utils/BalancedFloorLimits.java index f2d0314f3..da1e3515f 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/utils/BalancedFloorLimits.java +++ b/score-lib/src/main/java/network/balanced/score/lib/utils/BalancedFloorLimits.java @@ -30,36 +30,48 @@ public class BalancedFloorLimits { private static final String TAG = "BalancedFloorLimits"; static final DictDB floor = Context.newDictDB(TAG + "floor",BigInteger.class); static final DictDB lastUpdate = Context.newDictDB(TAG + "last_update",BigInteger.class); - static final DictDB disabled = Context.newDictDB(TAG + "disabled",Boolean.class); + static final DictDB minFloor = Context.newDictDB(TAG + "token_minimum_floor_limits",BigInteger.class); + static final DictDB withdrawPercentage = Context.newDictDB(TAG + "withdraw_percentage",BigInteger.class); + static final DictDB delayInUs = Context.newDictDB(TAG + "delay_in_us", BigInteger.class); + // Legacy + static final DictDB disabled = Context.newDictDB(TAG + "disabled",Boolean.class); static final VarDB percentage = Context.newVarDB(TAG + "percentage",BigInteger.class); static final VarDB delay = Context.newVarDB(TAG + "delay",BigInteger.class); - public static void setFloorPercentage(BigInteger points) { + public static void setFloorPercentage(Address token, BigInteger points) { Context.require(POINTS.compareTo(points) >= 0, TAG + ": points value must be between 0 and " + POINTS); - Context.require(BigInteger.ZERO.compareTo(points) < 0, TAG + ": points value must be between 0 and " + POINTS); - percentage.set(points); + Context.require(BigInteger.ZERO.compareTo(points) <= 0, TAG + ": points value must be between 0 and " + POINTS); + withdrawPercentage.set(token, points); } - public static BigInteger getFloorPercentage() { - return percentage.get(); - } + public static BigInteger getFloorPercentage(Address token, boolean readonly) { + BigInteger percent = withdrawPercentage.get(token); + if (percent == null && !disabled.getOrDefault(token, true)) { + percent = percentage.getOrDefault(BigInteger.ZERO); + if (!readonly) { + withdrawPercentage.set(token, percent); + } + } - public static void setTimeDelayMicroSeconds(BigInteger us) { - delay.set(us); + return percent; } - public static BigInteger getTimeDelayMicroSeconds() { - return delay.get(); + public static void setTimeDelayMicroSeconds(Address token, BigInteger us) { + delayInUs.set(token, us); } - public static void setDisabled(Address token, boolean _disabled) { - disabled.set(token, _disabled); - } + public static BigInteger getTimeDelayMicroSeconds(Address token, boolean readonly) { + BigInteger us = delayInUs.get(token); + if (us == null && !disabled.getOrDefault(token, true)) { + us = delay.getOrDefault(BigInteger.ZERO); + if (!readonly) { + delayInUs.set(token, us); + } + } - public static Boolean isDisabled(Address token) { - return disabled.getOrDefault(token, false); + return us; } public static void setMinimumFloor(Address token, BigInteger min) { @@ -82,14 +94,15 @@ public static BigInteger getCurrentFloor(Address tokenAddress) { } private static BigInteger updateFloor(Address address, BigInteger balance, boolean readonly) { - if (disabled.getOrDefault(address, true)) { + BigInteger percentageInPoints = getFloorPercentage(address, readonly); + + if (percentageInPoints == null || percentageInPoints.equals(BigInteger.ZERO)) { return BigInteger.ZERO; } else if (isBelowLimit(address, balance)) { return BigInteger.ZERO; } - BigInteger percentageInPoints = percentage.get(); - BigInteger delayInUs = delay.get(); + BigInteger delayInUs = getTimeDelayMicroSeconds(address, readonly); BigInteger lastUpdateUs = lastUpdate.getOrDefault(address, BigInteger.ZERO); BigInteger lastFloor = floor.getOrDefault(address, BigInteger.ZERO); diff --git a/score-lib/src/main/java/network/balanced/score/lib/utils/FloorLimited.java b/score-lib/src/main/java/network/balanced/score/lib/utils/FloorLimited.java index 66efb197a..c9206b183 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/utils/FloorLimited.java +++ b/score-lib/src/main/java/network/balanced/score/lib/utils/FloorLimited.java @@ -8,46 +8,30 @@ public abstract class FloorLimited implements FloorLimitedInterface { @External - public void setFloorPercentage(BigInteger points) { - Check.onlyGovernance(); - BalancedFloorLimits.setFloorPercentage(points); + public void setFloorPercentage(Address token, BigInteger points) { + Check.onlyOwner(); + BalancedFloorLimits.setFloorPercentage(token, points); } @External(readonly = true) - public BigInteger getFloorPercentage() { - return BalancedFloorLimits.getFloorPercentage(); + public BigInteger getFloorPercentage(Address token) { + return BalancedFloorLimits.getFloorPercentage(token, true); } @External - public void setTimeDelayMicroSeconds(BigInteger us) { - Check.onlyGovernance(); - BalancedFloorLimits.setTimeDelayMicroSeconds(us); + public void setTimeDelayMicroSeconds(Address token, BigInteger us) { + Check.onlyOwner(); + BalancedFloorLimits.setTimeDelayMicroSeconds(token, us); } @External(readonly = true) - public BigInteger getTimeDelayMicroSeconds() { - return BalancedFloorLimits.getTimeDelayMicroSeconds(); - } - - @External - public void enableFloors(Address[] tokens) { - Check.onlyGovernance(); - for (Address token: tokens) { - BalancedFloorLimits.setDisabled(token, false); - } - } - - @External - public void disableFloors(Address[] tokens) { - Check.onlyGovernance(); - for (Address token: tokens) { - BalancedFloorLimits.setDisabled(token, true); - } + public BigInteger getTimeDelayMicroSeconds(Address token) { + return BalancedFloorLimits.getTimeDelayMicroSeconds(token, true); } @External public void setMinimumFloor(Address token, BigInteger minFloor) { - Check.onlyGovernance(); + Check.onlyOwner(); BalancedFloorLimits.setMinimumFloor(token, minFloor); } @@ -56,17 +40,6 @@ public BigInteger getMinimumFloor(Address token) { return BalancedFloorLimits.getMinimumFloor(token); } - @External - public void setDisabled(Address token, boolean disabled) { - Check.onlyGovernance(); - BalancedFloorLimits.setDisabled(token, disabled); - } - - @External(readonly = true) - public boolean isDisabled(Address token) { - return BalancedFloorLimits.isDisabled(token); - } - @External(readonly = true) public BigInteger getCurrentFloor(Address tokenAddress) { return BalancedFloorLimits.getCurrentFloor(tokenAddress); 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 7ec482445..bf02afe6e 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,4 +44,7 @@ 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 SPOKE_ASSET_MANAGER = "Balanced Spoke Asset Manager"; + public final static String SPOKE_XCALL_MANAGER = "Balanced Spoke XCall Manager"; } \ No newline at end of file diff --git a/score-lib/src/main/java/network/balanced/score/lib/utils/RLPUtils.java b/score-lib/src/main/java/network/balanced/score/lib/utils/RLPUtils.java new file mode 100644 index 000000000..6b94dcdf8 --- /dev/null +++ b/score-lib/src/main/java/network/balanced/score/lib/utils/RLPUtils.java @@ -0,0 +1,44 @@ + +/* + * 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.lib.utils; + +import score.ObjectReader; +import scorex.util.ArrayList; + +import java.util.List; + +public class RLPUtils { + + public static String[] readStringArray(ObjectReader r) { + if (!r.hasNext()) { + return new String[] {}; + } + + r.beginList(); + List lst = new ArrayList<>(); + while (r.hasNext()) { + lst.add(r.readString()); + } + int size = lst.size(); + String[] arr = new String[size]; + for (int i = 0; i < size; i++) { + arr[i] = lst.get(i); + } + r.end(); + return arr; + } +} diff --git a/score-lib/src/main/java/network/balanced/score/lib/utils/StringUtils.java b/score-lib/src/main/java/network/balanced/score/lib/utils/StringUtils.java index 7de47f29d..5bc308706 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/utils/StringUtils.java +++ b/score-lib/src/main/java/network/balanced/score/lib/utils/StringUtils.java @@ -16,6 +16,8 @@ package network.balanced.score.lib.utils; +import com.eclipsesource.json.JsonArray; +import score.Context; import score.UserRevertException; import java.math.BigInteger; @@ -32,4 +34,5 @@ public static BigInteger convertStringToBigInteger(String number) { throw new UserRevertException("Invalid numeric value: " + number); } } + } diff --git a/score-lib/src/main/java/network/balanced/score/lib/utils/Versions.java b/score-lib/src/main/java/network/balanced/score/lib/utils/Versions.java index dcdf3b4e6..243dfdfb8 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/utils/Versions.java +++ b/score-lib/src/main/java/network/balanced/score/lib/utils/Versions.java @@ -29,19 +29,23 @@ public class Versions { public final static String REWARDS = "v1.1.1"; public final static String STABILITY = "v1.1.1"; public final static String BALANCEDORACLE = "v1.1.0"; - public final static String DAOFUND = "v1.1.0"; - public final static String DEX = "v1.1.0"; - public final static String GOVERNANCE = "v1.0.1"; + public final static String DAOFUND = "v1.1.1"; + public final static String DEX = "v1.1.1"; + public final static String GOVERNANCE = "v1.0.2"; public final static String REBALANCING = "v1.0.0"; - public final static String ROUTER = "v1.1.1"; + public final static String ROUTER = "v1.1.3"; public final static String STAKEDLP = "v1.0.1"; public final static String BOOSTED_BALN = "v1.1.0"; public final static String BRIBING = "v1.0.1"; public final static String BALANCED_OTC = "v1.0.0"; public final static String BALANCED_ASSETS = "v1.0.0"; - public final static String BALANCED_ASSET_MANAGER = "v1.0.3"; + public final static String BALANCED_ASSET_MANAGER = "v1.0.5"; public final static String XCALL_MANAGER = "v1.0.0"; public final static String BURNER = "v1.0.0"; public final static String SAVINGS = "v1.0.0"; public final static String TRICKLER = "v1.0.0"; + + public final static String SPOKE_ASSET_MANAGER = "v1.0.0"; + public final static String SPOKE_XCALL_MANAGER = "v1.0.0"; + public final static String SPOKE_BNUSD = "v1.0.0"; } diff --git a/score-lib/src/main/java/network/balanced/score/lib/utils/XCallUtils.java b/score-lib/src/main/java/network/balanced/score/lib/utils/XCallUtils.java index fe48f3ebb..ba64c4819 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/utils/XCallUtils.java +++ b/score-lib/src/main/java/network/balanced/score/lib/utils/XCallUtils.java @@ -19,7 +19,7 @@ import score.Context; import score.VarDB; import foundation.icon.xcall.NetworkAddress; - +import network.balanced.score.lib.structs.ProtocolConfig; import java.math.BigInteger; import java.util.Map; @@ -48,7 +48,7 @@ public static Map getProtocols(String nid) { public static void sendCall(BigInteger fee, NetworkAddress to, byte[] data, byte[] rollback) { Map protocols = getProtocols(to.net()); - Context.call(fee, BalancedAddressManager.getXCall(), "sendCallMessage", to.toString(), data, rollback, protocols.get("sources"), protocols.get("destinations")); + Context.call(fee, BalancedAddressManager.getXCall(), "sendCallMessage", to.toString(), data, rollback, protocols.get(ProtocolConfig.sourcesKey), protocols.get(ProtocolConfig.destinationsKey)); } } \ No newline at end of file diff --git a/score-lib/src/test/java/network/balanced/score/lib/tokens/HubTokenTest.java b/score-lib/src/test/java/network/balanced/score/lib/tokens/HubTokenTest.java index c405a7546..b8ea6cc78 100644 --- a/score-lib/src/test/java/network/balanced/score/lib/tokens/HubTokenTest.java +++ b/score-lib/src/test/java/network/balanced/score/lib/tokens/HubTokenTest.java @@ -44,10 +44,8 @@ import network.balanced.score.lib.utils.BalancedAddressManager; import score.Context; import score.Address; -import xcall.score.lib.interfaces.XTokenReceiver; -import xcall.score.lib.interfaces.XTokenReceiverScoreInterface; import foundation.icon.xcall.NetworkAddress; -import network.balanced.score.lib.interfaces.tokens.HubTokenMessages; +import network.balanced.score.lib.interfaces.tokens.*; import network.balanced.score.lib.interfaces.*; class HubTokenTest extends TestBase { diff --git a/score-lib/src/test/java/network/balanced/score/lib/tokens/SpokeTokenTest.java b/score-lib/src/test/java/network/balanced/score/lib/tokens/SpokeTokenTest.java index aae4b258a..952d59d4a 100644 --- a/score-lib/src/test/java/network/balanced/score/lib/tokens/SpokeTokenTest.java +++ b/score-lib/src/test/java/network/balanced/score/lib/tokens/SpokeTokenTest.java @@ -36,13 +36,13 @@ import network.balanced.score.lib.test.mock.MockContract; import network.balanced.score.lib.interfaces.tokens.SpokeTokenMessages; import network.balanced.score.lib.interfaces.XCall; +import network.balanced.score.lib.interfaces.tokens.XTokenReceiver; +import network.balanced.score.lib.interfaces.tokens.XTokenReceiverScoreInterface; import network.balanced.score.lib.utils.BalancedAddressManager; import score.Context; import score.DictDB; import score.Address; import score.annotation.External; -import xcall.score.lib.interfaces.XTokenReceiver; -import xcall.score.lib.interfaces.XTokenReceiverScoreInterface; import foundation.icon.xcall.NetworkAddress; class SpokeTokenTest extends TestBase { diff --git a/core-contracts/Governance/src/test/java/network/balanced/score/core/governance/ArbitraryCallManagerTest.java b/score-lib/src/test/java/network/balanced/score/lib/utils/ArbitraryCallManagerTest.java similarity index 98% rename from core-contracts/Governance/src/test/java/network/balanced/score/core/governance/ArbitraryCallManagerTest.java rename to score-lib/src/test/java/network/balanced/score/lib/utils/ArbitraryCallManagerTest.java index 883e7698d..7cc2404d9 100644 --- a/core-contracts/Governance/src/test/java/network/balanced/score/core/governance/ArbitraryCallManagerTest.java +++ b/score-lib/src/test/java/network/balanced/score/lib/utils/ArbitraryCallManagerTest.java @@ -14,12 +14,11 @@ * limitations under the License. */ -package network.balanced.score.core.governance; +package network.balanced.score.lib.utils; import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; -import network.balanced.score.core.governance.utils.ArbitraryCallManager; import network.balanced.score.lib.test.UnitTest; import org.junit.jupiter.api.Assertions; diff --git a/score-lib/src/test/java/network/balanced/score/lib/utils/BalancedFloorLimitsTest.java b/score-lib/src/test/java/network/balanced/score/lib/utils/BalancedFloorLimitsTest.java index 20c0ad971..4f1a108f0 100644 --- a/score-lib/src/test/java/network/balanced/score/lib/utils/BalancedFloorLimitsTest.java +++ b/score-lib/src/test/java/network/balanced/score/lib/utils/BalancedFloorLimitsTest.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.function.Executable; import score.ArrayDB; +import score.Address; import scorex.util.ArrayList; import java.math.BigInteger; @@ -51,15 +52,26 @@ public class BalancedFloorLimitsTest extends UnitTest { private static final long BLOCKS_IN_A_DAY = 86400/2; + public static class FloorLimitedTest extends BalancedFloorLimits { + + public static void setLegacyPercentage(BigInteger percent) { + BalancedFloorLimits.percentage.set(percent); + } + + public static void setLegacyDelay(BigInteger us) { + BalancedFloorLimits.delay.set(us); + } + + public static void setLegacyDisable(Address token, boolean _disabled) { + BalancedFloorLimits.disabled.set(token, _disabled); + } + } @BeforeEach public void setup() throws Exception { token1 = new MockContract(IRC2ScoreInterface.class, sm, owner); token2 = new MockContract(IRC2ScoreInterface.class, sm, owner); - score = sm.deploy(owner, BalancedFloorLimits.class); - score.invoke(owner, "setDisabled", token1.getAddress(), false); - score.invoke(owner, "setDisabled", token2.getAddress(), false); - score.invoke(owner, "setDisabled", EOA_ZERO, false); + score = sm.deploy(owner, FloorLimitedTest.class); } @Test @@ -67,8 +79,8 @@ public void initialFloorLimit(){ // Arrange BigInteger balance = BigInteger.valueOf(1000); BigInteger percentage = BigInteger.valueOf(1000);// 10% - score.invoke(owner, "setFloorPercentage", percentage); - score.invoke(owner, "setTimeDelayMicroSeconds", MICRO_SECONDS_IN_A_DAY); + score.invoke(owner, "setFloorPercentage", token1.getAddress(), percentage); + score.invoke(owner, "setTimeDelayMicroSeconds", token1.getAddress(), MICRO_SECONDS_IN_A_DAY); when(token1.mock.balanceOf(score.getAddress())).thenReturn(balance); // Act @@ -87,8 +99,8 @@ public void floorLimitReturnsToMin(){ // Arrange BigInteger balance = BigInteger.valueOf(1000); BigInteger percentage = BigInteger.valueOf(1000);// 10% - score.invoke(owner, "setFloorPercentage", percentage); - score.invoke(owner, "setTimeDelayMicroSeconds", MICRO_SECONDS_IN_A_DAY); + score.invoke(owner, "setFloorPercentage", token1.getAddress(), percentage); + score.invoke(owner, "setTimeDelayMicroSeconds", token1.getAddress(), MICRO_SECONDS_IN_A_DAY); when(token1.mock.balanceOf(score.getAddress())).thenReturn(balance); // Act @@ -109,8 +121,8 @@ public void nativeFloorLimit(){ // Arrange BigInteger balance = BigInteger.valueOf(1000); BigInteger percentage = BigInteger.valueOf(1000);// 10% - score.invoke(owner, "setFloorPercentage", percentage); - score.invoke(owner, "setTimeDelayMicroSeconds", MICRO_SECONDS_IN_A_DAY); + score.invoke(owner, "setFloorPercentage", EOA_ZERO, percentage); + score.invoke(owner, "setTimeDelayMicroSeconds", EOA_ZERO, MICRO_SECONDS_IN_A_DAY); score.getAccount().addBalance("ICX", balance); // Act @@ -133,8 +145,8 @@ public void decay() { // Arrange BigInteger balance = BigInteger.valueOf(1000); BigInteger percentage = BigInteger.valueOf(1000);// 10% - score.invoke(owner, "setFloorPercentage", percentage); - score.invoke(owner, "setTimeDelayMicroSeconds", MICRO_SECONDS_IN_A_DAY); + score.invoke(owner, "setFloorPercentage", token1.getAddress(), percentage); + score.invoke(owner, "setTimeDelayMicroSeconds", token1.getAddress(), MICRO_SECONDS_IN_A_DAY); when(token1.mock.balanceOf(score.getAddress())).thenReturn(balance); // Act @@ -168,8 +180,8 @@ public void disableFloorLimit() { // Arrange BigInteger balance = BigInteger.valueOf(1000); BigInteger percentage = BigInteger.valueOf(1000);// 10% - score.invoke(owner, "setFloorPercentage", percentage); - score.invoke(owner, "setTimeDelayMicroSeconds", MICRO_SECONDS_IN_A_DAY); + score.invoke(owner, "setFloorPercentage", token1.getAddress(), percentage); + score.invoke(owner, "setTimeDelayMicroSeconds", token1.getAddress(), MICRO_SECONDS_IN_A_DAY); when(token1.mock.balanceOf(score.getAddress())).thenReturn(balance); score.invoke(owner, "verifyWithdraw", token1.getAddress(), BigInteger.valueOf(100)); when(token1.mock.balanceOf(score.getAddress())).thenReturn(balance.subtract(BigInteger.valueOf(100))); @@ -178,7 +190,7 @@ public void disableFloorLimit() { assertTrue(floor.compareTo(BigInteger.ZERO) > 0); // Act - score.invoke(owner, "setDisabled", token1.getAddress(), true); + score.invoke(owner, "setFloorPercentage", token1.getAddress(), BigInteger.ZERO); // Assert floor = (BigInteger) score.call("getCurrentFloor", token1.getAddress()); @@ -192,8 +204,11 @@ public void multipleTokens() { BigInteger balance2 = BigInteger.valueOf(5000); BigInteger percentage = BigInteger.valueOf(2500);// 25% - score.invoke(owner, "setFloorPercentage", percentage); - score.invoke(owner, "setTimeDelayMicroSeconds", MICRO_SECONDS_IN_A_DAY.multiply(BigInteger.TWO)); + score.invoke(owner, "setFloorPercentage", token1.getAddress(), percentage); + score.invoke(owner, "setTimeDelayMicroSeconds", token1.getAddress(), MICRO_SECONDS_IN_A_DAY.multiply(BigInteger.TWO)); + score.invoke(owner, "setFloorPercentage", token2.getAddress(), percentage); + score.invoke(owner, "setTimeDelayMicroSeconds", token2.getAddress(), MICRO_SECONDS_IN_A_DAY.multiply(BigInteger.TWO)); + when(token1.mock.balanceOf(score.getAddress())).thenReturn(balance1); when(token2.mock.balanceOf(score.getAddress())).thenReturn(balance2); @@ -235,8 +250,8 @@ public void minimumFloor_native(){ // Arrange BigInteger balance = BigInteger.valueOf(1000); BigInteger percentage = BigInteger.valueOf(1000);// 10% - score.invoke(owner, "setFloorPercentage", percentage); - score.invoke(owner, "setTimeDelayMicroSeconds", MICRO_SECONDS_IN_A_DAY); + score.invoke(owner, "setFloorPercentage", EOA_ZERO, percentage); + score.invoke(owner, "setTimeDelayMicroSeconds", EOA_ZERO, MICRO_SECONDS_IN_A_DAY); score.getAccount().addBalance("ICX", balance); // Act @@ -261,8 +276,8 @@ public void minimumFloor(){ // Arrange BigInteger balance = BigInteger.valueOf(1000); BigInteger percentage = BigInteger.valueOf(1000);// 10% - score.invoke(owner, "setFloorPercentage", percentage); - score.invoke(owner, "setTimeDelayMicroSeconds", MICRO_SECONDS_IN_A_DAY); + score.invoke(owner, "setFloorPercentage", token1.getAddress(), percentage); + score.invoke(owner, "setTimeDelayMicroSeconds", token1.getAddress(), MICRO_SECONDS_IN_A_DAY); when(token1.mock.balanceOf(score.getAddress())).thenReturn(balance); // Act @@ -281,4 +296,26 @@ public void minimumFloor(){ floor = (BigInteger) score.call("getCurrentFloor", token1.getAddress()); assertEquals(BigInteger.valueOf(1800), floor); } + + @Test + public void migrate(){ + // Arrange + BigInteger balance = BigInteger.valueOf(1000); + BigInteger percentage = BigInteger.valueOf(1000);// 10% + score.invoke(owner, "setLegacyPercentage", percentage); + score.invoke(owner, "setLegacyDelay", MICRO_SECONDS_IN_A_DAY); + score.invoke(owner, "setLegacyDisable", token1.getAddress(), false); + when(token1.mock.balanceOf(score.getAddress())).thenReturn(balance); + + // Act + Executable withdrawMoreThanAllowed = () -> score.invoke(owner, "verifyWithdraw", token1.getAddress(), BigInteger.valueOf(101)); + Executable withdrawAllowed = () -> score.invoke(owner, "verifyWithdraw", token1.getAddress(), BigInteger.valueOf(100)); + BigInteger floor = (BigInteger) score.call("getCurrentFloor", token1.getAddress()); + + // Assert + assertEquals(BigInteger.valueOf(900), floor); + expectErrorMessage(withdrawMoreThanAllowed, BalancedFloorLimits.getErrorMessage()); + assertDoesNotThrow(withdrawAllowed); + } + } diff --git a/settings.gradle b/settings.gradle index f4098ed5f..05fce79a5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -52,6 +52,7 @@ project(':StakedLP').projectDir = file("core-contracts/StakedLP") include 'score-client' include 'score-lib' +include 'xcall-annotations' include 'test-lib' include(':WorkerToken') @@ -126,4 +127,11 @@ project(':Savings').projectDir = file("core-contracts/Savings") include(':Trickler') project(':Trickler').projectDir = file("util-contracts/Trickler") -includeBuild 'xcall-lib' +include(':SpokeAssetManager') +project(':SpokeAssetManager').projectDir = file("spoke-contracts/SpokeAssetManager") + +include(':SpokeXCallManager') +project(':SpokeXCallManager').projectDir = file("spoke-contracts/SpokeXCallManager") + +include(':SpokeBalancedDollar') +project(':SpokeBalancedDollar').projectDir = file("spoke-contracts/SpokeBalancedDollar") diff --git a/spoke-contracts/SpokeAssetManager/build.gradle b/spoke-contracts/SpokeAssetManager/build.gradle new file mode 100644 index 000000000..234cb3e09 --- /dev/null +++ b/spoke-contracts/SpokeAssetManager/build.gradle @@ -0,0 +1,114 @@ +/* + * 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() +} + +dependencies { + compileOnly Dependencies.javaeeApi + implementation Dependencies.javaeeScorex + implementation Dependencies.minimalJson + implementation project(':score-lib') + implementation 'xyz.venture23:xcall-lib:0.1.1' + + testImplementation 'foundation.icon:javaee-unittest:0.12.1' + testImplementation Dependencies.javaeeTokens + // Use JUnit Jupiter for testing. + + testImplementation Dependencies.junitJupiter + testRuntimeOnly Dependencies.junitJupiterEngine + testImplementation Dependencies.mockitoCore + testImplementation Dependencies.mockitoInline + + intTestImplementation project(":score-client") + intTestAnnotationProcessor project(":score-client") + intTestImplementation Dependencies.iconSdk + intTestImplementation Dependencies.jacksonDatabind + + testImplementation project(':test-lib') +} + +optimizedJar { + mainClassName = 'network.balanced.score.spoke.asset.manager.SpokeAssetManagerImpl' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + altair { + uri = 'https://ctz.altair.havah.io/api/v3' + nid = 0x111 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + } + + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + arg('_xCall',"cxf35c6158382096ea8cf7c54ee338ddfcaf2869a3") + arg('_iconAssetManager', "0x2.icon/cxe9d69372f6233673a6ebe07862e12af4c2dca632") + arg('_xCallManager', "TBD") + } +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = false + csv.required = false + html.outputLocation = layout.buildDirectory.dir('jacocoHtml') + } +} + +task integrationTest(type: Test) { + useJUnitPlatform() + + rootProject.allprojects { + if (it.getTasks().findByName('optimizedJar')) { + dependsOn(it.getTasks().getByName('optimizedJar')) + } + } + + options { + testLogging.showStandardStreams = true + description = 'Runs integration tests.' + group = 'verification' + + testClassesDirs = sourceSets.intTest.output.classesDirs + classpath = sourceSets.intTest.runtimeClasspath + } + +} \ No newline at end of file diff --git a/spoke-contracts/SpokeAssetManager/src/main/java/network/balanced/score/spoke/asset/manager/SpokeAssetManagerImpl.java b/spoke-contracts/SpokeAssetManager/src/main/java/network/balanced/score/spoke/asset/manager/SpokeAssetManagerImpl.java new file mode 100644 index 000000000..ed1d50101 --- /dev/null +++ b/spoke-contracts/SpokeAssetManager/src/main/java/network/balanced/score/spoke/asset/manager/SpokeAssetManagerImpl.java @@ -0,0 +1,196 @@ +/* + * 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.spoke.asset.manager; + +import network.balanced.score.lib.interfaces.SpokeAssetManager; +import network.balanced.score.lib.interfaces.AssetManagerMessages; +import network.balanced.score.lib.interfaces.SpokeAssetManagerMessages; +import network.balanced.score.lib.interfaces.SpokeAssetManagerXCall; +import network.balanced.score.lib.utils.Names; +import network.balanced.score.lib.utils.FloorLimited; +import network.balanced.score.lib.utils.Versions; +import score.*; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; + +import foundation.icon.xcall.NetworkAddress; + +import java.math.BigInteger; +import java.util.Map; + + +import static network.balanced.score.lib.utils.Check.*; +import static network.balanced.score.lib.utils.Constants.EOA_ZERO; +import static network.balanced.score.lib.utils.BalancedFloorLimits.verifyNativeWithdraw; + +public class SpokeAssetManagerImpl extends FloorLimited implements SpokeAssetManager { + + public static final String VERSION = "version"; + public static final String XCALL = "xcall"; + public static final String XCALL_NETWORK_ADDRESS = "xcall_network_address"; + public static final String ICON_ASSET_MANAGER = "icon_asset_manager"; + public static final String XCALL_MANAGER = "xcall_manager"; + + + private final VarDB currentVersion = Context.newVarDB(VERSION, String.class); + private final VarDB
xCall = Context.newVarDB(XCALL, Address.class); + private final VarDB xCallNetworkAddress = Context.newVarDB(XCALL_NETWORK_ADDRESS, String.class); + private final VarDB iconAssetManager = Context.newVarDB(ICON_ASSET_MANAGER, String.class); + private final VarDB
xCallManager = Context.newVarDB(XCALL_MANAGER, Address.class); + + public static final String TAG = Names.SPOKE_ASSET_MANAGER; + + public SpokeAssetManagerImpl(Address _xCall, String _iconAssetManager, Address _xCallManager) { + if (currentVersion.get() == null) { + xCall.set(_xCall); + xCallNetworkAddress.set(Context.call(String.class, _xCall, "getNetworkAddress")); + iconAssetManager.set(_iconAssetManager); + xCallManager.set(_xCallManager); + } + + if (this.currentVersion.getOrDefault("").equals(Versions.SPOKE_ASSET_MANAGER)) { + Context.revert("Can't Update same version of code"); + } + this.currentVersion.set(Versions.SPOKE_ASSET_MANAGER); + } + + @External(readonly = true) + public String name() { + return Names.SPOKE_ASSET_MANAGER; + } + + @External(readonly = true) + public String version() { + return currentVersion.getOrDefault(""); + } + + public void WithdrawTo(String from, String tokenAddress, String toAddress, BigInteger amount) { + Context.require(from.equals(iconAssetManager.get()), "Only ICON Asset Manager"); + withdraw(Address.fromString(tokenAddress), Address.fromString(toAddress), amount); + } + + public void WithdrawNativeTo(String from, String tokenAddress, String toAddress, BigInteger amount) { + Context.require(from.equals(iconAssetManager.get()), "Only ICON Asset Manager"); + withdraw(Address.fromString(tokenAddress), Address.fromString(toAddress), amount); + } + + public void DepositRevert(String from, Address token, Address to, BigInteger amount) { + Context.require(from.equals(xCallNetworkAddress.get()), "Only XCall"); + withdraw(token, to, amount); + } + + private void withdraw(Address token, Address to, BigInteger amount) { + if (!token.equals(EOA_ZERO)) { + Context.revert("Only native token is currently supported"); + } + + verifyNativeWithdraw(amount); + Context.transfer(to, amount); + } + + @External + public void setXCallManager(Address address) { + onlyOwner(); + xCallManager.set(address); + } + + @External(readonly = true) + public Address getXCallManager() { + return xCallManager.get(); + } + + @External + public void setICONAssetManager(String address) { + onlyOwner(); + iconAssetManager.set(address); + } + + @External(readonly = true) + public String getICONAssetManager() { + return iconAssetManager.get(); + } + + @External + public void setXCall(Address address) { + onlyOwner(); + xCall.set(address); + } + + @External(readonly = true) + public Address getXCall() { + return xCall.get(); + } + + @External + @Payable + public void deposit(@Optional String _to, @Optional byte[] _data) { + if (_to == null) { + _to = ""; + } + + if (_data == null) { + _data = new byte[0]; + } + _deposit(EOA_ZERO, Context.getCaller(), _to, BigInteger.ZERO, _data); + } + + // No way to support token deposit due to no way to recv fees, but not needed for Havah initially. + // @External + // public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { + // String unpackedData = new String(_data); + // Context.require(!unpackedData.equals(""), TAG + ": Token Fallback: Data can't be empty"); + // JsonObject json = Json.parse(unpackedData).asObject(); + // String to = json.getString("to", ""); + // byte[] data = json.getString("data", "").getBytes(); + + // _deposit(Context.getCaller(), _from, to, _value, data); + // } + + private void _deposit(Address token, Address from, String _to, BigInteger amount, byte[] _data) { + NetworkAddress iconAssetManager = NetworkAddress.valueOf(this.iconAssetManager.get()); + Map protocols = getProtocols(); + Address xCall = this.xCall.get(); + BigInteger fee = Context.getValue(); + if (isNative(token)) { + fee = Context.call(BigInteger.class, xCall, "getFee", iconAssetManager.net(), true, protocols.get("sources")); + amount = Context.getValue().subtract(fee); + } + + Context.require(amount.compareTo(BigInteger.ZERO) > 0, "amount must be larger than 0"); + byte[] depositMsg = AssetManagerMessages.deposit(EOA_ZERO.toString(), from.toString(), _to, amount, _data); + byte[] revertMsg = SpokeAssetManagerMessages.DepositRevert(EOA_ZERO, from, amount); + + Context.call(fee, xCall, "sendCallMessage", iconAssetManager.toString(), depositMsg, revertMsg, protocols.get("sources"), protocols.get("destinations")); + } + + private boolean isNative(Address token) { + return token.equals(EOA_ZERO); + } + + @External + public void handleCallMessage(String _from, byte[] _data, @Optional String[] _protocols) { + only(xCall.get()); + Context.call(xCallManager.get(), "verifyProtocols", (Object)_protocols); + SpokeAssetManagerXCall.process(this, _from, _data); + } + + @SuppressWarnings("unchecked") + public Map getProtocols() { + return (Map) Context.call(xCallManager.get(), "getProtocols"); + } +} diff --git a/spoke-contracts/SpokeAssetManager/src/test/java/network/balanced/score/spoke/asset/manager/SpokeAssetManagerTest.java b/spoke-contracts/SpokeAssetManager/src/test/java/network/balanced/score/spoke/asset/manager/SpokeAssetManagerTest.java new file mode 100644 index 000000000..1feb3da1c --- /dev/null +++ b/spoke-contracts/SpokeAssetManager/src/test/java/network/balanced/score/spoke/asset/manager/SpokeAssetManagerTest.java @@ -0,0 +1,211 @@ +/* + * 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.spoke.asset.manager; + +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 network.balanced.score.lib.interfaces.*; +import network.balanced.score.lib.test.mock.MockContract; +import network.balanced.score.lib.utils.Names; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import score.RevertedException; + +import foundation.icon.xcall.NetworkAddress; +import java.math.BigInteger; +import java.util.Map; + +import static network.balanced.score.lib.test.UnitTest.assertOnlyCallableBy; +import static network.balanced.score.lib.test.UnitTest.assertOnlyCallableByOwner; +import static network.balanced.score.lib.test.UnitTest.expectErrorMessage; +import static network.balanced.score.lib.utils.Constants.EOA_ZERO; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class SpokeAssetManagerTest extends TestBase { + private static final ServiceManager sm = getServiceManager(); + + private static Account owner; + private static Account user; + + public MockContract xCall; + public MockContract xCallManager; + public Score assetManager; + public static final String ICON_ASSET_MANAGER = "0x1.icon/cx1"; + public static final String NID = "0x111.icon"; + public static final String[] SOURCES = new String[] { "source1", "source2" }; + public static final String[] DESTINATIONS = new String[] { "dst1", "dst2" }; + + @BeforeEach + void setup() throws Exception { + owner = sm.createAccount(); + user = sm.createAccount(); + xCall = new MockContract<>(XCallScoreInterface.class, XCall.class, sm, owner); + xCallManager = new MockContract<>(SpokeXCallManagerScoreInterface.class, SpokeXCallManager.class, sm, owner); + when(xCall.mock.getNetworkAddress()).thenReturn(new NetworkAddress(NID, xCall.getAddress()).toString()); + when(xCallManager.mock.getProtocols()).thenReturn(Map.of("sources", SOURCES, "destinations", DESTINATIONS)); + assetManager = sm.deploy(owner, SpokeAssetManagerImpl.class, xCall.getAddress(), ICON_ASSET_MANAGER, + xCallManager.getAddress()); + } + + @Test + void name() { + assertEquals(Names.SPOKE_ASSET_MANAGER, assetManager.call("name")); + } + + @Test + void withdrawTo() { + // Arrange + BigInteger amount = BigInteger.TEN; + byte[] withdrawMsg = SpokeAssetManagerMessages.WithdrawTo(EOA_ZERO.toString(), user.getAddress().toString(), + amount); + assetManager.getAccount().addBalance(amount); + + // Act + assetManager.invoke(xCall.account, "handleCallMessage", ICON_ASSET_MANAGER, withdrawMsg, SOURCES); + + // Assert + assertEquals(user.getBalance(), amount); + } + + @Test + void withdrawTo_onlyAssetManager() { + // Arrange + BigInteger amount = BigInteger.TEN; + byte[] withdrawMsg = SpokeAssetManagerMessages.WithdrawTo(EOA_ZERO.toString(), user.getAddress().toString(), + amount); + assetManager.getAccount().addBalance(amount); + + // Act + Executable invalidCaller = () -> assetManager.invoke(xCall.account, "handleCallMessage", "other", withdrawMsg, + SOURCES); + + // Assert + assertEquals(user.getBalance(), BigInteger.ZERO); + expectErrorMessage(invalidCaller, "Only ICON Asset Manager"); + } + + @Test + void withdrawNativeTo() { + // Arrange + BigInteger amount = BigInteger.TEN; + byte[] withdrawMsg = SpokeAssetManagerMessages.WithdrawNativeTo(EOA_ZERO.toString(), + user.getAddress().toString(), amount); + assetManager.getAccount().addBalance(amount); + + // Act + assetManager.invoke(xCall.account, "handleCallMessage", ICON_ASSET_MANAGER, withdrawMsg, SOURCES); + + // Assert + assertEquals(user.getBalance(), amount); + } + + @Test + void withdrawNativeTo_onlyAssetManager() { + // Arrange + BigInteger amount = BigInteger.TEN; + byte[] withdrawMsg = SpokeAssetManagerMessages.WithdrawNativeTo(EOA_ZERO.toString(), + user.getAddress().toString(), amount); + assetManager.getAccount().addBalance(amount); + + // Act + Executable invalidCaller = () -> assetManager.invoke(xCall.account, "handleCallMessage", "other", withdrawMsg, + SOURCES); + + // Assert + assertEquals(user.getBalance(), BigInteger.ZERO); + expectErrorMessage(invalidCaller, "Only ICON Asset Manager"); + } + + @Test + void handleCallMessage_invalidProtocols() { + // Arrange + doThrow(RevertedException.class).when(xCallManager.mock).verifyProtocols(SOURCES); + + // Act + Executable invalidProtocols = () -> assetManager.invoke(xCall.account, "handleCallMessage", ICON_ASSET_MANAGER, + new byte[0], SOURCES); + + // Assert + assertThrows(RevertedException.class, invalidProtocols); + } + + @Test + void deposit() { + // Arrange + BigInteger amount = BigInteger.valueOf(100); + BigInteger fee = BigInteger.TEN; + String to = new NetworkAddress("0x1.icon", "hx1").toString(); + user.addBalance(amount); + when(xCall.mock.getFee("0x1.icon", true, SOURCES)).thenReturn(fee); + + byte[] depositMsg = AssetManagerMessages.deposit(EOA_ZERO.toString(), user.getAddress().toString(), to, + amount.subtract(fee), new byte[0]); + byte[] revertMsg = SpokeAssetManagerMessages.DepositRevert(EOA_ZERO, user.getAddress(), amount.subtract(fee)); + + // Act + assetManager.invoke(user, amount, "deposit", to, new byte[0]); + + // Assert + verify(xCall.mock).sendCallMessage(ICON_ASSET_MANAGER, depositMsg, revertMsg, SOURCES, DESTINATIONS); + } + + @Test + void depositRevert() { + // Arrange + BigInteger amount = BigInteger.TEN; + byte[] depositRevert = SpokeAssetManagerMessages.DepositRevert(EOA_ZERO, user.getAddress(), amount); + assetManager.getAccount().addBalance(amount); + + // Act + assetManager.invoke(xCall.account, "handleCallMessage", new NetworkAddress(NID, xCall.getAddress()).toString(), + depositRevert, SOURCES); + + // Assert + assertEquals(user.getBalance(), amount); + } + + @Test + void depositRevert_onlyXCall() { + // Arrange + BigInteger amount = BigInteger.TEN; + byte[] depositRevert = SpokeAssetManagerMessages.DepositRevert(EOA_ZERO, user.getAddress(), amount); + assetManager.getAccount().addBalance(amount); + + // Act + Executable invalidCaller = () -> assetManager.invoke(xCall.account, "handleCallMessage", "other", depositRevert, + SOURCES); + + // Assert + assertEquals(user.getBalance(), BigInteger.ZERO); + expectErrorMessage(invalidCaller, "Only XCall"); + } + + @Test + void permissions() { + assertOnlyCallableBy(xCall.getAddress(), assetManager, "handleCallMessage", ICON_ASSET_MANAGER, new byte[0], + SOURCES); + assertOnlyCallableByOwner(owner.getAddress(), assetManager, "setXCallManager", user.getAddress()); + assertOnlyCallableByOwner(owner.getAddress(), assetManager, "setICONAssetManager", ""); + assertOnlyCallableByOwner(owner.getAddress(), assetManager, "setXCall", user.getAddress()); + } +} \ No newline at end of file diff --git a/spoke-contracts/SpokeBalancedDollar/build.gradle b/spoke-contracts/SpokeBalancedDollar/build.gradle new file mode 100644 index 000000000..e61b221f0 --- /dev/null +++ b/spoke-contracts/SpokeBalancedDollar/build.gradle @@ -0,0 +1,114 @@ +/* + * 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() +} + +dependencies { + compileOnly Dependencies.javaeeApi + implementation Dependencies.javaeeScorex + implementation Dependencies.minimalJson + implementation project(':score-lib') + implementation 'xyz.venture23:xcall-lib:0.1.1' + + testImplementation Dependencies.javaeeUnitTest + testImplementation Dependencies.javaeeTokens + // Use JUnit Jupiter for testing. + + testImplementation Dependencies.junitJupiter + testRuntimeOnly Dependencies.junitJupiterEngine + testImplementation Dependencies.mockitoCore + testImplementation Dependencies.mockitoInline + + intTestImplementation project(":score-client") + intTestAnnotationProcessor project(":score-client") + intTestImplementation Dependencies.iconSdk + intTestImplementation Dependencies.jacksonDatabind + + testImplementation project(':test-lib') +} + +optimizedJar { + mainClassName = 'network.balanced.score.spoke.bnusd.SpokeBalancedDollarImpl' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + altair { + uri = 'https://ctz.altair.havah.io/api/v3' + nid = 0x111 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + } + + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + arg('_xCall',"cxf35c6158382096ea8cf7c54ee338ddfcaf2869a3") + arg('_iconAssetManager', "0x2.icon/cxe9d69372f6233673a6ebe07862e12af4c2dca632") + arg('_xCallManager', "TBD") + } +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = false + csv.required = false + html.outputLocation = layout.buildDirectory.dir('jacocoHtml') + } +} + +task integrationTest(type: Test) { + useJUnitPlatform() + + rootProject.allprojects { + if (it.getTasks().findByName('optimizedJar')) { + dependsOn(it.getTasks().getByName('optimizedJar')) + } + } + + options { + testLogging.showStandardStreams = true + description = 'Runs integration tests.' + group = 'verification' + + testClassesDirs = sourceSets.intTest.output.classesDirs + classpath = sourceSets.intTest.runtimeClasspath + } + +} \ No newline at end of file diff --git a/spoke-contracts/SpokeBalancedDollar/src/main/java/network/balanced/score/spoke/bnusd/SpokeBalancedDollarImpl.java b/spoke-contracts/SpokeBalancedDollar/src/main/java/network/balanced/score/spoke/bnusd/SpokeBalancedDollarImpl.java new file mode 100644 index 000000000..37c7b2dd4 --- /dev/null +++ b/spoke-contracts/SpokeBalancedDollar/src/main/java/network/balanced/score/spoke/bnusd/SpokeBalancedDollarImpl.java @@ -0,0 +1,156 @@ +/* + * 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.spoke.bnusd; + +import network.balanced.score.lib.interfaces.SpokeBalancedDollarMessages; +import network.balanced.score.lib.interfaces.SpokeBalancedDollarXCall; +import network.balanced.score.lib.interfaces.SpokeBalancedDollar; +import network.balanced.score.lib.tokens.IRC2Base; +import network.balanced.score.lib.utils.Names; +import network.balanced.score.lib.utils.Versions; +import score.*; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; +import foundation.icon.xcall.NetworkAddress; + +import java.math.BigInteger; +import java.util.Map; + +import static network.balanced.score.lib.utils.Check.*; + +public class SpokeBalancedDollarImpl extends IRC2Base implements SpokeBalancedDollar { + + public static final String VERSION = "version"; + public static final String XCALL = "xcall"; + public static final String XCALL_NETWORK_ADDRESS = "xcall_network_address"; + public static final String NETWORK_ID = "network_id"; + public static final String ICON_BNUSD = "icon_bnusd"; + public static final String XCALL_MANAGER = "xcall_manager"; + + private final VarDB currentVersion = Context.newVarDB(VERSION, String.class); + private final VarDB
xCall = Context.newVarDB(XCALL, Address.class); + private final VarDB xCallNetworkAddress = Context.newVarDB(XCALL_NETWORK_ADDRESS, String.class); + private final VarDB nid = Context.newVarDB(NETWORK_ID, String.class); + private final VarDB iconBnUSD = Context.newVarDB(ICON_BNUSD, String.class); + private final VarDB
xCallManager = Context.newVarDB(XCALL_MANAGER, Address.class); + + private static final String SYMBOL_NAME = "bnUSD"; + + public SpokeBalancedDollarImpl(Address _xCall, String _iconBnUSD, Address _xCallManager) { + super(Names.BNUSD, SYMBOL_NAME, null); + if (currentVersion.get() == null) { + xCall.set(_xCall); + NetworkAddress _xCallNetworkAddress = NetworkAddress.valueOf(Context.call(String.class, _xCall, "getNetworkAddress")); + xCallNetworkAddress.set(_xCallNetworkAddress.toString()); + nid.set(_xCallNetworkAddress.net()); + iconBnUSD.set(_iconBnUSD); + xCallManager.set(_xCallManager); + } else { + NetworkAddress _xCallNetworkAddress = NetworkAddress.valueOf(Context.call(String.class, xCall.get(), "getNetworkAddress")); + xCallNetworkAddress.set(_xCallNetworkAddress.toString()); + nid.set(_xCallNetworkAddress.net()); + } + + if (this.currentVersion.getOrDefault("").equals(Versions.SPOKE_BNUSD)) { + Context.revert("Can't Update same version of code"); + } + this.currentVersion.set(Versions.SPOKE_BNUSD); + + } + + @External(readonly = true) + public String name() { + return Names.BNUSD; + } + + @External(readonly = true) + public String version() { + return currentVersion.getOrDefault(""); + } + + @External + public void setXCallManager(Address address) { + onlyOwner(); + xCallManager.set(address); + } + + @External(readonly = true) + public Address getXCallManager() { + return xCallManager.get(); + } + + @External + public void setICONBnUSD(String address) { + onlyOwner(); + iconBnUSD.set(address); + } + + @External(readonly = true) + public String getICONBnUSD() { + return iconBnUSD.get(); + } + + @External + public void setXCall(Address address) { + onlyOwner(); + xCall.set(address); + } + + @External(readonly = true) + public Address getXCall() { + return xCall.get(); + } + + @External + @Payable + public void crossTransfer(String _to, BigInteger _value, @Optional byte[] _data) { + burn(Context.getCaller(), _value); + if (_data == null) { + _data = new byte[0]; + } + + Map protocols = getProtocols(); + String from = new NetworkAddress(nid.get(), Context.getCaller()).toString(); + byte[] transferMsg = SpokeBalancedDollarMessages.xCrossTransfer(from, _to, _value, _data); + byte[] revertMsg = SpokeBalancedDollarMessages.xCrossTransferRevert(Context.getCaller(), _value); + + Context.call(Context.getValue(), xCall.get(), "sendCallMessage", iconBnUSD.get(), transferMsg, revertMsg, protocols.get("sources"), protocols.get("destinations")); + } + + public void xCrossTransfer(String from, String _from, String _to, BigInteger _value, byte[] _data) { + Context.require(from.equals(iconBnUSD.get()), "Only ICON Balanced dollar"); + super.mint(Address.fromString(NetworkAddress.valueOf(_to).account()), _value); + } + + public void xCrossTransferRevert(String from, Address _to, BigInteger _value) { + Context.require(from.equals(xCallNetworkAddress.get()), "Only XCall"); + super.mint(_to, _value); + } + + @External + public void handleCallMessage(String _from, byte[] _data, @Optional String[] _protocols) { + only(xCall.get()); + Context.call(xCallManager.get(), "verifyProtocols", (Object)_protocols); + SpokeBalancedDollarXCall.process(this, _from, _data); + } + + @SuppressWarnings("unchecked") + public Map getProtocols() { + return (Map) Context.call(xCallManager.get(), "getProtocols"); + } +} diff --git a/spoke-contracts/SpokeBalancedDollar/src/test/java/network/balanced/score/spoke/bnusd/SpokeBalancedDollarTest.java b/spoke-contracts/SpokeBalancedDollar/src/test/java/network/balanced/score/spoke/bnusd/SpokeBalancedDollarTest.java new file mode 100644 index 000000000..3ce18d230 --- /dev/null +++ b/spoke-contracts/SpokeBalancedDollar/src/test/java/network/balanced/score/spoke/bnusd/SpokeBalancedDollarTest.java @@ -0,0 +1,172 @@ +/* + * 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.spoke.bnusd; + +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 network.balanced.score.lib.interfaces.*; +import network.balanced.score.lib.test.mock.MockContract; +import network.balanced.score.lib.utils.Names; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import score.Address; +import foundation.icon.xcall.NetworkAddress; + +import java.math.BigInteger; +import java.util.Map; + +import static network.balanced.score.lib.test.UnitTest.assertOnlyCallableBy; +import static network.balanced.score.lib.test.UnitTest.assertOnlyCallableByOwner; +import static network.balanced.score.lib.test.UnitTest.expectErrorMessage; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class SpokeBalancedDollarTest extends TestBase { + private static final ServiceManager sm = getServiceManager(); + + private static final Account owner = sm.createAccount(); + private static final Account user = sm.createAccount(); + public MockContract xCall; + public MockContract xCallManager; + public Score bnUSD; + public static final String ICON_BNUSD = "0x1.icon/cx1"; + public static final String NID = "0x111.icon"; + public static final String[] SOURCES = new String[]{"source1","source2"}; + public static final String[] DESTINATIONS = new String[]{"dst1","dst2"}; + + @BeforeEach + void setup() throws Exception { + xCall = new MockContract<>(XCallScoreInterface.class, XCall.class, sm, owner); + xCallManager = new MockContract<>(SpokeXCallManagerScoreInterface.class, SpokeXCallManager.class, sm, owner); + + when(xCall.mock.getNetworkAddress()).thenReturn(new NetworkAddress(NID, xCall.getAddress()).toString()); + when(xCallManager.mock.getProtocols()).thenReturn(Map.of("sources", SOURCES, "destinations", DESTINATIONS)); + bnUSD = sm.deploy(owner, SpokeBalancedDollarImpl.class, xCall.getAddress(), ICON_BNUSD, xCallManager.getAddress()); + } + + @Test + void name() { + assertEquals(Names.BNUSD, bnUSD.call("name")); + } + + @Test + void xCrossTransfer() { + // Arrange + BigInteger amount = BigInteger.TEN; + String to = new NetworkAddress(NID, user.getAddress()).toString(); + byte[] transferMsg = SpokeBalancedDollarMessages.xCrossTransfer("0x1.icon/hx2", to, amount, new byte[0]); + + // Act + bnUSD.invoke(xCall.account, "handleCallMessage", ICON_BNUSD, transferMsg, SOURCES); + + // Assert + assertEquals(amount, bnUSD.call("balanceOf", user.getAddress())); + } + + + @Test + void xCrossTransfer_onlyICONBnUSD() { + // Arrange + BigInteger amount = BigInteger.TEN; + String to = new NetworkAddress(NID, user.getAddress()).toString(); + byte[] transferMsg = SpokeBalancedDollarMessages.xCrossTransfer("0x1.icon/hx2", to, amount, new byte[0]); + + // Act + Executable onlyICONBnUSD = () -> bnUSD.invoke(xCall.account, "handleCallMessage", "Other", transferMsg, SOURCES); + + // Assert + expectErrorMessage(onlyICONBnUSD, "Only ICON Balanced dollar"); + } + + + @Test + void handleCallMessage_invalidProtocols() { + // Arrange + doThrow(AssertionError.class).when(xCallManager.mock).verifyProtocols(SOURCES); + + // Act + Executable invalidProtocols = () -> bnUSD.invoke(xCall.account, "handleCallMessage", ICON_BNUSD, new byte[0], SOURCES); + + // Assert + assertThrows(AssertionError.class, invalidProtocols); + } + + @Test + void crossTransfer() { + // Arrange + BigInteger amount = BigInteger.TEN; + String to = new NetworkAddress("0x1.icon", "hx1").toString(); + String userAddress = new NetworkAddress(NID, user.getAddress()).toString(); + addBalance(user.getAddress(), amount); + byte[] expectedMessage = SpokeBalancedDollarMessages.xCrossTransfer(userAddress, to, amount, new byte[0]); + byte[] revertMsg = SpokeBalancedDollarMessages.xCrossTransferRevert(user.getAddress(), amount); + + // Act + bnUSD.invoke(user, "crossTransfer", to, amount, new byte[0]); + + // Assert + assertEquals(BigInteger.ZERO, bnUSD.call("balanceOf", user.getAddress())); + verify(xCall.mock).sendCallMessage(ICON_BNUSD, expectedMessage, revertMsg, SOURCES, DESTINATIONS); + } + + @Test + void xTransferRevert_notXCall() { + // Arrange + BigInteger amount = BigInteger.TEN; + byte[] revertMsg = SpokeBalancedDollarMessages.xCrossTransferRevert(user.getAddress(), amount); + + // Act + Executable onlyICONBnUSD = () -> bnUSD.invoke(xCall.account, "handleCallMessage", "Other", revertMsg, SOURCES); + + // Assert + expectErrorMessage(onlyICONBnUSD, "Only XCall"); + } + + + @Test + void xTransferRevert() { + // Arrange + BigInteger amount = BigInteger.TEN; + byte[] revertMsg = SpokeBalancedDollarMessages.xCrossTransferRevert(user.getAddress(), amount); + + // Act + bnUSD.invoke(xCall.account, "handleCallMessage", new NetworkAddress(NID, xCall.getAddress()).toString(), revertMsg, SOURCES); + + // Assert + assertEquals(amount, bnUSD.call("balanceOf", user.getAddress())); + } + + @Test + void permissions() { + assertOnlyCallableBy(xCall.getAddress(), bnUSD, "handleCallMessage", ICON_BNUSD, new byte[0], SOURCES); + assertOnlyCallableByOwner(owner.getAddress(), bnUSD, "setXCallManager", user.getAddress()); + assertOnlyCallableByOwner(owner.getAddress(), bnUSD, "setICONBnUSD", ""); + assertOnlyCallableByOwner(owner.getAddress(), bnUSD, "setXCall", user.getAddress()); + } + + void addBalance(Address user, BigInteger amount) { + String to = new NetworkAddress(NID, user).toString(); + byte[] transferMsg = SpokeBalancedDollarMessages.xCrossTransfer("0x1.icon/hx2", to, amount, new byte[0]); + bnUSD.invoke(xCall.account, "handleCallMessage", ICON_BNUSD, transferMsg, SOURCES); + } + +} \ No newline at end of file diff --git a/spoke-contracts/SpokeXCallManager/build.gradle b/spoke-contracts/SpokeXCallManager/build.gradle new file mode 100644 index 000000000..8b39c7775 --- /dev/null +++ b/spoke-contracts/SpokeXCallManager/build.gradle @@ -0,0 +1,115 @@ +/* + * 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() +} + +dependencies { + compileOnly Dependencies.javaeeApi + implementation Dependencies.javaeeScorex + implementation Dependencies.minimalJson + implementation project(':score-lib') + implementation 'xyz.venture23:xcall-lib:0.1.1' + + testImplementation Dependencies.javaeeUnitTest + testImplementation Dependencies.javaeeTokens + // Use JUnit Jupiter for testing. + + testImplementation Dependencies.junitJupiter + testRuntimeOnly Dependencies.junitJupiterEngine + testImplementation Dependencies.mockitoCore + testImplementation Dependencies.mockitoInline + + intTestImplementation project(":score-client") + intTestAnnotationProcessor project(":score-client") + intTestImplementation Dependencies.iconSdk + intTestImplementation Dependencies.jacksonDatabind + + testImplementation project(':test-lib') +} + +optimizedJar { + mainClassName = 'network.balanced.score.spoke.xcall.manager.SpokeXCallManagerImpl' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + altair { + uri = 'https://ctz.altair.havah.io/api/v3' + nid = 0x111 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + + } + + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + arg('_xCall', "cxf35c6158382096ea8cf7c54ee338ddfcaf2869a3") + arg('_iconGovernance', "0x2.icon/cxdb3d3e2717d4896b336874015a4b23871e62fb6b") + arg('_protocolConfig', "{\"sources\":[\"cx60c1557a511326d16768b735c944023b514b55dc\"],\"destinations\":[\"cx2e230f2f91f7fe0f0b9c6fe1ce8dbba9f74f961a\"]}") + } +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = false + csv.required = false + html.outputLocation = layout.buildDirectory.dir('jacocoHtml') + } +} + +task integrationTest(type: Test) { + useJUnitPlatform() + + rootProject.allprojects { + if (it.getTasks().findByName('optimizedJar')) { + dependsOn(it.getTasks().getByName('optimizedJar')) + } + } + + options { + testLogging.showStandardStreams = true + description = 'Runs integration tests.' + group = 'verification' + + testClassesDirs = sourceSets.intTest.output.classesDirs + classpath = sourceSets.intTest.runtimeClasspath + } + +} \ No newline at end of file diff --git a/spoke-contracts/SpokeXCallManager/src/main/java/network/balanced/score/spoke/xcall/manager/SpokeXCallManagerImpl.java b/spoke-contracts/SpokeXCallManager/src/main/java/network/balanced/score/spoke/xcall/manager/SpokeXCallManagerImpl.java new file mode 100644 index 000000000..b90a502e9 --- /dev/null +++ b/spoke-contracts/SpokeXCallManager/src/main/java/network/balanced/score/spoke/xcall/manager/SpokeXCallManagerImpl.java @@ -0,0 +1,199 @@ +/* + * 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.spoke.xcall.manager; + +import network.balanced.score.lib.interfaces.SpokeXCallManager; +import network.balanced.score.lib.interfaces.SpokeXCallManagerXCall; +import network.balanced.score.lib.utils.Names; +import network.balanced.score.lib.utils.Versions; +import network.balanced.score.lib.structs.ProtocolConfig; +import network.balanced.score.lib.utils.ArbitraryCallManager; + +import score.Context; +import score.ObjectReader; +import score.Address; +import score.VarDB; +import score.annotation.External; +import score.annotation.Optional; + +import java.util.Map; + +import static network.balanced.score.lib.utils.Check.onlyOwnerOrContract; +import static network.balanced.score.lib.utils.Check.only; + +public class SpokeXCallManagerImpl implements SpokeXCallManager { + + public static final String VERSION = "version"; + public static final String XCALL = "xcall"; + public static final String XCALL_NETWORK_ADDRESS = "xcall_network_address"; + public static final String ICON_GOVERNANCE = "icon_governance"; + public static final String XCALL_MANAGER = "xcall_manager"; + public static final String PROTOCOL_CONFIG = "protocol_config"; + public static final String PROPOSED_REMOVAL = "proposed_removal"; + public static final String ADMIN = "admin"; + + private final VarDB currentVersion = Context.newVarDB(VERSION, String.class); + private final VarDB
xCall = Context.newVarDB(XCALL, Address.class); + private final VarDB xCallNetworkAddress = Context.newVarDB(XCALL_NETWORK_ADDRESS, String.class); + private final VarDB iconGovernance = Context.newVarDB(ICON_GOVERNANCE, String.class); + private final VarDB protocolConfig = Context.newVarDB(PROTOCOL_CONFIG, ProtocolConfig.class); + private final VarDB proposedRemoval = Context.newVarDB(PROPOSED_REMOVAL, String.class); + private final VarDB
admin = Context.newVarDB(ADMIN, Address.class); + public static final String TAG = Names.SPOKE_XCALL_MANAGER; + + public SpokeXCallManagerImpl(Address _xCall, String _iconGovernance, ProtocolConfig _protocolConfig) { + if (currentVersion.get() == null) { + xCall.set(_xCall); + xCallNetworkAddress.set(Context.call(String.class, _xCall, "getNetworkAddress")); + iconGovernance.set(_iconGovernance); + protocolConfig.set(_protocolConfig); + admin.set(Context.getCaller()); + } + + if (this.currentVersion.getOrDefault("").equals(Versions.SPOKE_XCALL_MANAGER)) { + Context.revert("Can't Update same version of code"); + } + this.currentVersion.set(Versions.SPOKE_XCALL_MANAGER); + } + + @External(readonly = true) + public String name() { + return Names.SPOKE_XCALL_MANAGER; + } + + @External(readonly = true) + public String version() { + return currentVersion.getOrDefault(""); + } + + public void execute(String from, String transactions) { + ArbitraryCallManager.executeTransactions(transactions); + } + + public void configureProtocols(String from, String[] sources, String[] destinations) { + protocolConfig.set(new ProtocolConfig(sources, destinations)); + proposedRemoval.set(null); + } + + @External(readonly = true) + public Map getProtocols() { + ProtocolConfig cfg = protocolConfig.get(); + Context.require(cfg != null, TAG + ": Network is not configured"); + + return Map.of("sources", cfg.sources, "destinations", cfg.destinations); + } + + @External(readonly = true) + public void verifyProtocols(String[] protocols) { + Context.require(_verifyProtocols(protocols), "Invalid protocols used to deliver message"); + } + + public boolean _verifyProtocols(String[] protocols) { + ProtocolConfig cfg = protocolConfig.get(); + if (cfg.sources.length == 0) { + return protocols == null || protocols.length == 0; + } + + for (String source : cfg.sources) { + if (!hasSource(source, protocols)) { + return false; + } + } + + return true; + } + + private boolean hasSource(String source, String[] protocols) { + for (String protocol : protocols) { + if (protocol.equals(source)) { + return true; + } + } + + return false; + } + + @External + public void proposeRemoval(String address) { + only(admin); + proposedRemoval.set(address); + } + + @External(readonly = true) + public String getProposedRemoval() { + return proposedRemoval.get(); + } + + @External + public void setAdmin(Address _admin) { + only(admin); + admin.set(_admin); + } + + @External(readonly = true) + public Address getAdmin() { + return admin.get(); + } + + @External + public void setXCall(Address address) { + onlyOwnerOrContract(); + xCall.set(address); + } + + @External(readonly = true) + public Address getXCall() { + return xCall.get(); + } + + @External + public void handleCallMessage(String _from, byte[] _data, @Optional String[] _protocols) { + only(xCall.get()); + Context.require(_from.equals(iconGovernance.get()), "Only ICON Governance"); + if (!_verifyProtocols(_protocols)) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", _data); + reader.beginList(); + String method = reader.readString().toLowerCase(); + Context.require(method.equals("configureprotocols"), "Invalid protocols used to deliver message"); + Context.require(_verifyProtocolsWithProposal(_protocols), "Invalid protocols used to deliver message"); + } + + SpokeXCallManagerXCall.process(this, _from, _data); + } + + public boolean _verifyProtocolsWithProposal(String[] protocols) { + String proposedRemoval = this.proposedRemoval.get(); + Context.require(proposedRemoval != null, "Invalid protocols used to deliver message"); + ProtocolConfig cfg = protocolConfig.get(); + if (cfg.sources.length == 1) { + Context.require(cfg.sources[0].equals(proposedRemoval), "Invalid protocols used to deliver message"); + return protocols == null || protocols.length == 0; + } + + for (String source : cfg.sources) { + if (source.equals(proposedRemoval)) { + continue; + } + + if (!hasSource(source, protocols)) { + return false; + } + } + + return true; + } +} diff --git a/spoke-contracts/SpokeXCallManager/src/test/java/network/balanced/score/spoke/xcall/manager/SpokeXCallManagerTest.java b/spoke-contracts/SpokeXCallManager/src/test/java/network/balanced/score/spoke/xcall/manager/SpokeXCallManagerTest.java new file mode 100644 index 000000000..1617b4387 --- /dev/null +++ b/spoke-contracts/SpokeXCallManager/src/test/java/network/balanced/score/spoke/xcall/manager/SpokeXCallManagerTest.java @@ -0,0 +1,246 @@ +/* + * 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.spoke.xcall.manager; + +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 network.balanced.score.lib.interfaces.*; +import network.balanced.score.lib.test.mock.MockContract; +import network.balanced.score.lib.utils.Names; +import network.balanced.score.lib.structs.ProtocolConfig; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import foundation.icon.xcall.NetworkAddress; + +import java.util.Map; + +import com.eclipsesource.json.JsonArray; +import com.eclipsesource.json.JsonObject; + +import static network.balanced.score.lib.test.UnitTest.assertOnlyCallableBy; +import static network.balanced.score.lib.test.UnitTest.expectErrorMessage; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.Mockito.*; + +class SpokeXCallManagerTest extends TestBase { + private static final ServiceManager sm = getServiceManager(); + + private static final Account owner = sm.createAccount(); + private static final Account user = sm.createAccount(); + public MockContract xCall; + public MockContract xCallManager; + public Score manager; + public static final String ICON_GOVERNANCE = "0x1.icon/cx1"; + public static final String NID = "0x111.icon"; + public static final String[] SOURCES = new String[] { "source1", "source2" }; + public static final String[] DESTINATIONS = new String[] { "dst1", "dst2" }; + + @BeforeEach + void setup() throws Exception { + xCall = new MockContract<>(XCallScoreInterface.class, XCall.class, sm, owner); + + when(xCall.mock.getNetworkAddress()).thenReturn(new NetworkAddress(NID, xCall.getAddress()).toString()); + manager = sm.deploy(owner, SpokeXCallManagerImpl.class, xCall.getAddress(), ICON_GOVERNANCE, + new ProtocolConfig(SOURCES, DESTINATIONS)); + } + + @Test + void name() { + assertEquals(Names.SPOKE_XCALL_MANAGER, manager.call("name")); + } + + @Test + void handleCallMessage_notGovernance() { + // Act + Executable nonGovernance = () -> manager.invoke(xCall.account, "handleCallMessage", "Not Governance", + new byte[0], SOURCES); + + // Assert + expectErrorMessage(nonGovernance, "Only ICON Governance"); + } + + @Test + void handleCallMessage_invalidProtocols() { + // Arrange + byte[] msg = SpokeXCallManagerMessages.execute(""); + + // Act + Executable invalidProtocols = () -> manager.invoke(xCall.account, "handleCallMessage", ICON_GOVERNANCE, msg, + DESTINATIONS); + + // Assert + assertThrows(AssertionError.class, invalidProtocols); + } + + @Test + @SuppressWarnings("unchecked") + void configureProtocol() { + // Arrange + String[] sources = new String[] { "new src" }; + String[] destinations = new String[] { "new dst" }; + byte[] msg = SpokeXCallManagerMessages.configureProtocols(sources, destinations); + + // Act + manager.invoke(xCall.account, "handleCallMessage", ICON_GOVERNANCE, msg, SOURCES); + + // Assert + Map cfg = (Map) manager.call("getProtocols"); + assertArrayEquals(sources, cfg.get("sources")); + assertArrayEquals(destinations, cfg.get("destinations")); + assertDoesNotThrow(() -> manager.call("verifyProtocols", (Object) sources)); + expectErrorMessage(() -> manager.call("verifyProtocols", (Object) SOURCES), + "Invalid protocols used to deliver message"); + + } + + @Test + @SuppressWarnings("unchecked") + void configureProtocol_default() { + // Arrange + byte[] msg = SpokeXCallManagerMessages.configureProtocols(new String[] {}, new String[] {}); + + // Act + manager.invoke(xCall.account, "handleCallMessage", ICON_GOVERNANCE, msg, SOURCES); + + // Assert + Map cfg = (Map) manager.call("getProtocols"); + assertArrayEquals(new String[] {}, cfg.get("sources")); + assertArrayEquals(new String[] {}, cfg.get("destinations")); + assertDoesNotThrow(() -> manager.call("verifyProtocols", (Object) new String[] {})); + expectErrorMessage(() -> manager.call("verifyProtocols", (Object) new String[] { "Single" }), + "Invalid protocols used to deliver message"); + expectErrorMessage(() -> manager.call("verifyProtocols", (Object) SOURCES), + "Invalid protocols used to deliver message"); + + } + + @Test + @SuppressWarnings("unchecked") + void configureProtocol_withProposedRemoval() { + // Arrange + String[] sources = new String[] { "new src" }; + String[] destinations = new String[] { "new dst" }; + String[] deliverySources = new String[] { SOURCES[0] }; + String removedSource = SOURCES[1]; + byte[] msg = SpokeXCallManagerMessages.configureProtocols(sources, destinations); + + // Act + manager.invoke(owner, "proposeRemoval", removedSource); + manager.invoke(xCall.account, "handleCallMessage", ICON_GOVERNANCE, msg, deliverySources); + + // Assert + Map cfg = (Map) manager.call("getProtocols"); + assertArrayEquals(sources, cfg.get("sources")); + assertArrayEquals(destinations, cfg.get("destinations")); + assertDoesNotThrow(() -> manager.call("verifyProtocols", (Object) sources)); + expectErrorMessage(() -> manager.call("verifyProtocols", (Object) SOURCES), + "Invalid protocols used to deliver message"); + } + + @Test + @SuppressWarnings("unchecked") + void configureProtocol_backToDefault_withProposedRemoval() { + // Arrange + String[] sources = new String[] { "src" }; + String[] destinations = new String[] { "dst" }; + byte[] setupMsg = SpokeXCallManagerMessages.configureProtocols(sources, destinations); + manager.invoke(xCall.account, "handleCallMessage", ICON_GOVERNANCE, setupMsg, SOURCES); + + String[] newSources = new String[] { "new src" }; + String[] newDestinations = new String[] { "new dst" }; + byte[] msg = SpokeXCallManagerMessages.configureProtocols(newSources, newDestinations); + + // Act + manager.invoke(owner, "proposeRemoval", sources[0]); + manager.invoke(xCall.account, "handleCallMessage", ICON_GOVERNANCE, msg, new String[] {}); + + // Assert + Map cfg = (Map) manager.call("getProtocols"); + assertArrayEquals(newSources, cfg.get("sources")); + assertArrayEquals(newDestinations, cfg.get("destinations")); + assertDoesNotThrow(() -> manager.call("verifyProtocols", (Object) newSources)); + expectErrorMessage(() -> manager.call("verifyProtocols", (Object) sources), + "Invalid protocols used to deliver message"); + expectErrorMessage(() -> manager.call("verifyProtocols", (Object) new String[] {}), + "Invalid protocols used to deliver message"); + } + + @Test + void configureProtocol_invalidProtocol() { + // Arrange + String[] sources = new String[] { "new src" }; + String[] destinations = new String[] { "new dst" }; + String removedSource = SOURCES[1]; + byte[] msg = SpokeXCallManagerMessages.configureProtocols(sources, destinations); + + // Act + manager.invoke(owner, "proposeRemoval", removedSource); + Executable invalidProtocols = () -> manager.invoke(xCall.account, "handleCallMessage", ICON_GOVERNANCE, msg, + DESTINATIONS); + + // Assert + expectErrorMessage(invalidProtocols, "Invalid protocols used to deliver message"); + } + + @Test + void execute() { + // Arrange + JsonObject param = new JsonObject() + .add("type", "Address") + .add("value", user.getAddress().toString()); + + JsonObject data = new JsonObject() + .add("address", manager.getAddress().toString()) + .add("method", "setXCall") + .add("parameters", new JsonArray().add(param)); + String transactions = new JsonArray().add(data).toString(); + byte[] executeMessage = SpokeXCallManagerMessages.execute(transactions); + + // Act + manager.invoke(xCall.account, "handleCallMessage", ICON_GOVERNANCE, executeMessage, SOURCES); + + // Assert + assertEquals(user.getAddress(), manager.call("getXCall")); + } + + @Test + void permissions() { + Account admin = sm.createAccount(); + manager.invoke(owner, "setAdmin", admin.getAddress()); + + assertOnlyCallableBy(xCall.getAddress(), manager, "handleCallMessage", ICON_GOVERNANCE, new byte[0], SOURCES); + assertOnlyCallableBy(admin.getAddress(), manager, "setAdmin", user.getAddress()); + assertOnlyCallableBy(admin.getAddress(), manager, "proposeRemoval", ""); + assertOnlyCallableByContractOrOwner("setXCall", user.getAddress()); + } + + protected void assertOnlyCallableByContractOrOwner(String method, Object... params) { + Account nonAuthorizedCaller = sm.createAccount(); + String expectedErrorMessage = "Reverted(0): SenderNotScoreOwnerOrContract: Sender=" + + nonAuthorizedCaller.getAddress() + + " Owner=" + owner.getAddress() + " Contract=" + manager.getAccount().getAddress(); + Executable unAuthorizedCall = () -> manager.invoke(nonAuthorizedCaller, method, params); + expectErrorMessage(unAuthorizedCall, expectedErrorMessage); + } +} \ No newline at end of file diff --git a/test-lib/build.gradle b/test-lib/build.gradle index b1800c577..84bb518b1 100644 --- a/test-lib/build.gradle +++ b/test-lib/build.gradle @@ -33,7 +33,6 @@ dependencies { implementation project(':score-lib') annotationProcessor project(':score-client') implementation project(':score-client') - implementation 'xcall-lib:score-lib' implementation Dependencies.junitJupiterApi implementation Dependencies.jacksonDatabind implementation Dependencies.javaeeUnitTest diff --git a/test-lib/src/main/java/network/balanced/score/lib/test/UnitTest.java b/test-lib/src/main/java/network/balanced/score/lib/test/UnitTest.java index 6b93fb40f..f5a828be9 100644 --- a/test-lib/src/main/java/network/balanced/score/lib/test/UnitTest.java +++ b/test-lib/src/main/java/network/balanced/score/lib/test/UnitTest.java @@ -62,7 +62,7 @@ public void teardownMocks() { } public static void expectErrorMessage(Executable contractCall, String expectedErrorMessage) { - AssertionError e = Assertions.assertThrows(AssertionError.class, contractCall); + Throwable e = Assertions.assertThrows(Throwable.class, contractCall); assertTrue(e.getMessage().contains(expectedErrorMessage)); } @@ -190,4 +190,13 @@ public static void assertOnlyCallableBy(Address caller, Score contractUnderTest, Executable unAuthorizedCall = () -> contractUnderTest.invoke(nonAuthorizedCaller, method, params); expectErrorMessage(unAuthorizedCall, expectedErrorMessage); } + + public static void assertOnlyCallableByOwner(Address owner, Score contractUnderTest, String method, Object... params) { + Account nonAuthorizedCaller = sm.createAccount(); + String expectedErrorMessage = + "Reverted(0): SenderNotScoreOwner: Sender=" + nonAuthorizedCaller.getAddress() + + "Owner=" + owner; + Executable unAuthorizedCall = () -> contractUnderTest.invoke(nonAuthorizedCaller, method, params); + expectErrorMessage(unAuthorizedCall, expectedErrorMessage); + } } diff --git a/test-lib/src/main/java/network/balanced/score/lib/test/integration/Balanced.java b/test-lib/src/main/java/network/balanced/score/lib/test/integration/Balanced.java index 6ae63f8ec..36b619dd8 100644 --- a/test-lib/src/main/java/network/balanced/score/lib/test/integration/Balanced.java +++ b/test-lib/src/main/java/network/balanced/score/lib/test/integration/Balanced.java @@ -123,7 +123,6 @@ public void deployContracts() { governanceClient.deploy(getContractData("Loans"), governanceParam); governanceClient.deploy(getContractData("Rebalancing"), governanceParam); governanceClient.deploy(getContractData("Rewards"), governanceParam); - governanceClient.deploy(getContractData("Staking"), "[]"); governanceClient.deploy(getContractData("BalancedDollar"), governanceParam); governanceClient.deploy(getContractData("DAOfund"), governanceParam); governanceClient.deploy(getContractData("Dividends"), governanceParam); diff --git a/util-contracts/Burner/src/main/java/network/balanced/score/util/burner/ICONBurnerImpl.java b/util-contracts/Burner/src/main/java/network/balanced/score/util/burner/ICONBurnerImpl.java index 190c91ce8..d9033ec96 100644 --- a/util-contracts/Burner/src/main/java/network/balanced/score/util/burner/ICONBurnerImpl.java +++ b/util-contracts/Burner/src/main/java/network/balanced/score/util/burner/ICONBurnerImpl.java @@ -24,6 +24,8 @@ import score.annotation.Payable; import java.math.BigInteger; +import java.util.List; +import java.util.Map; import com.eclipsesource.json.Json; import com.eclipsesource.json.JsonObject; @@ -83,6 +85,27 @@ public BigInteger getBurnedAmount() { return totalBurn.getOrDefault(BigInteger.ZERO); } + @External(readonly = true) + public BigInteger getPendingBurn() { + BigInteger sICXbalance = Context.call(BigInteger.class, getSicx(), "balanceOf", Context.getAddress()); + BigInteger bnUSDBalance = Context.call(BigInteger.class, getBnusd(), "balanceOf", Context.getAddress()); + BigInteger sICXPrice = Context.call(BigInteger.class, getBalancedOracle(), "getLastPriceInLoop", "sICX"); + BigInteger bnUSDPrice = Context.call(BigInteger.class, getBalancedOracle(), "getLastPriceInLoop", "bnUSD"); + return sICXPrice.multiply(sICXbalance).divide(EXA).add(bnUSDBalance.multiply(bnUSDPrice).divide(EXA)); + } + + @External(readonly = true) + public BigInteger getUnstakingBurn() { + List> unstakeData = (List>) Context.call(getStaking(), "getUserUnstakeInfo", Context.getAddress()); + BigInteger total = BigInteger.ZERO; + for (Map data : unstakeData) { + total = total.add((BigInteger)data.get("amount")); + } + BigInteger claimableICX = Context.call(BigInteger.class, getStaking(), "claimableICX", Context.getAddress()); + + return total.add(claimableICX); + } + @External public void swapBnUSD(BigInteger amount) { BigInteger sICXPriceInUSD = Context.call(BigInteger.class, getBalancedOracle(), "getPriceInUSD", "sICX"); diff --git a/util-contracts/XCallManager/src/main/java/network/balanced/score/util/xcall/manager/XCallManagerImpl.java b/util-contracts/XCallManager/src/main/java/network/balanced/score/util/xcall/manager/XCallManagerImpl.java index d33aa1378..94f7fbdbf 100644 --- a/util-contracts/XCallManager/src/main/java/network/balanced/score/util/xcall/manager/XCallManagerImpl.java +++ b/util-contracts/XCallManager/src/main/java/network/balanced/score/util/xcall/manager/XCallManagerImpl.java @@ -34,6 +34,7 @@ import static network.balanced.score.lib.utils.BalancedAddressManager.setGovernance; import static network.balanced.score.lib.utils.BalancedAddressManager.resetAddress; import static network.balanced.score.lib.utils.BalancedAddressManager.getAddressByName; +import static network.balanced.score.lib.utils.BalancedAddressManager.getXCall; import static network.balanced.score.lib.utils.Check.isContract; import static network.balanced.score.lib.utils.Check.onlyOwner; import static network.balanced.score.lib.utils.Check.checkStatus; @@ -46,12 +47,14 @@ public class XCallManagerImpl implements XCallManager { DictDB protocols = Context.newDictDB(PROTOCOLS, ProtocolConfig.class); public static final String TAG = Names.XCALL_MANAGER; + public static String NATIVE_NID; public XCallManagerImpl(Address _governance) { if (this.currentVersion.get() == null) { setGovernance(_governance); } + NATIVE_NID = Context.call(String.class, getXCall(), "getNetworkId"); if (this.currentVersion.getOrDefault("").equals(Versions.XCALL_MANAGER)) { Context.revert("Can't Update same version of code"); } @@ -96,6 +99,11 @@ public Map getProtocols(String nid) { @External(readonly = true) public void verifyProtocols(String nid, @Optional String[] protocols) { + if (nid.equals(NATIVE_NID)) { + // Is a rollback + return; + } + ProtocolConfig cfg = this.protocols.get(nid); Context.require(cfg != null, TAG + ": Network is not configured"); if (cfg.sources.length == 0) { diff --git a/xcall-annotations/build.gradle b/xcall-annotations/build.gradle new file mode 100644 index 000000000..1ab0fc306 --- /dev/null +++ b/xcall-annotations/build.gradle @@ -0,0 +1,43 @@ +/* + * 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.Dependencies + +plugins { + id 'java' +} + +version '0.1.0' + +repositories { + mavenCentral() +} + +optimizedJar.enabled = false + +dependencies { + implementation Dependencies.javaeeApi + implementation Dependencies.javaeeScorex + implementation Dependencies.minimalJson + implementation("foundation.icon:javaee-annotation-processor:0.9.0") + compileOnly("com.squareup:javapoet:1.12.1") + + compileOnly Dependencies.javaeeScoreClient + annotationProcessor Dependencies.javaeeScoreClient + + implementation Dependencies.jacksonDatabind + implementation Dependencies.iconSdk +} diff --git a/xcall-annotations/src/main/java/network/balanced/score/lib/annotations/XCall.java b/xcall-annotations/src/main/java/network/balanced/score/lib/annotations/XCall.java new file mode 100644 index 000000000..d2cf77ad4 --- /dev/null +++ b/xcall-annotations/src/main/java/network/balanced/score/lib/annotations/XCall.java @@ -0,0 +1,28 @@ +/* + * 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.lib.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.SOURCE) +public @interface XCall { + String suffix() default "XCall"; +} \ No newline at end of file diff --git a/xcall-annotations/src/main/java/network/balanced/score/lib/annotations/XCallProcessor.java b/xcall-annotations/src/main/java/network/balanced/score/lib/annotations/XCallProcessor.java new file mode 100644 index 000000000..971393193 --- /dev/null +++ b/xcall-annotations/src/main/java/network/balanced/score/lib/annotations/XCallProcessor.java @@ -0,0 +1,247 @@ +/* + * 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.lib.annotations; + +import com.squareup.javapoet.*; +import foundation.icon.annotation_processor.AbstractProcessor; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.*; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import java.io.IOException; +import java.util.*; +import java.util.stream.Stream; +import java.util.stream.Collectors; + +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.annotation.Optional; + +import network.balanced.score.lib.utils.RLPUtils; + +public class XCallProcessor extends AbstractProcessor { + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + } + + @Override + public Set getSupportedAnnotationTypes() { + Set s = new HashSet<>(); + s.add(XCall.class.getCanonicalName()); + return s; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + Map> classMethods = new HashMap<>(); + Map classes = new HashMap<>(); + + boolean claimed = false; + for (TypeElement annotation : annotations) { + Set annotationElements = roundEnv.getElementsAnnotatedWith(annotation); + if (annotationElements.isEmpty()) { + continue; + } + + claimed = true; + for (Element element : annotationElements) { + TypeElement typeElement = (TypeElement) element.getEnclosingElement(); + String elementClassName = ClassName.get(typeElement).toString(); + List methods = classMethods.getOrDefault(elementClassName, new ArrayList()); + methods.add(element); + classMethods.put(elementClassName, methods); + classes.put(elementClassName, typeElement); + } + } + + classes.forEach((className, classType) -> { + if (classMethods.get(className).size() == 0) { + return; + } + + classType.getInterfaces().forEach(type -> { + List superList = classMethods.get(ClassName.get(type).toString()); + if (superList == null || superList.size() == 0) { + return; + } + List concatedList = Stream.concat(classMethods.get(className).stream(), superList.stream()).collect(Collectors.toList()); + classMethods.put(className, concatedList); + + }); + + if (!classType.getSuperclass().getKind().equals(TypeKind.NONE)) { + List superList = classMethods.get(ClassName.get(classType.getSuperclass()).toString()); + if (superList == null || superList.size() == 0) { + return; + } + + List concatedList = Stream.concat(classMethods.get(className).stream(), superList.stream()).collect(Collectors.toList()); + classMethods.put(className, concatedList); + } + }); + + classMethods.forEach((className, methodElements) -> { + generateProcessorClass(processingEnv.getFiler(), ClassName.bestGuess(className), methodElements); + generateMessageClass(processingEnv.getFiler(), ClassName.bestGuess(className), methodElements); + }); + + return claimed; + } + + private void generateProcessorClass(Filer filer, ClassName elementClassName, List elements) { + Element element = elements.iterator().next(); + XCall ann = element.getAnnotation(XCall.class); + ClassName className = ClassName.get(elementClassName.packageName(), elementClassName.simpleName() + ann.suffix()); + + TypeSpec typeSpec = processorTypeSpec(elementClassName, className, elements); + JavaFile javaFile = JavaFile.builder(className.packageName(), typeSpec).build(); + try { + javaFile.writeTo(filer); + } catch (IOException e) { + messager.warningMessage("create javaFile error : %s", e.getMessage()); + } + } + + private void generateMessageClass(Filer filer, ClassName elementClassName, List elements) { + ClassName className = ClassName.get(elementClassName.packageName(), elementClassName.simpleName() + "Messages"); + + TypeSpec typeSpec = messagesTypeSpec(className, elements); + JavaFile javaFile = JavaFile.builder(className.packageName(), typeSpec).build(); + try { + javaFile.writeTo(filer); + } catch (IOException e) { + messager.warningMessage("create javaFile error : %s", e.getMessage()); + } + } + + private TypeSpec processorTypeSpec(ClassName elementClassName, ClassName className, List elements) { + TypeSpec.Builder builder = TypeSpec + .classBuilder(className) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + MethodSpec.Builder handleMethod = MethodSpec.methodBuilder("process") + .addModifiers(Modifier.PUBLIC) + .addModifiers(Modifier.STATIC) + .addParameter(elementClassName, "score") + .addParameter(ParameterSpec.builder(String.class, "from").build()) + .addParameter(ParameterSpec.builder(byte[].class, "data").build()) + .addStatement("$T reader = $T.newByteArrayObjectReader(\"RLPn\", data)", ObjectReader.class, Context.class) + .addStatement("reader.beginList()") + .addStatement("String method = reader.readString().toLowerCase()") + .beginControlFlow("switch (method)"); + + for (Element element : elements) { + Name methodName = element.getSimpleName(); + ExecutableElement executableElement = (ExecutableElement) element; + + List parameters = executableElement.getParameters(); + VariableElement fromParam = parameters.get(0); + if(!fromParam.toString().equals("from") || !fromParam.asType().toString().endsWith(".String")) { + throw new RuntimeException("First parameter in a XCall must be the from parameter, (String from)"); + } + + handleMethod.addCode("case $S: \n", methodName.toString().toLowerCase()); + handleMethod.addCode("$> score." + methodName + "(from"); + + for (int i = 1; i < parameters.size(); i++) { + String nullable = ""; + if (parameters.get(i).getAnnotation(Optional.class) != null) { + nullable = "Nullable"; + } + TypeMirror type = parameters.get(i).asType(); + if (type.getKind() == TypeKind.ARRAY) { + if (((ArrayType)type).getComponentType().toString().equals("java.lang.String")) { + handleMethod.addCode(", $T.readStringArray(reader)", RLPUtils.class); + } else { + handleMethod.addCode(", reader.read$L($T.class)", nullable, type); + } + } else { + handleMethod.addCode(", reader.read$L($T.class)", nullable, type); + } + } + + handleMethod.addCode(");\n$<"); + handleMethod.addStatement("$>break$<"); + + } + + handleMethod.addCode("default: \n"); + handleMethod.addStatement("$>$T.revert($S)$<", Context.class, "Method does not exist"); + handleMethod.endControlFlow(); + builder.addMethod(handleMethod.build()); + + return builder.build(); + } + + private TypeSpec messagesTypeSpec(ClassName className, List elements) { + TypeSpec.Builder builder = TypeSpec + .classBuilder(className) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + for (Element element : elements) { + Name methodName = element.getSimpleName(); + ExecutableElement executableElement = (ExecutableElement) element; + List parameters = executableElement.getParameters(); + + MethodSpec.Builder createMethod = MethodSpec.methodBuilder(methodName.toString()) + .addModifiers(Modifier.PUBLIC) + .addModifiers(Modifier.STATIC) + .returns(byte[].class) + .addStatement("$T writer = $T.newByteArrayObjectWriter(\"RLPn\")", ByteArrayObjectWriter.class, Context.class) + .addStatement("writer.beginList($L)", parameters.size()) + .addStatement("writer.write($S)", methodName.toString()); + + for (int i = 1; i < parameters.size(); i++) { + TypeMirror typeMirror = parameters.get(i).asType(); + TypeName type = TypeName.get(typeMirror); + Name name = parameters.get(i).getSimpleName(); + createMethod.addParameter(type, name.toString()); + + if (typeMirror.getKind() == TypeKind.ARRAY) { + ArrayType arrayType = (ArrayType)typeMirror; + if (arrayType.getComponentType().toString().equals("java.lang.String")) { + createMethod.addStatement("writer.beginList($L.length)", name.toString()) + .beginControlFlow("for ($T item: $L)", arrayType.getComponentType(), name.toString()) + .addStatement("writer.write(item)") + .endControlFlow() + .addStatement("writer.end()"); + continue; + } + } + createMethod.addStatement("writer.write($L)", name.toString()); + } + + createMethod.addStatement("writer.end()"); + createMethod.addStatement("return writer.toByteArray()"); + builder.addMethod(createMethod.build()); + } + + return builder.build(); + } +} \ No newline at end of file diff --git a/xcall-annotations/src/main/java/network/balanced/score/lib/utils/RLPUtils.java b/xcall-annotations/src/main/java/network/balanced/score/lib/utils/RLPUtils.java new file mode 100644 index 000000000..b8ffb8f0b --- /dev/null +++ b/xcall-annotations/src/main/java/network/balanced/score/lib/utils/RLPUtils.java @@ -0,0 +1,48 @@ + +/* + * 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.lib.utils; + +import score.ByteArrayObjectWriter; +import score.Context; +import score.ObjectReader; +import score.ObjectWriter; +import scorex.util.ArrayList; + +import java.math.BigInteger; +import java.util.List; + +public class RLPUtils { + + public static String[] readStringArray(ObjectReader r) { + if( !r.hasNext() ) { + return new String[]{}; + } + + r.beginList(); + List lst = new ArrayList<>(); + while(r.hasNext()) { + lst.add(r.readString()); + } + int size = lst.size(); + String[] arr = new String[size]; + for(int i=0; i < size; i++) { + arr[i] = lst.get(i); + } + r.end(); + return arr; + } +} diff --git a/xcall-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/xcall-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 000000000..81086626a --- /dev/null +++ b/xcall-annotations/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +network.balanced.score.lib.annotations.XCallProcessor \ No newline at end of file diff --git a/xcall-lib b/xcall-lib deleted file mode 160000 index fb8163489..000000000 --- a/xcall-lib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fb8163489f2cbb5851718eeb3d60452f5f05e754