@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
1,836 lines (1,715 loc) • 70.4 kB
text/typescript
import { beforeEach, describe, expect, test, vi } from 'vitest';
import {
addrDetails,
calculateLeverageFromCollateralRatio,
calculateTotalCollateralForRedemption,
cdpCollateralRatioPercentage,
fromSystemParamsAsset,
getInlineDatumOrThrow,
leverageCdpWithRob,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
MIN_ROB_COLLATERAL_AMT,
openRob,
randomRobsSubsetSatisfyingTargetCollateral,
Rational,
rationalToFloat,
robCollateralAmtToSpend,
RobDatum,
SystemParams,
} from '../../src';
import {
addAssets,
Assets,
Emulator,
EmulatorAccount,
fromHex,
fromText,
generateEmulatorAccount,
Lucid,
slotToUnixTime,
toHex,
UTxO,
} from '@lucid-evolution/lucid';
import { LucidContext, runAndAwaitTx } from '../test-helpers';
import { init } from '../endpoints/initialize';
import {
DEFAULT_PRICE,
iusdInitialAssetCfg,
iusdInitialAssetCfgWithPyth,
mkBaseCollateralAsset,
} from '../mock/assets-mock';
import { assertValueInRange } from '../utils/asserts';
import {
adaAssetClass,
AssetClass,
assetClassToUnit,
assetClassValueOf,
isSameAssetClass,
lovelacesAmt,
mkAssetsOf,
mkLovelacesOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import {
findAllNecessaryOrefs,
findCdp,
findPriceOracleFromCollateralAsset,
} from '../cdp/cdp-queries';
import { parsePriceOracleDatum } from '../../src/contracts/price-oracle/types-new';
import { parseInterestOracleDatum } from '../../src/contracts/interest-oracle/types-new';
import { benchmarkAndAwaitTx } from '../utils/benchmark-utils';
import { findAllRobs } from './rob-queries';
import { MAINNET_PROTOCOL_PARAMETERS } from '../indigo-test-helpers';
import { match, P } from 'ts-pattern';
import { ParsedFeedPayload } from '@pythnetwork/pyth-lazer-sdk';
import { createPythMessage } from '../pyth/helpers';
import { retrieveAdjustedPrice } from '../../src/utils/oracle-helpers';
type MyContext = LucidContext<{
admin: EmulatorAccount;
user: EmulatorAccount;
}>;
const collateralAssetA: AssetClass = {
currencySymbol: fromHex(
// random generated
'cc072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dea',
),
tokenName: fromHex(fromText('A')),
};
async function openBuyRobs(
context: MyContext,
sysParams: SystemParams,
iasset: string,
collateralAsset: AssetClass,
amountsToSpend: bigint[],
maxPrice: Rational,
): Promise<void> {
for (const amt of amountsToSpend) {
await runAndAwaitTx(
context.lucid,
openRob(
iasset,
amt,
{ BuyIAssetOrder: { collateralAsset: collateralAsset, maxPrice } },
context.lucid,
sysParams,
),
);
}
}
function hadRobRedemption(
lrp: { utxo: UTxO; datum: RobDatum },
sysParams: SystemParams,
): boolean {
return (
assetClassValueOf(lrp.utxo.assets, {
currencySymbol: fromHex(
sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
),
tokenName: lrp.datum.iasset,
}) > 0
);
}
describe('randomRobsSubsetSatisfyingTargetCollateral', () => {
const mockUtxo = (ada: bigint, otherAssets: Assets = {}): UTxO => ({
address: '',
assets: addAssets(mkLovelacesOf(ada), otherAssets),
outputIndex: 0,
txHash: '',
});
test('1', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
120_000_000n,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toEqual(expect.arrayContaining(lrps));
});
test('2', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(150_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
100_000_000n,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toEqual(lrps);
});
test('3', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(10_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
const mockedShuffle = vi.fn().mockImplementation(() => lrps);
expect(
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
120_000_000n,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
mockedShuffle,
),
).toEqual(expect.arrayContaining(lrps));
});
test('4 (min rob collateral causes less redeemable)', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
const mockedShuffle = vi.fn().mockImplementation(() => lrps);
expect(() =>
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
100_000_000n,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
mockedShuffle,
),
).toThrow(new Error("Couldn't achieve target lovelaces"));
});
test('5 (too small amount to redeem - payout 0)', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(() =>
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
5n,
{ numerator: 10n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toThrow('Must redeem and payout more than 0.');
});
test('6 (too small amount to redeem - redeemable 0)', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(() =>
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
0n,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toThrow('Must redeem and payout more than 0.');
});
test('7 (dont pick fully redeemed rob)', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(5n + MIN_ROB_COLLATERAL_AMT),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 20n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
const mockedShuffle = vi.fn().mockImplementation(() => lrps);
expect(
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
100_000n,
{ numerator: 10n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
mockedShuffle,
),
).toEqual(expect.arrayContaining([lrps[1]]));
});
test('8 (prevent redeeming 0 or payout 0 to any ROB)', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100n + MIN_ROB_COLLATERAL_AMT),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 20n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(10n + MIN_ROB_COLLATERAL_AMT),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 20n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(30n + MIN_ROB_COLLATERAL_AMT),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 20n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
const mockedShuffle = vi.fn().mockImplementation(() => lrps);
// Since the second ROB would cause the target redeemable unachievable because otherwise,
// the remaining ROB would payout 0.
// Since there's a larger ROB, replace the second one with the last one achieving the target in full.
expect(
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
115n,
{ numerator: 10n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
mockedShuffle,
),
).toEqual(expect.arrayContaining([lrps[0], lrps[2]]));
});
test('9 (cant redeem more to achieve target because it would cause 0 payout)', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100n + MIN_ROB_COLLATERAL_AMT),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 20n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100n + MIN_ROB_COLLATERAL_AMT),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 20n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
const mockedShuffle = vi.fn().mockImplementation(() => lrps);
expect(
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
105n,
{ numerator: 10n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
mockedShuffle,
),
).toEqual(expect.arrayContaining([lrps[0]]));
});
test('filtering by iasset 1', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iBTC')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
110_000_000n,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toEqual(expect.arrayContaining([lrps[0], lrps[2]]));
});
test('filtering by iasset 2', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iBTC')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(() =>
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
110_000_000n,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toThrow("Couldn't achieve target lovelaces");
});
test('filtering by collateral asset 1', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n, mkAssetsOf(collateralAssetA, 100n)),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iBTC')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n, mkAssetsOf(collateralAssetA, 100n)),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
collateralAssetA,
110n,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toEqual(expect.arrayContaining([lrps[0], lrps[3]]));
});
test('filtering by collateral asset 2', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100n),
{
iasset: fromHex(fromText('iBTC')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100n, mkAssetsOf(collateralAssetA, 100n)),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(() =>
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
collateralAssetA,
110n,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toThrow("Couldn't achieve target lovelaces");
});
test('filtering by price 1', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 15n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(() =>
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
adaAssetClass,
120n,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toThrow("Couldn't achieve target lovelaces");
});
test('filtering by price 2', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(10n, mkAssetsOf(collateralAssetA, 100n)),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 15n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(10n, mkAssetsOf(collateralAssetA, 100n)),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(10n, mkAssetsOf(collateralAssetA, 100n)),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
collateralAssetA,
120n,
{ numerator: 11n, denominator: 10n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toEqual(expect.arrayContaining([lrps[0], lrps[2]]));
});
test('max redemptions check 1', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(10n, mkAssetsOf(collateralAssetA, 100n)),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(10n, mkAssetsOf(collateralAssetA, 90n)),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(10n, mkAssetsOf(collateralAssetA, 80n)),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(10n, mkAssetsOf(collateralAssetA, 70n)),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(10n, mkAssetsOf(collateralAssetA, 100n)),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
const mockedShuffle = vi.fn().mockImplementation(() => lrps);
expect(MAX_REDEMPTIONS_WITH_CDP_OPEN).toBe(4);
// replaces the lrps[3] with lrps[4] since it's larger and already hitting max redemptions
expect(
randomRobsSubsetSatisfyingTargetCollateral(
fromHex(fromText('iUSD')),
collateralAssetA,
360n,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
mockedShuffle,
),
).toEqual(expect.arrayContaining([lrps[0], lrps[1], lrps[2], lrps[4]]));
});
});
describe('robAmountToSpend', () => {
const mockUtxo = (ada: bigint, otherAssets: Assets = {}): UTxO => ({
address: '',
assets: addAssets(mkLovelacesOf(ada), otherAssets),
outputIndex: 0,
txHash: '',
});
test('1', () => {
expect(
robCollateralAmtToSpend(
mockUtxo(100n, mkAssetsOf(collateralAssetA, 30n)).assets,
{
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
),
).toEqual<bigint>(30n);
});
test('2', () => {
expect(
robCollateralAmtToSpend(mockUtxo(20_000_000n).assets, {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 13n, denominator: 10n },
},
}),
).toEqual<bigint>(20_000_000n - MIN_ROB_COLLATERAL_AMT);
});
test('Less than min rob collateral', () => {
expect(
robCollateralAmtToSpend(mockUtxo(2_000_000n).assets, {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 13n, denominator: 10n },
},
}),
).toEqual<bigint>(0n);
});
});
describe('calculateTotalCollateralForRedemption', () => {
const mockUtxo = (ada: bigint, otherAssets: Assets = {}): UTxO => ({
address: '',
assets: addAssets(mkLovelacesOf(ada), otherAssets),
outputIndex: 0,
txHash: '',
});
test('1', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(
calculateTotalCollateralForRedemption(
fromHex(fromText('iUSD')),
adaAssetClass,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toEqual<bigint>(200_000_000n - MIN_ROB_COLLATERAL_AMT * 2n);
});
test('2', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(1000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(
calculateTotalCollateralForRedemption(
fromHex(fromText('iUSD')),
adaAssetClass,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toEqual<bigint>(100_000_000n - MIN_ROB_COLLATERAL_AMT);
});
test('filtering by assets 1', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iBTC')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iETH')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 13n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(
calculateTotalCollateralForRedemption(
fromHex(fromText('iUSD')),
adaAssetClass,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toEqual<bigint>(100_000_000n - MIN_ROB_COLLATERAL_AMT);
});
test('filtering by price 1', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(1000_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(1000_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 15n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(1000_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 8n, denominator: 10n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(
calculateTotalCollateralForRedemption(
fromHex(fromText('iUSD')),
adaAssetClass,
{ numerator: 11n, denominator: 10n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
).toEqual<bigint>(1000_000_000n - MIN_ROB_COLLATERAL_AMT);
});
test('capping by max redemptions 1', () => {
const lrps: [UTxO, RobDatum][] = [
[
mockUtxo(100_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(140_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(160_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(180_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
[
mockUtxo(200_000_000n),
{
iasset: fromHex(fromText('iUSD')),
orderType: {
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 1n, denominator: 1n },
},
},
owner: fromHex(''),
robRefInput: { outputIndex: 0n, txHash: fromHex('') },
},
],
];
expect(MAX_REDEMPTIONS_WITH_CDP_OPEN).toBe(4);
expect(
calculateTotalCollateralForRedemption(
fromHex(fromText('iUSD')),
adaAssetClass,
{ numerator: 1n, denominator: 1n },
lrps,
MAX_REDEMPTIONS_WITH_CDP_OPEN,
),
// I.e. the one with 1000n lovelaces is dropped
).toEqual<bigint>(680_000_000n - MIN_ROB_COLLATERAL_AMT * 4n);
});
});
describe('LRP leverage', () => {
beforeEach<MyContext>(async (context: MyContext) => {
context.users = {
admin: generateEmulatorAccount(
addAssets(
mkLovelacesOf(100_000_000_000_000n),
mkAssetsOf(collateralAssetA, 100_000_000_000n),
),
),
user: generateEmulatorAccount(addAssets(mkLovelacesOf(150_000_000n))),
};
context.emulator = new Emulator(
[context.users.admin, context.users.user],
MAINNET_PROTOCOL_PARAMETERS,
);
context.lucid = await Lucid(context.emulator, 'Custom');
});
test<MyContext>('Open 2x leveraged CDP; 1 LRP; price ~1.1; f_r=.01; f_m=.005', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[
{
...iusdInitialAssetCfg(),
collateralAssets: [
{
...mkBaseCollateralAsset(adaAssetClass, 0n, {
numerator: 1_104_093n,
denominator: 1_000_000n,
}),
maintenanceRatio: { numerator: 15n, denominator: 10n },
},
],
debtMintingFeeRatio: { numerator: 1n, denominator: 200n },
redemptionReimbursementRatio: { numerator: 1n, denominator: 100n },
},
],
context.emulator.slot,
);
await openBuyRobs(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
[100_000_000n],
{ numerator: 15n, denominator: 10n },
);
const allRobs = await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
);
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
if (priceOracleUtxo == null) {
throw new Error('Expected oracle UTXO');
}
const baseCollateral = 20_000_000n;
await benchmarkAndAwaitTx(
'Leverage - CDP open with 1 LRP',
await leverageCdpWithRob(
2,
baseCollateral,
priceOracleUtxo,
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
orefs.cdpCreatorUtxo,
orefs.interestOracleUtxo,
orefs.treasuryUtxo,
sysParams,
context.lucid,
allRobs.map((lrps) => [lrps.utxo, lrps.datum]),
context.emulator.slot,
),
context.lucid,
context.emulator,
);
const [pkh, skh] = await addrDetails(context.lucid);
const res = await findCdp(
context.lucid,
sysParams.validatorHashes.cdpHash,
fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
pkh.hash,
skh,
);
// Assert leverage
assertValueInRange(
Number(lovelacesAmt(res.utxo.assets)) / Number(baseCollateral),
{
min: 1.999,
max: 2.0,
},
);
// Assert collateral ratio
assertValueInRange(
cdpCollateralRatioPercentage(
context.emulator.slot,
parsePriceOracleDatum(getInlineDatumOrThrow(priceOracleUtxo)).price,
res.utxo,
res.datum,
parseInterestOracleDatum(
getInlineDatumOrThrow(orefs.interestOracleUtxo),
),
context.lucid.config().network!,
),
{
min: 197,
max: 197.001,
},
);
});
test<MyContext>('Pyth oracle - Open 2x leveraged CDP; 1 LRP; price ~1.1; f_r=.01; f_m=.005', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[],
context.emulator.slot,
(pythStateNft: AssetClass) => [
iusdInitialAssetCfgWithPyth(pythStateNft, (pythStatePolicyId) => [
mkBaseCollateralAsset(
adaAssetClass,
0n,
DEFAULT_PRICE,
0n,
pythStatePolicyId,
{
tag: 'value',
val: { priceFeedId: 1 },
},
),
]),
],
);
await openBuyRobs(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
[100_000_000n],
{ numerator: 15n, denominator: 10n },
);
const allRobs = await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
);
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
const currentTime = BigInt(
slotToUnixTime(context.lucid.config().network!, context.emulator.slot),
);
const iUsdFeed: ParsedFeedPayload = {
priceFeedId: 1,
price: '1104093',
exponent: -6,
};
const message = toHex(
await createPythMessage([iUsdFeed], currentTime * 1_000n),
);
const pythStateOref = await context.lucid.utxoByUnit(
assetClassToUnit(
fromSystemParamsAsset(sysParams.pythConfig.pythStateAssetClass),
),
);
const [price, _] = await retrieveAdjustedPrice(
orefs.iasset.datum.assetName,
orefs.collateralAsset.datum.collateralAsset,
orefs.collateralAsset.datum.priceInfo,
orefs.collateralAsset.datum.extraDecimals,
priceOracleUtxo,
message,
sysParams.pythConfig,
context.lucid,
);
const baseCollateral = 20_000_000n;
await benchmarkAndAwaitTx(
'Leverage - CDP open with 1 LRP using Pyth oracle',
await leverageCdpWithRob(
2,
baseCollateral,
priceOracleUtxo,
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
orefs.cdpCreatorUtxo,
orefs.interestOracleUtxo,
orefs.treasuryUtxo,
sysParams,
context.lucid,
allRobs.map((lrps) => [lrps.utxo, lrps.datum]),
context.emulator.slot,
message,
pythStateOref,
),
context.lucid,
context.emulator,
);
const [pkh, skh] = await addrDetails(context.lucid);
const res = await findCdp(
context.lucid,
sysParams.validatorHashes.cdpHash,
fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
pkh.hash,
skh,
);
// Assert leverage
assertValueInRange(
Number(lovelacesAmt(res.utxo.assets)) / Number(baseCollateral),
{
min: 1.999,
max: 2.0,
},
);
// Assert collateral ratio
assertValueInRange(
cdpCollateralRatioPercentage(
context.emulator.slot,
price,
res.utxo,
res.datum,
parseInterestOracleDatum(
getInlineDatumOrThrow(orefs.interestOracleUtxo),
),
context.lucid.config().network!,
),
{
min: 197,
max: 197.001,
},
);
});
test<MyContext>('Open 2x leveraged CDP; 4 LRPs; price ~0.9; f_r=.01; f_m=.005', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[
{
...iusdInitialAssetCfg(),
collateralAssets: [
{
...mkBaseCollateralAsset(adaAssetClass, 0n, {
numerator: 904_093n,
denominator: 1_000_000n,
}),
maintenanceRatio: { numerator: 15n, denominator: 10n },
},
],
debtMintingFeeRatio: { numerator: 1n, denominator: 200n },
redemptionReimbursementRatio: { numerator: 1n, denominator: 100n },
},
],
context.emulator.slot,
);
await openBuyRobs(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
[26_250_000n, 26_250_000n, 26_250_000n, 26_250_000n],
{ numerator: 15n, denominator: 10n },
);
const allRobs = await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
);
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
if (priceOracleUtxo == null) {
throw new Error('Expected oracle UTXO');
}
const baseCollateral = 100_000_000n;
await runAndAwaitTx(
context.lucid,
leverageCdpWithRob(
2,
baseCollateral,
priceOracleUtxo,
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
orefs.cdpCreatorUtxo,
orefs.interestOracleUtxo,
orefs.treasuryUtxo,
sysParams,
context.lucid,
allRobs.map((lrps) => [lrps.utxo, lrps.datum]),
context.emulator.slot,
),
);
const [pkh, skh] = await addrDetails(context.lucid);
const res = await findCdp(
context.lucid,
sysParams.validatorHashes.cdpHash,
fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
pkh.hash,
skh,
);
// Assert leverage
assertValueInRange(
Number(lovelacesAmt(res.utxo.assets)) / Number(baseCollateral),
{
min: 1.999,
max: 2.0,
},
);
// Assert collateral ratio
assertValueInRange(
cdpCollateralRatioPercentage(
context.emulator.slot,
parsePriceOracleDatum(getInlineDatumOrThrow(priceOracleUtxo)).price,
res.utxo,
res.datum,
parseInterestOracleDatum(
getInlineDatumOrThrow(orefs.interestOracleUtxo),
),
context.lucid.config().network!,
),
{
min: 197,
max: 197.001,
},
);
{
const lrps = await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
);
expect(
lrps.every((lrp) => hadRobRedemption(lrp, sysParams)),
).toBeTruthy();
}
});
test<MyContext>('Open 2.3x leveraged CDP; 4 LRPs; price ~1.03; f_r=.01; f_m=.013', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[
{
...iusdInitialAssetCfg(),
collateralAssets: [
{
...mkBaseCollateralAsset(adaAssetClass, 0n, {
numerator: 1_037_093n,
denominator: 1_000_000n,
}),
maintenanceRatio: { numerator: 15n, denominator: 10n },
},
],
debtMintingFeeRatio: { numerator: 13n, denominator: 1_000n },
redemptionReimbursementRatio: { numerator: 1n, denominator: 100n },
},
],
context.emulator.slot,
);
await openBuyRobs(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
[35_139_729n, 35_000_397n, 35_001_079n, 35_107_049n],
{ numerator: 15n, denominator: 10n },
);
const allLrps = await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
);
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
if (priceOracleUtxo == null) {
throw new Error('Expected oracle UTXO');
}
const baseCollateral = 100_000_000n;
await runAndAwaitTx(
context.lucid,
leverageCdpWithRob(
2.3,
baseCollateral,
priceOracleUtxo,
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
orefs.cdpCreatorUtxo,
orefs.interestOracleUtxo,
orefs.treasuryUtxo,
sysParams,
context.lucid,
allLrps.map((lrps) => [lrps.utxo, lrps.datum]),
context.emulator.slot,
),
);
const [pkh, skh] = await addrDetails(context.lucid);
const res = await findCdp(
context.lucid,
sysParams.validatorHashes.cdpHash,
fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken),
pkh.hash,
skh,
);
// Assert leverage
assertValueInRange(
Number(lovelacesAmt(res.utxo.assets)) / Number(baseCollateral),
{
min: 2.29999,
max: 2.3,
},
);
// Assert collateral ratio
assertValueInRange(
cdpCollateralRatioPercentage(
context.emulator.slot,
parsePriceOracleDatum(getInlineDatumOrThrow(priceOracleUtxo)).price,
res.utxo,
res.datum,
parseInterestOracleDatum(
getInlineDatumOrThrow(orefs.interestOracleUtxo),
),
context.lucid.config().network!,
),
{
min: 172,
max: 172.1,
},
);
{
const lrps = await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
);
expect(
lrps.every((lrp) => hadRobRedemption(lrp, sysParams)),
).toBeTruthy();
}
});
test<MyContext>('Open 1.2x leveraged CDP 3 LRPs price ~1.46; f_r=.02; f_m=.007', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[
{
...iusdInitialAssetCfg(),
collateralAssets: [
{
...mkBaseCollateralAsset(adaAssetClass, 0n, {
numerator: 1_461_093n,
denominator: 1_000_000n,
}),
maintenanceRatio: { numerator: 15n, denominator: 10n },
},
],
debtMintingFeeRatio: { numerator: 7n, denominator: 1_000n },
redemptionReimbursementRatio: { numerator: 2n, den