UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

893 lines (795 loc) 25.3 kB
import { beforeEach, describe, expect, test } from 'vitest'; import { IndigoTestContext, repeat, runAndAwaitTx } from '../test-helpers'; import { expectScriptFailure } from '../utils/asserts'; import { createIndigoTestContext, EXAMPLE_TOKEN_1, EXAMPLE_TOKEN_2, EXAMPLE_TOKEN_3, } from '../indigo-test-helpers'; import { findAdminInterestCollectors as findAdminInterestCollector, findAdminInterestCollectors, findAllInterestCollectors, findRandomNonAdminInterestCollector, } from './interest-collector-queries'; import { benchmarkAndAwaitTx } from '../utils/benchmark-utils'; import { batchCollectInterest, distributeInterest, updatePermissions, } from '../../src/contracts/interest-collection/transactions'; import { adaAssetClass, assetClassValueOf, matchSingle, mkAssetsOf, mkLovelacesOf, } from '@3rd-eye-labs/cardano-offchain-common'; import { addAssets, fromHex, paymentCredentialOf, stakeCredentialOf, } from '@lucid-evolution/lucid'; import { testCollectInterest, testDistributeInterest, } from './transactions-mutated'; import { signersAllOf, fromSystemParamsAsset } from '../../src'; import { findAllActiveCdps, findAllNecessaryOrefs, findPriceOracleFromCollateralAsset, } from '../cdp/cdp-queries'; import { feedPriceOracleTx } from '../../src/contracts/price-oracle/transactions'; import { createUtxosAtInterestCollector } from '../endpoints/interest-collector'; import { runOpenCdp } from '../cdp/actions'; import { runFeedPriceToOracle } from '../price-oracle/actions'; import { runTestDepositCdpWithInterestVar } from '../cdp/transactions-mutated'; import { findGov } from '../gov/governance-queries'; import { toDataMultisig } from '../../src/types/multisig'; import { createMultipleUtxosAtTreasury } from '../endpoints/treasury'; import { rationalFromInt } from '../../src/types/rational'; describe('Interest Collection', () => { beforeEach<IndigoTestContext>(async (context: IndigoTestContext) => { await createIndigoTestContext(context); }); test<IndigoTestContext>('Collect Interest - Cannot collect without interacting with a CDP', async (context: IndigoTestContext) => { const interestCollectionUtxo = await findRandomNonAdminInterestCollector( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); await expectScriptFailure( 'Single CDP input in the transaction.', testCollectInterest( mkLovelacesOf(1000000n), context.lucid, context.systemParams, context.lucid.newTx(), interestCollectionUtxo, { kind: 'CollectWithoutCDP' }, ), ); }); test<IndigoTestContext>('Collect Interest - Cannot collect from interest admin', async (context: IndigoTestContext) => { const adminInterestCollectorUtxo = await findAdminInterestCollectors( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); const [iAssetConfig] = context.assetConfigs; context.lucid.selectWallet.fromSeed(context.users.user.seedPhrase); await runAndAwaitTx( context.lucid, runOpenCdp( context, context.systemParams, 'iUSD', adaAssetClass, 10_000_000n, 500_000n, ), ); context.emulator.awaitSlot(6500); await runFeedPriceToOracle( context, context.systemParams, iAssetConfig, adaAssetClass, rationalFromInt(1n), ); context.lucid.selectWallet.fromSeed(context.users.user.seedPhrase); await expectScriptFailure( 'Spent UTxO cannot have admin NFT', runTestDepositCdpWithInterestVar( context, context.systemParams, 'iUSD', adaAssetClass, adminInterestCollectorUtxo, { kind: 'CollectFromInterestAdmin', }, ), ); }); test<IndigoTestContext>('Collect Interest - Cannot collect from multiple interest collectors', async (context: IndigoTestContext) => { const [iAssetConfig] = context.assetConfigs; context.lucid.selectWallet.fromSeed(context.users.user.seedPhrase); await runAndAwaitTx( context.lucid, runOpenCdp( context, context.systemParams, 'iUSD', adaAssetClass, 10_000_000n, 500_000n, ), ); context.emulator.awaitSlot(6500); await runFeedPriceToOracle( context, context.systemParams, iAssetConfig, adaAssetClass, rationalFromInt(1n), ); const interestCollectors = await findAllInterestCollectors( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, ); const interestCollectorsUtxos = interestCollectors.filter( (utxo) => assetClassValueOf( utxo.assets, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ) === 0n, ); if (interestCollectorsUtxos.length < 2) { throw new Error('Expected at least 2 interest collectors'); } context.lucid.selectWallet.fromSeed(context.users.user.seedPhrase); // This can fail in the CDP script or the interest collection script as the // check of single interest collector is redundant. await expectScriptFailure( 'crashed', runTestDepositCdpWithInterestVar( context, context.systemParams, 'iUSD', adaAssetClass, interestCollectorsUtxos[0], { kind: 'CollectFromMultipleInterestCollectors', utxo: interestCollectorsUtxos[1], }, ), ); }); test<IndigoTestContext>('Collect Interest - ADA and 3 assets', async (context: IndigoTestContext) => { context.lucid.selectWallet.fromSeed(context.users.user.seedPhrase); const [iAssetConfig] = context.assetConfigs; await runAndAwaitTx( context.lucid, runOpenCdp( context, context.systemParams, 'iUSD', adaAssetClass, 10_000_000n, 500_000n, ), ); context.emulator.awaitSlot(6500); await runFeedPriceToOracle( context, context.systemParams, iAssetConfig, adaAssetClass, rationalFromInt(1n), ); const interestCollectionUtxo = await findRandomNonAdminInterestCollector( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); context.lucid.selectWallet.fromSeed(context.users.user.seedPhrase); await expectScriptFailure( 'D', runTestDepositCdpWithInterestVar( context, context.systemParams, 'iUSD', adaAssetClass, interestCollectionUtxo, { kind: 'CollectCustomValue', value: addAssets( mkLovelacesOf(1000000n), mkAssetsOf(EXAMPLE_TOKEN_1, 1_000n), mkAssetsOf(EXAMPLE_TOKEN_2, 1_000n), mkAssetsOf(EXAMPLE_TOKEN_3, 1_000n), ), }, ), ); }); // This test settles the biggest possible batch of CDPs test<IndigoTestContext>('Batch collect - 16 CDPs', async (context: IndigoTestContext) => { context.lucid.selectWallet.fromSeed(context.users.user.seedPhrase); await createMultipleUtxosAtTreasury( mkLovelacesOf(2n), 12n, context.systemParams, context, ); const [iAssetConfig] = context.assetConfigs; const numberOfCdps = 16; await repeat(numberOfCdps, async () => { await runAndAwaitTx( context.lucid, runOpenCdp( context, context.systemParams, iAssetConfig.iassetTokenNameAscii, adaAssetClass, 12_000_000n, 6_000_000n, ), ); }); context.emulator.awaitSlot( Number( context.systemParams.interestCollectionParams .interestSettlementCooldown, ) / 1_000, ); const orefs = await findAllNecessaryOrefs( context.lucid, context.systemParams, 'iUSD', adaAssetClass, ); const priceOracleUtxo = await findPriceOracleFromCollateralAsset( context.lucid, orefs.collateralAsset, ); const cdpsInfo = await findAllActiveCdps( context.lucid, context.systemParams, 'iUSD', stakeCredentialOf(context.users.user.address), ); expect( cdpsInfo.length === numberOfCdps, `Expected ${numberOfCdps} cdps`, ).toBeTruthy(); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); await runAndAwaitTx( context.lucid, feedPriceOracleTx( context.lucid, priceOracleUtxo!, rationalFromInt(1n), iAssetConfig.collateralAssets[0].oracleParams!, context.emulator.slot, ), ); await benchmarkAndAwaitTx( 'Interest Collection - Batch Collect 16 CDPs', await batchCollectInterest( orefs.collateralAsset.utxo, orefs.interestCollectorUtxo, orefs.interestOracleUtxo, cdpsInfo.map((cdp) => cdp.utxo), context.systemParams, context.lucid, context.emulator.slot, ), context.lucid, context.emulator, ); }); test<IndigoTestContext>('Batch collect fails when no cooldown elapsed', async (context: IndigoTestContext) => { context.lucid.selectWallet.fromSeed(context.users.user.seedPhrase); await createMultipleUtxosAtTreasury( mkLovelacesOf(2n), 10n, context.systemParams, context, ); const [iAssetConfig] = context.assetConfigs; const numberOfCdps = 10; await repeat(numberOfCdps, async () => { await runAndAwaitTx( context.lucid, runOpenCdp( context, context.systemParams, iAssetConfig.iassetTokenNameAscii, adaAssetClass, 12_000_000n, 6_000_000n, ), ); }); // NOTE: Awaiting the cooldown period minus 39 seconds makes the transaction succeed, // this is probably due to the awaitTx time. context.emulator.awaitSlot( Number( context.systemParams.interestCollectionParams .interestSettlementCooldown, ) / 1_000 - 40, ); const orefs = await findAllNecessaryOrefs( context.lucid, context.systemParams, 'iUSD', adaAssetClass, ); const priceOracleUtxo = await findPriceOracleFromCollateralAsset( context.lucid, orefs.collateralAsset, ); const cdpsInfo = await findAllActiveCdps( context.lucid, context.systemParams, 'iUSD', stakeCredentialOf(context.users.user.address), ); expect( cdpsInfo.length === numberOfCdps, `Expected ${numberOfCdps} cdps`, ).toBeTruthy(); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); await runAndAwaitTx( context.lucid, feedPriceOracleTx( context.lucid, priceOracleUtxo!, rationalFromInt(1n), iAssetConfig.collateralAssets[0].oracleParams!, context.emulator.slot, ), ); await expectScriptFailure( 'It is too soon to settle interest', batchCollectInterest( orefs.collateralAsset.utxo, orefs.interestCollectorUtxo, orefs.interestOracleUtxo, cdpsInfo.map((cdp) => cdp.utxo), context.systemParams, context.lucid, context.emulator.slot, ), ); }); test<IndigoTestContext>('Distribute', async (context: IndigoTestContext) => { context.lucid.selectWallet.fromSeed(context.users.user.seedPhrase); await createUtxosAtInterestCollector( 1, addAssets(mkLovelacesOf(1000000n), mkAssetsOf(EXAMPLE_TOKEN_1, 1_000n)), context.systemParams, context, ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const interestDistributeUtxo = matchSingle( ( await findAllInterestCollectors( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, ) ).filter((utxo) => assetClassValueOf(utxo.assets, EXAMPLE_TOKEN_1)), (_) => new Error('Expected a single interest distribute UTXO'), ); const interestAdminUtxo = await findAdminInterestCollector( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); await benchmarkAndAwaitTx( 'Interest Collection - Distribute', await distributeInterest( [interestDistributeUtxo], interestAdminUtxo, context.systemParams, context.lucid, ), context.lucid, context.emulator, ); }); test<IndigoTestContext>('Distribute from multiple collectors - 58', async (context: IndigoTestContext) => { context.lucid.selectWallet.fromSeed(context.users.user.seedPhrase); const numberOfInterestCollectionUtxos = 58; await createUtxosAtInterestCollector( numberOfInterestCollectionUtxos, addAssets(mkLovelacesOf(1000000n), mkAssetsOf(EXAMPLE_TOKEN_1, 1_000n)), context.systemParams, context, ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const interestDistributeUtxos = ( await findAllInterestCollectors( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, ) ).filter((utxo) => assetClassValueOf(utxo.assets, EXAMPLE_TOKEN_1)); const interestAdminUtxo = await findAdminInterestCollector( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); await benchmarkAndAwaitTx( 'Interest collection - Distribute from multiple collectors, 58', await distributeInterest( interestDistributeUtxos, interestAdminUtxo, context.systemParams, context.lucid, ), context.lucid, context.emulator, ); }); test<IndigoTestContext>('Distribute - No admin signature', async (context: IndigoTestContext) => { context.lucid.selectWallet.fromSeed(context.users.user.seedPhrase); await createUtxosAtInterestCollector( 1, addAssets(mkLovelacesOf(1000000n), mkAssetsOf(EXAMPLE_TOKEN_1, 1_000n)), context.systemParams, context, ); context.lucid.selectWallet.fromSeed(context.users.admin.seedPhrase); const interestDistributeUtxo = matchSingle( ( await findAllInterestCollectors( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, ) ).filter((utxo) => assetClassValueOf(utxo.assets, EXAMPLE_TOKEN_1)), (_) => new Error('Expected a single interest distribute UTXO'), ); const interestAdminUtxo = await findAdminInterestCollector( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); await expectScriptFailure( 'Admin multisig must be satisfied', testDistributeInterest( interestDistributeUtxo, interestAdminUtxo, context.users.user.address, undefined, context.systemParams, context.lucid, { kind: 'DistributeInterestNoAdmin' }, ), ); }); test<IndigoTestContext>('Update Permissions', async (context: IndigoTestContext) => { const interestAdminUtxo = await findAdminInterestCollector( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); const govUtxo = await findGov( context.lucid, context.systemParams.validatorHashes.govHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.govAuthTk, ), ); const newAdminPermissions = { Signature: { keyHash: fromHex(paymentCredentialOf(context.users.user.address).hash), }, }; await benchmarkAndAwaitTx( 'Interest Collection - Update Permissions', await updatePermissions( interestAdminUtxo, govUtxo.utxo, newAdminPermissions, [ ...signersAllOf(govUtxo.datum.protocolParams.foundationMultisig), ...signersAllOf(newAdminPermissions), ], context.systemParams, context.lucid, ), context.lucid, context.emulator, [context.users.user.seedPhrase], ); }); test<IndigoTestContext>('Update Permissions - all of multiple signers', async (context: IndigoTestContext) => { const interestAdminUtxo = await findAdminInterestCollector( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); const govUtxo = await findGov( context.lucid, context.systemParams.validatorHashes.govHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.govAuthTk, ), ); const newAdminPermissions = { AtLeast: { required: 1n, authSignatories: [ toDataMultisig({ Signature: { keyHash: fromHex( paymentCredentialOf(context.users.user.address).hash, ), }, }), toDataMultisig({ Signature: { keyHash: fromHex( paymentCredentialOf(context.users.user2.address).hash, ), }, }), ], }, }; await benchmarkAndAwaitTx( 'Interest Collection - Update Permissions - all of multiple signers', await updatePermissions( interestAdminUtxo, govUtxo.utxo, newAdminPermissions, [ ...signersAllOf(govUtxo.datum.protocolParams.foundationMultisig), ...signersAllOf(newAdminPermissions), ], context.systemParams, context.lucid, ), context.lucid, context.emulator, [context.users.user.seedPhrase, context.users.user2.seedPhrase], ); }); test<IndigoTestContext>('Update Permissions - some of multiple signers', async (context: IndigoTestContext) => { const interestAdminUtxo = await findAdminInterestCollector( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); const govUtxo = await findGov( context.lucid, context.systemParams.validatorHashes.govHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.govAuthTk, ), ); const newAdminPermissions = { AtLeast: { required: 2n, authSignatories: [ toDataMultisig({ Signature: { keyHash: fromHex( paymentCredentialOf(context.users.admin.address).hash, ), }, }), toDataMultisig({ Signature: { keyHash: fromHex( paymentCredentialOf(context.users.user.address).hash, ), }, }), toDataMultisig({ Signature: { keyHash: fromHex( paymentCredentialOf(context.users.user2.address).hash, ), }, }), ], }, }; await runAndAwaitTx( context.lucid, updatePermissions( interestAdminUtxo, govUtxo.utxo, newAdminPermissions, [ ...signersAllOf(govUtxo.datum.protocolParams.foundationMultisig), ...[paymentCredentialOf(context.users.user.address).hash], ], context.systemParams, context.lucid, ), [context.users.user.seedPhrase], ); }); test<IndigoTestContext>('Update Permissions - nested multisig', async (context: IndigoTestContext) => { const interestAdminUtxo = await findAdminInterestCollector( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); const govUtxo = await findGov( context.lucid, context.systemParams.validatorHashes.govHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.govAuthTk, ), ); const newAdminPermissions = { AtLeast: { required: 2n, authSignatories: [ toDataMultisig({ Signature: { keyHash: fromHex( paymentCredentialOf(context.users.admin.address).hash, ), }, }), toDataMultisig({ AtLeast: { required: 1n, authSignatories: [ toDataMultisig({ Signature: { keyHash: fromHex( paymentCredentialOf(context.users.user.address).hash, ), }, }), toDataMultisig({ Signature: { keyHash: fromHex( paymentCredentialOf(context.users.user2.address).hash, ), }, }), ], }, }), ], }, }; await runAndAwaitTx( context.lucid, updatePermissions( interestAdminUtxo, govUtxo.utxo, newAdminPermissions, [ ...signersAllOf(govUtxo.datum.protocolParams.foundationMultisig), ...[paymentCredentialOf(context.users.user.address).hash], ], context.systemParams, context.lucid, ), [context.users.user.seedPhrase], ); }); test<IndigoTestContext>('Update Permissions - Fail when new admin is not signed', async (context: IndigoTestContext) => { const interestAdminUtxo = await findAdminInterestCollector( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); const govUtxo = await findGov( context.lucid, context.systemParams.validatorHashes.govHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.govAuthTk, ), ); const newAdminPermissions = { Signature: { keyHash: fromHex(paymentCredentialOf(context.users.user.address).hash), }, }; await expectScriptFailure( 'New admin multisig must be satisfied', updatePermissions( interestAdminUtxo, govUtxo.utxo, newAdminPermissions, signersAllOf(govUtxo.datum.protocolParams.foundationMultisig), context.systemParams, context.lucid, ), ); }); test<IndigoTestContext>('Update Permissions - Fail when not enough signers of new admin', async (context: IndigoTestContext) => { const interestAdminUtxo = await findAdminInterestCollector( context.lucid, context.systemParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.multisigUtxoNft, ), ); const govUtxo = await findGov( context.lucid, context.systemParams.validatorHashes.govHash, fromSystemParamsAsset( context.systemParams.interestCollectionParams.govAuthTk, ), ); const newAdminPermissions = { AtLeast: { required: 2n, authSignatories: [ toDataMultisig({ Signature: { keyHash: fromHex( paymentCredentialOf(context.users.admin.address).hash, ), }, }), toDataMultisig({ Signature: { keyHash: fromHex( paymentCredentialOf(context.users.user.address).hash, ), }, }), toDataMultisig({ Signature: { keyHash: fromHex( paymentCredentialOf(context.users.user2.address).hash, ), }, }), ], }, }; await expectScriptFailure( 'New admin multisig must be satisfied', updatePermissions( interestAdminUtxo, govUtxo.utxo, newAdminPermissions, signersAllOf(govUtxo.datum.protocolParams.foundationMultisig), context.systemParams, context.lucid, ), ); }); });