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