Skip to content

Commit

Permalink
Merge pull request #629 from peterslany/peter/loans-fix-incentive-rew…
Browse files Browse the repository at this point in the history
…ards-computation

fix: add incentive reward estimation
  • Loading branch information
peterslany authored May 11, 2023
2 parents e9378bb + 24ea89a commit a6aae81
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 10 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@interlay/interbtc-api",
"version": "2.2.0",
"version": "2.2.1",
"description": "JavaScript library to interact with interBTC",
"main": "build/src/index.js",
"typings": "build/src/index.d.ts",
Expand Down
129 changes: 123 additions & 6 deletions src/parachain/loans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
CollateralPosition,
UndercollateralizedPosition,
ExtrinsicData,
AccruedRewards,
} from "../types";
import { ApiPromise } from "@polkadot/api";
import Big, { RoundingMode } from "big.js";
Expand All @@ -35,6 +36,7 @@ import {
} from "../utils";
import { InterbtcPrimitivesCurrencyId, LoansMarket } from "@polkadot/types/lookup";
import { OracleAPI } from "./oracle";
import { CurrencyId } from "../interfaces";

/**
* @category Lending protocol
Expand Down Expand Up @@ -90,12 +92,12 @@ export interface LoansAPI {
getLendTokens(): Promise<Array<LendToken>>;

/**
* Get accrued subsidy rewards amount for the account
* Get accrued subsidy rewards amounts for the account.
*
* @param accountId Account to get rewards for
* @returns {MonetaryAmount<CurrencyExt>} Amount how much rewards the account can claim.
* @returns {Promise<AccruedRewards>} Total amount how much rewards the account can claim and rewards per market.
*/
getAccruedRewardsOfAccount(accountId: AccountId): Promise<MonetaryAmount<CurrencyExt>>;
getAccruedRewardsOfAccount(accountId: AccountId): Promise<AccruedRewards>;

/**
* Lend currency to protocol for borrowing.
Expand Down Expand Up @@ -675,13 +677,128 @@ export class DefaultLoansAPI implements LoansAPI {
return loanAssets;
}

async getAccruedRewardsOfAccount(accountId: AccountId): Promise<MonetaryAmount<CurrencyExt>> {
const [rewardAccrued, rewardCurrency] = await Promise.all([
async _getLatestSupplyIndex(
underlyingCurrencyId: CurrencyId,
lendTokenId: CurrencyId,
currentBlockNumber: number
): Promise<Big> {
const [marketSupplyState, marketSupplySpeed, totalIssuance] = await Promise.all([
this.api.query.loans.rewardSupplyState(underlyingCurrencyId),
// Total KINT / INTR tokens awarded per block to suppliers of this market
this.api.query.loans.rewardSupplySpeed(underlyingCurrencyId),
this.api.query.tokens.totalIssuance(lendTokenId),
]);
const lastIndex = decodeFixedPointType(marketSupplyState.index);
const supplyRewardSpeed = Big(marketSupplySpeed.toString());
const totalSupply = Big(totalIssuance.toString());

if (totalSupply.eq(0)) {
return Big(0);
}

const deltaBlocks = currentBlockNumber - marketSupplyState.block.toNumber();
const deltaIndex = supplyRewardSpeed.mul(deltaBlocks).div(totalSupply);
return lastIndex.add(deltaIndex);
}

async _getAccruedSupplyReward(
accountId: AccountId,
underlyingCurrencyId: CurrencyId,
lendTokenId: CurrencyId,
rewardCurrency: CurrencyExt,
currentBlock: number
): Promise<MonetaryAmount<CurrencyExt>> {
const [latestSupplyIndex, rewardSupplierIndex, lendTokenRawBalance] = await Promise.all([
this._getLatestSupplyIndex(underlyingCurrencyId, lendTokenId, currentBlock),
this.api.query.loans.rewardSupplierIndex(underlyingCurrencyId, accountId),
this.api.query.tokens.accounts(accountId, lendTokenId),
]);
const supplierIndex = decodeFixedPointType(rewardSupplierIndex);
const lendTokenBalance = Big(lendTokenRawBalance.free.toString()).add(lendTokenRawBalance.reserved.toString());
const deltaIndex = latestSupplyIndex.sub(supplierIndex);

const accruedRewardInPlanck = deltaIndex.mul(lendTokenBalance);
const accruedSupplyReward = newMonetaryAmount(accruedRewardInPlanck, rewardCurrency);
return accruedSupplyReward;
}

async _getLatestBorrowIndex(underlyingCurrencyId: CurrencyId, currentBlockNumber: number): Promise<Big> {
const [marketBorrowState, marketBorrowSpeed, totalBorrowsRaw] = await Promise.all([
this.api.query.loans.rewardBorrowState(underlyingCurrencyId),
// Total KINT / INTR tokens awarded per block to suppliers of this market
this.api.query.loans.rewardBorrowSpeed(underlyingCurrencyId),
this.api.query.loans.totalBorrows(underlyingCurrencyId),
]);
const lastBorrowIndex = decodeFixedPointType(marketBorrowState.index);
const borrowRewardSpeed = Big(marketBorrowSpeed.toString());
const totalBorrowed = Big(totalBorrowsRaw.toString());

if (totalBorrowed.eq(0)) {
return Big(0);
}

const deltaBlocks = currentBlockNumber - marketBorrowState.block.toNumber();
const deltaIndex = borrowRewardSpeed.mul(deltaBlocks).div(totalBorrowed);

return lastBorrowIndex.add(deltaIndex);
}

async _getAccruedBorrowReward(
accountId: AccountId,
underlyingCurrencyId: CurrencyId,
rewardCurrency: CurrencyExt,
currentBlock: number
): Promise<MonetaryAmount<CurrencyExt>> {
const [latestBorrowIndex, rewardBorrowerIndex, accountBorrowSnapshot] = await Promise.all([
this._getLatestBorrowIndex(underlyingCurrencyId, currentBlock),
this.api.query.loans.rewardBorrowerIndex(underlyingCurrencyId, accountId),
this.api.query.loans.accountBorrows(underlyingCurrencyId, accountId),
]);
const borrowedAmount = Big(accountBorrowSnapshot.principal.toString());
const borrowerIndex = decodeFixedPointType(rewardBorrowerIndex);
const deltaIndex = latestBorrowIndex.sub(borrowerIndex);
const accruedRewardInPlanck = deltaIndex.mul(borrowedAmount);
const accruedBorrowReward = newMonetaryAmount(accruedRewardInPlanck, rewardCurrency);
return accruedBorrowReward;
}

async getAccruedRewardsOfAccount(accountId: AccountId): Promise<AccruedRewards> {
const [rewardAccrued, rewardCurrency, markets, blockNumber] = await Promise.all([
this.api.query.loans.rewardAccrued(accountId),
this._getRewardCurrency(),
this.getLoansMarkets(),
this.api.query.system.number(),
]);

return newMonetaryAmount(rewardAccrued.toString(), rewardCurrency);
const totalAccrued = newMonetaryAmount(rewardAccrued.toString(), rewardCurrency);
const currentBlock = blockNumber.toNumber();

let totalRewards = totalAccrued;
const rewardsPerMarket: TickerToData<{
lend: MonetaryAmount<CurrencyExt> | null;
borrow: MonetaryAmount<CurrencyExt> | null;
}> = {};
for (const [underlyingCurrency, market] of markets) {
// the following computes the reward not claimed and not yet computed, for a single market
const underlyingCurrencyId = newCurrencyId(this.api, underlyingCurrency);

const [lendReward, borrowReward] = await Promise.all([
this._getAccruedSupplyReward(
accountId,
underlyingCurrencyId,
market.lendTokenId,
rewardCurrency,
currentBlock
),
this._getAccruedBorrowReward(accountId, underlyingCurrencyId, rewardCurrency, currentBlock),
]);
rewardsPerMarket[underlyingCurrency.ticker] = {
lend: lendReward.toBig().gt(0) ? lendReward : null,
borrow: borrowReward.toBig().gt(0) ? borrowReward : null,
};
totalRewards = totalRewards.add(lendReward).add(borrowReward);
}
return { total: totalRewards, perMarket: rewardsPerMarket };
}

// Check that market for given currency is added and in active state.
Expand Down
9 changes: 9 additions & 0 deletions src/types/loans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,16 @@ type UndercollateralizedPosition = {
borrowPositions: Array<BorrowPosition>;
};

interface AccruedRewards {
total: MonetaryAmount<CurrencyExt>;
perMarket: TickerToData<{
lend: MonetaryAmount<CurrencyExt> | null;
borrow: MonetaryAmount<CurrencyExt> | null;
}>;
}

export type {
AccruedRewards,
LoanPosition,
CollateralPosition,
BorrowPosition,
Expand Down
69 changes: 66 additions & 3 deletions test/integration/parachain/staging/sequential/loans.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ describe("Loans", () => {
let userInterBtcAPI: InterBtcApi;
let user2InterBtcAPI: InterBtcApi;
let sudoInterBtcAPI: InterBtcApi;
let TransactionAPI: DefaultTransactionAPI;
let LoansAPI: DefaultLoansAPI;

let userAccount: KeyringPair;
Expand Down Expand Up @@ -354,8 +353,72 @@ describe("Loans", () => {
});

describe("getAccruedRewardsOfAccount", () => {
it("should return correct amount of reward", () => {
// TODO
before(async function () {
const addRewardExtrinsic = sudoInterBtcAPI.api.tx.loans.addReward("100000000000000");
const updateRewardSpeedExtrinsic_1 = sudoInterBtcAPI.api.tx.loans.updateMarketRewardSpeed(
underlyingCurrencyId,
"1000000000000",
"0"
);
const updateRewardSpeedExtrinsic_2 = sudoInterBtcAPI.api.tx.loans.updateMarketRewardSpeed(
underlyingCurrencyId2,
"0",
"1000000000000"
);

const updateRewardSpeed = api.tx.sudo.sudo(
sudoInterBtcAPI.api.tx.utility.batchAll([updateRewardSpeedExtrinsic_1, updateRewardSpeedExtrinsic_2])
);

const rewardExtrinsic = sudoInterBtcAPI.api.tx.utility.batchAll([addRewardExtrinsic, updateRewardSpeed]);

const result = await DefaultTransactionAPI.sendLogged(
api,
sudoAccount,
rewardExtrinsic,
api.events.sudo.Sudid
);

expect(result.isCompleted, "Sudo event to add rewards not found").to.be.true;
});

it("should return correct amount of rewards", async () => {
await submitExtrinsic(
userInterBtcAPI,
await userInterBtcAPI.loans.lend(underlyingCurrency, newMonetaryAmount(1, underlyingCurrency, true)),
false
);

const rewards = await userInterBtcAPI.loans.getAccruedRewardsOfAccount(userAccountId);

expect(rewards.total.toBig().eq(1)).to.be.true;

await submitExtrinsic(userInterBtcAPI, {
extrinsic: userInterBtcAPI.api.tx.utility.batchAll([
(
await userInterBtcAPI.loans.lend(
underlyingCurrency2,
newMonetaryAmount(0.1, underlyingCurrency2, true)
)
).extrinsic,
(await userInterBtcAPI.loans.enableAsCollateral(underlyingCurrency)).extrinsic,
(
await userInterBtcAPI.loans.borrow(
underlyingCurrency2,
newMonetaryAmount(0.1, underlyingCurrency2, true)
)
).extrinsic,
]),
event: userInterBtcAPI.api.events.loans.Borrowed,
});

const rewardsAfterBorrow = await userInterBtcAPI.loans.getAccruedRewardsOfAccount(userAccountId);

expect(rewardsAfterBorrow.total.toBig().eq(2)).to.be.true;

// repay the loan to clean the state
await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.repayAll(underlyingCurrency2));
await submitExtrinsic(userInterBtcAPI, await userInterBtcAPI.loans.withdrawAll(underlyingCurrency2));
});
});

Expand Down

0 comments on commit a6aae81

Please sign in to comment.