@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
1,717 lines (1,493 loc) • 190 kB
text/typescript
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