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 a321f6f53..36395a7cc 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 @@ -245,8 +245,18 @@ public void xTokenFallback(String _from, BigInteger _value, byte[] _data) { executeRoute(_from, _data); } + private RouteData getRouteData(byte[] data){ + RouteData routeData; + try { + routeData = RouteData.fromBytes(data); + }catch(IllegalStateException ignored ){ + routeData = RouteData.fromBytesOld(data); + } + return routeData; + } + private void executeRoute(String _from, byte[] data) { - RouteData routeData = RouteData.fromBytes(data); + RouteData routeData = getRouteData(data); Context.require(routeData.method.contains("_swap"), TAG + ": Fallback directly not allowed."); Address fromToken = Context.getCaller(); @@ -261,8 +271,11 @@ private void executeRoute(String _from, byte[] data) { } else { receiver = _from; } - - route(receiver, fromToken, routeData.actions, minimumReceive, EMPTY_DATA); + byte[] _data = EMPTY_DATA; + if(routeData.data!=null){ + _data = routeData.data; + } + route(receiver, fromToken, routeData.actions, minimumReceive, _data); } private void jsonRoute(String _from, byte[] data) { @@ -308,7 +321,11 @@ private void jsonRoute(String _from, byte[] data) { } Address fromToken = Context.getCaller(); - route(receiver, fromToken, actions, minimumReceive, EMPTY_DATA); + byte[] _data = EMPTY_DATA; + if(params.contains("data")){ + _data = params.get("data").asString().getBytes(); + } + route(receiver, fromToken, actions, minimumReceive, _data); } @Payable @@ -318,4 +335,4 @@ public void fallback() { @EventLog(indexed = 1) public void Route(Address from, BigInteger fromAmount, Address to, BigInteger toAmount) { } -} +} \ No newline at end of file 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 dcd5823fd..f16e7764e 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 @@ -28,14 +28,10 @@ import network.balanced.score.lib.structs.RouteData; import network.balanced.score.lib.test.mock.MockBalanced; import network.balanced.score.lib.test.mock.MockContract; -import network.balanced.score.lib.tokens.HubTokenImpl; -import network.balanced.score.lib.tokens.IRC2Base; -import network.balanced.score.lib.tokens.IRC2Mintable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import score.Address; -import score.Context; import scorex.util.ArrayList; import java.math.BigInteger; @@ -44,11 +40,8 @@ import static network.balanced.score.core.router.RouterImpl.*; import static network.balanced.score.lib.test.UnitTest.*; -import static network.balanced.score.lib.utils.Constants.EXA; 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; @@ -182,14 +175,7 @@ void tokenFallback() throws Exception { when(balanced.baln.mock.balanceOf(routerScore.getAddress())).thenReturn(BigInteger.TEN); when(balanced.sicx.mock.balanceOf(routerScore.getAddress())).thenReturn(BigInteger.TEN); -// byte[] invalidPathWithSicxTerminalToken = tokenData("_swap", Map.of("path", -// 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", @@ -501,7 +487,42 @@ void tokenFallback_swapStable() throws Exception { } Account newReceiver = sm.createAccount(); - byte[] data = new RouteData("_swap", newReceiver.getAddress().toString(), BigInteger.ZERO, actions).toBytes(); + byte[] data = new RouteData("_swap", newReceiver.getAddress().toString(), BigInteger.ZERO, actions, null).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_swapStableOldRouteData() 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, null).toBytesOld(); // Act routerScore.invoke(balanced.baln.account, "tokenFallback", owner.getAddress(), balnToSwap, @@ -531,7 +552,30 @@ void tokenFallback_swapToICX() throws Exception { routerScore.getAccount().addBalance("ICX", ICXResult); Account newReceiver = sm.createAccount(); - byte[] data = new RouteData("_swap", newReceiver.getAddress().toString(), BigInteger.ZERO, actions).toBytes(); + byte[] data = new RouteData("_swap", newReceiver.getAddress().toString(), BigInteger.ZERO, actions, null).toBytes(); + + // Act + routerScore.invoke(balanced.baln.account, "tokenFallback", owner.getAddress(), USDToSwap, + data); + + // Assert + assertEquals(ICXResult, newReceiver.getBalance()); + } + + @Test + void tokenFallback_swapToICXOldRouteData() throws Exception { + // Arrange + BigInteger USDToSwap = BigInteger.TEN.multiply(ICX); + BigInteger sICXResult = BigInteger.valueOf(100).multiply(ICX); + BigInteger ICXResult = BigInteger.valueOf(110).multiply(ICX); + List actions = new ArrayList<>(2); + actions.add(new RouteAction(SWAP, balanced.sicx.getAddress())); + actions.add(new RouteAction(SWAP, null)); + when(balanced.sicx.mock.balanceOf(routerScore.getAddress())).thenReturn(sICXResult); + routerScore.getAccount().addBalance("ICX", ICXResult); + + Account newReceiver = sm.createAccount(); + byte[] data = new RouteData("_swap", newReceiver.getAddress().toString(), BigInteger.ZERO, actions, null).toBytesOld(); // Act routerScore.invoke(balanced.baln.account, "tokenFallback", owner.getAddress(), USDToSwap, @@ -586,4 +630,51 @@ private void resetInRoute() { // in Production this happens between each tx ((RouterImpl)routerScore.getInstance()).inRoute = false; } + + @Test + void stakeToSavingsWithJSONRoute(){ + // Arrange + //baln as usdc + when(balanced.baln.mock.balanceOf(routerScore.getAddress())).thenReturn(BigInteger.TEN); + when(balanced.bnUSD.mock.balanceOf(routerScore.getAddress())).thenReturn(BigInteger.TEN); + + String data = new String(tokenData("_lock", Map.of())); + Account newReceiver = balanced.savings.account; + byte[] pathWithUSDCBnUSD = tokenData("_swap", Map.of("path", + new Object[]{balanced.bnUSD.getAddress().toString()}, "receiver", + newReceiver.getAddress().toString(), "data", data)); + + // Act + routerScore.invoke(balanced.baln.account, "tokenFallback", owner.getAddress(), BigInteger.TEN, + pathWithUSDCBnUSD); + + + // Verify + verify(balanced.bnUSD.mock).transfer(balanced.savings.getAddress(), BigInteger.TEN, data.getBytes()); + } + + @Test + void stakeToSavingsWithRLPData(){ + // Arrange + //baln as usdc + BigInteger balnToSwap = BigInteger.TEN.multiply(ICX); + when(balanced.baln.mock.balanceOf(routerScore.getAddress())).thenReturn(balnToSwap); + when(balanced.bnUSD.mock.balanceOf(routerScore.getAddress())).thenReturn(balnToSwap); + + List actions = new ArrayList<>(1); + actions.add(new RouteAction(SWAP, balanced.bnUSD.getAddress())); + + byte[] data = tokenData("_lock", Map.of()); + Account newReceiver = balanced.savings.account; + byte[] routeData = new RouteData("_swap", newReceiver.getAddress().toString(), BigInteger.ZERO, actions, data).toBytes(); + + // Act + routerScore.invoke(balanced.baln.account, "tokenFallback", owner.getAddress(), balnToSwap, + routeData); + + + // Verify + verify(balanced.bnUSD.mock).transfer(balanced.savings.getAddress(), balnToSwap, data); + } + } diff --git a/core-contracts/Savings/build.gradle b/core-contracts/Savings/build.gradle index 4ac6a4273..522cfad9a 100644 --- a/core-contracts/Savings/build.gradle +++ b/core-contracts/Savings/build.gradle @@ -30,6 +30,7 @@ dependencies { implementation Dependencies.javaeeScorex implementation Dependencies.minimalJson implementation project(':score-lib') + implementation 'xyz.venture23:xcall-lib:2.1.0' testImplementation Dependencies.javaeeUnitTest testImplementation Dependencies.javaeeTokens diff --git a/core-contracts/Savings/src/intTest/java/network/balanced/score/core/savings/SavingsIntegrationTest.java b/core-contracts/Savings/src/intTest/java/network/balanced/score/core/savings/SavingsIntegrationTest.java index 2276fe7b9..0b0faec7c 100644 --- a/core-contracts/Savings/src/intTest/java/network/balanced/score/core/savings/SavingsIntegrationTest.java +++ b/core-contracts/Savings/src/intTest/java/network/balanced/score/core/savings/SavingsIntegrationTest.java @@ -24,15 +24,19 @@ import network.balanced.score.lib.test.integration.ScoreIntegrationTest; import network.balanced.score.lib.interfaces.*; + import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import score.Address; import foundation.icon.xcall.NetworkAddress; +import score.ByteArrayObjectWriter; +import score.Context; import java.math.BigInteger; import java.time.Instant; +import java.util.HashMap; import java.util.Map; import static network.balanced.score.lib.test.integration.BalancedUtils.*; @@ -125,6 +129,7 @@ static void setup() throws Exception { // set initial rate to 1 USD updatePrice(rate); + addCollateral(balanced.ethBaseAsset, "ETH"); } @Test @@ -134,7 +139,6 @@ void takeLoanAndLockInSavings() throws Exception { BigInteger loanAmount = BigInteger.TEN.pow(21); user.stakeDepositAndBorrow(collateralAmount, loanAmount); - JsonObject deposit = new JsonObject().add("method", "_deposit"); JsonObject lock = new JsonObject().add("method", "_lock"); user.bnUSD.transfer(balanced.savings._address(), loanAmount, lock.toString().getBytes()); @@ -285,7 +289,7 @@ void depositYieldAssetToStability() throws Exception { @Test @Order(7) - void withdrawYieldAssetFromStability() throws Exception { + void withdrawYieldAssetFromStability() { // Arrange BigInteger collateralAmount = BigInteger.TEN.pow(22); BigInteger amount = BigInteger.TEN.pow(20); @@ -307,6 +311,150 @@ void withdrawYieldAssetFromStability() throws Exception { assertEquals(prevYield, reader.feeHandler.getStabilityFundYieldFeesAccrued()); } + private static void addCollateral(foundation.icon.jsonrpc.Address collateralAddress, String peg) { + BigInteger lockingRatio = BigInteger.valueOf(40_000); + BigInteger debtCeiling = BigInteger.TEN.pow(30); + + BigInteger liquidationRatio = BigInteger.valueOf(14_000); + BigInteger liquidatorFee = BigInteger.valueOf(800); + BigInteger daofundFee = BigInteger.valueOf(200); + + JsonArray addCollateralParameters = new JsonArray() + .add(createParameter(collateralAddress)) + .add(createParameter(true)) + .add(createParameter(peg)) + .add(createParameter(lockingRatio)) + .add(createParameter(debtCeiling)) + .add(createParameter(liquidationRatio)) + .add(createParameter(liquidatorFee)) + .add(createParameter(daofundFee)); + + JsonArray actions = new JsonArray() + .add(createTransaction(balanced.governance._address(), "addCollateral", addCollateralParameters)); + + String symbol = reader.irc2(collateralAddress).symbol(); + owner.governance.execute(actions.toString()); + + assertEquals(lockingRatio, reader.loans.getLockingRatio(symbol)); + assertEquals(debtCeiling, reader.loans.getDebtCeiling(symbol)); + } + + @Test + @Order(8) + void crossChainLock(){ + + // Arrange - prepare accounts + NetworkAddress ethBnUSD = new NetworkAddress(balanced.ETH_NID, balanced.ETH_BNUSD_ADDRESS); + NetworkAddress ethAccount = new NetworkAddress(balanced.ETH_NID, "0x123"); + String savingsAddress = new NetworkAddress(balanced.ICON_NID, balanced.savings._address()).toString(); + String loanAddress = new NetworkAddress(balanced.ICON_NID, balanced.loans._address()).toString(); + BigInteger loanAmount = BigInteger.valueOf(20).multiply(EXA); + + // Arrange - deposit and borrow bnUSD loan + JsonObject loanData = new JsonObject() + .add("_amount", loanAmount.toString()); + byte[] depositAndBorrowETH = AssetManagerMessages.deposit(balanced.ETH_TOKEN_ADDRESS, "0x123", loanAddress, BigInteger.valueOf(200).multiply(EXA), loanData.toString().getBytes()); + owner.xcall.recvCall(balanced.assetManager._address(), new NetworkAddress(balanced.ETH_NID, balanced.ETH_ASSET_MANAGER).toString(), depositAndBorrowETH); + + // Arrange - prepare data + byte[] data = tokenData("_lock", new JsonObject()); + byte[] lockData = getCrossTransferData(ethAccount.toString(), savingsAddress, loanAmount, data); + + // Act + owner.xcall.recvCall(owner.bnUSD._address(), ethBnUSD.toString(), lockData); + + //Verify + BigInteger lockedAmount = owner.savings.getLockedAmount(ethAccount.toString()); + assertEquals(lockedAmount, loanAmount); + + } + + @Test + @Order(9) + void crossChainClaimRewards(){ + + // Arrange - prepare accounts + NetworkAddress ethBnUSD = new NetworkAddress(balanced.ETH_NID, balanced.ETH_BNUSD_ADDRESS); + NetworkAddress ethAccount = new NetworkAddress(balanced.ETH_NID, "0x123"); + String savingsAddress = new NetworkAddress(balanced.ICON_NID, balanced.savings._address()).toString(); + String loanAddress = new NetworkAddress(balanced.ICON_NID, balanced.loans._address()).toString(); + BigInteger loanAmount = BigInteger.valueOf(20).multiply(EXA); + + // Arrange - deposit and borrow bnUSD loan + JsonObject loanData = new JsonObject() + .add("_amount", loanAmount.toString()); + byte[] depositAndBorrowETH = AssetManagerMessages.deposit(balanced.ETH_TOKEN_ADDRESS, "0x123", loanAddress, BigInteger.valueOf(200).multiply(EXA), loanData.toString().getBytes()); + owner.xcall.recvCall(balanced.assetManager._address(), new NetworkAddress(balanced.ETH_NID, balanced.ETH_ASSET_MANAGER).toString(), depositAndBorrowETH); + + // Arrange - prepare data + byte[] data = tokenData("_lock", new JsonObject()); + byte[] lockData = getCrossTransferData(ethAccount.toString(), savingsAddress, loanAmount, data); + + // Arrange - lock bnUSD + owner.xcall.recvCall(owner.bnUSD._address(), ethBnUSD.toString(), lockData); + + // Arrange - set xcall fee permission + JsonArray setXCallFeePermissionParameters = new JsonArray() + .add(createParameter(balanced.savings._address())).add(createParameter(balanced.ETH_NID)).add(createParameter(true)); + JsonArray actions = new JsonArray() + .add(createTransaction(balanced.daofund._address(), "setXCallFeePermission", setXCallFeePermissionParameters)); + owner.governance.execute(actions.toString()); + + // Act + afterNextDays(2); + owner.xcall.recvCall(owner.savings._address(), ethAccount.toString(), getClaimRewardsData()); + + // Verify + + } + + @Test + @Order(10) + void crossChainUnlock(){ + + // Arrange - prepare accounts + NetworkAddress ethBnUSD = new NetworkAddress(balanced.ETH_NID, balanced.ETH_BNUSD_ADDRESS); + NetworkAddress ethAccount = new NetworkAddress(balanced.ETH_NID, "0x123"); + String savingsAddress = new NetworkAddress(balanced.ICON_NID, balanced.savings._address()).toString(); + String loanAddress = new NetworkAddress(balanced.ICON_NID, balanced.loans._address()).toString(); + BigInteger loanAmount = BigInteger.valueOf(20).multiply(EXA); + + // Arrange - deposit and borrow bnUSD loan + JsonObject loanData = new JsonObject() + .add("_amount", loanAmount.toString()); + byte[] depositAndBorrowETH = AssetManagerMessages.deposit(balanced.ETH_TOKEN_ADDRESS, "0x123", loanAddress, BigInteger.valueOf(200).multiply(EXA), loanData.toString().getBytes()); + owner.xcall.recvCall(balanced.assetManager._address(), new NetworkAddress(balanced.ETH_NID, balanced.ETH_ASSET_MANAGER).toString(), depositAndBorrowETH); + + // Arrange - prepare data + byte[] data = tokenData("_lock", new JsonObject()); + byte[] lockData = getCrossTransferData(ethAccount.toString(), savingsAddress, loanAmount, data); + + // Arrange - lock bnUSD + owner.xcall.recvCall(owner.bnUSD._address(), ethBnUSD.toString(), lockData); + + // Arrange - set xcall fee permission + JsonArray setXCallFeePermissionParameters = new JsonArray() + .add(createParameter(balanced.savings._address())).add(createParameter(balanced.ETH_NID)).add(createParameter(true)); + JsonArray actions = new JsonArray() + .add(createTransaction(balanced.daofund._address(), "setXCallFeePermission", setXCallFeePermissionParameters)); + owner.governance.execute(actions.toString()); + + // Act + afterNextDays(2); + BigInteger unlockAmount = loanAmount.divide(BigInteger.TWO); + BigInteger previousLockedAmount = owner.savings.getLockedAmount(ethAccount.toString()); + owner.xcall.recvCall(owner.savings._address(), ethAccount.toString(), getUnlockData(unlockAmount)); + + //Verify + BigInteger lockedAmount = owner.savings.getLockedAmount(ethAccount.toString()); + assertEquals(lockedAmount, previousLockedAmount.subtract(unlockAmount)); + + } + + void afterNextDays(int days) { + balanced.increaseDay(days); + } + private static void depositHyUSDC(String from, String to, BigInteger amount, byte[] data) { byte[] deposit = AssetManagerMessages.deposit(hyUSDCAddress, from, to, amount, data); owner.xcall.recvCall(balanced.assetManager._address(), @@ -319,4 +467,40 @@ private static void updatePrice(BigInteger rate) { owner.xcall.recvCall(balanced.balancedOracle._address(), new NetworkAddress(balanced.ETH_NID, externalOracle).toString(), priceUpdate); } + + public static byte[] tokenData(String method, JsonObject params) { + JsonObject data = new JsonObject(); + data.set("method", method); + data.set("params", params); + return data.toString().getBytes(); + } + + static byte[] getCrossTransferData(String from, String to, BigInteger value, byte[] data) { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + writer.beginList(5); + writer.write("xcrosstransfer"); + writer.write(from); + writer.write(to); + writer.write(value); + writer.write(data); + writer.end(); + return writer.toByteArray(); + } + + static byte[] getClaimRewardsData() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + writer.beginList(1); + writer.write("xclaimrewards"); + writer.end(); + return writer.toByteArray(); + } + + static byte[] getUnlockData(BigInteger amount) { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + writer.beginList(2); + writer.write("xunlock"); + writer.write(amount); + writer.end(); + return writer.toByteArray(); + } } diff --git a/core-contracts/Savings/src/main/java/network/balanced/score/core/savings/RewardsManager.java b/core-contracts/Savings/src/main/java/network/balanced/score/core/savings/RewardsManager.java index 1a2f6329e..92c44f324 100644 --- a/core-contracts/Savings/src/main/java/network/balanced/score/core/savings/RewardsManager.java +++ b/core-contracts/Savings/src/main/java/network/balanced/score/core/savings/RewardsManager.java @@ -8,6 +8,7 @@ import network.balanced.score.lib.utils.BalancedFloorLimits; import network.balanced.score.lib.utils.EnumerableSetDB; +import network.balanced.score.lib.utils.TokenTransfer; import score.Address; import score.BranchDB; import score.Context; @@ -74,7 +75,6 @@ public static void addWeight(Address token, BigInteger amount) { } public static void changeLock(String user, BigInteger change) { - checkAddressIsICON(user); BigInteger prevAmount = workingBalance.getOrDefault(user, BigInteger.ZERO); BigInteger prevTotal = totalWorkingBalance.getOrDefault(BigInteger.ZERO); updateAllUserRewards(user, prevAmount); @@ -85,10 +85,10 @@ public static void changeLock(String user, BigInteger change) { totalWorkingBalance.set(prevTotal.add(change)); } - public static void claimRewards(Address user) { - updateAllUserRewards(user.toString()); + public static void claimRewards(String user) { + updateAllUserRewards(user); int numberOfTokens = allowedTokens.length(); - DictDB rewards = userRewards.at(user.toString()); + DictDB rewards = userRewards.at(user); for (int i = 0; i < numberOfTokens; i++) { Address token = allowedTokens.at(i); @@ -96,7 +96,7 @@ public static void claimRewards(Address user) { rewards.set(token, null); BalancedFloorLimits.verifyWithdraw(token, amount); if (!amount.equals(BigInteger.ZERO)) { - Context.call(token, "transfer", user, amount, new byte[0]); + TokenTransfer.transfer(token, user, amount, new byte[0]); } } } @@ -130,10 +130,4 @@ public static void removeToken(Address token) { allowedTokens.remove(token); } - // For now only allow ICON addresses to lock - // But keep as string to allow it in the future easily - private static void checkAddressIsICON(String str) { - Context.require(str.length() == Address.LENGTH * 2, "Only ICON addresses are allowed to lock into the saving account at this time"); - Context.require(str.startsWith("hx") || str.startsWith("cx"), "Only ICON addresses are allowed to lock into the saving account at this time"); - } } diff --git a/core-contracts/Savings/src/main/java/network/balanced/score/core/savings/SavingsImpl.java b/core-contracts/Savings/src/main/java/network/balanced/score/core/savings/SavingsImpl.java index 838ba26be..93adf7e0b 100644 --- a/core-contracts/Savings/src/main/java/network/balanced/score/core/savings/SavingsImpl.java +++ b/core-contracts/Savings/src/main/java/network/balanced/score/core/savings/SavingsImpl.java @@ -21,9 +21,7 @@ import static network.balanced.score.lib.utils.BalancedAddressManager.getLoans; import static network.balanced.score.lib.utils.BalancedAddressManager.getTrickler; import static network.balanced.score.lib.utils.BalancedAddressManager.resetAddress; -import static network.balanced.score.lib.utils.Check.checkStatus; -import static network.balanced.score.lib.utils.Check.onlyGovernance; -import static network.balanced.score.lib.utils.Constants.EXA; +import static network.balanced.score.lib.utils.Check.*; import java.math.BigInteger; import java.util.Map; @@ -31,26 +29,23 @@ import com.eclipsesource.json.Json; import com.eclipsesource.json.JsonObject; -import network.balanced.score.lib.utils.BalancedAddressManager; -import network.balanced.score.lib.utils.Names; -import network.balanced.score.lib.utils.Versions; +import network.balanced.score.lib.interfaces.SavingsXCall; +import network.balanced.score.lib.utils.*; import network.balanced.score.lib.interfaces.Savings; -import network.balanced.score.lib.utils.FloorLimited; -import network.balanced.score.lib.utils.BalancedFloorLimits; import score.Address; import score.Context; import score.VarDB; import score.DictDB; import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; public class SavingsImpl extends FloorLimited implements Savings { - public static final String LOCKED_SAVINGS = "Locked savings"; public static final String VERSION = "version"; private static final DictDB totalPayout = Context.newDictDB("TOTAL_PAYOUT", BigInteger.class); private final VarDB currentVersion = Context.newVarDB(VERSION, String.class); - public static final String TAG = Names.SAVINGS; public SavingsImpl(Address _governance) { BalancedAddressManager.setGovernance(_governance); @@ -81,10 +76,6 @@ public Address getAddress(String name) { return getAddressByName(name); } - private BigInteger getBnUSDBalance() { - return Context.call(BigInteger.class, getBnusd(), "balanceOf", Context.getAddress()); - } - @External public void gatherRewards() { try { @@ -98,19 +89,40 @@ public void gatherRewards() { @External public void unlock(BigInteger amount) { + unlockInternal(Context.getCaller().toString(), amount); + } + + public void xUnlock(String from, BigInteger amount) { + unlockInternal(from, amount); + } + + private void unlockInternal(String user, BigInteger amount){ Context.require(amount.compareTo(BigInteger.ZERO) > 0, "Cannot unlock a negative or zero amount"); - RewardsManager.changeLock(Context.getCaller().toString(), amount.negate()); + RewardsManager.changeLock(user, amount.negate()); Address bnUSD = getBnusd(); BalancedFloorLimits.verifyWithdraw(bnUSD, amount); BigInteger balance = Context.call(BigInteger.class, bnUSD, "balanceOf", Context.getAddress()); Context.require(RewardsManager.getTotalWorkingbalance().compareTo(balance.subtract(amount)) <= 0, "Sanity check, unauthorized withdrawals"); - Context.call(bnUSD, "transfer", Context.getCaller(), amount, new byte[0]); + TokenTransfer.transfer(bnUSD, user, amount); + } + + @External + public void handleCallMessage(String _from, byte[] _data, @Optional String[] _protocols) { + checkStatus(); + only(BalancedAddressManager.getXCall()); + XCallUtils.verifyXCallProtocols(_from, _protocols); + SavingsXCall.process(this, _from, _data); + } + + public void xClaimRewards(String from) { + gatherRewards(); + RewardsManager.claimRewards(from); } @External public void claimRewards() { gatherRewards(); - RewardsManager.claimRewards(Context.getCaller()); + RewardsManager.claimRewards(Context.getCaller().toString()); } @External(readonly = true) @@ -145,6 +157,11 @@ public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { handleTokenDeposit(_from.toString(), _value, _data); } + @External + public void xTokenFallback(String _from, BigInteger _value, byte[] _data) { + handleTokenDeposit(_from, _value, _data); + } + private void handleTokenDeposit(String _from, BigInteger _value, byte[] _data) { checkStatus(); Context.require(_value.compareTo(BigInteger.ZERO) > 0, "Zero transfers not allowed"); @@ -158,9 +175,14 @@ private void handleTokenDeposit(String _from, BigInteger _value, byte[] _data) { String unpackedData = new String(_data); Context.require(!unpackedData.equals(""), "Token Fallback: Data can't be empty"); - JsonObject json = Json.parse(unpackedData).asObject(); - + JsonObject json = Json.parse(unpackedData).asObject(); String method = json.get("method").asString(); + if(json.contains("params")) { + JsonObject params = json.get("params").asObject(); + if (params.get("to") != null) { + _from = params.get("to").asString(); + } + } switch (method) { case "_lock": { Context.require(token.equals(getBnusd()), "Only BnUSD can be locked"); @@ -173,4 +195,9 @@ private void handleTokenDeposit(String _from, BigInteger _value, byte[] _data) { break; } } + + @Payable + public void fallback() { + + } } diff --git a/core-contracts/Savings/src/test/java/network/balanced/score/core/savings/SavingsTest.java b/core-contracts/Savings/src/test/java/network/balanced/score/core/savings/SavingsTest.java index d4f054a76..a0b58d3aa 100644 --- a/core-contracts/Savings/src/test/java/network/balanced/score/core/savings/SavingsTest.java +++ b/core-contracts/Savings/src/test/java/network/balanced/score/core/savings/SavingsTest.java @@ -19,17 +19,20 @@ import com.iconloop.score.test.Account; import com.iconloop.score.test.Score; import com.iconloop.score.test.ServiceManager; +import foundation.icon.xcall.NetworkAddress; import network.balanced.score.lib.test.UnitTest; import network.balanced.score.lib.test.mock.MockBalanced; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; +import score.Address; +import score.ByteArrayObjectWriter; +import score.Context; import java.math.BigInteger; import java.util.Map; import static network.balanced.score.lib.utils.Constants.EXA; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.*; @@ -41,15 +44,18 @@ class SavingsTest extends UnitTest { private MockBalanced mockBalanced; private Score savings; + private final String NATIVE_NID = "0x1.ICON"; @BeforeEach void setup() throws Exception { mockBalanced = new MockBalanced(sm, owner); + when(mockBalanced.xCall.mock.getNetworkId()).thenReturn(NATIVE_NID); savings = sm.deploy(owner, SavingsImpl.class, mockBalanced.governance.getAddress()); savings.invoke(mockBalanced.governance.account, "addAcceptedToken", mockBalanced.sicx.getAddress()); savings.invoke(mockBalanced.governance.account, "addAcceptedToken", mockBalanced.baln.getAddress()); savings.invoke(mockBalanced.governance.account, "addAcceptedToken", mockBalanced.bnUSD.getAddress()); + } @Test @@ -208,4 +214,146 @@ void permissions() { assertOnlyCallableBy(mockBalanced.governance.getAddress(), savings, "addAcceptedToken", mockBalanced.sicx.getAddress()); assertOnlyCallableBy(mockBalanced.governance.getAddress(), savings, "removeAcceptedToken", mockBalanced.sicx.getAddress()); } + + @Test + void lockCrosschainBnUSDWithOutTo() { + //Arrange + String user = new NetworkAddress("0x1.ETH", "0x120").toString(); + BigInteger lockAmount = BigInteger.valueOf(100).multiply(EXA); + + // Act + byte[] data = tokenData("_lock", Map.of()); + savings.invoke(mockBalanced.bnUSD.account, "xTokenFallback", user, lockAmount, data); + + // Assert + BigInteger amountLocked = (BigInteger) savings.call("getLockedAmount", user); + assertEquals(lockAmount, amountLocked); + } + + @Test + void lockCrosschainBnUSDWithTo() { + //Arrange + String user = new NetworkAddress("0x1.ETH", "0x120").toString(); + String toUser = new NetworkAddress("0x1.ETH", "0x121").toString(); + BigInteger lockAmount = BigInteger.valueOf(100).multiply(EXA); + + // Act + byte[] data = tokenData("_lock", Map.of("to", toUser )); + savings.invoke(mockBalanced.bnUSD.account, "xTokenFallback", user, lockAmount, data); + + // Assert + BigInteger amountLocked = (BigInteger) savings.call("getLockedAmount", toUser); + assertEquals(lockAmount, amountLocked); + } + + @Test + @SuppressWarnings("unchecked") + void crossChainClaimRewards() { + //Arrange + String user1 = new NetworkAddress("0x1.ETH", "0x120").toString(); + String user2 = new NetworkAddress("0x1.ETH", "0x121").toString(); + String user3 = new NetworkAddress("0x1.ETH", "0x122").toString(); + BigInteger lockAmount1 = BigInteger.valueOf(100).multiply(EXA); + BigInteger lockAmount2 = BigInteger.valueOf(200).multiply(EXA); + BigInteger lockAmount3 = BigInteger.valueOf(200).multiply(EXA); + + byte[] data = tokenData("_lock", Map.of()); + savings.invoke(mockBalanced.bnUSD.account, "xTokenFallback", user1, lockAmount1, data); + savings.invoke(mockBalanced.bnUSD.account, "xTokenFallback", user2, lockAmount2, data); + + // Act + BigInteger balnRewards = BigInteger.valueOf(100).multiply(EXA); + BigInteger sICXRewards = BigInteger.valueOf(100).multiply(EXA); + BigInteger bnUSDRewards = BigInteger.valueOf(2000).multiply(EXA); + savings.invoke(mockBalanced.baln.account, "tokenFallback", mockBalanced.daofund.getAddress(), balnRewards, new byte[0]); + savings.invoke(mockBalanced.sicx.account, "tokenFallback", mockBalanced.daofund.getAddress(), sICXRewards, new byte[0]); + savings.invoke(mockBalanced.bnUSD.account, "tokenFallback", mockBalanced.daofund.getAddress(), bnUSDRewards, new byte[0]); + savings.invoke(mockBalanced.bnUSD.account, "xTokenFallback", user3, lockAmount3, data); + + // Assert + Map rewards1 = (Map) savings.call("getUnclaimedRewards", user1); + Map rewards2 = (Map) savings.call("getUnclaimedRewards", user2); + Map rewards3 = (Map) savings.call("getUnclaimedRewards", user3); + + BigInteger total = lockAmount1.add(lockAmount2); + BigInteger sICXWeight = sICXRewards.multiply(EXA).divide(total); + BigInteger balnWeight = balnRewards.multiply(EXA).divide(total); + BigInteger bnUSDWeight = bnUSDRewards.multiply(EXA).divide(total); + assertEquals(sICXWeight.multiply(lockAmount1).divide(EXA), rewards1.get(mockBalanced.sicx.getAddress().toString())); + assertEquals(balnWeight.multiply(lockAmount1).divide(EXA), rewards1.get(mockBalanced.baln.getAddress().toString())); + assertEquals(bnUSDWeight.multiply(lockAmount1).divide(EXA), rewards1.get(mockBalanced.bnUSD.getAddress().toString())); + assertEquals(sICXWeight.multiply(lockAmount2).divide(EXA), rewards2.get(mockBalanced.sicx.getAddress().toString())); + assertEquals(balnWeight.multiply(lockAmount2).divide(EXA), rewards2.get(mockBalanced.baln.getAddress().toString())); + assertEquals(bnUSDWeight.multiply(lockAmount2).divide(EXA), rewards2.get(mockBalanced.bnUSD.getAddress().toString())); + assertEquals(BigInteger.ZERO, rewards3.get(mockBalanced.sicx.getAddress().toString())); + assertEquals(BigInteger.ZERO, rewards3.get(mockBalanced.baln.getAddress().toString())); + assertEquals(BigInteger.ZERO, rewards3.get(mockBalanced.bnUSD.getAddress().toString())); + + + // Act + savings.invoke(mockBalanced.baln.account, "tokenFallback", mockBalanced.daofund.getAddress(), balnRewards, new byte[0]); + savings.invoke(mockBalanced.sicx.account, "tokenFallback", mockBalanced.daofund.getAddress(), sICXRewards, new byte[0]); + + // Assert + rewards1 = (Map) savings.call("getUnclaimedRewards", user1); + rewards2 = (Map) savings.call("getUnclaimedRewards", user2); + rewards3 = (Map) savings.call("getUnclaimedRewards", user3); + + BigInteger newTotal = lockAmount1.add(lockAmount2).add(lockAmount3); + BigInteger newSICXWeight = sICXWeight.add(sICXRewards.multiply(EXA).divide(newTotal)); + BigInteger newBalnWeight = balnWeight.add(balnRewards.multiply(EXA).divide(newTotal)); + assertEquals(newSICXWeight.multiply(lockAmount1).divide(EXA), rewards1.get(mockBalanced.sicx.getAddress().toString())); + assertEquals(newBalnWeight.multiply(lockAmount1).divide(EXA), rewards1.get(mockBalanced.baln.getAddress().toString())); + assertEquals(newSICXWeight.multiply(lockAmount2).divide(EXA), rewards2.get(mockBalanced.sicx.getAddress().toString())); + assertEquals(newBalnWeight.multiply(lockAmount2).divide(EXA), rewards2.get(mockBalanced.baln.getAddress().toString())); + assertEquals(newSICXWeight.subtract(sICXWeight).multiply(lockAmount3).divide(EXA), rewards3.get(mockBalanced.sicx.getAddress().toString())); + assertEquals(newBalnWeight.subtract(balnWeight).multiply(lockAmount3).divide(EXA), rewards3.get(mockBalanced.baln.getAddress().toString())); + + assertEquals(sICXRewards.multiply(BigInteger.TWO), savings.call("getTotalPayout", mockBalanced.sicx.getAddress())); + assertEquals(balnRewards.multiply(BigInteger.TWO), savings.call("getTotalPayout", mockBalanced.baln.getAddress())); + assertEquals(bnUSDRewards, savings.call("getTotalPayout", mockBalanced.bnUSD.getAddress())); + + // Act + when(mockBalanced.daofund.mock.getXCallFeePermission(any(Address.class), any(String.class))).thenReturn(true); + savings.invoke(mockBalanced.xCall.account, "handleCallMessage", user1, getClaimRewardsData(), new String[0]); + + // Assert + verify(mockBalanced.sicx.mock).crossTransfer(user1, rewards1.get(mockBalanced.sicx.getAddress().toString()), new byte[0]); + verify(mockBalanced.baln.mock).crossTransfer(user1, rewards1.get(mockBalanced.baln.getAddress().toString()), new byte[0]); + } + + @Test + void xUnlock() { + //Arrange + String user = new NetworkAddress("0x1.ETH", "0x120").toString(); + BigInteger lockAmount = BigInteger.valueOf(100).multiply(EXA); + byte[] data = tokenData("_lock", Map.of()); + savings.invoke(mockBalanced.bnUSD.account, "xTokenFallback", user, lockAmount, data); + + // Act + BigInteger unlockAmount = lockAmount.divide(BigInteger.TWO); + when(mockBalanced.bnUSD.mock.balanceOf(savings.getAddress())).thenReturn(lockAmount); + when(mockBalanced.daofund.mock.getXCallFeePermission(any(Address.class), any(String.class))).thenReturn(true); + savings.invoke(mockBalanced.xCall.account, "handleCallMessage", user, getUnlockData(unlockAmount), new String[0]); + + // Assert + verify(mockBalanced.bnUSD.mock).crossTransfer(user, unlockAmount, new byte[0]); + } + + static byte[] getUnlockData(BigInteger amount) { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + writer.beginList(2); + writer.write("xunlock"); + writer.write(amount); + writer.end(); + return writer.toByteArray(); + } + + static byte[] getClaimRewardsData() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + writer.beginList(1); + writer.write("xclaimrewards"); + writer.end(); + return writer.toByteArray(); + } } \ No newline at end of file diff --git a/crosschain_savings.md b/crosschain_savings.md new file mode 100644 index 000000000..26059a6b3 --- /dev/null +++ b/crosschain_savings.md @@ -0,0 +1,139 @@ +## Crosschain Savings Feature Implementation Steps +The Crosschain Savings feature allows users to deposit(lock) their bnUSD balance on any spoke +chain into savings contract on ICON blockchain to receive the savings rewards as per savings rate. + +#### Deposit bnUSD + Depositing bnUSD on spoke chain is the normal process of bnUSD crossTransfer from spoke chain. But + the data field shouldn't be empty in this case. Data should contain a rlp encoded value as specified on + below code for solana spoke chain: +```javascript + let data = tokenData("_lock", {"to":"solana-test/"+withdrawerKeyPair.publicKey}); + function tokenData(method: string, params: Record): Buffer { + const map = { + method: method, + params: params, + }; + + const jsonString = JSON.stringify(map); + + return Buffer.from(jsonString, "utf-8"); + } +``` + Another way of bnUSD deposit from the spoke chain using other supported tokens is deposit token to + Asset Manager contract of the spoke chain. Doing that the `to` field should be the 'ICON Router + Contract Address' and data should be prepared on the following way. +```javascript + function getLockData(method: string, params: Record): Buffer { + const map = { + method: method, + params: params, + }; + const jsonString = JSON.stringify(map); + return Buffer.from(jsonString, "utf-8"); + } + + function getData( + ): Uint8Array { + let lockData = getLockData("_lock", {"to": to_netwrok_address}); + const receiver = Buffer.from(icon_savings_network_address, "utf8"); + let rlpInput: rlp.Input = [ + "_swap", + receiver, + "0x00", // minimum amount to receive + lockData, + ["0x01", icon_bn_usd] + ]; + return rlp.encode(rlpInput); + } +``` +### Claim rewards +To claim the accumulated rewards as per the savings rate, following is the example used from the solana +spooke chain: +```javascript + function getClaimRewardData( + ): Uint8Array { + let rlpInput: rlp.Input = [ + "xclaimrewards", + ]; + return rlp.encode(rlpInput); + } + + let sources=xmState.sources; + let destinations=xmState.destinations; + let data = getClaimRewardData(); + let envelope = new Envelope( + MessageType.CallMessage, + new CallMessage(data).encode(), + sources, + destinations + ).encode(); + const to = { "0": "0x2.icon/"+icon_savings }; + let sendCallIx = await xcall_program.methods + .sendCall(Buffer.from(envelope), to)... + + function getClaimRewardData( + ): Uint8Array { + let rlpInput: rlp.Input = [ + "xclaimrewards", + ]; + return rlp.encode(rlpInput); + } +``` +### Unlock +To unlock the locked bnUSD: +```javascript + function getUnlockData( + amount: number, + ): Uint8Array { + let rlpInput: rlp.Input = [ + "xunlock", + amount + ]; + return rlp.encode(rlpInput); + } + + let sources=xmState.sources; + let destinations=xmState.destinations; + let data = getUnlockData(amount); // amount to unlock + let envelope = new Envelope( + MessageType.CallMessage, + new CallMessage(data).encode(), + sources, + destinations + ).encode(); + const to = { "0": "0x2.icon/"+icon_savings }; + console.log(Buffer.from(envelope)); + let sendCallIx = await xcall_program.methods + .sendCall(Buffer.from(envelope), to)... + function getUnlockData( + amount: number, + ): Uint8Array { + let rlpInput: rlp.Input = [ + "xunlock", + amount + ]; + return rlp.encode(rlpInput); + } +``` +## USDC Staking feature +USDC staking is a feature in which USDC can be locked to the savings contract to earn the savings +rate rewards. When staking USDC the Route contract is used to convert USDC to bnUSD and then it is transferred +to the savings contract. When unlocking it user will get bnUSD as normal bnUSD unlocking. By crosschain +savings features enabled even spoke chain USDC can be staked now(example is provided above- any supported +token can be staked now from the spoke chain). In ICON chain it can be done by transfering USDC to `Router` +Contract with following data build with java code for example: +```javascript + public static byte[] tokenData(String method, Map params) { + Map map = new HashMap<>(); + map.put("method", method); + map.put("params", params); + JSONObject data = new JSONObject(map); + return data.toString().getBytes(); + } + + String tokenData = new String(tokenData("_lock", Map.of())); + Account newReceiver = balanced.savings.account; + byte[] data = tokenData("_swap", Map.of("path", + new Object[]{balanced.bnUSD.getAddress().toString()}, "receiver", + newReceiver.getAddress().toString(), "data", tokenData)); +``` \ No newline at end of file diff --git a/score-lib/src/main/java/network/balanced/score/lib/interfaces/Savings.java b/score-lib/src/main/java/network/balanced/score/lib/interfaces/Savings.java index a92a41635..c45cd301c 100644 --- a/score-lib/src/main/java/network/balanced/score/lib/interfaces/Savings.java +++ b/score-lib/src/main/java/network/balanced/score/lib/interfaces/Savings.java @@ -18,12 +18,13 @@ 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.addresses.AddressManager; import network.balanced.score.lib.interfaces.base.Name; import network.balanced.score.lib.interfaces.base.Version; -import network.balanced.score.lib.interfaces.tokens.XTokenReceiver; import score.annotation.External; import score.Address; +import score.annotation.Optional; import java.math.BigInteger; import java.util.Map; @@ -37,9 +38,14 @@ public interface Savings extends Name, Version, AddressManager, FloorLimitedInte @External void unlock(BigInteger amount); + @XCall + void xUnlock(String from, BigInteger amount); + @External(readonly = true) BigInteger getTotalPayout(Address token); + @XCall + void xClaimRewards(String from); @External void claimRewards(); 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 index 8b3194b2d..43d091b8a 100644 --- 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 @@ -12,13 +12,16 @@ public class RouteData { public String method; public String receiver; public BigInteger minimumReceive; + public byte[] data; public List actions; + public RouteData(){} - public RouteData(String method, String receiver, BigInteger minimumReceive, List actions) { + public RouteData(String method, String receiver, BigInteger minimumReceive, List actions, byte[] data) { this.method = method; this.receiver = receiver; this.minimumReceive = minimumReceive; + this.data = data; this.actions = actions; } @@ -28,6 +31,23 @@ public RouteData(String method, List 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)); + obj.data = reader.readNullable(byte[].class); + while (reader.hasNext()) { + RouteAction data = reader.read(RouteAction.class); + actions.add(data); + } + obj.actions = actions; + reader.end(); + return obj; + } + + public static RouteData readObjectOld(ObjectReader reader) { RouteData obj = new RouteData(); reader.beginList(); List actions = new ArrayList<>(); @@ -44,6 +64,18 @@ public static RouteData readObject(ObjectReader reader) { } public static void writeObject(ObjectWriter w, RouteData obj) { + w.beginList(obj.actions.size()+4); + w.write(obj.method); + w.writeNullable(obj.receiver); + w.writeNullable(obj.minimumReceive); + w.writeNullable(obj.data); + for (RouteAction action : obj.actions) { + w.write(action); + } + w.end(); + } + + public static void writeObjectOld(ObjectWriter w, RouteData obj) { w.beginList(obj.actions.size()+3); w.write(obj.method); w.writeNullable(obj.receiver); @@ -59,10 +91,20 @@ public static RouteData fromBytes(byte[] bytes) { return RouteData.readObject(reader); } + public static RouteData fromBytesOld(byte[] bytes) { + ObjectReader reader = Context.newByteArrayObjectReader("RLPn", bytes); + return RouteData.readObjectOld(reader); + } + public byte[] toBytes() { ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); RouteData.writeObject(writer, this); return writer.toByteArray(); } + public byte[] toBytesOld() { + ByteArrayObjectWriter writer = Context.newByteArrayObjectWriter("RLPn"); + RouteData.writeObjectOld(writer, this); + return writer.toByteArray(); + } } 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 38bfaa68d..14de8c230 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 @@ -33,7 +33,7 @@ public class Versions { public final static String DEX = "v1.1.8"; public final static String GOVERNANCE = "v1.0.2"; public final static String REBALANCING = "v1.0.0"; - public final static String ROUTER = "v1.1.8"; + public final static String ROUTER = "v1.2.4"; public final static String STAKEDLP = "v1.0.9"; public final static String BOOSTED_BALN = "v1.1.0"; public final static String BRIBING = "v1.0.1"; @@ -42,7 +42,7 @@ public class Versions { public final static String BALANCED_ASSET_MANAGER = "v1.0.6"; public final static String XCALL_MANAGER = "v1.0.2"; public final static String BURNER = "v1.0.0"; - public final static String SAVINGS = "v1.0.0"; + public final static String SAVINGS = "v1.0.1"; public final static String TRICKLER = "v1.0.0"; public final static String SPOKE_ASSET_MANAGER = "v1.0.2";