Skip to content

Commit 7d37d49

Browse files
authored
Merge pull request #50 from balancednetwork/feature/add-batch-disbursement-contract
Batch Disbursement contract
2 parents bc7d031 + 5032e20 commit 7d37d49

10 files changed

Lines changed: 676 additions & 2 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,4 @@ fabric.properties
8282
# Ignore Gradle build output directory
8383
build
8484
.DS_Store
85-
.vscode
85+
.vscode
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright (c) 2022-2022 Balanced.network.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
version = '0.1.0'
18+
19+
repositories {
20+
// Use Maven Central for resolving dependencies.
21+
mavenCentral()
22+
}
23+
24+
dependencies {
25+
compileOnly 'foundation.icon:javaee-api:0.9.1'
26+
implementation 'foundation.icon:javaee-scorex:0.5.2'
27+
28+
testImplementation 'com.github.sink772:javaee-tokens:0.6.1'
29+
testImplementation 'foundation.icon:javaee-unittest:0.9.2'
30+
// Use JUnit Jupiter for testing.
31+
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
32+
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
33+
testImplementation 'org.mockito:mockito-core:4.1.0'
34+
}
35+
36+
optimizedJar {
37+
mainClassName = 'network.balanced.score.core.batchDisbursement.BatchDisbursement'
38+
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
39+
from {
40+
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
41+
}
42+
enableDebug = false
43+
}
44+
45+
deployJar {
46+
endpoints {
47+
sejong {
48+
uri = 'https://sejong.net.solidwallet.io/api/v3'
49+
nid = 0x53
50+
to = "cx3ddc7e3599d270cfa65cc933e1a3487f11f6b6f6"
51+
}
52+
local {
53+
uri = 'http://localhost:9082/api/v3'
54+
nid = 0x3
55+
}
56+
mainnet {
57+
uri = 'https://ctz.solidwallet.io/api/v3'
58+
nid = 0x1
59+
}
60+
}
61+
keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : ''
62+
password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : ''
63+
parameters { arg('_governance', "cxdeeabbbdd77a3f648cf4ce4a5f3d4bdd1e3833b3") }
64+
}
65+
66+
tasks.named('test') {
67+
// Use JUnit Platform for unit tests.
68+
useJUnitPlatform()
69+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright (c) 2022-2022 Balanced.network.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package network.balanced.score.core.batchDisbursement;
18+
19+
import score.*;
20+
import score.annotation.EventLog;
21+
import score.annotation.External;
22+
import scorex.util.HashMap;
23+
24+
import java.math.BigInteger;
25+
import java.util.Map;
26+
27+
import static network.balanced.score.core.batchDisbursement.Checks.onlyGovernance;
28+
import static network.balanced.score.core.batchDisbursement.Checks.onlyOwner;
29+
30+
public class BatchDisbursement {
31+
32+
public static final VarDB<Address> governance = Context.newVarDB("governance", Address.class);
33+
public final BranchDB<Address, DictDB<Address, BigInteger>> userClaimableTokens = Context.newBranchDB(
34+
"user_claimable_tokens", BigInteger.class);
35+
public final VarDB<Address> daofund = Context.newVarDB("daofund", Address.class);
36+
public final VarDB<Address> reserveFund = Context.newVarDB("reserve_fund", Address.class);
37+
public final EnumerableSetDB<Address> allowedTokenAddress = new EnumerableSetDB<>("allowed_token_address",
38+
Address.class);
39+
40+
public static final String TAG = "BatchDisbursement";
41+
42+
public BatchDisbursement(Address _governance) {
43+
Context.require(_governance.isContract(), TAG + ": Governance address should be a contract");
44+
governance.set(_governance);
45+
}
46+
47+
public static class Disbursement {
48+
public Address tokenAddress;
49+
public BigInteger tokenAmount;
50+
}
51+
52+
public static class DisbursementRecipient {
53+
public Address recipient;
54+
public Disbursement[] disbursement;
55+
}
56+
57+
/*
58+
* Event Logs
59+
*/
60+
61+
@EventLog(indexed = 1)
62+
public void Claim(Address user, Address tokenAddress, BigInteger amount) {
63+
}
64+
65+
@EventLog(indexed = 1)
66+
protected void TokenTransfer(Address recipient, BigInteger amount, String note) {
67+
}
68+
69+
/*
70+
* Setters and getters
71+
*/
72+
@External(readonly = true)
73+
public String name() {
74+
return "Balanced Batch Disbursement";
75+
}
76+
77+
@External
78+
public void setGovernance(Address _address) {
79+
onlyOwner();
80+
Context.require(_address.isContract(), TAG + ": Address provided is an EOA Address. Contract address required");
81+
governance.set(_address);
82+
}
83+
84+
@External(readonly = true)
85+
public Address getGovernance() {
86+
return governance.get();
87+
}
88+
89+
@External
90+
public void setDaofund(Address _address) {
91+
onlyOwner();
92+
Context.require(_address.isContract(), TAG + ": Address provided is an EOA Address. Contract address required");
93+
daofund.set(_address);
94+
}
95+
96+
@External(readonly = true)
97+
public Address getDaofund() {
98+
return daofund.get();
99+
}
100+
101+
@External
102+
public void setReserveFund(Address _address) {
103+
onlyOwner();
104+
Context.require(_address.isContract(), TAG + ": Address provided is an EOA Address. Contract address required");
105+
reserveFund.set(_address);
106+
}
107+
108+
@External(readonly = true)
109+
public Address getReserveFund() {
110+
return reserveFund.get();
111+
}
112+
113+
@External(readonly = true)
114+
public Map<String, BigInteger> getTokenBalances() {
115+
Map<String, BigInteger> balances = new HashMap<>();
116+
for (int arrayIndex = 0; arrayIndex < allowedTokenAddress.length(); arrayIndex++) {
117+
Address tokenAddress = allowedTokenAddress.at(arrayIndex);
118+
BigInteger balance = (BigInteger) Context.call(tokenAddress, "balanceOf", Context.getAddress());
119+
balances.put(tokenAddress.toString(), balance);
120+
}
121+
return balances;
122+
}
123+
124+
@External
125+
public void uploadDisbursementData(DisbursementRecipient[] disbursementRecipients) {
126+
onlyOwner();
127+
128+
for (DisbursementRecipient disbursementRecipient : disbursementRecipients) {
129+
Address user = disbursementRecipient.recipient;
130+
DictDB<Address, BigInteger> userTokens = userClaimableTokens.at(user);
131+
for (Disbursement disbursement : disbursementRecipient.disbursement) {
132+
Address token = disbursement.tokenAddress;
133+
BigInteger currentAmount = userTokens.getOrDefault(token, BigInteger.ZERO);
134+
userTokens.set(disbursement.tokenAddress, currentAmount.add(disbursement.tokenAmount));
135+
}
136+
}
137+
}
138+
139+
@External(readonly = true)
140+
public Map<String, Object> getDisbursementDetail(Address _user) {
141+
142+
Map<String, BigInteger> userClaimableTokens = new HashMap<>();
143+
DictDB<Address, BigInteger> userTokens = this.userClaimableTokens.at(_user);
144+
145+
for (int arrayIndex = 0; arrayIndex < allowedTokenAddress.length(); arrayIndex++) {
146+
Address token = allowedTokenAddress.at(arrayIndex);
147+
BigInteger tokenAmount = userTokens.getOrDefault(token, BigInteger.ZERO);
148+
userClaimableTokens.put(token.toString(), tokenAmount);
149+
}
150+
return Map.of("user", _user, "claimableTokens", userClaimableTokens);
151+
}
152+
153+
@External
154+
public void claim() {
155+
Address sender = Context.getCaller();
156+
DictDB<Address, BigInteger> userTokens = userClaimableTokens.at(sender);
157+
158+
for (int arrayIndex = 0; arrayIndex < allowedTokenAddress.length(); arrayIndex++) {
159+
Address token = allowedTokenAddress.at(arrayIndex);
160+
BigInteger tokenAmount = userTokens.getOrDefault(token, BigInteger.ZERO);
161+
if (tokenAmount != null && tokenAmount.signum() > 0) {
162+
userTokens.set(token, BigInteger.ZERO);
163+
sendToken(token, sender, tokenAmount, TAG + ": tokens claimed");
164+
Claim(sender, token, tokenAmount);
165+
}
166+
}
167+
}
168+
169+
@External
170+
public void batchDisburse(Address _source) {
171+
onlyGovernance();
172+
Context.call(_source, "claim");
173+
}
174+
175+
@External
176+
public void tokenFallback(Address _from, BigInteger _value, byte[] _data) {
177+
Address tokenContract = Context.getCaller();
178+
179+
Context.require(_from.equals(getDaofund()) || _from.equals(getReserveFund()), TAG+ ": Only receivable from " +
180+
"daofund or reserve contract");
181+
if (!allowedTokenAddress.contains(tokenContract)) {
182+
allowedTokenAddress.add(tokenContract);
183+
}
184+
}
185+
186+
private void sendToken(Address tokenAddress, Address to, BigInteger amount, String message) {
187+
try {
188+
Context.call(tokenAddress, "transfer", to, amount, new byte[0]);
189+
TokenTransfer(to, amount, message + ": " + amount + "" + tokenAddress + " tokens sent to " + to);
190+
} catch (Exception e) {
191+
Context.println(e.getMessage());
192+
Context.revert(TAG + ": Error in sending tokens to user- " + to);
193+
}
194+
}
195+
196+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2022-2022 Balanced.network.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package network.balanced.score.core.batchDisbursement;
18+
19+
import score.Address;
20+
import score.Context;
21+
22+
public class Checks {
23+
public static Address defaultAddress = new Address(new byte[Address.LENGTH]);
24+
25+
public static void onlyOwner() {
26+
Address caller = Context.getCaller();
27+
Address owner = Context.getOwner();
28+
Context.require(caller.equals(owner), "SenderNotScoreOwner: Sender=" + caller + "Owner=" + owner);
29+
}
30+
31+
public static void onlyGovernance() {
32+
Address sender = Context.getCaller();
33+
Address governance = BatchDisbursement.governance.getOrDefault(defaultAddress);
34+
Context.require(!governance.equals(defaultAddress), BatchDisbursement.TAG + ": Governance address not set");
35+
Context.require(sender.equals(governance), BatchDisbursement.TAG + ": Sender not governance contract");
36+
}
37+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Copyright (c) 2022-2022 Balanced.network.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package network.balanced.score.core.batchDisbursement;
18+
19+
import score.ArrayDB;
20+
import score.Context;
21+
import score.DictDB;
22+
23+
public class EnumerableSetDB<V> {
24+
private final ArrayDB<V> entries;
25+
private final DictDB<V, Integer> indexes;
26+
27+
public EnumerableSetDB(String varKey, Class<V> valueClass) {
28+
// array of valueClass
29+
this.entries = Context.newArrayDB(varKey + "_es_entries", valueClass);
30+
// value => array index
31+
this.indexes = Context.newDictDB(varKey + "_es_index", Integer.class);
32+
}
33+
34+
public int length() {
35+
return entries.size();
36+
}
37+
38+
public V at(int index) {
39+
return entries.get(index);
40+
}
41+
42+
public boolean contains(V value) {
43+
return indexes.get(value) != null;
44+
}
45+
46+
public Integer indexOf(V value) {
47+
// returns null if value doesn't exist
48+
Integer result = indexes.get(value);
49+
if (result != null) {
50+
return result - 1;
51+
}
52+
return null;
53+
}
54+
55+
public void add(V value) {
56+
if (!contains(value)) {
57+
//add new value
58+
entries.add(value);
59+
indexes.set(value, entries.size());
60+
}
61+
}
62+
63+
public V remove(V value) {
64+
Integer valueIndex = indexOf(value);
65+
66+
if (valueIndex != null) {
67+
int lastIndex = entries.size();
68+
V lastValue = entries.pop();
69+
indexes.set(value, null);
70+
if (lastIndex != valueIndex) {
71+
entries.set(valueIndex - 1, lastValue);
72+
indexes.set(lastValue, valueIndex);
73+
return lastValue;
74+
}
75+
}
76+
return null;
77+
}
78+
}

0 commit comments

Comments
 (0)