Skip to content

Commit

Permalink
Merge pull request #277 from interlay/dan/burn-redeem
Browse files Browse the repository at this point in the history
feat(redeem): Liquidation (burn) redeem
  • Loading branch information
nud3l authored Apr 7, 2021
2 parents 0dad6cd + 83ad194 commit 429cf39
Show file tree
Hide file tree
Showing 32 changed files with 735 additions and 552 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: test

on: push
on:
push:
branches:
- master

jobs:
build:
Expand Down
20 changes: 20 additions & 0 deletions .github/workflows/test-staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: test

on:
pull_request:
branches:
- master

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: setup node
uses: actions/setup-node@v1
with:
node-version: "14.x"
- name: Run and set up the parachain, oracle, staked relayer and vault
run: docker-compose up -d
- run: yarn install
- run: yarn ci:test:staging
17 changes: 15 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,11 @@ services:
- |
echo "Sleeping..."
sleep 5
faucet --keyring=ferdie --polka-btc-url 'ws://polkabtc:9944' --user-allowance 1 --vault-allowance 500 --http-addr '[::0]:3035'
faucet --keyring=ferdie --polka-btc-url 'ws://polkabtc:9944' --user-allowance 1 --vault-allowance 500 --http-addr '[::0]:3036'
environment:
RUST_LOG: info
ports:
- "3035:3035"
- "3036:3036"
vault_1:
image: "registry.gitlab.com/interlay/polkabtc-clients/vault:0.6.1"
command:
Expand Down Expand Up @@ -134,6 +134,19 @@ services:
vault --keyring=eve --auto-register-with-collateral 1000000000000 --no-issue-execution --polka-btc-url 'ws://polkabtc:9944'
environment:
<<: *client-env
vault_to_liquidate:
image: "registry.gitlab.com/interlay/polkabtc-clients/vault:0.6.1"
command:
- /bin/sh
- -c
- |
echo "Sleeping..."
# sleep for 30s to wait for bitcoin to create the Ferdie wallet
# and also to ensure that the issue period and redeem period are set
sleep 30
vault --keyring=bob --auto-register-with-collateral 1000000000000 --polka-btc-url 'ws://polkabtc:9944'
environment:
<<: *client-env
testdata_gen:
image: "registry.gitlab.com/interlay/polkabtc-clients/testdata-gen:0.6.1"
command:
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@interlay/polkabtc",
"version": "0.11.2",
"version": "0.12.0",
"description": "JavaScript library to interact with PolkaBTC",
"main": "build/index.js",
"typings": "build/index.d.ts",
Expand All @@ -18,6 +18,8 @@
"fix:prettier": "prettier \"src/**/*.ts\" --write",
"fix:lint": "eslint --fix . --ext .ts",
"ci:test": "run-s build test:lint test:unit test:integration",
"ci:test:staging": "run-s build test:lint test:unit test:integration:staging",
"ci:test:release": "run-s build test:integration:release",
"ci:test-with-coverage": "nyc -r lcov -e .ts -x \"*.test.ts\" yarn ci:test",
"docs": "./generate_docs",
"generate:defs": "ts-node --skip-project node_modules/.bin/polkadot-types-from-defs --package @interlay/polkabtc/interfaces --input ./src/interfaces",
Expand All @@ -26,6 +28,8 @@
"test:lint": "eslint src --ext .ts",
"test:unit": "mocha test/unit/*.test.ts test/unit/**/*.test.ts",
"test:integration": "mocha test/integration/**/*.test.ts --timeout 10000000",
"test:integration:staging": "mocha test/integration/**/staging/*.test.ts --timeout 10000000",
"test:integration:release": "mocha test/integration/**/release/*.test.ts --timeout 10000000",
"watch:build": "tsc -p tsconfig.json -w",
"watch:test": "mocha --watch test/**/*.test.ts"
},
Expand Down
41 changes: 21 additions & 20 deletions src/parachain/collateral.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AccountId, Balance } from "@polkadot/types/interfaces/runtime";
import { AccountId } from "@polkadot/types/interfaces/runtime";
import { ApiPromise } from "@polkadot/api";
import { ACCOUNT_NOT_SET_ERROR_MESSAGE, Transaction } from "../utils";
import { ACCOUNT_NOT_SET_ERROR_MESSAGE, planckToDOT, Transaction } from "../utils";
import { AddressOrPair } from "@polkadot/api/submittable/types";

import Big from "big.js";
/**
* @category PolkaBTC Bridge
*/
Expand All @@ -13,25 +13,25 @@ export interface CollateralAPI {
*/
setAccount(account: AddressOrPair): void;
/**
* @returns Total locked DOT collateral
* @returns Total locked collateral
*/
totalLockedDOT(): Promise<Balance>;
totalLocked(): Promise<Big>;
/**
* @param id The ID of an account
* @returns The reserved DOT balance of the given account
* @returns The reserved balance of the given account
*/
balanceLockedDOT(id: AccountId): Promise<Balance>;
balanceLocked(id: AccountId): Promise<Big>;
/**
* @param id The ID of an account
* @returns The free DOT balance of the given account
* @returns The free balance of the given account
*/
balanceDOT(id: AccountId): Promise<Balance>;
balance(id: AccountId): Promise<Big>;
/**
* Send a transaction that transfers DOT from the caller's address to another address
* @param address The recipient of the DOT transfer
* @param amount The DOT balance to transfer
* Send a transaction that transfers from the caller's address to another address
* @param address The recipient of the transfer
* @param amount The balance to transfer
*/
transferDOT(address: string, amount: string | number): Promise<void>;
transfer(address: string, amount: string | number): Promise<void>;
}

export class DefaultCollateralAPI implements CollateralAPI {
Expand All @@ -41,24 +41,25 @@ export class DefaultCollateralAPI implements CollateralAPI {
this.transaction = new Transaction(api);
}

async totalLockedDOT(): Promise<Balance> {
async totalLocked(): Promise<Big> {
const head = await this.api.rpc.chain.getFinalizedHead();
return this.api.query.collateral.totalCollateral.at(head);
const totalLockedBN = await this.api.query.collateral.totalCollateral.at(head);
return new Big(planckToDOT(totalLockedBN.toString()));
}

async balanceLockedDOT(id: AccountId): Promise<Balance> {
async balanceLocked(id: AccountId): Promise<Big> {
const head = await this.api.rpc.chain.getFinalizedHead();
const account = await this.api.query.dot.account.at(head, id);
return account.reserved;
return new Big(planckToDOT(account.reserved.toString()));
}

async balanceDOT(id: AccountId): Promise<Balance> {
async balance(id: AccountId): Promise<Big> {
const head = await this.api.rpc.chain.getFinalizedHead();
const account = await this.api.query.dot.account.at(head, id);
return account.free;
return new Big(planckToDOT(account.free.toString()));
}

async transferDOT(address: string, amount: string | number): Promise<void> {
async transfer(address: string, amount: string | number): Promise<void> {
if (!this.account) {
return Promise.reject(ACCOUNT_NOT_SET_ERROR_MESSAGE);
}
Expand Down
17 changes: 9 additions & 8 deletions src/parachain/fee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export interface FeeAPI {
* @param feesPolkaBTC Satoshi value representing the BTC fees accrued
* @param feesDOT Planck value representing the DOT fees accrued
* @param lockedDOT Planck value representing the value locked to gain yield
* @param dotToBtcRate Conversion rate
* @param dotToBtcRate (Optional) Conversion rate of the large denominations (DOT/BTC as opposed to Planck/Satoshi)
* @returns The APY, given the parameters
*/
calculateAPY(feesPolkaBTC: string, feesDOT: string, lockedDOT: string, dotToBtcRate: Big): string;
calculateAPY(feesPolkaBTC: Big, feesDOT: Big, lockedDOT: Big, dotToBtcRate?: Big): Promise<string>;
/**
* @returns The griefing collateral rate for issuing PolkaBTC
*/
Expand Down Expand Up @@ -70,13 +70,14 @@ export class DefaultFeeAPI implements FeeAPI {
return new Big(decodeFixedPointType(griefingCollateralRate));
}

calculateAPY(feesPolkaBTC: string, feesDOT: string, lockedDOT: string, dotToBtcRate: Big): string {
const feesPolkaBTCBig = new Big(feesPolkaBTC);
const feesPolkaBTCInDot = feesPolkaBTCBig.mul(dotToBtcRate);
const totalFees = new Big(feesDOT).add(feesPolkaBTCInDot);
const lockedDotBig = new Big(lockedDOT);
async calculateAPY(feesPolkaBTC: Big, feesDOT: Big, lockedDOT: Big, dotToBtcRate?: Big): Promise<string> {
if(dotToBtcRate === undefined) {
dotToBtcRate = await this.oracleAPI.getExchangeRate();
}
const feesPolkaBTCInDot = feesPolkaBTC.mul(dotToBtcRate);
const totalFees = feesDOT.add(feesPolkaBTCInDot);

// convert to percent
return totalFees.div(lockedDotBig).mul(100).toString();
return totalFees.div(lockedDOT).mul(100).toString();
}
}
51 changes: 49 additions & 2 deletions src/parachain/redeem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@ import {
decodeFixedPointType,
Transaction,
encodeParachainRequest,
ACCOUNT_NOT_SET_ERROR_MESSAGE
ACCOUNT_NOT_SET_ERROR_MESSAGE,
btcToSat,
satToBTC,
planckToDOT
} from "../utils";
import { BlockNumber } from "@polkadot/types/interfaces/runtime";
import { stripHexPrefix } from "../utils";
import { Network } from "bitcoinjs-lib";
import Big from "big.js";
import { ApiTypes, AugmentedEvent } from "@polkadot/api/types";
import type { AnyTuple } from "@polkadot/types/types";
import { CollateralAPI } from ".";
import { DefaultCollateralAPI } from "./collateral";

export type RequestResult = { id: Hash; redeemRequest: RedeemRequestExt };

Expand Down Expand Up @@ -118,16 +123,32 @@ export interface RedeemAPI {
* and required completion time by a user.
*/
getRedeemPeriod(): Promise<BlockNumber>;
/**
* Burn wrapped tokens for a premium
* @param amount The amount of PolkaBTC to burn, denominated as PolkaBTC
*/
burn(amount: Big): Promise<void>;
/**
* @returns The maximum amount of tokens that can be burned through a liquidation redeem
*/
getMaxBurnableTokens(): Promise<Big>;
/**
* @returns The exchange rate (collateral currency to wrapped token currency)
* used when burning tokens
*/
getBurnExchangeRate(): Promise<Big>;
}

export class DefaultRedeemAPI {
private vaultsAPI: VaultsAPI;
private collateralAPI: CollateralAPI;
requestHash: Hash = this.api.createType("Hash");
events: EventRecord[] = [];
transaction: Transaction;

constructor(private api: ApiPromise, private btcNetwork: Network, private account?: AddressOrPair) {
this.vaultsAPI = new DefaultVaultsAPI(api, btcNetwork);
this.vaultsAPI = new DefaultVaultsAPI(api, btcNetwork, account);
this.collateralAPI = new DefaultCollateralAPI(api, account);
this.transaction = new Transaction(api);
}

Expand Down Expand Up @@ -187,6 +208,32 @@ export class DefaultRedeemAPI {
await this.transaction.sendLogged(cancelRedeemTx, this.account, this.api.events.redeem.CancelRedeem);
}

async burn(amount: Big): Promise<void> {
if (!this.account) {
return Promise.reject(ACCOUNT_NOT_SET_ERROR_MESSAGE);
}
const amountSat = this.api.createType("Balance", btcToSat(amount.toString()));
const burnRedeemTx = this.api.tx.redeem.liquidationRedeem(amountSat);
await this.transaction.sendLogged(burnRedeemTx, this.account, this.api.events.redeem.LiquidationRedeem);
}

async getMaxBurnableTokens(): Promise<Big> {
const liquidationVault = await this.vaultsAPI.getLiquidationVault();
return new Big(satToBTC(liquidationVault.issued_tokens.toString()));
}

async getBurnExchangeRate(): Promise<Big> {
const liquidationVault = await this.vaultsAPI.getLiquidationVault();
const wrappedSatoshi = liquidationVault.issued_tokens.add(liquidationVault.to_be_issued_tokens);
if(wrappedSatoshi.isZero()) {
return Promise.reject("There are no burnable tokens. The burn exchange rate is undefined");
}
const wrappedBtc = new Big(satToBTC(wrappedSatoshi.toString()));
const collateralPlanck = await this.collateralAPI.balanceLocked(liquidationVault.id);
const collateralDot = new Big(planckToDOT(collateralPlanck.toString()));
return collateralDot.div(wrappedBtc);
}

async list(): Promise<RedeemRequestExt[]> {
const head = await this.api.rpc.chain.getFinalizedHead();
const redeemRequests = await this.api.query.redeem.redeemRequests.entriesAt(head);
Expand Down
27 changes: 13 additions & 14 deletions src/parachain/staked-relayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AccountId, BlockNumber, Moment } from "@polkadot/types/interfaces/runti
import { ApiPromise } from "@polkadot/api";
import { VaultsAPI, DefaultVaultsAPI } from "./vaults";
import BN from "bn.js";
import { pagedIterator, decodeFixedPointType, Transaction, ACCOUNT_NOT_SET_ERROR_MESSAGE } from "../utils";
import { pagedIterator, decodeFixedPointType, Transaction, ACCOUNT_NOT_SET_ERROR_MESSAGE, satToBTC, planckToDOT } from "../utils";
import { Network } from "bitcoinjs-lib";
import Big from "big.js";
import { DefaultOracleAPI, OracleAPI } from "./oracle";
Expand Down Expand Up @@ -84,14 +84,14 @@ export interface StakedRelayerAPI {
getAllStatusUpdates(): Promise<Array<{ id: u256; statusUpdate: StatusUpdate }>>;
/**
* @param stakedRelayerId The ID of a staked relayer
* @returns Total rewards in PolkaBTC, denoted in Satoshi, for the given staked relayer
* @returns Total rewards in PolkaBTC for the given staked relayer
*/
getFeesPolkaBTC(stakedRelayerId: AccountId): Promise<string>;
getFeesPolkaBTC(stakedRelayerId: AccountId): Promise<Big>;
/**
* @param stakedRelayerId The ID of a staked relayer
* @returns Total rewards in DOT, denoted in Planck, for the given staked relayer
* @returns Total rewards in DOT for the given staked relayer
*/
getFeesDOT(stakedRelayerId: AccountId): Promise<string>;
getFeesDOT(stakedRelayerId: AccountId): Promise<Big>;
/**
* Get the total APY for a staked relayer based on the income in PolkaBTC and DOT
* divided by the locked DOT.
Expand Down Expand Up @@ -306,7 +306,7 @@ export class DefaultStakedRelayerAPI implements StakedRelayerAPI {
const vaults = await this.vaultsAPI.list();

const collateralizationRates = await Promise.all(
vaults.map<Promise<[AccountId, Big | undefined]>>(async (vault) => [
vaults.filter(vault => vault.status.isActive).map<Promise<[AccountId, Big | undefined]>>(async (vault) => [
vault.id,
await this.vaultsAPI.getVaultCollateralization(vault.id),
])
Expand Down Expand Up @@ -370,26 +370,25 @@ export class DefaultStakedRelayerAPI implements StakedRelayerAPI {
return [...activeStatusUpdates, ...inactiveStatusUpdates];
}

async getFeesPolkaBTC(stakedRelayerId: AccountId): Promise<string> {
async getFeesPolkaBTC(stakedRelayerId: AccountId): Promise<Big> {
const head = await this.api.rpc.chain.getFinalizedHead();
const fees = await this.api.query.fee.totalRewardsPolkaBTC.at(head, stakedRelayerId);
return fees.toString();
return new Big(satToBTC(fees.toString()));
}

async getFeesDOT(stakedRelayerId: AccountId): Promise<string> {
async getFeesDOT(stakedRelayerId: AccountId): Promise<Big> {
const head = await this.api.rpc.chain.getFinalizedHead();
const fees = await this.api.query.fee.totalRewardsDOT.at(head, stakedRelayerId);
return fees.toString();
return new Big(planckToDOT(fees.toString()));
}

async getAPY(stakedRelayerId: AccountId): Promise<string> {
const [feesPolkaBTC, feesDOT, dotToBtcRate, lockedDOT] = await Promise.all([
const [feesPolkaBTC, feesDOT, lockedDOT] = await Promise.all([
await this.getFeesPolkaBTC(stakedRelayerId),
await this.getFeesDOT(stakedRelayerId),
await this.oracleAPI.getExchangeRate(),
await (await this.collateralAPI.balanceLockedDOT(stakedRelayerId)).toString(),
await this.collateralAPI.balanceLocked(stakedRelayerId),
]);
return this.feeAPI.calculateAPY(feesPolkaBTC, feesDOT, lockedDOT, dotToBtcRate);
return this.feeAPI.calculateAPY(feesPolkaBTC, feesDOT, lockedDOT);
}

async getSLA(stakedRelayerId: AccountId): Promise<number> {
Expand Down
Loading

0 comments on commit 429cf39

Please sign in to comment.