UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

1,717 lines (1,493 loc) 190 kB
import { assert, beforeEach, expect, test } from 'vitest'; import { LucidContext, repeat, runAndAwaitTx, runAndAwaitTxBuilder, } from './test-helpers'; import { addrDetails, annulRequest, createProposal, requestSpAccountCreation, fromSystemParamsAsset, MAX_E2S2S_ENTRIES_COUNT, } from '../src'; import { findStabilityPoolAccount, findStabilityPool, } from './queries/stability-pool-queries'; import { readonlyArray as RA, function as F, option as O, array as A, } from 'fp-ts'; import { findCollateralAsset, findIAsset } from './queries/iasset-queries'; import { benchmarkAndAwaitTx, benchmarkAndAwaitTxWithTxFee, } from './utils/benchmark-utils'; import { executeLiquidation, runFreezeCdp, runLiquidateCdp, runOpenCdp, } from './cdp/actions'; import { addAssets, Emulator, fromText, generateEmulatorAccount, Lucid, EmulatorAccount, fromHex, paymentCredentialOf, UTxO, Assets, } from '@lucid-evolution/lucid'; import { adaAssetClass, AssetClass, getInlineDatumOrThrow, isSameAssetClass, lovelacesAmt, matchSingle, mkAssetsOf, mkLovelacesOf, negateAssets, } from '@3rd-eye-labs/cardano-offchain-common'; import { describe } from 'vitest'; import { iusdInitialAssetCfg, mkBaseCollateralAsset, mkCollateralAssetsChain, } from './mock/assets-mock'; import { runCreateAdjustRequest, runCreateCloseRequest, runCreateE2s2sSnapshots, runOpenCdpAndCreateSPAccount, runProcessSpRequest, } from './stability-pool/actions'; import { getValueChangeAtAddressAfterAction, sendValueTo } from './utils'; import { calculateFeeFromRatio } from '../src/utils/indigo-helpers'; import { refreshPriceOracle, runFeedPriceToOracle, } from './price-oracle/actions'; import { expectScriptFailure, expectValue } from './utils/asserts'; import { findFrozenCDPs, findPrice } from './cdp/cdp-queries'; import { processSuccessfulProposal, whitelistCollateralAsset, } from './gov/actions'; import { findPriceOracle } from './price-oracle/price-oracle-queries'; import { match, P } from 'ts-pattern'; import { parsePriceOracleDatum } from '../src/contracts/price-oracle/types-new'; import { findGov } from './gov/governance-queries'; import { createMultipleUtxosAtTreasury } from './endpoints/treasury'; import { MAINNET_PROTOCOL_PARAMETERS } from './indigo-test-helpers'; import { rationalFromInt, rationalMul } from '../src/types/rational'; import { init } from './endpoints/initialize'; type MyContext = LucidContext<{ admin: EmulatorAccount; user1: EmulatorAccount; user2: EmulatorAccount; user3: EmulatorAccount; user4: EmulatorAccount; }>; const collateralAssetA: AssetClass = { currencySymbol: fromHex( // random generated 'cc072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8daa', ), tokenName: fromHex(fromText('A')), }; const collateralAssetB: AssetClass = { currencySymbol: fromHex( // random generated 'cc072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dab', ), tokenName: fromHex(fromText('B')), }; const collateralAssetC: AssetClass = { currencySymbol: fromHex( // random generated 'aa072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dac', ), tokenName: fromHex(fromText('C')), }; const collateralAssetD: AssetClass = { currencySymbol: fromHex( // random generated 'cc072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dad', ), tokenName: fromHex(fromText('D')), }; const collateralAssetE: AssetClass = { currencySymbol: fromHex( // random generated 'aa072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dae', ), tokenName: fromHex(fromText('E')), }; const collateralAssetF: AssetClass = { currencySymbol: fromHex( // random generated 'cc072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8daf', ), tokenName: fromHex(fromText('F')), }; const collateralAssetG: AssetClass = { currencySymbol: fromHex( // random generated 'aa072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dba', ), tokenName: fromHex(fromText('G')), }; const collateralAssetH: AssetClass = { currencySymbol: fromHex( // random generated 'aa072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dbb', ), tokenName: fromHex(fromText('H')), }; const collateralAssetI: AssetClass = { currencySymbol: fromHex( // random generated 'aa072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dbc', ), tokenName: fromHex(fromText('I')), }; const collateralAssetJ: AssetClass = { currencySymbol: fromHex( // random generated 'aa072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dbd', ), tokenName: fromHex(fromText('J')), }; const collateralAssetK: AssetClass = { currencySymbol: fromHex( // random generated 'aa072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dbe', ), tokenName: fromHex(fromText('K')), }; const collateralAssetL: AssetClass = { currencySymbol: fromHex( // random generated 'aa072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dbf', ), tokenName: fromHex(fromText('L')), }; const collateralAssetM: AssetClass = { currencySymbol: fromHex( // random generated 'aa072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dca', ), tokenName: fromHex(fromText('M')), }; const collateralAssetN: AssetClass = { currencySymbol: fromHex( // random generated 'aa072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dcb', ), tokenName: fromHex(fromText('N')), }; describe('Stability pool', () => { beforeEach<MyContext>(async (context: MyContext) => { context.users = { admin: generateEmulatorAccount( addAssets( mkLovelacesOf(100_000_000_000_000n), mkAssetsOf(collateralAssetA, 10_000_000_000_000n), ), ), user1: generateEmulatorAccount( addAssets( mkLovelacesOf(100_000_000_000_000n), mkAssetsOf(collateralAssetA, 10_000_000_000_000n), ), ), user2: generateEmulatorAccount( addAssets( mkLovelacesOf(100_000_000_000_000n), mkAssetsOf(collateralAssetA, 10_000_000_000_000n), ), ), user3: generateEmulatorAccount( addAssets( mkLovelacesOf(100_000_000_000_000n), mkAssetsOf(collateralAssetA, 10_000_000_000_000n), mkAssetsOf(collateralAssetB, 1_000_000_000_000n), mkAssetsOf(collateralAssetC, 1_000_000_000_000n), mkAssetsOf(collateralAssetD, 1_000_000_000_000n), mkAssetsOf(collateralAssetE, 1_000_000_000_000n), mkAssetsOf(collateralAssetF, 1_000_000_000_000n), mkAssetsOf(collateralAssetG, 1_000_000_000_000n), mkAssetsOf(collateralAssetH, 1_000_000_000_000n), mkAssetsOf(collateralAssetI, 1_000_000_000_000n), mkAssetsOf(collateralAssetJ, 1_000_000_000_000n), mkAssetsOf(collateralAssetK, 1_000_000_000_000n), mkAssetsOf(collateralAssetL, 1_000_000_000_000n), mkAssetsOf(collateralAssetM, 1_000_000_000_000n), mkAssetsOf(collateralAssetN, 1_000_000_000_000n), ), ), user4: generateEmulatorAccount( addAssets(mkLovelacesOf(100_000_000_000_000n)), ), }; context.emulator = new Emulator( Object.values(context.users), MAINNET_PROTOCOL_PARAMETERS, ); context.lucid = await Lucid(context.emulator, 'Custom'); }); test<MyContext>('Create Account', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); await runAndAwaitTx( context.lucid, runOpenCdp( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, 10_000_000n, 500_000n, ), ); await benchmarkAndAwaitTx( 'Stability Pool - Create Account - Request', await requestSpAccountCreation( iusdAssetInfo.iassetTokenNameAscii, 10n, sysParams, context.lucid, ), context.lucid, context.emulator, ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const initialAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); await benchmarkAndAwaitTx( 'Stability Pool - Create Account - Process', await runProcessSpRequest( context, sysParams, 'iUSD', paymentCredentialOf(context.users.user1.address).hash, ), context.lucid, context.emulator, ); const finalAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const adminValueDiff = addAssets( finalAdminValue, negateAssets(initialAdminValue), ); // TODO: Investigate why the admin loses a small amount of lovelaces. assert(lovelacesAmt(adminValueDiff) > -7_000n); }); describe('Annul', () => { test<MyContext>('Stability Pool - Create Account - Annul', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); const [pkh, _] = await addrDetails(context.lucid); await runAndAwaitTx( context.lucid, runOpenCdp( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, 1_000_000_000n, 20n, ), ); await runAndAwaitTx( context.lucid, requestSpAccountCreation( iusdAssetInfo.iassetTokenNameAscii, 10n, sysParams, context.lucid, ), ); const accountUtxo = await findStabilityPoolAccount( context.lucid, sysParams, pkh.hash, iusdAssetInfo.iassetTokenNameAscii, ); await benchmarkAndAwaitTx( 'Stability Pool - Create Account - Annul', await annulRequest(accountUtxo.utxo, sysParams, context.lucid), context.lucid, context.emulator, ); }); test<MyContext>('Stability Pool - Adjust Account - Annul', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); const [pkh, _] = await addrDetails(context.lucid); await runAndAwaitTx( context.lucid, runOpenCdp( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, 10_000_000n, 500_000n, ), ); await runAndAwaitTx( context.lucid, requestSpAccountCreation( iusdAssetInfo.iassetTokenNameAscii, 10n, sysParams, context.lucid, ), ); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, 'iUSD', paymentCredentialOf(context.users.user1.address).hash, ), ); await runAndAwaitTx( context.lucid, runCreateAdjustRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, 10n, ), ); const accountUtxo = await findStabilityPoolAccount( context.lucid, sysParams, pkh.hash, iusdAssetInfo.iassetTokenNameAscii, ); await benchmarkAndAwaitTx( 'Stability Pool - Adjust Account - Annul', await annulRequest(accountUtxo.utxo, sysParams, context.lucid), context.lucid, context.emulator, ); }); test<MyContext>('Stability Pool - Close Account - Annul', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); const [pkh, _] = await addrDetails(context.lucid); await runAndAwaitTx( context.lucid, runOpenCdp( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, 10_000_000n, 500_000n, ), ); await runAndAwaitTx( context.lucid, requestSpAccountCreation( iusdAssetInfo.iassetTokenNameAscii, 10n, sysParams, context.lucid, ), ); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, 'iUSD', paymentCredentialOf(context.users.user1.address).hash, ), ); await runAndAwaitTx( context.lucid, runCreateCloseRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, ), ); const accountUtxo = await findStabilityPoolAccount( context.lucid, sysParams, pkh.hash, iusdAssetInfo.iassetTokenNameAscii, ); await benchmarkAndAwaitTx( 'Stability Pool - Close Account - Annul', await annulRequest(accountUtxo.utxo, sysParams, context.lucid), context.lucid, context.emulator, ); }); }); describe('Adjust', () => { test<MyContext>('Deposit Account no liquidation reward', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); await runAndAwaitTx( context.lucid, runOpenCdp( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, 20_000_000n, 5_000_000n, ), ); await runAndAwaitTx( context.lucid, requestSpAccountCreation( iusdAssetInfo.iassetTokenNameAscii, 1_000n, sysParams, context.lucid, ), ); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(context.users.user1.address).hash, ), ); await benchmarkAndAwaitTx( 'Stability Pool - Adjust Account - Request', await runCreateAdjustRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, 10n, ), context.lucid, context.emulator, ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const initialAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const [_, fundsReceived] = await getValueChangeAtAddressAfterAction( context.lucid, context.users.user1.address, async () => await benchmarkAndAwaitTx( 'Stability Pool - Deposit Account no liquidation reward - Process', await runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(context.users.user1.address).hash, ), context.lucid, context.emulator, ), ); // Receives whole create fee for himself const expectedRewardFromFees = BigInt( sysParams.stabilityPoolParams.accountCreateFeeLovelaces, ); expectValue( fundsReceived, 'Expected to receive only the reward after processing my deposit request', ).toEqual(mkLovelacesOf(expectedRewardFromFees)); const finalAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const adminValueDiff = addAssets( finalAdminValue, negateAssets(initialAdminValue), ); // TODO: Investigate why the admin loses a significant amount of lovelaces. assert(lovelacesAmt(adminValueDiff) > -700_000n); }); test<MyContext>('Deposit Account ADA liquidation reward', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); const ACCOUNT_DEPOSIT = 20_000_000n; for (const u of [context.users.user1, context.users.user2]) { context.lucid.selectWallet.fromSeed(u.seedPhrase); await runAndAwaitTx( context.lucid, runOpenCdp( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, 100_000_000n, 30_000_000n, ), ); await runAndAwaitTx( context.lucid, requestSpAccountCreation( iusdAssetInfo.iassetTokenNameAscii, ACCOUNT_DEPOSIT, sysParams, context.lucid, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(u.address).hash, ), ); } // After price doubles the collateral ratio will be 110% const LIQUIDATED_DEBT = 5_000_000n; const LIQUIDATED_COLLATERAL = 11_000_000n; context.lucid.selectWallet.fromSeed(context.users.user4.seedPhrase); await executeLiquidation( context, sysParams, LIQUIDATED_DEBT, LIQUIDATED_COLLATERAL, adaAssetClass, iusdAssetInfo, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); await runAndAwaitTx( context.lucid, runCreateAdjustRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, 1_000n, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const initialAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const [_, fundsReceived] = await getValueChangeAtAddressAfterAction( context.lucid, context.users.user1.address, async () => benchmarkAndAwaitTx( 'Stability Pool - Deposit Account ADA liquidation reward - Process', await runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(context.users.user1.address).hash, ), context.lucid, context.emulator, ), ); const iasset = await findIAsset( context.lucid, sysParams.validatorHashes.iassetHash, fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken), iusdAssetInfo.iassetTokenNameAscii, ); const expectedRewardFromLiquidations = (LIQUIDATED_COLLATERAL - calculateFeeFromRatio( iasset.datum.liquidationProcessingFeeRatio, LIQUIDATED_COLLATERAL, )) / 2n; // Receives whole create fee for himself since first account and then half for the other user (split between 2). const expectedRewardFromFees = BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) + BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) / 2n; expectValue( fundsReceived, 'Expected to receive only the reward after processing my deposit request', ).toEqual( mkLovelacesOf(expectedRewardFromLiquidations + expectedRewardFromFees), ); const finalAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const adminValueDiff = addAssets( finalAdminValue, negateAssets(initialAdminValue), ); // TODO: Investigate why the admin loses a small amount of lovelaces. assert(lovelacesAmt(adminValueDiff) > -7_000n); }); test<MyContext>('Deposit Account non ADA liquidation reward', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [ { ...iusdInitialAssetCfg(), collateralAssets: [mkBaseCollateralAsset(collateralAssetA, 0n)], }, ], context.emulator.slot, ); const ACCOUNT_DEPOSIT = 20_000_000n; for (const u of [context.users.user1, context.users.user2]) { context.lucid.selectWallet.fromSeed(u.seedPhrase); await runAndAwaitTx( context.lucid, runOpenCdp( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, collateralAssetA, 100_000_000n, 30_000_000n, ), ); await runAndAwaitTx( context.lucid, requestSpAccountCreation( iusdAssetInfo.iassetTokenNameAscii, ACCOUNT_DEPOSIT, sysParams, context.lucid, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(u.address).hash, ), ); } // After price doubles the collateral ratio will be 110% const LIQUIDATED_DEBT = 5_000_000n; const LIQUIDATED_COLLATERAL = 11_000_000n; context.lucid.selectWallet.fromSeed(context.users.user3.seedPhrase); // Send funds to user4 so liquidation is performed from a clean address. await sendValueTo( context.users.user4.address, // The extra collateral asset is to be sent to the treasury. mkAssetsOf(collateralAssetA, LIQUIDATED_COLLATERAL + 1_000_000n), context.lucid, ); context.emulator.awaitSlot(1000); await refreshPriceOracle( context, sysParams, iusdAssetInfo, collateralAssetA, ); context.lucid.selectWallet.fromSeed(context.users.user4.seedPhrase); await executeLiquidation( context, sysParams, LIQUIDATED_DEBT, LIQUIDATED_COLLATERAL, collateralAssetA, iusdAssetInfo, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); await runAndAwaitTx( context.lucid, runCreateAdjustRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, 1_000n, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const initialAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const [_, fundsReceived] = await getValueChangeAtAddressAfterAction( context.lucid, context.users.user1.address, async () => benchmarkAndAwaitTx( 'Stability Pool - Deposit Account non ADA liquidation reward - Process', await runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(context.users.user1.address).hash, ), context.lucid, context.emulator, ), ); const iasset = await findIAsset( context.lucid, sysParams.validatorHashes.iassetHash, fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken), iusdAssetInfo.iassetTokenNameAscii, ); const expectedReward = (LIQUIDATED_COLLATERAL - calculateFeeFromRatio( iasset.datum.liquidationProcessingFeeRatio, LIQUIDATED_COLLATERAL, )) / 2n; // Receives whole create fee for himself since first account and then half for the other user (split between 2). const expectedRewardFromFees = BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) + BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) / 2n; expectValue( fundsReceived, 'Expected to receive correct non ADA reward after processing deposit request', ).toEqual( addAssets( mkAssetsOf(collateralAssetA, expectedReward), mkLovelacesOf(expectedRewardFromFees), ), ); const finalAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const adminValueDiff = addAssets( finalAdminValue, negateAssets(initialAdminValue), ); // TODO: Investigate why the admin loses a small amount of lovelaces. assert(lovelacesAmt(adminValueDiff) > -7_000n); }); test<MyContext>('Withdraw Account no liquidation reward', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); await runAndAwaitTx( context.lucid, runOpenCdp( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, 20_000_000n, 5_000_000n, ), ); await runAndAwaitTx( context.lucid, requestSpAccountCreation( iusdAssetInfo.iassetTokenNameAscii, 10_000n, sysParams, context.lucid, ), ); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(context.users.user1.address).hash, ), ); await runAndAwaitTx( context.lucid, runCreateAdjustRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, -1_000n, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const initialAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const [_, fundsReceived] = await getValueChangeAtAddressAfterAction( context.lucid, context.users.user1.address, async () => benchmarkAndAwaitTx( 'Stability Pool - Withdraw Account no liquidation reward - Process', await runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(context.users.user1.address).hash, ), context.lucid, context.emulator, ), ); const iassetAc: AssetClass = { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: fromHex(fromText(iusdAssetInfo.iassetTokenNameAscii)), }; const iasset = await findIAsset( context.lucid, sysParams.validatorHashes.iassetHash, fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken), iusdAssetInfo.iassetTokenNameAscii, ); // Receives whole create fee for himself. const expectedRewardFromFees = BigInt( sysParams.stabilityPoolParams.accountCreateFeeLovelaces, ); expectValue( fundsReceived, 'Expected only the withdrawn amount in the output not taking into account ADA', ).toEqual( addAssets( mkAssetsOf( iassetAc, 1_000n - calculateFeeFromRatio( iasset.datum.stabilityPoolWithdrawalFeeRatio, 1_000n, ), ), mkLovelacesOf(expectedRewardFromFees), ), ); const finalAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const adminValueDiff = addAssets( finalAdminValue, negateAssets(initialAdminValue), ); // TODO: Investigate why the admin loses a significant amount of lovelaces. assert(lovelacesAmt(adminValueDiff) > -700_000n); }); test<MyContext>('Withdraw Account ADA liquidation reward', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); const ACCOUNT_DEPOSIT = 20_000_000n; for (const u of [context.users.user1, context.users.user2]) { context.lucid.selectWallet.fromSeed(u.seedPhrase); await runAndAwaitTx( context.lucid, runOpenCdpAndCreateSPAccount( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, ACCOUNT_DEPOSIT, ), ); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(u.address).hash, ), ); } // After price doubles the collateral ratio will be 110% const LIQUIDATED_DEBT = 5_000_000n; const LIQUIDATED_COLLATERAL = 11_000_000n; context.lucid.selectWallet.fromSeed(context.users.user4.seedPhrase); await executeLiquidation( context, sysParams, LIQUIDATED_DEBT, LIQUIDATED_COLLATERAL, adaAssetClass, iusdAssetInfo, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); await runAndAwaitTx( context.lucid, runCreateAdjustRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, -1_000n, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const initialAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const [_, fundsReceived] = await getValueChangeAtAddressAfterAction( context.lucid, context.users.user1.address, async () => benchmarkAndAwaitTx( 'Stability Pool - Withdraw Account ADA liquidation reward - Process', await runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(context.users.user1.address).hash, ), context.lucid, context.emulator, ), ); const iassetAc: AssetClass = { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: fromHex(fromText(iusdAssetInfo.iassetTokenNameAscii)), }; const iasset = await findIAsset( context.lucid, sysParams.validatorHashes.iassetHash, fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken), iusdAssetInfo.iassetTokenNameAscii, ); const expectedReward = (LIQUIDATED_COLLATERAL - calculateFeeFromRatio( iasset.datum.liquidationProcessingFeeRatio, LIQUIDATED_COLLATERAL, )) / 2n; // Receives whole create fee for himself since first account and then half for the other user (split between 2). const expectedRewardFromFees = BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) + BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) / 2n; const expectedValReceived = addAssets( mkLovelacesOf(expectedReward), mkLovelacesOf(expectedRewardFromFees), mkAssetsOf( iassetAc, 1_000n - calculateFeeFromRatio( iasset.datum.stabilityPoolWithdrawalFeeRatio, 1_000n, ), ), ); expectValue( fundsReceived, 'Expected the withdrawn amount and the reward in the output', ).toEqual(expectedValReceived); const finalAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const adminValueDiff = addAssets( finalAdminValue, negateAssets(initialAdminValue), ); // TODO: Investigate why the admin loses a small amount of lovelaces. assert(lovelacesAmt(adminValueDiff) > -7_000n); }); test<MyContext>('Withdraw Account non ADA liquidation reward', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [ { ...iusdInitialAssetCfg(), collateralAssets: [mkBaseCollateralAsset(collateralAssetA, 0n)], }, ], context.emulator.slot, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); const ACCOUNT_DEPOSIT = 20_000_000n; for (const u of [context.users.user1, context.users.user2]) { context.lucid.selectWallet.fromSeed(u.seedPhrase); await runAndAwaitTx( context.lucid, runOpenCdpAndCreateSPAccount( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, collateralAssetA, ACCOUNT_DEPOSIT, ), ); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(u.address).hash, ), ); } // After price doubles the collateral ratio will be 110% const LIQUIDATED_DEBT = 5_000_000n; const LIQUIDATED_COLLATERAL = 11_000_000n; context.lucid.selectWallet.fromSeed(context.users.user3.seedPhrase); // Send funds to user4 so liquidation is performed from a clean address. await sendValueTo( context.users.user4.address, // The extra collateral asset is to be sent to the treasury. mkAssetsOf(collateralAssetA, LIQUIDATED_COLLATERAL + 1_000_000n), context.lucid, ); context.emulator.awaitSlot(1000); await refreshPriceOracle( context, sysParams, iusdAssetInfo, collateralAssetA, ); context.lucid.selectWallet.fromSeed(context.users.user4.seedPhrase); await executeLiquidation( context, sysParams, LIQUIDATED_DEBT, LIQUIDATED_COLLATERAL, collateralAssetA, iusdAssetInfo, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); await runAndAwaitTx( context.lucid, runCreateAdjustRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, -1_000n, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const initialAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const [_, fundsReceived] = await getValueChangeAtAddressAfterAction( context.lucid, context.users.user1.address, async () => benchmarkAndAwaitTx( 'Stability Pool - Withdraw Account non ADA liquidation reward - Process', await runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(context.users.user1.address).hash, ), context.lucid, context.emulator, ), ); const iassetAc: AssetClass = { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: fromHex(fromText(iusdAssetInfo.iassetTokenNameAscii)), }; const iasset = await findIAsset( context.lucid, sysParams.validatorHashes.iassetHash, fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken), iusdAssetInfo.iassetTokenNameAscii, ); const expectedReward = (LIQUIDATED_COLLATERAL - calculateFeeFromRatio( iasset.datum.liquidationProcessingFeeRatio, LIQUIDATED_COLLATERAL, )) / 2n; const expectedValReceived = addAssets( mkAssetsOf(collateralAssetA, expectedReward), mkAssetsOf( iassetAc, 1_000n - calculateFeeFromRatio( iasset.datum.stabilityPoolWithdrawalFeeRatio, 1_000n, ), ), ); // Receives whole create fee for himself since first account and then half for the other user (split between 2). const expectedRewardFromFees = BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) + BigInt(sysParams.stabilityPoolParams.accountCreateFeeLovelaces) / 2n; expectValue( fundsReceived, 'Expected the withdrawn amount and the reward in the output not taking into account ADA', ).toEqual( addAssets(expectedValReceived, mkLovelacesOf(expectedRewardFromFees)), ); const finalAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const adminValueDiff = addAssets( finalAdminValue, negateAssets(initialAdminValue), ); // TODO: Investigate why the admin loses a small amount of lovelaces. assert(lovelacesAmt(adminValueDiff) > -7_000n); }); }); describe('Close', () => { test<MyContext>('Stability Pool - Close Account with expired oracle', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); const [pkh, _] = await addrDetails(context.lucid); await runAndAwaitTx( context.lucid, runOpenCdp( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, 1_000_000_000n, 20n, ), ); await runAndAwaitTx( context.lucid, requestSpAccountCreation( iusdAssetInfo.iassetTokenNameAscii, 10n, sysParams, context.lucid, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, pkh.hash, ), ); const collateralAsset = await findCollateralAsset( context.lucid, sysParams, adaAssetClass, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, ); const priceOracleUtxo = await findPriceOracle( context.lucid, match(collateralAsset.datum.priceInfo) .with({ OracleNft: P.select() }, (oracleNft) => oracleNft) .otherwise(() => { throw new Error('Expected active oracle'); }), ); const priceOracleDatum = parsePriceOracleDatum( getInlineDatumOrThrow(priceOracleUtxo), ); //Await until oracle is expired context.emulator.awaitSlot(Number(priceOracleDatum.expirationTime)); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); await runAndAwaitTx( context.lucid, runCreateCloseRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, pkh.hash, ), ); }); test<MyContext>('Stability Pool - Close Account with delisted collateral asset', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); const [pkh, _] = await addrDetails(context.lucid); await runAndAwaitTx( context.lucid, runOpenCdp( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, 1_000_000_000n, 20n, ), ); await runAndAwaitTx( context.lucid, requestSpAccountCreation( iusdAssetInfo.iassetTokenNameAscii, 10n, sysParams, context.lucid, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, pkh.hash, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const collateralAsset = await findCollateralAsset( context.lucid, sysParams, adaAssetClass, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, ); const [tx, pollId] = await createProposal( { UpdateCollateralAsset: { correspondingIAsset: fromHex( fromText(iusdAssetInfo.iassetTokenNameAscii), ), collateralAsset: adaAssetClass, newAssetExtraDecimals: 0n, newAssetPriceInfo: { Delisted: { price: rationalFromInt(1n) }, }, newInterestOracleNft: collateralAsset.datum.interestOracleNft, newLiquidationRatio: { numerator: 120n, denominator: 100n }, newMaintenanceRatio: { numerator: 150n, denominator: 100n }, newRedemptionRatio: { numerator: 150n, denominator: 100n }, newMinCollateralAmt: 10_000_000n, }, }, null, sysParams, context.lucid, context.emulator.slot, ( await findGov( context.lucid, sysParams.validatorHashes.govHash, fromSystemParamsAsset(sysParams.govParams.govNFT), ) ).utxo, [], ); await runAndAwaitTxBuilder(context.lucid, tx); await processSuccessfulProposal( pollId, null, null, null, null, ( await findCollateralAsset( context.lucid, sysParams, adaAssetClass, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, ) ).utxo, null, sysParams, context, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); await runAndAwaitTx( context.lucid, runCreateCloseRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, pkh.hash, ), ); }); test<MyContext>('Close Account no rewards (last SP account)', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); await runAndAwaitTx( context.lucid, runOpenCdpAndCreateSPAccount( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, 10_000n, ), ); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(context.users.user1.address).hash, ), ); await benchmarkAndAwaitTx( 'Stability Pool - Close Account - Request', await runCreateCloseRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, ), context.lucid, context.emulator, ); await benchmarkAndAwaitTx( 'Stability Pool - Close Account no rewards - Process (last SP account)', await runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(context.users.user1.address).hash, ), context.lucid, context.emulator, ); }); test<MyContext>('Close Account no rewards (more SP accounts)', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const [sysParams, [iusdAssetInfo]] = await init( context.lucid, [iusdInitialAssetCfg()], context.emulator.slot, ); for (const u of [context.users.user1, context.users.user2]) { context.lucid.selectWallet.fromSeed(u.seedPhrase); await runAndAwaitTx( context.lucid, runOpenCdpAndCreateSPAccount( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, adaAssetClass, 1000n, ), ); await runAndAwaitTx( context.lucid, runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(u.address).hash, ), ); } context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase); await runAndAwaitTx( context.lucid, runCreateCloseRequest( context.lucid, sysParams, iusdAssetInfo.iassetTokenNameAscii, ), ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const initialAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); await benchmarkAndAwaitTx( 'Stability Pool - Close Account no liquidation rewards - Process (more SP accounts)', await runProcessSpRequest( context, sysParams, iusdAssetInfo.iassetTokenNameAscii, paymentCredentialOf(context.users.user1.address).hash, ), context.lucid, context.emulator, ); const finalAdminValue = A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets), )(await context.lucid.utxosAt(context.users.admin.address)); const adminValueDiff = addAssets( finalAdminValue, negateAssets(initialAdminValue), ); // TODO: Investigate why the admin loses a small amount of lovelaces. assert(lovelacesAmt(adminValueDiff) > -7_000n); }); test<MyContext>('Close Account with ADA liquidation rewards (more SP accounts)', async (context: MyContext) => { context.lucid.selectWallet.fromSeed(context.users.admin.seedPh