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/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 ef5d7149b..a4fa3c76c 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,8 +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.Check.*; import java.math.BigInteger; import java.util.Map; @@ -30,18 +29,17 @@ 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.RewardsXCall; +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; public class SavingsImpl extends FloorLimited implements Savings { public static final String VERSION = "version"; @@ -104,10 +102,23 @@ public void unlock(BigInteger amount) { Context.call(bnUSD, "transfer", Context.getCaller(), amount, new byte[0]); } + @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) @@ -142,6 +153,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"); @@ -156,8 +172,11 @@ 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(); - String method = json.get("method").asString(); + 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"); 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..d010afdde 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,11 +19,15 @@ 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; @@ -41,15 +45,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 +215,119 @@ 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]); + } + + 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/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 8fc7e6875..a8597c04e 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,11 +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 score.annotation.External; import score.Address; +import score.annotation.Optional; import java.math.BigInteger; import java.util.Map; @@ -39,6 +41,8 @@ public interface Savings extends Name, Version, AddressManager, FloorLimitedInte @External(readonly = true) BigInteger getTotalPayout(Address token); + @XCall + void xClaimRewards(String from); @External void claimRewards();