UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

1,836 lines (1,715 loc) 70.4 kB
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