From 09a8802c4dcc2ab090bcaedb667bdd446e27e462 Mon Sep 17 00:00:00 2001 From: Night Owl Date: Fri, 11 Feb 2022 20:23:16 +0545 Subject: [PATCH 1/5] Add first version of Batch Disbursement contract Signed-off-by: Night Owl --- core-contracts/BatchDisbursement/build.gradle | 70 +++++++ .../batchDisbursement/BatchDisbursement.java | 195 ++++++++++++++++++ .../score/core/batchDisbursement/Checks.java | 37 ++++ .../batchDisbursement/EnumerableSetDB.java | 78 +++++++ settings.gradle | 1 + 5 files changed, 381 insertions(+) create mode 100644 core-contracts/BatchDisbursement/build.gradle create mode 100644 core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java create mode 100644 core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/Checks.java create mode 100644 core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/EnumerableSetDB.java diff --git a/core-contracts/BatchDisbursement/build.gradle b/core-contracts/BatchDisbursement/build.gradle new file mode 100644 index 000000000..a97d5a6c6 --- /dev/null +++ b/core-contracts/BatchDisbursement/build.gradle @@ -0,0 +1,70 @@ + +/* + * 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. + */ + +version = '0.1.0' + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + compileOnly 'foundation.icon:javaee-api:0.9.1' + implementation 'foundation.icon:javaee-scorex:0.5.2' + + testImplementation 'foundation.icon:javaee-unittest:0.9.2' + // Use JUnit Jupiter for testing. + testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + testImplementation 'org.mockito:mockito-core:4.1.0' +} + +optimizedJar { + mainClassName = 'network.balanced.score.core.batchDisbursement.BatchDisbursement' + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from { + configurations.runtimeClasspath.collect{it.isDirectory() ? it : zipTree(it)} + } + enableDebug = false +} + +deployJar { + endpoints { + sejong { + uri = 'https://sejong.net.solidwallet.io/api/v3' + nid = 0x53 + to = "cx75256fadf232ad1124d9c6cd70c9b1ec122a0f47" + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + mainnet { + uri = 'https://ctz.solidwallet.io/api/v3' + nid = 0x1 + to = "cx793970c9ec84eb0dcb6164965c74fe678474d7c7" + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters {} +} + +tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} diff --git a/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java b/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java new file mode 100644 index 000000000..d360ee991 --- /dev/null +++ b/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.batchDisbursement; + +import score.*; +import score.annotation.EventLog; +import score.annotation.External; +import scorex.util.HashMap; + +import java.math.BigInteger; +import java.util.Map; + +import static network.balanced.score.core.batchDisbursement.Checks.onlyGovernance; +import static network.balanced.score.core.batchDisbursement.Checks.onlyOwner; + +public class BatchDisbursement { + + public static final VarDB
governance = Context.newVarDB("governance", Address.class); + public final BranchDB> userClaimableTokens = Context.newBranchDB( + "user_claimable_tokens", BigInteger.class); + public final VarDB
daofund = Context.newVarDB("daofund", Address.class); + public final VarDB
reserveFund = Context.newVarDB("reserve_fund", Address.class); + public final EnumerableSetDB
allowedTokenAddress = new EnumerableSetDB<>("allowed_token_address", + Address.class); + + public static final String TAG = "BatchDisbursement"; + + public BatchDisbursement(Address _governance) { + Context.require(_governance.isContract(), TAG + ": Governance address should be a contract"); + governance.set(_governance); + } + + public static class Disbursement { + public Address tokenAddress; + public BigInteger tokenAmount; + } + + public static class DisbursementRecipient { + public Address recipient; + public Disbursement[] disbursement; + } + + /* + * Event Logs + */ + + @EventLog(indexed = 1) + public void Claim(Address user, Address tokenAddress, BigInteger amount) { + } + + @EventLog(indexed = 1) + protected void TokenTransfer(Address recipient, BigInteger amount, String note) { + } + + /* + * Setters and getters + */ + @External(readonly = true) + public String name() { + return "Balanced Batch Disbursement"; + } + + @External + public void setGovernance(Address _address) { + onlyOwner(); + Context.require(_address.isContract(), TAG + ": Address provided is an EOA Address. Contract address required"); + governance.set(_address); + } + + @External(readonly = true) + public Address getGovernance() { + return governance.get(); + } + + @External + public void setDaofund(Address _address) { + onlyOwner(); + Context.require(_address.isContract(), TAG + ": Address provided is an EOA Address. Contract address required"); + daofund.set(_address); + } + + @External(readonly = true) + public Address getDaofund() { + return daofund.get(); + } + + @External + public void setReserveFund(Address _address) { + onlyOwner(); + Context.require(_address.isContract(), TAG + ": Address provided is an EOA Address. Contract address required"); + reserveFund.set(_address); + } + + @External(readonly = true) + public Address getReserveFund() { + return reserveFund.get(); + } + + @External(readonly = true) + public Map getTokenBalances() { + Map balances = new HashMap<>(); + for (int arrayIndex = 0; arrayIndex < allowedTokenAddress.length(); arrayIndex++) { + Address tokenAddress = allowedTokenAddress.at(arrayIndex); + BigInteger balance = (BigInteger) Context.call(tokenAddress, "balanceOf", Context.getAddress()); + balances.put(tokenAddress, balance); + } + return balances; + } + + @External + public void uploadDisbursementData(DisbursementRecipient[] disbursementRecipients) { + onlyOwner(); + + for (DisbursementRecipient disbursementRecipient : disbursementRecipients) { + Address user = disbursementRecipient.recipient; + DictDB userTokens = userClaimableTokens.at(user); + for (Disbursement disbursement : disbursementRecipient.disbursement) { + Address token = disbursement.tokenAddress; + BigInteger currentAmount = userTokens.getOrDefault(token, BigInteger.ZERO); + userTokens.set(disbursement.tokenAddress, currentAmount.add(disbursement.tokenAmount)); + } + } + } + + @External(readonly = true) + public Map getDisbursementDetail(Address _user) { + + Map userClaimableTokens = new HashMap<>(); + DictDB userTokens = this.userClaimableTokens.at(_user); + + for (int arrayIndex = 0; arrayIndex < allowedTokenAddress.length(); arrayIndex++) { + Address token = allowedTokenAddress.at(arrayIndex); + userClaimableTokens.put(token.toString(), userTokens.getOrDefault(token, BigInteger.ZERO)); + } + return Map.of("user", _user, "claimableTokens", userClaimableTokens); + } + + @External + public void claim() { + Address sender = Context.getCaller(); + DictDB userTokens = userClaimableTokens.at(sender); + + for (int arrayIndex = 0; arrayIndex < allowedTokenAddress.length(); arrayIndex++) { + Address token = allowedTokenAddress.at(arrayIndex); + BigInteger tokenAmount = userTokens.getOrDefault(token, BigInteger.ZERO); + if (tokenAmount != null && tokenAmount.signum() > 0) { + userTokens.set(token, BigInteger.ZERO); + sendToken(token, sender, tokenAmount, TAG + ": tokens claimed"); + Claim(sender, token, tokenAmount); + } + } + } + + @External + public void batchDisburse(Address _source) { + onlyGovernance(); + Context.call(_source, "claim"); + } + + @External + public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { + Address tokenContract = Context.getCaller(); + + Context.require(_from.equals(getDaofund()) || _from.equals(getReserveFund()), TAG+ ": Only receivable from " + + "daofund or reserve contract"); + if (!allowedTokenAddress.contains(tokenContract)) { + allowedTokenAddress.add(tokenContract); + } + } + + private void sendToken(Address tokenAddress, Address to, BigInteger amount, String message) { + try { + Context.call(tokenAddress, "transfer", to, amount, new byte[0]); + TokenTransfer(to, amount, message + ": " + amount + "" + tokenAddress + " tokens sent to " + to); + } catch (Exception e) { + Context.println(e.getMessage()); + Context.revert(TAG + ": Error in sending tokens to user- " + to); + } + } + +} diff --git a/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/Checks.java b/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/Checks.java new file mode 100644 index 000000000..9ccb6d421 --- /dev/null +++ b/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/Checks.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.batchDisbursement; + +import score.Address; +import score.Context; + +public class Checks { + public static Address defaultAddress = new Address(new byte[Address.LENGTH]); + + public static void onlyOwner() { + Address caller = Context.getCaller(); + Address owner = Context.getOwner(); + Context.require(caller.equals(owner), "SenderNotScoreOwner: Sender=" + caller + "Owner=" + owner); + } + + public static void onlyGovernance() { + Address sender = Context.getCaller(); + Address governance = BatchDisbursement.governance.getOrDefault(defaultAddress); + Context.require(!governance.equals(defaultAddress), BatchDisbursement.TAG + ": Governance address not set"); + Context.require(sender.equals(governance), BatchDisbursement.TAG + ": Sender not governance contract"); + } +} diff --git a/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/EnumerableSetDB.java b/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/EnumerableSetDB.java new file mode 100644 index 000000000..fb612e250 --- /dev/null +++ b/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/EnumerableSetDB.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.batchDisbursement; + +import score.ArrayDB; +import score.Context; +import score.DictDB; + +public class EnumerableSetDB { + private final ArrayDB entries; + private final DictDB indexes; + + public EnumerableSetDB(String varKey, Class valueClass) { + // array of valueClass + this.entries = Context.newArrayDB(varKey + "_es_entries", valueClass); + // value => array index + this.indexes = Context.newDictDB(varKey + "_es_index", Integer.class); + } + + public int length() { + return entries.size(); + } + + public V at(int index) { + return entries.get(index); + } + + public boolean contains(V value) { + return indexes.get(value) != null; + } + + public Integer indexOf(V value) { + // returns null if value doesn't exist + Integer result = indexes.get(value); + if (result != null) { + return result - 1; + } + return null; + } + + public void add(V value) { + if (!contains(value)) { + //add new value + entries.add(value); + indexes.set(value, entries.size()); + } + } + + public V remove(V value) { + Integer valueIndex = indexOf(value); + + if (valueIndex != null) { + int lastIndex = entries.size(); + V lastValue = entries.pop(); + indexes.set(value, null); + if (lastIndex != valueIndex) { + entries.set(valueIndex - 1, lastValue); + indexes.set(lastValue, valueIndex); + return lastValue; + } + } + return null; + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 2ab7f4156..0ab4a7905 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,4 +26,5 @@ rootProject.name = 'balanced-java-contracts' include(':core-contracts:Multicall', + ':core-contracts:BatchDisbursement', ':token-contracts') From 5a5d223253eb00e64dd6cd7bfc98c71f95c09001 Mon Sep 17 00:00:00 2001 From: Night Owl Date: Fri, 11 Feb 2022 20:40:30 +0545 Subject: [PATCH 2/5] Remove contract address of previous score Signed-off-by: Night Owl --- core-contracts/BatchDisbursement/build.gradle | 2 -- 1 file changed, 2 deletions(-) diff --git a/core-contracts/BatchDisbursement/build.gradle b/core-contracts/BatchDisbursement/build.gradle index a97d5a6c6..a539b8d69 100644 --- a/core-contracts/BatchDisbursement/build.gradle +++ b/core-contracts/BatchDisbursement/build.gradle @@ -47,7 +47,6 @@ deployJar { sejong { uri = 'https://sejong.net.solidwallet.io/api/v3' nid = 0x53 - to = "cx75256fadf232ad1124d9c6cd70c9b1ec122a0f47" } local { uri = 'http://localhost:9082/api/v3' @@ -56,7 +55,6 @@ deployJar { mainnet { uri = 'https://ctz.solidwallet.io/api/v3' nid = 0x1 - to = "cx793970c9ec84eb0dcb6164965c74fe678474d7c7" } } keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' From 4925e42bc66a2dec95f1ec1be119ba696dff8241 Mon Sep 17 00:00:00 2001 From: Night Owl Date: Fri, 11 Feb 2022 20:43:04 +0545 Subject: [PATCH 3/5] Ignore files Signed-off-by: Night Owl --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4f17b7c9c..8b232c533 100644 --- a/.gitignore +++ b/.gitignore @@ -81,3 +81,5 @@ fabric.properties # Ignore Gradle build output directory build + +.DS_Store From fca2d4b23689be5adeb1a7a8b19f0dede73d02d9 Mon Sep 17 00:00:00 2001 From: Night Owl Date: Tue, 15 Feb 2022 14:53:17 +0545 Subject: [PATCH 4/5] Add unit tests for the BatchDisbursement methods Signed-off-by: Night Owl --- core-contracts/BatchDisbursement/build.gradle | 6 +- .../batchDisbursement/BatchDisbursement.java | 5 +- .../BatchDisbursementTest.java | 208 ++++++++++++++++++ .../core/batchDisbursement/MockDaofund.java | 43 ++++ .../core/batchDisbursement/MockReserve.java | 43 ++++ 5 files changed, 300 insertions(+), 5 deletions(-) create mode 100644 core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/BatchDisbursementTest.java create mode 100644 core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/MockDaofund.java create mode 100644 core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/MockReserve.java diff --git a/core-contracts/BatchDisbursement/build.gradle b/core-contracts/BatchDisbursement/build.gradle index a539b8d69..e233dac00 100644 --- a/core-contracts/BatchDisbursement/build.gradle +++ b/core-contracts/BatchDisbursement/build.gradle @@ -1,4 +1,3 @@ - /* * Copyright (c) 2022-2022 Balanced.network. * @@ -26,6 +25,7 @@ dependencies { compileOnly 'foundation.icon:javaee-api:0.9.1' implementation 'foundation.icon:javaee-scorex:0.5.2' + testImplementation 'com.github.sink772:javaee-tokens:0.6.1' testImplementation 'foundation.icon:javaee-unittest:0.9.2' // Use JUnit Jupiter for testing. testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1' @@ -37,7 +37,7 @@ optimizedJar { mainClassName = 'network.balanced.score.core.batchDisbursement.BatchDisbursement' duplicatesStrategy = DuplicatesStrategy.EXCLUDE from { - configurations.runtimeClasspath.collect{it.isDirectory() ? it : zipTree(it)} + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } } enableDebug = false } @@ -59,7 +59,7 @@ deployJar { } keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' - parameters {} + parameters { arg('_governance', "cx541e2e8b9673e736b727e3f6313ada687539f50f") } } tasks.named('test') { diff --git a/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java b/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java index d360ee991..7149928ea 100644 --- a/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java +++ b/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java @@ -139,12 +139,13 @@ public void uploadDisbursementData(DisbursementRecipient[] disbursementRecipient @External(readonly = true) public Map getDisbursementDetail(Address _user) { - Map userClaimableTokens = new HashMap<>(); + Map userClaimableTokens = new HashMap<>(); DictDB userTokens = this.userClaimableTokens.at(_user); for (int arrayIndex = 0; arrayIndex < allowedTokenAddress.length(); arrayIndex++) { Address token = allowedTokenAddress.at(arrayIndex); - userClaimableTokens.put(token.toString(), userTokens.getOrDefault(token, BigInteger.ZERO)); + BigInteger tokenAmount = userTokens.getOrDefault(token, BigInteger.ZERO); + userClaimableTokens.put(token, tokenAmount); } return Map.of("user", _user, "claimableTokens", userClaimableTokens); } diff --git a/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/BatchDisbursementTest.java b/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/BatchDisbursementTest.java new file mode 100644 index 000000000..dfa9505cc --- /dev/null +++ b/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/BatchDisbursementTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.batchDisbursement; + +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 com.iconloop.score.token.irc2.IRC2Mintable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import score.Address; +import score.Context; + +import java.lang.reflect.InvocationTargetException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class BatchDisbursementTest extends TestBase { + + public static final ServiceManager sm = getServiceManager(); + public static final Account owner = sm.createAccount(); + public static final Account governanceScore = Account.newScoreAccount(1); + + private Score batchDisbursement; + private Score balnTokenScore; + private Score daofundScore; + private Score reserveFundScore; + + List recipients = new ArrayList<>(); + + public static class BalnToken extends IRC2Mintable { + public BalnToken(String _name, String _symbol, int _decimals) { + super(_name, _symbol, _decimals); + } + } + + private void expectErrorMessage(Executable contractCall, String expectedErrorMessage) { + AssertionError e = Assertions.assertThrows(AssertionError.class, contractCall); + assertEquals(expectedErrorMessage, e.getMessage()); + } + + @BeforeEach + void setUp() throws Exception { + batchDisbursement = sm.deploy(owner, BatchDisbursement.class, governanceScore.getAddress()); + assert (batchDisbursement.getAddress().isContract()); + + balnTokenScore = sm.deploy(owner, BalnToken.class, "Balanced Token", "BALN", 18); + assert (balnTokenScore.getAddress().isContract()); + + daofundScore = sm.deploy(owner, MockDaofund.class, balnTokenScore.getAddress()); + assert (daofundScore.getAddress().isContract()); + + reserveFundScore = sm.deploy(owner, MockReserve.class, balnTokenScore.getAddress()); + assert (reserveFundScore.getAddress().isContract()); + + balnTokenScore.invoke(owner, "mintTo", daofundScore.getAddress(), BigInteger.TEN.pow(23)); + balnTokenScore.invoke(owner, "mintTo", reserveFundScore.getAddress(), BigInteger.TEN.pow(23)); + + for (int i = 0; i < 10; i++) { + recipients.add(sm.createAccount()); + } + } + + @Test + @DisplayName("Deployment with non contract address") + void testDeploy() { + Account notContract = sm.createAccount(); + Executable deploymentWithNonContract = () -> sm.deploy(owner, BatchDisbursement.class, + notContract.getAddress()); + + String expectedErrorMessage = "BatchDisbursement: Governance address should be a contract"; + InvocationTargetException e = Assertions.assertThrows(InvocationTargetException.class, + deploymentWithNonContract); + assertEquals(expectedErrorMessage, e.getCause().getMessage()); + } + + @Test + void name() { + String contractName = "Balanced Batch Disbursement"; + assertEquals(contractName, batchDisbursement.call("name")); + } + + @Test + void setAndGetGovernance() { + assertEquals(governanceScore.getAddress(), batchDisbursement.call("getGovernance")); + Address newGovernance = Account.newScoreAccount(2).getAddress(); + batchDisbursement.invoke(owner, "setGovernance", newGovernance); + assertEquals(newGovernance, batchDisbursement.call("getGovernance")); + } + + @Test + void setAndGetDaofund() { + Account nonOwner = sm.createAccount(); + + Executable nonOwnerCall = () -> batchDisbursement.invoke(nonOwner, "setDaofund", daofundScore.getAddress()); + String expectedErrorMessage = + "SenderNotScoreOwner: Sender=" + nonOwner.getAddress() + "Owner=" + owner.getAddress(); + expectErrorMessage(nonOwnerCall, expectedErrorMessage); + + batchDisbursement.invoke(owner, "setDaofund", daofundScore.getAddress()); + assertEquals(daofundScore.getAddress(), batchDisbursement.call("getDaofund")); + } + + @Test + void setAndGetReserveFund() { + batchDisbursement.invoke(owner, "setReserveFund", reserveFundScore.getAddress()); + assertEquals(reserveFundScore.getAddress(), batchDisbursement.call("getReserveFund")); + } + + @Test + void getTokenBalances() { + assertEquals(Map.of(), batchDisbursement.call("getTokenBalances")); + + setAndGetDaofund(); + setAndGetReserveFund(); + + batchDisbursement.invoke(governanceScore, "batchDisburse", daofundScore.getAddress()); + assertEquals(Map.of(balnTokenScore.getAddress(), BigInteger.TEN.pow(22)), batchDisbursement.call( + "getTokenBalances")); + + batchDisbursement.invoke(governanceScore, "batchDisburse", reserveFundScore.getAddress()); + assertEquals(Map.of(balnTokenScore.getAddress(), BigInteger.TEN.pow(22).multiply(BigInteger.TWO)), + batchDisbursement.call("getTokenBalances")); + } + + @Test + void uploadDisbursementData() { + getTokenBalances(); + + BatchDisbursement.DisbursementRecipient[] batchRecipients = new BatchDisbursement.DisbursementRecipient[10]; + BatchDisbursement.Disbursement disbursement = new BatchDisbursement.Disbursement(); + disbursement.tokenAddress = balnTokenScore.getAddress(); + disbursement.tokenAmount = BigInteger.valueOf(500L); + + for (int j = 0; j < 10; j++) { + batchRecipients[j] = new BatchDisbursement.DisbursementRecipient(); + batchRecipients[j].recipient = recipients.get(j).getAddress(); + batchRecipients[j].disbursement = new BatchDisbursement.Disbursement[]{disbursement}; + } + + batchDisbursement.invoke(owner, "uploadDisbursementData", (Object) batchRecipients); + for (int i = 0; i < 10; i++) { + Address expectedRecipient = batchRecipients[i].recipient; + BigInteger expectedAmount = batchRecipients[i].disbursement[0].tokenAmount; + Address expectedTokenAddress = batchRecipients[i].disbursement[0].tokenAddress; + + Map disbursementDetail = (Map) batchDisbursement.call( + "getDisbursementDetail", expectedRecipient); + Address actualRecipient = (Address) disbursementDetail.get("user"); + Map claimableTokens = (Map) disbursementDetail.get( + "claimableTokens"); + BigInteger actualAmount = claimableTokens.get(expectedTokenAddress); + + assertEquals(expectedRecipient, actualRecipient); + assertEquals(expectedAmount, actualAmount); + } + } + + @Test + void claim() { + uploadDisbursementData(); + + for (int i = 0; i < 10; i++) { + Account user = recipients.get(i); + assertEquals(BigInteger.ZERO, balnTokenScore.call("balanceOf", user.getAddress())); + batchDisbursement.invoke(recipients.get(i), "claim"); + assertEquals(BigInteger.valueOf(500L), balnTokenScore.call("balanceOf", user.getAddress())); + + // Try claiming once more, should not disburse tokens + batchDisbursement.invoke(recipients.get(i), "claim"); + assertEquals(BigInteger.valueOf(500L), balnTokenScore.call("balanceOf", user.getAddress())); + } + } + + @Test + void tokenFallback() { + setAndGetDaofund(); + setAndGetReserveFund(); + + balnTokenScore.invoke(owner, "mint", BigInteger.TEN.pow(25)); + Executable transferFromInvalidAddress = () -> balnTokenScore.invoke(owner, "transfer", + batchDisbursement.getAddress(), BigInteger.TEN.pow(21), new byte[0]); + String expectedErrorMessage = "BatchDisbursement: Only receivable from daofund or reserve contract"; + expectErrorMessage(transferFromInvalidAddress, expectedErrorMessage); + } +} \ No newline at end of file diff --git a/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/MockDaofund.java b/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/MockDaofund.java new file mode 100644 index 000000000..7818902c7 --- /dev/null +++ b/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/MockDaofund.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.batchDisbursement; + +import score.Address; +import score.Context; +import score.VarDB; +import score.annotation.External; + +import java.math.BigInteger; + +public class MockDaofund { + + private VarDB
balnToken = Context.newVarDB("baln_token", Address.class); + + public MockDaofund(Address balnToken) { + this.balnToken.set(balnToken); + } + + @External + public void claim() { + Context.call(balnToken.get(), "transfer", Context.getCaller(), BigInteger.TEN.pow(22), new byte[0]); + } + + @External + public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { + + } +} diff --git a/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/MockReserve.java b/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/MockReserve.java new file mode 100644 index 000000000..337c0afdf --- /dev/null +++ b/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/MockReserve.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package network.balanced.score.core.batchDisbursement; + +import score.Address; +import score.Context; +import score.VarDB; +import score.annotation.External; + +import java.math.BigInteger; + +public class MockReserve { + + private VarDB
balnToken = Context.newVarDB("baln_token", Address.class); + + public MockReserve(Address balnToken) { + this.balnToken.set(balnToken); + } + + @External + public void claim() { + Context.call(balnToken.get(), "transfer", Context.getCaller(), BigInteger.TEN.pow(22), new byte[0]); + } + + @External + public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { + + } +} From a9d7fb01b566046e6fc65d1637596ab608aec2a9 Mon Sep 17 00:00:00 2001 From: Night Owl Date: Thu, 17 Feb 2022 21:11:44 +0545 Subject: [PATCH 5/5] Fix readonly methods The return type for Map should be a valid Json. They Key and value should be according to the JSON standard. Signed-off-by: Night Owl --- core-contracts/BatchDisbursement/build.gradle | 3 ++- .../core/batchDisbursement/BatchDisbursement.java | 10 +++++----- .../core/batchDisbursement/BatchDisbursementTest.java | 11 +++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/core-contracts/BatchDisbursement/build.gradle b/core-contracts/BatchDisbursement/build.gradle index e233dac00..3917ae5b2 100644 --- a/core-contracts/BatchDisbursement/build.gradle +++ b/core-contracts/BatchDisbursement/build.gradle @@ -47,6 +47,7 @@ deployJar { sejong { uri = 'https://sejong.net.solidwallet.io/api/v3' nid = 0x53 + to = "cx3ddc7e3599d270cfa65cc933e1a3487f11f6b6f6" } local { uri = 'http://localhost:9082/api/v3' @@ -59,7 +60,7 @@ deployJar { } keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' - parameters { arg('_governance', "cx541e2e8b9673e736b727e3f6313ada687539f50f") } + parameters { arg('_governance', "cxdeeabbbdd77a3f648cf4ce4a5f3d4bdd1e3833b3") } } tasks.named('test') { diff --git a/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java b/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java index 7149928ea..8588025eb 100644 --- a/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java +++ b/core-contracts/BatchDisbursement/src/main/java/network/balanced/score/core/batchDisbursement/BatchDisbursement.java @@ -111,12 +111,12 @@ public Address getReserveFund() { } @External(readonly = true) - public Map getTokenBalances() { - Map balances = new HashMap<>(); + public Map getTokenBalances() { + Map balances = new HashMap<>(); for (int arrayIndex = 0; arrayIndex < allowedTokenAddress.length(); arrayIndex++) { Address tokenAddress = allowedTokenAddress.at(arrayIndex); BigInteger balance = (BigInteger) Context.call(tokenAddress, "balanceOf", Context.getAddress()); - balances.put(tokenAddress, balance); + balances.put(tokenAddress.toString(), balance); } return balances; } @@ -139,13 +139,13 @@ public void uploadDisbursementData(DisbursementRecipient[] disbursementRecipient @External(readonly = true) public Map getDisbursementDetail(Address _user) { - Map userClaimableTokens = new HashMap<>(); + Map userClaimableTokens = new HashMap<>(); DictDB userTokens = this.userClaimableTokens.at(_user); for (int arrayIndex = 0; arrayIndex < allowedTokenAddress.length(); arrayIndex++) { Address token = allowedTokenAddress.at(arrayIndex); BigInteger tokenAmount = userTokens.getOrDefault(token, BigInteger.ZERO); - userClaimableTokens.put(token, tokenAmount); + userClaimableTokens.put(token.toString(), tokenAmount); } return Map.of("user", _user, "claimableTokens", userClaimableTokens); } diff --git a/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/BatchDisbursementTest.java b/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/BatchDisbursementTest.java index dfa9505cc..3219f8fed 100644 --- a/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/BatchDisbursementTest.java +++ b/core-contracts/BatchDisbursement/src/test/java/network/balanced/score/core/batchDisbursement/BatchDisbursementTest.java @@ -27,7 +27,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.function.Executable; import score.Address; -import score.Context; import java.lang.reflect.InvocationTargetException; import java.math.BigInteger; @@ -35,7 +34,7 @@ import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class BatchDisbursementTest extends TestBase { @@ -137,11 +136,11 @@ void getTokenBalances() { setAndGetReserveFund(); batchDisbursement.invoke(governanceScore, "batchDisburse", daofundScore.getAddress()); - assertEquals(Map.of(balnTokenScore.getAddress(), BigInteger.TEN.pow(22)), batchDisbursement.call( + assertEquals(Map.of(balnTokenScore.getAddress().toString(), BigInteger.TEN.pow(22)), batchDisbursement.call( "getTokenBalances")); batchDisbursement.invoke(governanceScore, "batchDisburse", reserveFundScore.getAddress()); - assertEquals(Map.of(balnTokenScore.getAddress(), BigInteger.TEN.pow(22).multiply(BigInteger.TWO)), + assertEquals(Map.of(balnTokenScore.getAddress().toString(), BigInteger.TEN.pow(22).multiply(BigInteger.TWO)), batchDisbursement.call("getTokenBalances")); } @@ -169,9 +168,9 @@ void uploadDisbursementData() { Map disbursementDetail = (Map) batchDisbursement.call( "getDisbursementDetail", expectedRecipient); Address actualRecipient = (Address) disbursementDetail.get("user"); - Map claimableTokens = (Map) disbursementDetail.get( + Map claimableTokens = (Map) disbursementDetail.get( "claimableTokens"); - BigInteger actualAmount = claimableTokens.get(expectedTokenAddress); + BigInteger actualAmount = claimableTokens.get(expectedTokenAddress.toString()); assertEquals(expectedRecipient, actualRecipient); assertEquals(expectedAmount, actualAmount);