Skip to content

Commit 4675b3e

Browse files
committed
feat: Add proof of liquidity bucket contract
1 parent adc1c0c commit 4675b3e

File tree

7 files changed

+561
-0
lines changed

7 files changed

+561
-0
lines changed

proof-of-liquidity/README.md

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Introduction
2+
The savings rate contract stakes ICX and relays it to the balanced handler for the savings rate given to bnUSD stakers.
3+
## Overview
4+
5+
### Requirements
6+
* The contract should be owned by governance.
7+
* The contract should swap incoming inflation to sICX and distribute to balanced target contract.
8+
* The contract should be able to manually stake in case of failure during fallback.
9+
10+
# Design
11+
12+
## Storage and Structs
13+
```java
14+
15+
LiquidityDistribution {
16+
BigInteger share;
17+
// Source name in balanced rewards contract
18+
String source;
19+
}
20+
21+
// All below dbs should be configurable by governance only.
22+
ArrayDB<LiquidityDistribution> distribution;
23+
VarDB<Address> staking;
24+
VarDB<Address> sICX;
25+
VarDB<Address> balancedRewards;
26+
27+
28+
BigInteger TOTAL_SHARE = EXA;
29+
30+
```
31+
32+
33+
## Methods
34+
35+
```java
36+
/**
37+
* Tries to stake the whole ICX balance to the balanced receiver
38+
*
39+
*/
40+
@Payable
41+
public void fallback() {
42+
try {
43+
distribute();
44+
} catch (Exception e) {
45+
}
46+
}
47+
```
48+
49+
```java
50+
/**
51+
*
52+
* Returns the current distribution config
53+
*/
54+
@External(readonly = true)
55+
public LiquidityDistribution[] getDistributions() {
56+
return distribution
57+
}
58+
59+
```
60+
61+
```java
62+
/**
63+
* Allocated the rewards to the balanced rewards contract for the configured distributions
64+
*
65+
* @param amount The amount to stake and send
66+
*/
67+
@External
68+
public void distribute() {
69+
BigInteger balance = Context.getBalance(Context.getAddress());
70+
BigInteger sICXAmount = staking.stakeICX(amount, balancedReceiver)
71+
JsonArray data = new JsonArray();
72+
for (dist: distribution) {
73+
BigInteger share = dist.share.multiply(sICXAmount).divide(TOTAL_SHARE);
74+
data.add(
75+
'{
76+
"source": dist.source
77+
"amount": share
78+
}'
79+
);
80+
}
81+
82+
sICX.transfer(balancedRewards, sICXAmount, jsonData)
83+
}
84+
85+
86+
```java
87+
/**
88+
*
89+
* Configures the rewards
90+
*
91+
* @param _distribution The source names and their share, which adds up to 10**18
92+
*/
93+
@External
94+
public void configureDistributions(LiquidityDistribution[] _distribution) {
95+
onlyOwner();
96+
require(sum(_distribution.share) == TOTAL_SHARE)
97+
}
98+
99+
```
100+

proof-of-liquidity/build.gradle

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
version = '0.1.0'
2+
3+
dependencies {
4+
compileOnly("foundation.icon:javaee-api:$javaeeVersion")
5+
implementation("foundation.icon:javaee-scorex:$scorexVersion")
6+
implementation project(':score-lib')
7+
implementation "com.github.sink772:minimal-json:$jsonVersion";
8+
9+
testImplementation project(':test-lib')
10+
testImplementation("foundation.icon:javaee-unittest:$javaeeUnittestVersion")
11+
testImplementation("org.junit.jupiter:junit-jupiter-api:$jupiterApiVersion")
12+
testImplementation "com.fasterxml.jackson.core:jackson-databind:2.13.3"
13+
testImplementation("org.mockito:mockito-core:$mockitoCoreVersion")
14+
}
15+
16+
test {
17+
useJUnitPlatform()
18+
finalizedBy jacocoTestReport
19+
}
20+
21+
jacocoTestReport {
22+
dependsOn test
23+
reports {
24+
xml.required = true
25+
csv.required = false
26+
html.outputLocation = layout.buildDirectory.dir('jacocoHtml')
27+
}
28+
}
29+
30+
optimizedJar {
31+
dependsOn project(':score-lib').jar
32+
mainClassName = 'icon.inflation.score.pol.ProofOfLiquidity'
33+
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
34+
from {
35+
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
36+
}
37+
}
38+
39+
deployJar {
40+
endpoints {
41+
sejong {
42+
uri = 'https://sejong.net.solidwallet.io/api/v3'
43+
nid = 0x53
44+
}
45+
berlin {
46+
uri = 'https://berlin.net.solidwallet.io/api/v3'
47+
nid = 0x7
48+
}
49+
lisbon {
50+
uri = 'https://lisbon.net.solidwallet.io/api/v3'
51+
nid = 0x2
52+
}
53+
local {
54+
uri = 'http://localhost:9082/api/v3'
55+
nid = 0x3
56+
}
57+
mainnet {
58+
uri = 'https://ctz.solidwallet.io/api/v3'
59+
nid = 0x1
60+
}
61+
}
62+
keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : ''
63+
password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : ''
64+
}
65+
66+
task integrationTest(type: Test) {
67+
useJUnitPlatform()
68+
69+
rootProject.allprojects {
70+
if (it.getTasks().findByName('optimizedJar')) {
71+
dependsOn(it.getTasks().getByName('optimizedJar'))
72+
}
73+
}
74+
75+
options {
76+
testLogging.showStandardStreams = true
77+
description = 'Runs integration tests.'
78+
group = 'verification'
79+
80+
testClassesDirs = sourceSets.intTest.output.classesDirs
81+
classpath = sourceSets.intTest.runtimeClasspath
82+
83+
systemProperty "java", optimizedJar.outputJarName
84+
}
85+
86+
}
87+
88+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package icon.inflation.score.pol;
2+
3+
4+
public class Errors {
5+
public static final String NEGATIVE_PERCENTAGE = "The share cannot be negative";
6+
public static final String INVALID_SUM = "The total shares of all distributions must add up to 100%";
7+
public static final String NOT_CONFIGURED = "Distributions has not yet been configured";
8+
public static final String EMPTY_BALANCE = "Current ICX Balance is empty";
9+
public static final String NO_REENTRY = "No reentry allowed";
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package icon.inflation.score.pol;
2+
3+
import java.math.BigInteger;
4+
5+
import icon.inflation.score.structs.LiquidityDistribution;
6+
import icon.inflation.score.util.DBUtils;
7+
import score.Context;
8+
import score.ArrayDB;
9+
import score.VarDB;
10+
import score.Address;
11+
import score.annotation.External;
12+
import score.annotation.Payable;
13+
14+
import com.eclipsesource.json.JsonObject;
15+
import com.eclipsesource.json.JsonValue;
16+
import com.eclipsesource.json.JsonArray;
17+
18+
import static icon.inflation.score.util.Constants.EXA;
19+
import static icon.inflation.score.util.Checks.onlyOwner;
20+
21+
public class ProofOfLiquidity {
22+
23+
public static final ArrayDB<LiquidityDistribution> distribution = Context.newArrayDB("DISTRIBUTIONS", LiquidityDistribution.class);
24+
25+
public static final VarDB<Address> staking = Context.newVarDB("STAKING_ADDRESS", Address.class);
26+
public static final VarDB<Address> sICX = Context.newVarDB("STAKED_ICX", Address.class);
27+
public static final VarDB<Address> balancedRewards = Context.newVarDB("BALANCED_REWARDS", Address.class);
28+
29+
private static BigInteger TOTAL_SHARE = EXA;
30+
private static String NAME = "ICON Proof of liquidity Manager";
31+
32+
private static boolean distributing = false;
33+
34+
public ProofOfLiquidity(Address staking, Address sICX, Address balancedRewards) {
35+
ProofOfLiquidity.staking.set(staking);
36+
ProofOfLiquidity.sICX.set(sICX);
37+
ProofOfLiquidity.balancedRewards.set(balancedRewards);
38+
}
39+
40+
@External(readonly = true)
41+
public String name() {
42+
return NAME;
43+
}
44+
45+
@External(readonly = true)
46+
public LiquidityDistribution[] getDistributions() {
47+
int size = distribution.size();
48+
LiquidityDistribution[] _distribution = new LiquidityDistribution[size];
49+
for (int i = 0; i < size; i++) {
50+
_distribution[i] = distribution.get(i);
51+
}
52+
53+
return _distribution;
54+
}
55+
56+
@External
57+
public void configureDistributions(LiquidityDistribution[] _distribution) {
58+
onlyOwner();
59+
BigInteger sum = BigInteger.ZERO;
60+
DBUtils.clear(distribution);
61+
62+
for (LiquidityDistribution dist : _distribution) {
63+
Context.require(dist.share.compareTo(BigInteger.ZERO) > 0, Errors.NEGATIVE_PERCENTAGE);
64+
sum = sum.add(dist.share);
65+
distribution.add(dist);
66+
}
67+
68+
Context.require(sum.equals(TOTAL_SHARE), Errors.INVALID_SUM);
69+
}
70+
71+
@External
72+
public void distribute() {
73+
Context.require(!distributing, Errors.NO_REENTRY);
74+
75+
BigInteger balance = Context.getBalance(Context.getAddress());
76+
Context.require(balance.compareTo(BigInteger.ZERO) > 0, Errors.EMPTY_BALANCE);
77+
78+
int size = distribution.size();
79+
Context.require(size > 0, Errors.NOT_CONFIGURED);
80+
81+
distributing = true;
82+
83+
Context.call(balance, staking.get(), "stakeICX", null, null);
84+
BigInteger amount = Context.call(BigInteger.class, sICX.get(), "balanceOf", Context.getAddress());
85+
BigInteger sum = BigInteger.ZERO;
86+
87+
JsonArray data = new JsonArray();
88+
for (int i = 0; i < size; i++) {
89+
LiquidityDistribution dist = distribution.get(i);
90+
// Dev note: This way of calculating will create remaining dust. But since we
91+
// are dealing with large amounts this should not matter and a good trade of for
92+
// simplicity
93+
BigInteger share = dist.share.multiply(amount).divide(TOTAL_SHARE);
94+
sum = sum.add(share);
95+
JsonValue jsonData = new JsonObject()
96+
.add("source", dist.source)
97+
.add("amount", share.toString());
98+
data.add(jsonData);
99+
}
100+
101+
Context.call(sICX.get(), "transfer", balancedRewards.get(), sum, data.toString().getBytes());
102+
distributing = false;
103+
}
104+
105+
106+
@External
107+
public void tokenFallback(Address _from, BigInteger _value, byte[] _data) {
108+
}
109+
110+
@Payable
111+
public void fallback() {
112+
try {
113+
distribute();
114+
} catch (Exception e) {
115+
}
116+
}
117+
118+
}

0 commit comments

Comments
 (0)