Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@ fabric.properties
# Ignore Gradle build output directory
build
.DS_Store
.vscode
.vscode
69 changes: 69 additions & 0 deletions core-contracts/BatchDisbursement/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 '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'
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 = "cx3ddc7e3599d270cfa65cc933e1a3487f11f6b6f6"
}
local {
uri = 'http://localhost:9082/api/v3'
nid = 0x3
}
mainnet {
uri = 'https://ctz.solidwallet.io/api/v3'
nid = 0x1
}
}
keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : ''
password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : ''
parameters { arg('_governance', "cxdeeabbbdd77a3f648cf4ce4a5f3d4bdd1e3833b3") }
}

tasks.named('test') {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
/*
* 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<Address> governance = Context.newVarDB("governance", Address.class);
public final BranchDB<Address, DictDB<Address, BigInteger>> userClaimableTokens = Context.newBranchDB(
"user_claimable_tokens", BigInteger.class);
public final VarDB<Address> daofund = Context.newVarDB("daofund", Address.class);
public final VarDB<Address> reserveFund = Context.newVarDB("reserve_fund", Address.class);
public final EnumerableSetDB<Address> 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<String, BigInteger> getTokenBalances() {
Map<String, BigInteger> 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.toString(), balance);
}
return balances;
}

@External
public void uploadDisbursementData(DisbursementRecipient[] disbursementRecipients) {
onlyOwner();

for (DisbursementRecipient disbursementRecipient : disbursementRecipients) {
Address user = disbursementRecipient.recipient;
DictDB<Address, BigInteger> 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<String, Object> getDisbursementDetail(Address _user) {

Map<String, BigInteger> userClaimableTokens = new HashMap<>();
DictDB<Address, BigInteger> 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.toString(), tokenAmount);
}
return Map.of("user", _user, "claimableTokens", userClaimableTokens);
}

@External
public void claim() {
Address sender = Context.getCaller();
DictDB<Address, BigInteger> 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);
}
}

}
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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<V> {
private final ArrayDB<V> entries;
private final DictDB<V, Integer> indexes;

public EnumerableSetDB(String varKey, Class<V> 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;
}
}
Loading