Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Submission: ZBounty-2: Create Continuous Token Bonding Curve Scilla Contracts #93

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions reference/continuous-token-bonding-curve-contracts/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CUR_NETWORK=ISOLATED
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
74 changes: 74 additions & 0 deletions reference/continuous-token-bonding-curve-contracts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Bancor Continuous Token Bonding Curve Scilla Contracts

This repository contains 3 scilla contracts that issues its own tokens through buy and sell functions. To buy tokens, you send a reserve token that can be $zil or a stablecoin to the buy function which calculates the average price of the token in reserve token terms and issues you the correct amount. The sell function works in reverse: first you provide the bonding contract with permission to take the amount of tokens you want to sell and then you trigger the function to take those tokens from you. The contract will calculate the current average selling price and will send you the correct amount of $zil or stablecoin. There is a configurable spread (protected by a unchangable max spread) between buying and selling with the spread amount going to an address owned by the development team.

The contract are provided with *PRODUCTION READY TYPESCRIPT TYPESAFE SDKS*

## Overview

There are 3 contracts. Available in ./src/*

- BancorFormula

Does the bancor formula calculation for the sell and buy functions to work, it sends the result of the calculation in a callback of the Token contract.

- Operator

Has an admin that can set the current spread of the buy/sell functions, the bancor formula implementation and the fee beneficiary address.

- Token

The changed ZRC-2 contract that now also now can be pegged to a ZIL or any other ZRC2 token to make it its connected token (provides the aformentioned sell and buy functions for the token).

The contract has a one time initialization transition to initialize it with the market cap of the token and the initial CW (see the bancor paper for what is CW or Connector Weight)

Buy function: Simply send the connected token to the contract address, the contract will automatically send back its smart token!

Sell function: Simply send the smart token to the contract address, the contract will automatically send back its connected token!

## How does it look like?

![Chart of how it works](chart.png)

Q: Why is the bancor formula in a separate contract?

A: The contract computation is expensive so any evaluation of the entire contract content should be evaluated only when Sell or Buy is actually triggered! Otherwise we would be introducing massive costs to the ZRC-2 protocol!

## Info

Going to ./src/Operator ./src/BancorFormula ./src/Token, you can find their respective READMEs. As well as their PRODUCTION READY SDKS.

## Testing

There are unit and integration tests!

All tests are located in ./src/*./\_\_tests\_\_ directories!


### Running tests

In this directory (we need to run local isolated server for integration testing):

Make sure that you have the Docker desktop daemon runnnig and run

```bash
npm run demo
```

If you want to work on this just go to package json and look how the demo script is implemented. It clones and runs the zilliqa isolated server which is needed for integration testing and then runs it on port 5555 and calls npm i && npm run test

# Other

Tip: The Token contract supports both ZIL and ZRC2 out of the box however not to introduce unneeded gas costs I would recommed trimming the contract to support only ZIL or ZRC2 depending on the usecase!

./dev.ipynb is the notebook used for making of parts of the contracts and research.

Sources:

https://github.com/AragonBlack/fundraising/blob/master/apps/bancor-formula/contracts/BancorFormula.sol

https://github.com/AragonBlack/fundraising/blob/master/apps/tap/contracts/Tap.sol

https://discourse.sourcecred.io/t/bonding-curve-references/271

https://storage.googleapis.com/website-bancor/2018/04/01ba8253-bancor_protocol_whitepaper_en.pdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./infra-manipulation";
export * from "./signable";
export * from "./utill";
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Transaction } from "@zilliqa-js/account";
import { getNetworkName } from "./shared";
import { units, BN, toBech32Address } from "@zilliqa-js/zilliqa";

const RED = "\x1B[31m%s\x1b[0m";
const CYAN = "\x1B[36m%s\x1b[0m";
const GREEN = "\x1B[32m%s\x1b[0m";
const MAGENTA = "\x1B[35m%s\x1b[0m";

export function state(v: {}) {
const color = "\x1b[33m%s\x1b[0m";
console.log(color, JSON.stringify(v, null, 4));
}

export function balance(inQa: BN) {
const color = "\x1b[35m%s\x1b[0m";
console.log(
color,
`In Zil: ${units.fromQa(inQa, units.Units.Zil).toString()}`
);
console.log(color, `In Li: ${units.fromQa(inQa, units.Units.Li).toString()}`);
console.log(color, `In Qa: ${inQa.toString()}`);
}

export function txLink(t: Transaction, msg: string) {
const id = t.id;
const url = `https://viewblock.io/zilliqa/tx/0x${id}?network=${getNetworkName()}`;
console.log(MAGENTA, msg);
const receipt = t.getReceipt();
if (receipt) {
if (receipt.success) {
console.log(GREEN, "Success.");
} else {
console.log(RED, "Failed.");
console.log(RED, JSON.stringify(receipt, null, 2));
if (receipt.event_logs)
receipt.event_logs.forEach((e) =>
console.log(GREEN, JSON.stringify(e, null, 2))
);
}
}
if (!(getNetworkName() == "ISOLATED")) {
console.log(CYAN, url);
} else console.log(CYAN, t.txParams.toAddr);
}

export function contractLink(a: string, msg: string) {
console.log(RED, msg);
if (!(getNetworkName() == "ISOLATED")) {
const url = `https://viewblock.io/zilliqa/address/${a}?network=${getNetworkName()}&tab=state`;
console.log(RED, url);
}
}

export function logAccount(a: string) {
console.log(RED, `Bech32: ${toBech32Address(a)}`);
console.log(RED, `20 byte: ${a}`);
const url = `https://viewblock.io/zilliqa/address/${a}?network=${getNetworkName()}`;
console.log(CYAN, url);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { BN, Zilliqa } from "@zilliqa-js/zilliqa";
import { getZil } from "./setup";
import { Contract } from "@zilliqa-js/contract";
import { TxParams } from "@zilliqa-js/account";
import { ByStr20 } from "../signable";

const sleep = async (mil: number) =>
new Promise<void>((res, rej) => setTimeout(() => res(), mil));

export async function waitUntilBlock(target: string): Promise<void> {
const secondsPerTxBlock = await getSecondsPerBlock();
console.log(`Waiting ... target: ${target}`);
console.log(`Current seconds per tx block is ${secondsPerTxBlock}`);
const targetBNum = parseInt(target);
while (true) {
const cur = await getCurrentBlock();
if (cur < targetBNum) {
console.log(`Current block ${cur}`);
await sleep(secondsPerTxBlock * 1000);
continue;
} else {
break;
}
}
}

export async function getCurrentBlock(): Promise<number> {
const zil = getZil();
const txblock = await zil.blockchain.getLatestTxBlock();
if (typeof txblock.result == "undefined") {
throw new Error("Couldn't get tx block");
}
return parseInt(txblock.result.header.BlockNum);
}

export async function getSecondsPerBlock(): Promise<number> {
const zil = getZil();
const txblockRate = await zil.blockchain.getTxBlockRate();
if (typeof txblockRate.result == "undefined") {
throw new Error("Couldn't get tx block rate");
}
return Math.ceil(1 / txblockRate.result);
}

export async function getBlockNumber(secondsToAdd: number): Promise<string> {
const curBlockNumber = await getCurrentBlock();
const secondsPerTxBlock = await getSecondsPerBlock();
const res =
"" + (curBlockNumber + Math.round(secondsToAdd / secondsPerTxBlock));
console.log(`Current block number: ${curBlockNumber}`);
console.log(`Returned Block number: ${res}`);
return res;
}

export async function getMinGasPrice() {
const zil = getZil();
const res = await zil.blockchain.getMinimumGasPrice();
if (!res.result) {
throw "no gas price";
}
return new BN(res.result);
}

export interface Value {
vname: string;
type: string;
value: string | ADTValue | ADTValue[] | string[];
}
interface ADTValue {
constructor: string;
argtypes: string[];
arguments: Value[] | string[];
}

export function newContract(
zil: Zilliqa,
code: string,
init: Value[]
): Contract {
//@ts-ignore
return zil.contracts.new(code, init);
}

export function getContract(
zil: Zilliqa,
a: string
): Contract & {
call: (
transition: string,
args: Value[],
params: Pick<
TxParams,
"version" | "amount" | "gasPrice" | "gasLimit" | "nonce" | "pubKey"
>,
attempts?: number,
interval?: number,
toDs?: boolean
) => ReturnType<Contract["call"]>;
} {
const address = new ByStr20(a).toSend();
//@ts-ignore
return zil.contracts.at(address);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from "./shared";
export * from "./setup";
export * as log from "./Logger";
export * from "./calls";

export const sleep = (milis: number) =>
new Promise((res) => setTimeout(res, milis));
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Zilliqa } from "@zilliqa-js/zilliqa";
import { getNode, getCurNetwork, getNetworkName } from "./shared";

const color = "\x1b[41m\x1b[5m%s\x1b[0m";
const logColor = (s: string) => console.log(color, s);

logColor(`::: NETWORK => ${getCurNetwork()} !!!!`);

const isolatedServerAccounts = {
d90f2e538ce0df89c8273cad3b63ec44a3c4ed82: {
privateKey:
"e53d1c3edaffc7a7bab5418eb836cf75819a82872b4a1a0f1c7fcf5c3e020b89",
amount: "90000000000000000000000",
nonce: 0,
},
"381f4008505e940ad7681ec3468a719060caf796": {
privateKey:
"d96e9eb5b782a80ea153c937fa83e5948485fbfc8b7e7c069d7b914dbc350aba",
amount: "90000000000000000000000",
nonce: 0,
},
b028055ea3bc78d759d10663da40d171dec992aa: {
privateKey:
"e7f59a4beb997a02a13e0d5e025b39a6f0adc64d37bb1e6a849a4863b4680411",
amount: "90000000000000000000000",
nonce: 0,
},
};

var zil: Zilliqa;

export function getZil() {
if (zil) {
return zil;
}
const zils = new Zilliqa(getNode());
if (getNetworkName() == "ISOLATED") {
for (const [k, v] of Object.entries(isolatedServerAccounts)) {
zils.wallet.addByPrivateKey(v.privateKey);
}
}
zil = zils;
return zils;
}

export function getDefaultAccount() {
const zil = getZil();
const def = zil.wallet.defaultAccount;
if (!def) {
throw new Error("No default account!");
}
return def;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { bytes } from "@zilliqa-js/util";

type Nets = "TESTNET" | "MAINNET" | "ISOLATED";

type EnvVariables = { env: { [key: string]: string } };

var proc: EnvVariables = { env: {} };

if (require("dotenv")) {
const resolve = require("path").resolve;
require("dotenv").config({ path: resolve(process.cwd(), ".env") });
}
if (process) {
proc = process as EnvVariables;
} else {
console.log(
`Not in NodeJS env, waiting for env variables to be set using @setEnvVariables`
);
}

/*
* Add PRIV_${CUR_NETWORK}
* and any
* PRIV_${CUR_NETWORK}_${cur}
* in ascending order starting from 1
*/
export function setEnvVariables(envConfig: {
CUR_NETWORK: Nets;
[key: string]: string;
}) {
Object.entries(envConfig).forEach(([k, v]) => (proc.env[k] = v));
}

export function getCurNetwork() {
return proc.env!.CUR_NETWORK as Nets;
}

const nodes: { [key in Nets]: string } = {
TESTNET: "https://dev-api.zilliqa.com",
MAINNET: "https://ssn.zillacracy.com/api",
ISOLATED: "http://localhost:5555",
};

const version: { [key in Nets]: number } = {
TESTNET: bytes.pack(333, 1),
ISOLATED: bytes.pack(222, 3),
MAINNET: bytes.pack(1, 1),
};

const networkName: { [key in Nets]: "testnet" | "mainnet" | "ISOLATED" } = {
TESTNET: "testnet",
ISOLATED: "ISOLATED",
MAINNET: "mainnet",
};

export function getNetworkName(): "testnet" | "mainnet" | "ISOLATED" {
return networkName[getCurNetwork()];
}

export function getVersion() {
return version[getCurNetwork()];
}

export function getNode() {
return nodes[getCurNetwork()];
}
Loading