@indigo-labs/indigo-sdk
Version:
Indigo SDK for interacting with Indigo endpoints via lucid-evolution
893 lines (795 loc) • 25.3 kB
text/typescript
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,
),
);
});
});