@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
1,790 lines (1,593 loc) • 98.8 kB
text/typescript
import { assert, beforeEach, describe, expect, test } from 'vitest';
import {
Emulator,
EmulatorAccount,
fromHex,
fromText,
generateEmulatorAccount,
Lucid,
paymentCredentialOf,
addAssets,
} from '@lucid-evolution/lucid';
import {
adjustRob,
cancelRob,
claimRob,
openRob,
} from '../../src/contracts/rob/transactions';
import { findAllRobs, findSingleRob } from './rob-queries';
import { addrDetails } from '../../src/utils/lucid-utils';
import {
IndigoTestContext,
LucidContext,
repeat,
runAndAwaitTx,
runAndAwaitTxBuilder,
} from '../test-helpers';
import {
createProposal,
fromSystemParamsAsset,
MIN_ROB_COLLATERAL_AMT,
robAmtToSpend,
robCollateralAmtToSpend,
} from '../../src';
import { strictEqual } from 'assert';
import {
adaAssetClass,
AssetClass,
assetClassValueOf,
lovelacesAmt,
mkAssetsOf,
mkLovelacesOf,
} from '@3rd-eye-labs/cardano-offchain-common';
import {
iusdInitialAssetCfg,
mkBaseCollateralAsset,
} from '../mock/assets-mock';
import { init } from '../endpoints/initialize';
import {
findAllNecessaryOrefs,
findOwnCdpNew,
findPriceOracleFromCollateralAsset,
findRandomCdpCreatorNew,
} from '../cdp/cdp-queries';
import {
redeemWithCdpAdjust,
redeemWithCdpClose,
redeemWithCdpCreate,
testRedeemRob,
} from './transactions-mutated';
import { expectScriptFailure, expectValue } from '../utils/asserts';
import { findGov } from '../gov/governance-queries';
import {
findCollateralAsset,
findCollateralAssetNew,
findIAssetNew,
} from '../queries/iasset-queries';
import { processSuccessfulProposal } from '../gov/actions';
import { runOpenCdp } from '../cdp/actions';
import {
runFeedPriceToOracle,
waitForOracleExpiration,
} from '../price-oracle/actions';
import { runRedeemRob } from './actions';
import { calculateFeeFromRatio } from '../../src/utils/indigo-helpers';
import { benchmarkAndAwaitTx } from '../utils/benchmark-utils';
import { sendValueTo, totalValueAtAddress } from '../utils';
import { MAINNET_PROTOCOL_PARAMETERS } from '../indigo-test-helpers';
import {
Rational,
rationalDiv,
rationalFloor,
rationalFromInt,
rationalMul,
} from '../../src/types/rational';
type MyContext = LucidContext<{
admin: EmulatorAccount;
user1: EmulatorAccount;
user2: EmulatorAccount;
}>;
const collateralAssetA: AssetClass = {
currencySymbol: fromHex(
// random generated
'cc072059ae741791b7b9c23d9baea6a0b0d764dec617ce7e027a8dea',
),
tokenName: fromHex(fromText('A')),
};
describe('ROB', () => {
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(1_000_000_000_000n),
mkAssetsOf(collateralAssetA, 10_000_000_000_000n),
),
),
user2: generateEmulatorAccount(
addAssets(
mkLovelacesOf(1_000_000_000_000n),
mkAssetsOf(collateralAssetA, 10_000_000_000_000n),
),
),
};
context.emulator = new Emulator(
Object.values(context.users),
MAINNET_PROTOCOL_PARAMETERS,
);
context.lucid = await Lucid(context.emulator, 'Custom');
});
describe('Composite Txs', () => {
describe('redeem with CDP Adjust', () => {
test<MyContext>('ROB buy orders redeem with CDP deposit and mint (ADA case)', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[iusdInitialAssetCfg(0n)],
context.emulator.slot,
);
const robs_count = 8; // Adjusted for Pyth updates
await repeat(robs_count, async () => {
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
20_000_000n,
{
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: rationalFromInt(1n),
},
},
context.lucid,
sysParams,
),
);
});
context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase);
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
100_000_000n,
500_000n,
),
);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
);
const SINGLE_REDEMPTION_AMT = 1_000_000n;
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
await benchmarkAndAwaitTx(
`ROB composite - ${robs_count} buy orders redeem with CDP deposit and mint`,
await redeemWithCdpAdjust(
(
await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).map((rob) => [rob.utxo, SINGLE_REDEMPTION_AMT]),
5n,
BigInt(robs_count) * SINGLE_REDEMPTION_AMT,
(await findOwnCdpNew(context.lucid, sysParams)).utxo,
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
priceOracleUtxo!,
orefs.interestOracleUtxo,
orefs.treasuryUtxo,
orefs.interestCollectorUtxo,
context.emulator.slot,
context.lucid,
sysParams,
),
context.lucid,
context.emulator,
);
});
test<MyContext>('ROB buy orders redeem with CDP deposit and mint (non ADA case)', 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 robs_count = 7; // Adjusted for Pyth updates
await repeat(robs_count, async () => {
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
20_000_000n,
{
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: rationalFromInt(1n),
},
},
context.lucid,
sysParams,
),
);
});
context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase);
const allCollateralInWallet = assetClassValueOf(
await totalValueAtAddress(context.lucid, context.users.user1.address),
collateralAssetA,
);
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
collateralAssetA,
allCollateralInWallet,
500_000n,
),
);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
collateralAssetA,
);
const SINGLE_PAYOUT_IASSET_AMT = 1_000_000n;
const collateralAdjustment =
BigInt(robs_count) *
(SINGLE_PAYOUT_IASSET_AMT -
calculateFeeFromRatio(
(
await findIAssetNew(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).datum.redemptionReimbursementRatio,
SINGLE_PAYOUT_IASSET_AMT,
));
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
await benchmarkAndAwaitTx(
`ROB composite - ${robs_count} buy orders redeem with CDP deposit and mint (non ADA case)`,
await redeemWithCdpAdjust(
(
await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).map((rob) => [rob.utxo, SINGLE_PAYOUT_IASSET_AMT]),
// Since there's no more collateral in wallet (because all used as collateral before), this must be coming from redemptions.
collateralAdjustment,
BigInt(robs_count) * SINGLE_PAYOUT_IASSET_AMT,
(await findOwnCdpNew(context.lucid, sysParams)).utxo,
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
priceOracleUtxo!,
orefs.interestOracleUtxo,
orefs.treasuryUtxo,
orefs.interestCollectorUtxo,
context.emulator.slot,
context.lucid,
sysParams,
),
context.lucid,
context.emulator,
);
const res = await findOwnCdpNew(context.lucid, sysParams);
expect(
assetClassValueOf(res.utxo.assets, collateralAssetA),
'Expected different CDP collateral',
).toEqual(allCollateralInWallet + collateralAdjustment);
});
test<MyContext>('ROB sell orders redeem with CDP deposit and burn (ADA case)', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[iusdInitialAssetCfg(0n)],
context.emulator.slot,
);
const robs_count = 8;
const ROB_DEPOSIT = 20_000_000n;
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
1_000_000_000n,
ROB_DEPOSIT * BigInt(robs_count) +
// Should cover the minting fee
1_000_000n,
),
);
await repeat(robs_count, async () => {
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
ROB_DEPOSIT,
{
SellIAssetOrder: {
allowedCollateralAssets: [
[adaAssetClass, rationalFromInt(1n)],
],
},
},
context.lucid,
sysParams,
),
);
});
context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase);
const INITIAL_MINT = 20_000_000n;
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
100_000_000n,
INITIAL_MINT,
),
);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
);
const SINGLE_REDEMPTION_AMT = 1_000_000n;
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
await benchmarkAndAwaitTx(
`ROB composite - ${robs_count} sell orders redeem with CDP deposit and burn`,
await redeemWithCdpAdjust(
(
await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).map((rob) => [rob.utxo, SINGLE_REDEMPTION_AMT]),
5n,
-BigInt(robs_count) * SINGLE_REDEMPTION_AMT,
(await findOwnCdpNew(context.lucid, sysParams)).utxo,
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
priceOracleUtxo!,
orefs.interestOracleUtxo,
orefs.treasuryUtxo,
orefs.interestCollectorUtxo,
context.emulator.slot,
context.lucid,
sysParams,
),
context.lucid,
context.emulator,
);
const ownCdp = await findOwnCdpNew(context.lucid, sysParams);
expect(ownCdp.datum.mintedAmt, 'unexpected minted amt').toEqual(
INITIAL_MINT - BigInt(robs_count) * SINGLE_REDEMPTION_AMT,
);
});
test<MyContext>('ROB sell orders redeem with CDP withdraw and burn (non ADA case)', 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 robs_count = 7; // TODO: Verify against Pyth feeds
const ROB_DEPOSIT = 20_000_000n;
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
collateralAssetA,
1_000_000_000n,
ROB_DEPOSIT * BigInt(robs_count) +
// Should cover the minting fee
1_000_000n,
),
);
await repeat(robs_count, async () => {
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
ROB_DEPOSIT,
{
SellIAssetOrder: {
allowedCollateralAssets: [
[collateralAssetA, rationalFromInt(1n)],
],
},
},
context.lucid,
sysParams,
),
);
});
context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase);
const allCollateralInWallet = assetClassValueOf(
await totalValueAtAddress(context.lucid, context.users.user1.address),
collateralAssetA,
);
const INITIAL_MINT = 20_000_000n;
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
collateralAssetA,
allCollateralInWallet,
INITIAL_MINT,
),
);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
collateralAssetA,
);
const SINGLE_REDEMPTION_AMT = 1_000_000n;
const reimbursementFee = calculateFeeFromRatio(
orefs.iasset.datum.redemptionReimbursementRatio,
SINGLE_REDEMPTION_AMT,
);
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
await benchmarkAndAwaitTx(
`ROB composite - ${robs_count} sell orders redeem with CDP withdraw and burn (non ADA case)`,
await redeemWithCdpAdjust(
(
await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).map((rob) => [rob.utxo, SINGLE_REDEMPTION_AMT]),
// Withdrawing the amount to pay for redemptions
-BigInt(robs_count) * (SINGLE_REDEMPTION_AMT + reimbursementFee),
// redeeming iassets, and using them to burn
-BigInt(robs_count) * SINGLE_REDEMPTION_AMT,
(await findOwnCdpNew(context.lucid, sysParams)).utxo,
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
priceOracleUtxo!,
orefs.interestOracleUtxo,
orefs.treasuryUtxo,
orefs.interestCollectorUtxo,
context.emulator.slot,
context.lucid,
sysParams,
),
context.lucid,
context.emulator,
);
const ownCdp = await findOwnCdpNew(context.lucid, sysParams);
expect(ownCdp.datum.mintedAmt, 'unexpected minted amt').toEqual(
INITIAL_MINT - BigInt(robs_count) * SINGLE_REDEMPTION_AMT,
);
expect(
assetClassValueOf(ownCdp.utxo.assets, collateralAssetA),
'unexpected collateral amt',
).toEqual(
allCollateralInWallet -
BigInt(robs_count) * (SINGLE_REDEMPTION_AMT + reimbursementFee),
);
});
});
describe('redeem with CDP close', () => {
test<MyContext>('ROB sell orders redeem with CDP close (ADA case)', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[iusdInitialAssetCfg(0n)],
context.emulator.slot,
);
const robs_count = 5;
const ROB_DEPOSIT = 20_000_000n;
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
1_000_000_000n,
ROB_DEPOSIT * BigInt(robs_count) +
// Should cover the minting fee
1_000_000n,
),
);
await repeat(robs_count, async () => {
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
ROB_DEPOSIT,
{
SellIAssetOrder: {
allowedCollateralAssets: [
[adaAssetClass, rationalFromInt(1n)],
],
},
},
context.lucid,
sysParams,
),
);
});
context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase);
const SINGLE_PAYOUT_COLLATERAL_AMT = 1_000_000n;
const mintAmt =
BigInt(robs_count) *
(SINGLE_PAYOUT_COLLATERAL_AMT -
calculateFeeFromRatio(
(
await findIAssetNew(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).datum.redemptionReimbursementRatio,
SINGLE_PAYOUT_COLLATERAL_AMT,
));
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
100_000_000n,
mintAmt,
),
);
const iassetAc = {
currencySymbol: fromHex(
sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
),
tokenName: fromHex(fromText(iusdAssetInfo.iassetTokenNameAscii)),
};
await sendValueTo(
context.users.user2.address,
mkAssetsOf(
iassetAc,
assetClassValueOf(
await totalValueAtAddress(
context.lucid,
context.users.user1.address,
),
iassetAc,
),
),
context.lucid,
);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
);
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
await benchmarkAndAwaitTx(
`ROB composite - ${robs_count} sell orders redeem with CDP close (ADA case)`,
await redeemWithCdpClose(
(
await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).map((rob) => [rob.utxo, SINGLE_PAYOUT_COLLATERAL_AMT]),
(await findOwnCdpNew(context.lucid, sysParams)).utxo,
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
priceOracleUtxo!,
orefs.interestOracleUtxo,
orefs.interestCollectorUtxo,
sysParams,
context.lucid,
context.emulator.slot,
),
context.lucid,
context.emulator,
);
});
test<MyContext>('ROB sell orders redeem with CDP close (non ADA case)', 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 robs_count = 4;
const ROB_DEPOSIT = 20_000_000n;
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
collateralAssetA,
1_000_000_000n,
ROB_DEPOSIT * BigInt(robs_count) +
// Should cover the minting fee
1_000_000n,
),
);
await repeat(robs_count, async () => {
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
ROB_DEPOSIT,
{
SellIAssetOrder: {
allowedCollateralAssets: [
[collateralAssetA, rationalFromInt(1n)],
],
},
},
context.lucid,
sysParams,
),
);
});
context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase);
const SINGLE_PAYOUT_COLLATERAL_AMT = 1_000_000n;
const mintAmt =
BigInt(robs_count) *
(SINGLE_PAYOUT_COLLATERAL_AMT -
calculateFeeFromRatio(
(
await findIAssetNew(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).datum.redemptionReimbursementRatio,
SINGLE_PAYOUT_COLLATERAL_AMT,
));
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
collateralAssetA,
100_000_000n,
mintAmt,
),
);
const iassetAc = {
currencySymbol: fromHex(
sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
),
tokenName: fromHex(fromText(iusdAssetInfo.iassetTokenNameAscii)),
};
await sendValueTo(
context.users.user2.address,
mkAssetsOf(
iassetAc,
assetClassValueOf(
await totalValueAtAddress(
context.lucid,
context.users.user1.address,
),
iassetAc,
),
),
context.lucid,
);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
collateralAssetA,
);
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
await benchmarkAndAwaitTx(
`ROB composite - ${robs_count} sell orders redeem with CDP close (non ADA case)`,
await redeemWithCdpClose(
(
await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).map((rob) => [rob.utxo, SINGLE_PAYOUT_COLLATERAL_AMT]),
(await findOwnCdpNew(context.lucid, sysParams)).utxo,
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
priceOracleUtxo!,
orefs.interestOracleUtxo,
orefs.interestCollectorUtxo,
sysParams,
context.lucid,
context.emulator.slot,
),
context.lucid,
context.emulator,
);
});
});
describe('redeem with CDP create', () => {
test<MyContext>('ROB buy orders redeem with CDP create (ADA case)', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[iusdInitialAssetCfg(0n)],
context.emulator.slot,
);
const robs_count = 4;
await repeat(robs_count, async () => {
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
20_000_000n,
{
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: rationalFromInt(1n),
},
},
context.lucid,
sysParams,
),
);
});
context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
);
const SINGLE_PAYOUT_IASSET_AMT = 1_000_000n;
const mintedAmt =
BigInt(robs_count) * SINGLE_PAYOUT_IASSET_AMT +
// This is extra that minting fee is paid from and user keeps what we validate after this Tx.
SINGLE_PAYOUT_IASSET_AMT;
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
await benchmarkAndAwaitTx(
`ROB composite - ${robs_count} buy orders redeem with CDP create (ADA case)`,
await redeemWithCdpCreate(
(
await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).map((rob) => [rob.utxo, SINGLE_PAYOUT_IASSET_AMT]),
100_000_000n,
// everything that gets minted is sent to ROB
mintedAmt,
sysParams,
await findRandomCdpCreatorNew(context, sysParams),
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
priceOracleUtxo!,
orefs.interestOracleUtxo,
orefs.treasuryUtxo,
context.lucid,
context.emulator.slot,
),
context.lucid,
context.emulator,
);
expect(
assetClassValueOf(
await totalValueAtAddress(
context.lucid,
context.users.user1.address,
),
{
currencySymbol: fromHex(
sysParams.robParams.iassetPolicyId.unCurrencySymbol,
),
tokenName: fromHex(fromText(iusdAssetInfo.iassetTokenNameAscii)),
},
),
'Unexpected iassets owned by user',
).toEqual(
SINGLE_PAYOUT_IASSET_AMT -
calculateFeeFromRatio(
(
await findIAssetNew(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).datum.debtMintingFeeRatio,
mintedAmt,
),
);
});
test<MyContext>('ROB buy orders redeem with CDP create (non ADA case)', 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 robs_count = 3;
await repeat(robs_count, async () => {
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
20_000_000n,
{
BuyIAssetOrder: {
collateralAsset: collateralAssetA,
maxPrice: rationalFromInt(1n),
},
},
context.lucid,
sysParams,
),
);
});
context.lucid.selectWallet.fromSeed(context.users.user1.seedPhrase);
const orefs = await findAllNecessaryOrefs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
collateralAssetA,
);
const SINGLE_PAYOUT_IASSET_AMT = 1_000_000n;
const mintedAmt =
BigInt(robs_count) * SINGLE_PAYOUT_IASSET_AMT +
// This is extra that minting fee is paid from and user keeps what we validate after this Tx.
SINGLE_PAYOUT_IASSET_AMT;
const priceOracleUtxo = await findPriceOracleFromCollateralAsset(
context.lucid,
orefs.collateralAsset,
);
await benchmarkAndAwaitTx(
`ROB composite - ${robs_count} buy orders redeem with CDP create (non ADA case)`,
await redeemWithCdpCreate(
(
await findAllRobs(
context.lucid,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).map((rob) => [rob.utxo, SINGLE_PAYOUT_IASSET_AMT]),
// use all as collateral
assetClassValueOf(
await totalValueAtAddress(
context.lucid,
context.users.user1.address,
),
collateralAssetA,
),
// everything that gets minted is sent to ROB
mintedAmt,
sysParams,
await findRandomCdpCreatorNew(context, sysParams),
orefs.iasset.utxo,
orefs.collateralAsset.utxo,
priceOracleUtxo!,
orefs.interestOracleUtxo,
orefs.treasuryUtxo,
context.lucid,
context.emulator.slot,
),
context.lucid,
context.emulator,
);
const userVal = await totalValueAtAddress(
context.lucid,
context.users.user1.address,
);
expect(
assetClassValueOf(userVal, {
currencySymbol: fromHex(
sysParams.robParams.iassetPolicyId.unCurrencySymbol,
),
tokenName: fromHex(fromText(iusdAssetInfo.iassetTokenNameAscii)),
}),
'Unexpected iassets owned by user',
).toEqual(
SINGLE_PAYOUT_IASSET_AMT -
calculateFeeFromRatio(
(
await findIAssetNew(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).datum.debtMintingFeeRatio,
mintedAmt,
),
);
// Since everything was used as collateral, user owns only the amount after redemption.
expect(
assetClassValueOf(userVal, collateralAssetA),
'Unexpected collateral owned by user',
).toEqual(
BigInt(robs_count) *
(SINGLE_PAYOUT_IASSET_AMT -
calculateFeeFromRatio(
(
await findIAssetNew(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).datum.redemptionReimbursementRatio,
SINGLE_PAYOUT_IASSET_AMT,
)),
);
});
});
});
test<MyContext>('adjust order type BUY positive and negative', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[iusdInitialAssetCfg()],
context.emulator.slot,
);
const [ownPkh, _] = await addrDetails(context.lucid);
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
20_000_000n,
{
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: rationalFromInt(1n),
},
},
context.lucid,
sysParams,
),
);
{
const robBefore = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
const amtToSpendBefore = robCollateralAmtToSpend(
robBefore.utxo.assets,
robBefore.datum.orderType,
);
await runAndAwaitTx(
context.lucid,
adjustRob(
context.lucid,
robBefore.utxo,
-1_000_000n,
undefined,
sysParams,
),
);
const adjustedUtxo1 = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
const amtToSpendAfter = robCollateralAmtToSpend(
adjustedUtxo1.utxo.assets,
adjustedUtxo1.datum.orderType,
);
assert(amtToSpendBefore - amtToSpendAfter === 1_000_000n);
expect(
lovelacesAmt(adjustedUtxo1.utxo.assets) >= amtToSpendAfter,
'Lovelaces to spend has to be smaller than actual lovelaces in UTXO',
).toBeTruthy();
}
{
const robBefore = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
const amtToSpendBefore = robCollateralAmtToSpend(
robBefore.utxo.assets,
robBefore.datum.orderType,
);
await runAndAwaitTx(
context.lucid,
adjustRob(
context.lucid,
robBefore.utxo,
5_000_000n,
undefined,
sysParams,
),
);
const adjustedUtxo2 = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
const amtToSpendAfter = robCollateralAmtToSpend(
adjustedUtxo2.utxo.assets,
adjustedUtxo2.datum.orderType,
);
strictEqual(amtToSpendAfter - amtToSpendBefore, 5_000_000n);
expect(
lovelacesAmt(adjustedUtxo2.utxo.assets) >= amtToSpendAfter,
'Lovelaces to spend has to be smaller than actual lovelaces in UTXO',
).toBeTruthy();
}
});
test<MyContext>('adjust order type SELL positive and negative', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[iusdInitialAssetCfg()],
context.emulator.slot,
);
const [ownPkh, _] = await addrDetails(context.lucid);
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
100_000_000n,
30_000_000n,
),
);
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
20_000_000n,
{
SellIAssetOrder: {
allowedCollateralAssets: [[adaAssetClass, rationalFromInt(1n)]],
},
},
context.lucid,
sysParams,
),
);
await runAndAwaitTx(
context.lucid,
findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
).then((rob) =>
adjustRob(context.lucid, rob.utxo, -1_000_000n, undefined, sysParams),
),
);
const adjustedUtxo1 = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
const iassetAc: AssetClass = {
currencySymbol: fromHex(
sysParams.robParams.iassetPolicyId.unCurrencySymbol,
),
tokenName: fromHex(fromText(iusdAssetInfo.iassetTokenNameAscii)),
};
assert(
robAmtToSpend(adjustedUtxo1.utxo.assets, adjustedUtxo1.datum.orderType, {
currencySymbol: fromHex(
sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
),
tokenName: adjustedUtxo1.datum.iasset,
}) ===
20_000_000n - 1_000_000n,
);
expect(
assetClassValueOf(adjustedUtxo1.utxo.assets, iassetAc) ===
robAmtToSpend(
adjustedUtxo1.utxo.assets,
adjustedUtxo1.datum.orderType,
{
currencySymbol: fromHex(
sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
),
tokenName: adjustedUtxo1.datum.iasset,
},
),
'IAssets to spend has to equal iassets in UTXO',
).toBeTruthy();
await runAndAwaitTx(
context.lucid,
adjustRob(
context.lucid,
adjustedUtxo1.utxo,
5_000_000n,
undefined,
sysParams,
),
);
const adjustedUtxo2 = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
const expectedResultAdaAmt = 20_000_000n - 1_000_000n + 5_000_000n;
strictEqual(
robAmtToSpend(adjustedUtxo2.utxo.assets, adjustedUtxo2.datum.orderType, {
currencySymbol: fromHex(
sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
),
tokenName: adjustedUtxo2.datum.iasset,
}),
expectedResultAdaAmt,
);
expect(
assetClassValueOf(adjustedUtxo2.utxo.assets, iassetAc) >=
robAmtToSpend(
adjustedUtxo2.utxo.assets,
adjustedUtxo2.datum.orderType,
{
currencySymbol: fromHex(
sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
),
tokenName: adjustedUtxo2.datum.iasset,
},
),
'IAssets to spend has to equal iassets in UTXO',
).toBeTruthy();
});
test<MyContext>('claim from BUY order type after a redemption', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[iusdInitialAssetCfg()],
context.emulator.slot,
);
const [ownPkh, _] = await addrDetails(context.lucid);
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
100_000_000n,
30_000_000n,
),
);
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
20_000_000n,
{
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: rationalFromInt(1n),
},
},
context.lucid,
sysParams,
),
);
const robUtxo = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
const iassetAc: AssetClass = {
currencySymbol: fromHex(
sysParams.robParams.iassetPolicyId.unCurrencySymbol,
),
tokenName: fromHex(fromText(iusdAssetInfo.iassetTokenNameAscii)),
};
expect(
assetClassValueOf(robUtxo.utxo.assets, iassetAc),
'ROB should have no iassets before redemption',
).toBe(0n);
const redemptionIAssetAmt = 11_000_000n;
const amtToSpendBefore = robCollateralAmtToSpend(
robUtxo.utxo.assets,
robUtxo.datum.orderType,
);
await runAndAwaitTx(
context.lucid,
runRedeemRob(
context,
sysParams,
[[robUtxo, redemptionIAssetAmt]],
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
context.emulator.slot,
),
);
const redeemedRob = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
expect(
assetClassValueOf(redeemedRob.utxo.assets, iassetAc),
'ROB has wrong number of iassets after redemption',
).toEqual(redemptionIAssetAmt);
expect(
amtToSpendBefore -
robCollateralAmtToSpend(
redeemedRob.utxo.assets,
redeemedRob.datum.orderType,
),
'ROB has wrong number redeemed',
).toEqual(
// Since price is 1
redemptionIAssetAmt -
calculateFeeFromRatio(
(
await findIAssetNew(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).datum.redemptionReimbursementRatio,
redemptionIAssetAmt,
),
);
await runAndAwaitTx(
context.lucid,
claimRob(context.lucid, redeemedRob.utxo, sysParams),
);
const claimedRob = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
expect(
assetClassValueOf(claimedRob.utxo.assets, iassetAc),
'ROB has to have 0 redemption assets after claim',
).toBe(0n);
});
test<MyContext>('claim from BUY order type after a redemption when price is NOT 1:1', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[iusdInitialAssetCfg()],
context.emulator.slot,
);
const [ownPkh, _] = await addrDetails(context.lucid);
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
100_000_000n,
30_000_000n,
),
);
const initialDeposit = 20_000_000n;
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
initialDeposit,
{
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: rationalFromInt(1n),
},
},
context.lucid,
sysParams,
),
);
const robUtxo = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
const iassetAc: AssetClass = {
currencySymbol: fromHex(
sysParams.robParams.iassetPolicyId.unCurrencySymbol,
),
tokenName: fromHex(fromText(iusdAssetInfo.iassetTokenNameAscii)),
};
expect(
assetClassValueOf(robUtxo.utxo.assets, iassetAc),
'ROB should have no iassets before redemption',
).toBe(0n);
const newPrice: Rational = { numerator: 75n, denominator: 100n };
await runFeedPriceToOracle(
context,
sysParams,
iusdAssetInfo,
adaAssetClass,
newPrice,
);
const redemptionIAssetAmt = 7_500_000n;
await runAndAwaitTx(
context.lucid,
runRedeemRob(
context,
sysParams,
[[robUtxo, redemptionIAssetAmt]],
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
context.emulator.slot,
),
);
const redeemedRob = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
const expectedRedeemedAmt = rationalFloor(
rationalMul(
rationalFromInt(
redemptionIAssetAmt -
calculateFeeFromRatio(
(
await findIAssetNew(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).datum.redemptionReimbursementRatio,
redemptionIAssetAmt,
),
),
newPrice,
),
);
expect(
robCollateralAmtToSpend(
redeemedRob.utxo.assets,
redeemedRob.datum.orderType,
),
'Wrong amt to spend in datum',
).toEqual(initialDeposit - expectedRedeemedAmt);
expectValue(
redeemedRob.utxo.assets,
'Wrong value after redemption',
).toEqual(
addAssets(
mkAssetsOf(iassetAc, redemptionIAssetAmt),
mkLovelacesOf(
MIN_ROB_COLLATERAL_AMT + initialDeposit - expectedRedeemedAmt,
),
),
);
await runAndAwaitTx(
context.lucid,
claimRob(context.lucid, redeemedRob.utxo, sysParams),
);
const claimedRob = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
expect(
assetClassValueOf(claimedRob.utxo.assets, iassetAc),
'ROB has to have 0 redemption assets after claim',
).toBe(0n);
});
test<MyContext>('claim from SELL order type after a redemption when price is NOT 1:1', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[iusdInitialAssetCfg()],
context.emulator.slot,
);
const [ownPkh, _] = await addrDetails(context.lucid);
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
100_000_000n,
30_000_000n,
),
);
const initialDeposit = 20_000_000n;
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
initialDeposit,
{
SellIAssetOrder: {
allowedCollateralAssets: [[adaAssetClass, rationalFromInt(1n)]],
},
},
context.lucid,
sysParams,
),
);
const robUtxo = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
const iassetAc: AssetClass = {
currencySymbol: fromHex(
sysParams.robParams.iassetPolicyId.unCurrencySymbol,
),
tokenName: fromHex(fromText(iusdAssetInfo.iassetTokenNameAscii)),
};
expectValue(
robUtxo.utxo.assets,
'Wrong ROB value before redemption',
).toEqual(
addAssets(
mkLovelacesOf(MIN_ROB_COLLATERAL_AMT),
mkAssetsOf(iassetAc, initialDeposit),
),
);
const newPrice: Rational = { numerator: 125n, denominator: 100n };
await runFeedPriceToOracle(
context,
sysParams,
iusdAssetInfo,
adaAssetClass,
newPrice,
);
const payoutCollateralAmt = 7_500_000n;
await runAndAwaitTx(
context.lucid,
runRedeemRob(
context,
sysParams,
[[robUtxo, payoutCollateralAmt]],
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
context.emulator.slot,
),
);
const redeemedRob = await findSingleRob(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
ownPkh,
);
const expectedRedemptionAmt = rationalFloor(
rationalDiv(
rationalFromInt(
payoutCollateralAmt -
calculateFeeFromRatio(
(
await findIAssetNew(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
)
).datum.redemptionReimbursementRatio,
payoutCollateralAmt,
),
),
newPrice,
),
);
expect(
robAmtToSpend(redeemedRob.utxo.assets, redeemedRob.datum.orderType, {
currencySymbol: fromHex(
sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol,
),
tokenName: redeemedRob.datum.iasset,
}),
'Wrong amt to spend in datum',
).toEqual(initialDeposit - expectedRedemptionAmt);
expectValue(
redeemedRob.utxo.assets,
'Wrong value after redemption',
).toEqual(
addAssets(
mkAssetsOf(adaAssetClass, MIN_ROB_COLLATERAL_AMT + payoutCollateralAmt),
mkAssetsOf(iassetAc, initialDeposit - expectedRedemptionAmt),
),
);
await runAndAwaitTx(
context.lucid,
claimRob(context.lucid, redeemedRob.utxo, sysParams),
);
});
test<MyContext>('redemption of BUY order type when limit price not met fails', async (context: MyContext) => {
context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase);
const [sysParams, [iusdAssetInfo]] = await init(
context.lucid,
[iusdInitialAssetCfg()],
context.emulator.slot,
);
const [ownPkh, _] = await addrDetails(context.lucid);
await runAndAwaitTx(
context.lucid,
runOpenCdp(
context,
sysParams,
iusdAssetInfo.iassetTokenNameAscii,
adaAssetClass,
100_000_000n,
30_000_000n,
),
);
await runAndAwaitTx(
context.lucid,
openRob(
iusdAssetInfo.iassetTokenNameAscii,
20_000_000n,
{
BuyIAssetOrder: {
collateralAsset: adaAssetClass,
maxPrice: { numerator: 9n,