UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

1,790 lines (1,593 loc) 98.8 kB
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,