UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

1,730 lines (1,551 loc) 48.5 kB
import { addAssets, Data, fromHex, LucidEvolution, Credential, OutRef, slotToUnixTime, toHex, TxBuilder, UTxO, getInputIndices, credentialToAddress, validatorToScriptHash, credentialToRewardAddress, scriptHashToCredential, } from '@lucid-evolution/lucid'; import { fromSystemParamsAsset, fromSystemParamsScriptRef, SystemParams, } from '../../src/types/system-params'; import { addrDetails, calculateMinCollateralCappedIAssetRedemptionAmt, createScriptAddress, getInlineDatumOrThrow, matchSingle, mkPriceOracleValidator, mkTreasuryValidatorFromSP, ONE_SECOND, PriceOracleParams, treasuryFeeTx, } from '../../src'; import { parseCdpDatumOrThrow, serialiseCdpDatum, serialiseCdpRedeemer, serialiseRedeemCdpWithdrawalRedeemer, } from '../../src/contracts/cdp/types-new'; import { parseCollateralAssetDatumOrThrow, parseIAssetDatumOrThrow, } from '../../src/contracts/iasset/types'; import { parsePriceOracleDatum, serialisePriceOracleDatum, serialisePriceOracleRedeemer, } from '../../src/contracts/price-oracle/types-new'; import { parseInterestOracleDatum } from '../../src/contracts/interest-oracle/types-new'; import { parseGovDatumOrThrow } from '../../src/contracts/gov/types-new'; import { match, P } from 'ts-pattern'; import { calculateAccruedInterest, calculateUnitaryInterestSinceOracleLastUpdated, } from '../../src/contracts/interest-oracle/helpers'; import { calculateFeeFromRatio } from '../../src/utils/indigo-helpers'; import { AssetClass, assetClassValueOf, mkAssetsOf, mkLovelacesOf, } from '@3rd-eye-labs/cardano-offchain-common'; import { bigintMin } from '../../src/utils/bigint-utils'; import { oracleExpirationAwareValidity } from '../../src/contracts/price-oracle/helpers'; import { findAllNecessaryOrefs, findCdp, findPriceOracleFromCollateralAsset, } from './cdp-queries'; import { collectInterestTx } from '../../src/contracts/interest-collection/transactions'; import { findCollateralAsset, findIAsset } from '../queries/iasset-queries'; import { findInterestOracle } from '../queries/interest-oracle-queries'; import { AssetInfo } from '../endpoints/initialize'; import { LucidContext } from '../test-helpers'; import { option as O, function as F } from 'fp-ts'; import { findPriceOracle } from '../price-oracle/price-oracle-queries'; import { findRandomNonAdminInterestCollector } from '../interest-collection/interest-collector-queries'; import { serialiseCDPCreatorDatum, serialiseCDPCreatorRedeemer, } from '../../src/contracts/cdp-creator/types-new'; import { CollectInterestVariation, testCollectInterest, } from '../interest-collection/transactions-mutated'; import { findRandomTreasuryUtxoWithOnlyAda } from '../treasury/treasury-queries'; import { Rational, rationalFloor, rationalFromInt, rationalMul, } from '../../src/types/rational'; import { retrieveAdjustedPrice } from '../../src/utils/oracle-helpers'; import * as Core from '@evolution-sdk/evolution'; export async function mutatedRedeemCdp( /** * When the goal is to redeem the maximum possible, just pass in the total minted amount of the CDP. * The logic will automatically cap the amount to the max. */ attemptedRedemptionIAssetAmt: bigint, cdpOref: OutRef, iassetOref: OutRef, collateralAssetOref: OutRef, priceOracleOref: OutRef | undefined, interestOracleOref: OutRef, interestCollectorOref: OutRef, govOref: OutRef, sysParams: SystemParams, lucid: LucidEvolution, currentSlot: number, pythMessage?: string, ): Promise<TxBuilder> { const network = lucid.config().network!; const currentTime = BigInt(slotToUnixTime(network, currentSlot)); const cdpRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef), ]), (_) => new Error('Expected a single cdp Ref Script UTXO'), ); const cdpRedeemRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.cdpRedeemValidatorRef, ), ]), (_) => new Error('Expected a single cdp redeem Ref Script UTXO'), ); const iAssetTokenPolicyRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.iAssetTokenPolicyRef, ), ]), (_) => new Error('Expected a single iasset token policy Ref Script UTXO'), ); const cdpUtxo = matchSingle( await lucid.utxosByOutRef([cdpOref]), (_) => new Error('Expected a single cdp UTXO'), ); const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo)); const iassetUtxo = matchSingle( await lucid.utxosByOutRef([iassetOref]), (_) => new Error('Expected a single iasset UTXO'), ); const iassetDatum = parseIAssetDatumOrThrow( getInlineDatumOrThrow(iassetUtxo), ); const collateralAssetUtxo = matchSingle( await lucid.utxosByOutRef([collateralAssetOref]), (_) => new Error('Expected a single iasset UTXO'), ); const collateralAssetDatum = parseCollateralAssetDatumOrThrow( getInlineDatumOrThrow(collateralAssetUtxo), ); const isDelisted = match(collateralAssetDatum.priceInfo) .with({ Delisted: P.any }, () => true) .otherwise(() => false); if (!isDelisted && priceOracleOref === undefined) { throw new Error('Missing price oracle'); } const [adjustedPrice, priceOracleUtxo] = await retrieveAdjustedPrice( iassetDatum.assetName, collateralAssetDatum.collateralAsset, collateralAssetDatum.priceInfo, collateralAssetDatum.extraDecimals, priceOracleOref, pythMessage, sysParams.pythConfig, lucid, ); const interestOracleUtxo = matchSingle( await lucid.utxosByOutRef([interestOracleOref]), (_) => new Error('Expected a single interest oracle UTXO'), ); const interestOracleDatum = parseInterestOracleDatum( getInlineDatumOrThrow(interestOracleUtxo), ); const govUtxo = matchSingle( await lucid.utxosByOutRef([govOref]), (_) => new Error('Expected a single gov UTXO'), ); const govDatum = parseGovDatumOrThrow(getInlineDatumOrThrow(govUtxo)); const interestAmt = match(cdpDatum.cdpFees) .with({ FrozenCDPAccumulatedFees: P.any }, () => { throw new Error('CDP fees wrong'); }) .with({ ActiveCDPInterestTracking: P.select() }, (interest) => { return calculateAccruedInterest( currentTime, interest.unitaryInterestSnapshot, cdpDatum.mintedAmt, interest.lastSettled, interestOracleDatum, ); }) .exhaustive(); const collateralAmt = assetClassValueOf( cdpUtxo.assets, cdpDatum.collateralAsset, ); const totalCdpDebt = cdpDatum.mintedAmt + interestAmt; const [isPartial, redemptionIAssetAmt] = (() => { const res = calculateMinCollateralCappedIAssetRedemptionAmt( collateralAmt, totalCdpDebt, adjustedPrice, collateralAssetDatum.redemptionRatio, iassetDatum.redemptionReimbursementRatio, BigInt(collateralAssetDatum.minCollateralAmt), ); const redemptionAmt = bigintMin( attemptedRedemptionIAssetAmt, res.cappedIAssetRedemptionAmt, ); return [redemptionAmt < res.cappedIAssetRedemptionAmt, redemptionAmt]; })(); if (redemptionIAssetAmt <= 0) { throw new Error("There's no iAssets available for redemption."); } const redemptionCollateralAmt = rationalFloor( rationalMul(adjustedPrice, rationalFromInt(redemptionIAssetAmt)), ); const isPublicRedemption = !govDatum.protocolParams.cdpRedemptionRequiredSignature; const partialRedemptionFee = isPartial && isPublicRedemption ? BigInt(sysParams.cdpRedeemParams.partialRedemptionExtraFeeLovelace) : 0n; const processingFee = calculateFeeFromRatio( iassetDatum.redemptionProcessingFeeRatio, redemptionIAssetAmt, ); const reimburstmentFee = calculateFeeFromRatio( iassetDatum.redemptionReimbursementRatio, redemptionCollateralAmt, ); const referenceScripts = [ cdpRefScriptUtxo, iAssetTokenPolicyRefScriptUtxo, cdpRedeemRefScriptUtxo, ]; const referenceInputs = [ iassetUtxo, collateralAssetUtxo, interestOracleUtxo, govUtxo, ]; const tx = lucid .newTx() // Ref Script .readFrom(referenceScripts) // Ref inputs .readFrom(referenceInputs) .collectFrom([cdpUtxo], serialiseCdpRedeemer('RedeemCdp')) .mintAssets( mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }, interestAmt - redemptionIAssetAmt, ), Data.void(), ) .pay.ToContract( cdpUtxo.address, { kind: 'inline', value: serialiseCdpDatum({ ...cdpDatum, mintedAmt: totalCdpDebt - redemptionIAssetAmt, cdpFees: { ActiveCDPInterestTracking: { lastSettled: currentTime, unitaryInterestSnapshot: interestOracleDatum.unitaryInterest + calculateUnitaryInterestSinceOracleLastUpdated( currentTime, interestOracleDatum, ), }, }, }), }, addAssets( cdpUtxo.assets, mkLovelacesOf(-redemptionCollateralAmt), mkLovelacesOf(reimburstmentFee), ), ); if (priceOracleUtxo !== undefined) { const priceOracleDatum = parsePriceOracleDatum( getInlineDatumOrThrow(priceOracleUtxo), ); const txValidity = oracleExpirationAwareValidity( currentSlot, Number(sysParams.cdpCreatorParams.biasTime), Number(priceOracleDatum.expirationTime), network, ); referenceInputs.push(priceOracleUtxo); tx.validFrom(txValidity.validFrom) .validTo(txValidity.validTo) .readFrom([priceOracleUtxo]); } else { const validateFrom = slotToUnixTime(network, currentSlot - 1); const validateTo = validateFrom + Number(sysParams.cdpCreatorParams.biasTime); tx.validFrom(validateFrom).validTo(validateTo); } // Trigger CDP Redeem Withdrawal validator tx.withdraw( credentialToRewardAddress( lucid.config().network!, scriptHashToCredential(sysParams.cdpParams.cdpRedeemValHash), ), 0n, serialiseRedeemCdpWithdrawalRedeemer({ cdpOutReference: { txHash: fromHex(cdpUtxo.txHash), outputIndex: BigInt(cdpUtxo.outputIndex), }, currentTime: currentTime, priceOracleIdx: priceOracleUtxo ? { OracleRefInputIdx: getInputIndices( [priceOracleUtxo], [...referenceInputs, ...referenceScripts], )[0], } : 'OracleVoid', }), ); //TODO: Use a treasury input to save on ADA. tx.pay.ToContract( credentialToAddress(lucid.config().network!, { hash: validatorToScriptHash( mkTreasuryValidatorFromSP(sysParams.treasuryParams), ), type: 'Script', }), { kind: 'inline', value: Data.void() }, addAssets( mkAssetsOf(cdpDatum.collateralAsset, processingFee), mkLovelacesOf(partialRedemptionFee), ), ); if (interestAmt > 0n) { await collectInterestTx( mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }, interestAmt, ), lucid, sysParams, tx, interestCollectorOref, ); } return tx; } export async function runOpenCdpDelisted( context: LucidContext, sysParams: SystemParams, asset: string, collateralAsset: AssetClass, initialCollateral: bigint, initialMint: bigint, ): Promise<TxBuilder> { const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, asset, collateralAsset, ); const network = context.lucid.config().network!; const currentTime = BigInt(slotToUnixTime(network, context.emulator.slot)); const [pkh, skh] = await addrDetails(context.lucid); const cdpCreatorRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.cdpCreatorValidatorRef, ), ]), (_) => new Error('Expected a single cdp creator Ref Script UTXO'), ); const cdpAuthTokenPolicyRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.authTokenPolicies.cdpAuthTokenRef, ), ]), (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'), ); const iAssetTokenPolicyRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.iAssetTokenPolicyRef, ), ]), (_) => new Error('Expected a single iasset token policy Ref Script UTXO'), ); const iassetUtxo = matchSingle( await context.lucid.utxosByOutRef([orefs.iasset.utxo]), (_) => new Error('Expected a single iasset UTXO'), ); const iassetDatum = parseIAssetDatumOrThrow( getInlineDatumOrThrow(iassetUtxo), ); const collateralAssetUtxo = matchSingle( await context.lucid.utxosByOutRef([orefs.collateralAsset.utxo]), (_) => new Error('Expected a single iasset UTXO'), ); const collateralAssetDatum = parseCollateralAssetDatumOrThrow( getInlineDatumOrThrow(collateralAssetUtxo), ); const interestOracleUtxo = matchSingle( await context.lucid.utxosByOutRef([orefs.interestOracleUtxo]), (_) => new Error('Expected a single interest oracle UTXO'), ); const interestOracleDatum = parseInterestOracleDatum( getInlineDatumOrThrow(interestOracleUtxo), ); const cdpCreatorUtxo = matchSingle( await context.lucid.utxosByOutRef([orefs.cdpCreatorUtxo]), (_) => new Error('Expected a single CDP creator UTXO'), ); const cdpNftVal = mkAssetsOf( fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), 1n, ); const iassetClass = { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }; const iassetTokensVal = mkAssetsOf(iassetClass, initialMint); const refScripts: UTxO[] = [ cdpCreatorRefScriptUtxo, cdpAuthTokenPolicyRefScriptUtxo, iAssetTokenPolicyRefScriptUtxo, ]; const referenceInputs: UTxO[] = [ interestOracleUtxo, iassetUtxo, collateralAssetUtxo, ]; const tx = context.lucid .newTx() .readFrom(refScripts) .readFrom(referenceInputs) .mintAssets(cdpNftVal, Data.void()) .mintAssets(iassetTokensVal, Data.void()) .pay.ToContract( createScriptAddress(network, sysParams.validatorHashes.cdpHash, skh), { kind: 'inline', value: serialiseCdpDatum({ cdpOwner: fromHex(pkh.hash), iasset: iassetDatum.assetName, collateralAsset: collateralAssetDatum.collateralAsset, mintedAmt: initialMint, cdpFees: { ActiveCDPInterestTracking: { lastSettled: currentTime, unitaryInterestSnapshot: calculateUnitaryInterestSinceOracleLastUpdated( currentTime, interestOracleDatum, ) + interestOracleDatum.unitaryInterest, }, }, }), }, addAssets( cdpNftVal, mkAssetsOf(collateralAssetDatum.collateralAsset, initialCollateral), ), ) .pay.ToContract( cdpCreatorUtxo.address, { kind: 'inline', value: serialiseCDPCreatorDatum({ creatorInputOref: { outputIndex: BigInt(cdpCreatorUtxo.outputIndex), txHash: fromHex(cdpCreatorUtxo.txHash), }, }), }, cdpCreatorUtxo.assets, ); const debtMintingFee = calculateFeeFromRatio( iassetDatum.debtMintingFeeRatio, initialMint, ); const treasuryRefScriptUtxo = debtMintingFee > 0 ? await treasuryFeeTx( iassetClass, debtMintingFee, 0n, context.lucid, sysParams, tx, cdpCreatorUtxo, orefs.treasuryUtxo, ) : undefined; const validFrom = slotToUnixTime(network, context.emulator.slot - 1); const validTo = validFrom + Number(sysParams.cdpCreatorParams.biasTime); tx.validFrom(validFrom).validTo(validTo); // We need to take into account the treasury ref script. const refInputsIndices = getInputIndices(referenceInputs, [ ...referenceInputs, ...refScripts, ...(treasuryRefScriptUtxo != null ? [treasuryRefScriptUtxo] : []), ]); tx.collectFrom([cdpCreatorUtxo], { kind: 'self', makeRedeemer: (inputIdx) => { return serialiseCDPCreatorRedeemer({ CreateCDP: { cdpOwner: fromHex(pkh.hash), minted: initialMint, collateralAmt: initialCollateral, currentTime: currentTime, creatorInputIdx: inputIdx, creatorOutputIdx: 1n, cdpOutputIdx: 0n, iassetRefInputIdx: refInputsIndices[1], collateralAssetRefInputIdx: refInputsIndices[2], interestOracleRefInputIdx: refInputsIndices[0], priceOracleIdx: 'OracleVoid', }, }); }, }); return tx; } export async function runOpenCdpAndUpdateOracle( context: LucidContext, sysParams: SystemParams, asset: string, collateralAsset: AssetClass, initialCollateral: bigint, initialMint: bigint, oracleParams: PriceOracleParams, newPrice: Rational, ): Promise<TxBuilder> { const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, asset, collateralAsset, ); const network = context.lucid.config().network!; const currentTime = BigInt(slotToUnixTime(network, context.emulator.slot)); const [pkh, skh] = await addrDetails(context.lucid); const oracleValidator = mkPriceOracleValidator(oracleParams); const cdpCreatorRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.cdpCreatorValidatorRef, ), ]), (_) => new Error('Expected a single cdp creator Ref Script UTXO'), ); const cdpAuthTokenPolicyRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.authTokenPolicies.cdpAuthTokenRef, ), ]), (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'), ); const iAssetTokenPolicyRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.iAssetTokenPolicyRef, ), ]), (_) => new Error('Expected a single iasset token policy Ref Script UTXO'), ); const iassetUtxo = matchSingle( await context.lucid.utxosByOutRef([orefs.iasset.utxo]), (_) => new Error('Expected a single iasset UTXO'), ); const iassetDatum = parseIAssetDatumOrThrow( getInlineDatumOrThrow(iassetUtxo), ); const collateralAssetUtxo = matchSingle( await context.lucid.utxosByOutRef([orefs.collateralAsset.utxo]), (_) => new Error('Expected a single iasset UTXO'), ); const collateralAssetDatum = parseCollateralAssetDatumOrThrow( getInlineDatumOrThrow(collateralAssetUtxo), ); const priceOracleUtxo = await findPriceOracleFromCollateralAsset( context.lucid, orefs.collateralAsset, ); if (!priceOracleUtxo) throw new Error('Expected a price oracle'); const interestOracleUtxo = matchSingle( await context.lucid.utxosByOutRef([orefs.interestOracleUtxo]), (_) => new Error('Expected a single interest oracle UTXO'), ); const interestOracleDatum = parseInterestOracleDatum( getInlineDatumOrThrow(interestOracleUtxo), ); const cdpCreatorUtxo = matchSingle( await context.lucid.utxosByOutRef([orefs.cdpCreatorUtxo]), (_) => new Error('Expected a single CDP creator UTXO'), ); const cdpNftVal = mkAssetsOf( fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), 1n, ); const iassetClass = { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }; const iassetTokensVal = mkAssetsOf(iassetClass, initialMint); const refScripts: UTxO[] = [ cdpCreatorRefScriptUtxo, cdpAuthTokenPolicyRefScriptUtxo, iAssetTokenPolicyRefScriptUtxo, ]; const referenceInputs: UTxO[] = [ interestOracleUtxo, iassetUtxo, collateralAssetUtxo, ]; const tx = context.lucid .newTx() .validFrom(Number(currentTime - oracleParams.biasTime) + ONE_SECOND) .validTo(Number(currentTime + oracleParams.biasTime) - ONE_SECOND) .attach.SpendingValidator(oracleValidator) .readFrom(refScripts) .readFrom(referenceInputs) .mintAssets(cdpNftVal, Data.void()) .mintAssets(iassetTokensVal, Data.void()) .collectFrom( [priceOracleUtxo], serialisePriceOracleRedeemer({ currentTime: currentTime, newPrice: newPrice, }), ) .pay.ToContract( priceOracleUtxo.address, { kind: 'inline', value: serialisePriceOracleDatum({ price: newPrice, expirationTime: currentTime + oracleParams.expirationPeriod, auxiliaryData: Core.Data.fromCBORHex(Data.void()), }), }, priceOracleUtxo.assets, ) .pay.ToContract( createScriptAddress(network, sysParams.validatorHashes.cdpHash, skh), { kind: 'inline', value: serialiseCdpDatum({ cdpOwner: fromHex(pkh.hash), iasset: iassetDatum.assetName, collateralAsset: collateralAssetDatum.collateralAsset, mintedAmt: initialMint, cdpFees: { ActiveCDPInterestTracking: { lastSettled: currentTime, unitaryInterestSnapshot: calculateUnitaryInterestSinceOracleLastUpdated( currentTime, interestOracleDatum, ) + interestOracleDatum.unitaryInterest, }, }, }), }, addAssets( cdpNftVal, mkAssetsOf(collateralAssetDatum.collateralAsset, initialCollateral), ), ) .pay.ToContract( cdpCreatorUtxo.address, { kind: 'inline', value: serialiseCDPCreatorDatum({ creatorInputOref: { outputIndex: BigInt(cdpCreatorUtxo.outputIndex), txHash: fromHex(cdpCreatorUtxo.txHash), }, }), }, cdpCreatorUtxo.assets, ) .addSignerKey(pkh.hash); const debtMintingFee = calculateFeeFromRatio( iassetDatum.debtMintingFeeRatio, initialMint, ); const treasuryRefScriptUtxo = debtMintingFee > 0 ? await treasuryFeeTx( iassetClass, debtMintingFee, 0n, context.lucid, sysParams, tx, cdpCreatorUtxo, orefs.treasuryUtxo, ) : undefined; // We need to take into account the treasury ref script as well. const refInputsIndices = getInputIndices(referenceInputs, [ ...referenceInputs, ...refScripts, ...(treasuryRefScriptUtxo != null ? [treasuryRefScriptUtxo] : []), ]); tx.collectFrom([cdpCreatorUtxo], { kind: 'self', makeRedeemer: (inputIdx) => { return serialiseCDPCreatorRedeemer({ CreateCDP: { cdpOwner: fromHex(pkh.hash), minted: initialMint, collateralAmt: initialCollateral, currentTime: currentTime, creatorInputIdx: inputIdx, creatorOutputIdx: 2n, cdpOutputIdx: 1n, iassetRefInputIdx: refInputsIndices[1], collateralAssetRefInputIdx: refInputsIndices[2], interestOracleRefInputIdx: refInputsIndices[0], priceOracleIdx: { OracleOutputIdx: 0n }, }, }); }, }); return tx; } export async function runTestAdjustCdpDelisted( context: LucidContext, sysParams: SystemParams, asset: string, collateralAsset: AssetClass, collateralAdjustment: bigint, debtAdjustment: bigint, ): Promise<TxBuilder> { const network = context.lucid.config().network!; const currentTime = BigInt(slotToUnixTime(network, context.emulator.slot)); const [pkh, skh] = await addrDetails(context.lucid); const cdp = await findCdp( context.lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), pkh.hash, skh, ); const cdpRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef), ]), (_) => new Error('Expected a single cdp Ref Script UTXO'), ); const cdpUtxo = matchSingle( await context.lucid.utxosByOutRef([cdp.utxo]), (_) => new Error('Expected a single cdp UTXO'), ); const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo)); const iassetOutput = await findIAsset( context.lucid, sysParams.validatorHashes.iassetHash, fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken), asset, ); const iassetUtxo = matchSingle( await context.lucid.utxosByOutRef([iassetOutput.utxo]), (_) => new Error('Expected a single iasset UTXO'), ); const iassetDatum = parseIAssetDatumOrThrow( getInlineDatumOrThrow(iassetUtxo), ); const collateralAssetOutput = await findCollateralAsset( context.lucid, sysParams, fromSystemParamsAsset(sysParams.cdpParams.collateralAssetAuthToken), asset, collateralAsset, ); const collateralAssetUtxo = matchSingle( await context.lucid.utxosByOutRef([collateralAssetOutput.utxo]), (_) => new Error('Expected a single collateral asset UTXO'), ); const collateralAssetDatum = parseCollateralAssetDatumOrThrow( getInlineDatumOrThrow(collateralAssetUtxo), ); const interestOracleUtxo = await findInterestOracle( context.lucid, collateralAssetDatum.interestOracleNft, ); const interestOracleDatum = parseInterestOracleDatum( getInlineDatumOrThrow(interestOracleUtxo), ); const interestCollectorUtxo = await findRandomNonAdminInterestCollector( context.lucid, sysParams.validatorHashes.interestCollectionHash, fromSystemParamsAsset(sysParams.interestCollectionParams.multisigUtxoNft), ); const validateFrom = slotToUnixTime(network, context.emulator.slot - 1); const validateTo = validateFrom + Number(sysParams.cdpParams.biasTime) - 60_000; const interestAmt = match(cdpDatum.cdpFees) .with({ FrozenCDPAccumulatedFees: P.any }, () => { throw new Error('CDP fees wrong'); }) .with({ ActiveCDPInterestTracking: P.select() }, (interest) => { return calculateAccruedInterest( currentTime, interest.unitaryInterestSnapshot, cdpDatum.mintedAmt, interest.lastSettled, interestOracleDatum, ); }) .exhaustive(); const mintedAmountChange = debtAdjustment + interestAmt; const tx = context.lucid .newTx() .validFrom(validateFrom) .validTo(validateTo) .collectFrom( [cdpUtxo], serialiseCdpRedeemer({ AdjustCdp: { currentTime: currentTime, debtAdjustment, collateralAdjustment, priceOracleIdx: 'OracleVoid', }, }), ) .readFrom([cdpRefScriptUtxo]) .readFrom([iassetUtxo, collateralAssetUtxo, interestOracleUtxo]) .pay.ToContract( cdpUtxo.address, { kind: 'inline', value: serialiseCdpDatum({ ...cdpDatum, mintedAmt: cdpDatum.mintedAmt + mintedAmountChange, cdpFees: { ActiveCDPInterestTracking: { lastSettled: currentTime, unitaryInterestSnapshot: calculateUnitaryInterestSinceOracleLastUpdated( currentTime, interestOracleDatum, ) + interestOracleDatum.unitaryInterest, }, }, }), }, addAssets( cdpUtxo.assets, mkAssetsOf(cdpDatum.collateralAsset, collateralAdjustment), ), ); if (!cdpDatum.cdpOwner) { throw new Error('Expected active CDP'); } tx.addSignerKey(toHex(cdpDatum.cdpOwner)); if (mintedAmountChange !== 0n) { const iAssetTokenPolicyRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.iAssetTokenPolicyRef, ), ]), (_) => new Error('Expected a single iasset token policy Ref Script UTXO'), ); const iassetTokensVal = mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }, mintedAmountChange, ); tx.readFrom([iAssetTokenPolicyRefScriptUtxo]).mintAssets( iassetTokensVal, Data.void(), ); } const iAssetAc = { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }; if (interestAmt > 0n) { await collectInterestTx( mkAssetsOf(iAssetAc, interestAmt), context.lucid, sysParams, tx, interestCollectorUtxo, ); } let treasuryFee = 0n; if (debtAdjustment > 0n) { const treasuryUtxo = await findRandomTreasuryUtxoWithOnlyAda( context.lucid, sysParams, ); treasuryFee += calculateFeeFromRatio( iassetDatum.debtMintingFeeRatio, debtAdjustment, ); if (treasuryFee > 0n) { await treasuryFeeTx( iAssetAc, treasuryFee, 0n, context.lucid, sysParams, tx, cdpUtxo, treasuryUtxo, ); } } return tx; } export async function runTestDepositCdpWithInterestVar( context: LucidContext, sysParams: SystemParams, iasset: string, collateralAsset: AssetClass, interestCollectorUtxo: UTxO, interestVariation: CollectInterestVariation, amount: bigint = 1_000_000n, ): Promise<TxBuilder> { const network = context.lucid.config().network!; const currentTime = BigInt(slotToUnixTime(network, context.emulator.slot)); const [pkh, skh] = await addrDetails(context.lucid); const cdp = await findCdp( context.lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), pkh.hash, skh, ); const cdpRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef), ]), (_) => new Error('Expected a single cdp Ref Script UTXO'), ); const cdpUtxo = matchSingle( await context.lucid.utxosByOutRef([cdp.utxo]), (_) => new Error('Expected a single cdp UTXO'), ); const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo)); const iassetOutput = await findIAsset( context.lucid, sysParams.validatorHashes.iassetHash, fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken), iasset, ); const iassetUtxo = matchSingle( await context.lucid.utxosByOutRef([iassetOutput.utxo]), (_) => new Error('Expected a single iasset UTXO'), ); const iassetDatum = parseIAssetDatumOrThrow( getInlineDatumOrThrow(iassetUtxo), ); const collateralAssetOutput = await findCollateralAsset( context.lucid, sysParams, fromSystemParamsAsset(sysParams.cdpParams.collateralAssetAuthToken), iasset, collateralAsset, ); const collateralAssetUtxo = matchSingle( await context.lucid.utxosByOutRef([collateralAssetOutput.utxo]), (_) => new Error('Expected a single iasset UTXO'), ); const interestOracleUtxo = await findInterestOracle( context.lucid, collateralAssetOutput.datum.interestOracleNft, ); const interestOracleDatum = parseInterestOracleDatum( getInlineDatumOrThrow(interestOracleUtxo), ); const validateFrom = slotToUnixTime(network, context.emulator.slot - 1); const validateTo = validateFrom + Number(sysParams.cdpParams.biasTime) - 60_000; const interestAmt = match(cdpDatum.cdpFees) .with({ FrozenCDPAccumulatedFees: P.any }, () => { throw new Error('CDP fees wrong'); }) .with({ ActiveCDPInterestTracking: P.select() }, (interest) => { return calculateAccruedInterest( currentTime, interest.unitaryInterestSnapshot, cdpDatum.mintedAmt, interest.lastSettled, interestOracleDatum, ); }) .exhaustive(); const mintedAmountChange = interestAmt; const tx = context.lucid .newTx() .validFrom(validateFrom) .validTo(validateTo) .collectFrom( [cdpUtxo], serialiseCdpRedeemer({ AdjustCdp: { currentTime: currentTime, debtAdjustment: 0n, collateralAdjustment: amount, priceOracleIdx: 'OracleVoid', }, }), ) .readFrom([cdpRefScriptUtxo]) .readFrom([iassetUtxo, collateralAssetUtxo, interestOracleUtxo]) .pay.ToContract( cdpUtxo.address, { kind: 'inline', value: serialiseCdpDatum({ ...cdpDatum, mintedAmt: cdpDatum.mintedAmt + mintedAmountChange, cdpFees: { ActiveCDPInterestTracking: { lastSettled: currentTime, unitaryInterestSnapshot: calculateUnitaryInterestSinceOracleLastUpdated( currentTime, interestOracleDatum, ) + interestOracleDatum.unitaryInterest, }, }, }), }, addAssets(cdpUtxo.assets, mkLovelacesOf(amount)), ); if (!cdpDatum.cdpOwner) { throw new Error('Expected active CDP'); } tx.addSignerKey(toHex(cdpDatum.cdpOwner)); if (mintedAmountChange !== 0n) { const iAssetTokenPolicyRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.iAssetTokenPolicyRef, ), ]), (_) => new Error('Expected a single iasset token policy Ref Script UTXO'), ); const iassetTokensVal = mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }, mintedAmountChange, ); tx.readFrom([iAssetTokenPolicyRefScriptUtxo]).mintAssets( iassetTokensVal, Data.void(), ); } if (interestAmt > 0n) { await testCollectInterest( mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }, interestAmt, ), context.lucid, sysParams, tx, interestCollectorUtxo, interestVariation, ); } return tx; } export async function runCloseCdpWrongOracle( lucid: LucidEvolution, currentSlot: number, sysParams: SystemParams, iasset: string, wrongAsset: string, collateralAsset: AssetClass, ): Promise<TxBuilder> { const [pkh, skh] = await addrDetails(lucid); const cdp = await findCdp( lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), pkh.hash, skh, ); const orefs = await findAllNecessaryOrefs( lucid, sysParams, iasset, collateralAsset, ); const priceOracleOref = await findPriceOracleFromCollateralAsset( lucid, orefs.collateralAsset, ); const network = lucid.config().network!; const currentTime = BigInt(slotToUnixTime(network, currentSlot)); const cdpRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef), ]), (_) => new Error('Expected a single cdp Ref Script UTXO'), ); const cdpAuthTokenPolicyRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.authTokenPolicies.cdpAuthTokenRef, ), ]), (_) => new Error('Expected a single cdp auth token policy Ref Script UTXO'), ); const iAssetTokenPolicyRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.iAssetTokenPolicyRef, ), ]), (_) => new Error('Expected a single iasset token policy Ref Script UTXO'), ); const cdpUtxo = matchSingle( await lucid.utxosByOutRef([cdp.utxo]), (_) => new Error('Expected a single cdp UTXO'), ); const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo)); const iassetUtxo = matchSingle( await lucid.utxosByOutRef([orefs.iasset.utxo]), (_) => new Error('Expected a single iasset UTXO'), ); const iassetDatum = parseIAssetDatumOrThrow( getInlineDatumOrThrow(iassetUtxo), ); const priceOracleUtxo = matchSingle( await lucid.utxosByOutRef([priceOracleOref!]), (_) => new Error('Expected a single price oracle UTXO'), ); const priceOracleDatum = parsePriceOracleDatum( getInlineDatumOrThrow(priceOracleUtxo), ); const wrongCollateral = await findCollateralAsset( lucid, sysParams, fromSystemParamsAsset(sysParams.cdpParams.collateralAssetAuthToken), wrongAsset, collateralAsset, ); const wrongInterestOracleUtxo = await findInterestOracle( lucid, wrongCollateral.datum.interestOracleNft, ); const wrongInterestOracleDatum = parseInterestOracleDatum( getInlineDatumOrThrow(wrongInterestOracleUtxo), ); const txValidity = oracleExpirationAwareValidity( currentSlot, Number(sysParams.cdpCreatorParams.biasTime), Number(priceOracleDatum.expirationTime), network, ); const tx = lucid .newTx() .readFrom([ cdpRefScriptUtxo, iAssetTokenPolicyRefScriptUtxo, cdpAuthTokenPolicyRefScriptUtxo, ]) .readFrom([wrongCollateral.utxo, wrongInterestOracleUtxo]) .validFrom(txValidity.validFrom) .validTo(txValidity.validTo) .mintAssets( mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }, -cdpDatum.mintedAmt, ), Data.void(), ) .mintAssets( mkAssetsOf(fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), -1n), Data.void(), ) .collectFrom( [cdpUtxo], serialiseCdpRedeemer({ CloseCdp: { currentTime: currentTime } }), ); if (!cdpDatum.cdpOwner) { throw new Error('Expected active CDP'); } tx.addSignerKey(toHex(cdpDatum.cdpOwner)); const interestAmt = match(cdpDatum.cdpFees) .with({ FrozenCDPAccumulatedFees: P.any }, () => { throw new Error('CDP fees wrong'); }) .with({ ActiveCDPInterestTracking: P.select() }, (interest) => { return calculateAccruedInterest( currentTime, interest.unitaryInterestSnapshot, cdpDatum.mintedAmt, interest.lastSettled, wrongInterestOracleDatum, ); }) .exhaustive(); if (interestAmt > 0n) { await collectInterestTx( mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: cdpDatum.iasset, }, interestAmt, ), lucid, sysParams, tx, orefs.interestCollectorUtxo, ); } return tx; } export async function runRedeemCdpWrongOracle( context: LucidContext, sysParams: SystemParams, assetInfo: AssetInfo, wrongAssetInfo: AssetInfo, collateralAsset: AssetClass, pkh: string, skh: Credential | undefined, ): Promise<TxBuilder> { const cdp = await findCdp( context.lucid, sysParams.validatorHashes.cdpHash, fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), pkh, skh, ); const cdpRedeemRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.cdpRedeemValidatorRef, ), ]), (_) => new Error('Expected a single cdp redeem Ref Script UTXO'), ); const orefs = await findAllNecessaryOrefs( context.lucid, sysParams, assetInfo.iassetTokenNameAscii, collateralAsset, ); const wrongIasset = await findIAsset( context.lucid, sysParams.validatorHashes.iassetHash, fromSystemParamsAsset(sysParams.cdpParams.iAssetAuthToken), wrongAssetInfo.iassetTokenNameAscii, ); const wrongCollateral = await findCollateralAsset( context.lucid, sysParams, fromSystemParamsAsset(sysParams.cdpParams.collateralAssetAuthToken), wrongAssetInfo.iassetTokenNameAscii, collateralAsset, ); const wrongInterestOracleUtxo = await findInterestOracle( context.lucid, wrongCollateral.datum.interestOracleNft, ); const wrongInterestOracleDatum = parseInterestOracleDatum( getInlineDatumOrThrow(wrongInterestOracleUtxo), ); const wrongPriceOracleUtxo = await findPriceOracle( context.lucid, match(wrongCollateral.datum.priceInfo) .with({ OracleNft: P.select() }, (oracleNft) => oracleNft) .otherwise(() => { throw new Error('Expected active oracle'); }), ); const wrongPriceOracleDatum = parsePriceOracleDatum( getInlineDatumOrThrow(wrongPriceOracleUtxo), ); const network = context.lucid.config().network!; const currentTime = BigInt(slotToUnixTime(network, context.emulator.slot)); const cdpRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef), ]), (_) => new Error('Expected a single cdp Ref Script UTXO'), ); const iAssetTokenPolicyRefScriptUtxo = matchSingle( await context.lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.iAssetTokenPolicyRef, ), ]), (_) => new Error('Expected a single iasset token policy Ref Script UTXO'), ); const cdpUtxo = matchSingle( await context.lucid.utxosByOutRef([cdp.utxo]), (_) => new Error('Expected a single cdp UTXO'), ); const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo)); const govUtxo = matchSingle( await context.lucid.utxosByOutRef([orefs.govUtxo]), (_) => new Error('Expected a single gov UTXO'), ); const govDatum = parseGovDatumOrThrow(getInlineDatumOrThrow(govUtxo)); const interestAmt = match(cdpDatum.cdpFees) .with({ FrozenCDPAccumulatedFees: P.any }, () => { throw new Error('CDP fees wrong'); }) .with({ ActiveCDPInterestTracking: P.select() }, (interest) => { return calculateAccruedInterest( currentTime, interest.unitaryInterestSnapshot, cdpDatum.mintedAmt, interest.lastSettled, wrongInterestOracleDatum, ); }) .exhaustive(); const collateralAmt = assetClassValueOf( cdpUtxo.assets, cdpDatum.collateralAsset, ); const totalCdpDebt = cdpDatum.mintedAmt + interestAmt; const [isPartial, redemptionIAssetAmt] = (() => { const res = calculateMinCollateralCappedIAssetRedemptionAmt( collateralAmt, totalCdpDebt, wrongPriceOracleDatum.price, wrongCollateral.datum.redemptionRatio, wrongIasset.datum.redemptionReimbursementRatio, BigInt(wrongCollateral.datum.minCollateralAmt), ); const redemptionAmt = bigintMin( cdp.datum.mintedAmt, res.cappedIAssetRedemptionAmt, ); return [redemptionAmt < res.cappedIAssetRedemptionAmt, redemptionAmt]; })(); if (redemptionIAssetAmt <= 0) { throw new Error("There's no iAssets available for redemption."); } const redemptionCollateralAmt = rationalFloor( rationalMul( wrongPriceOracleDatum.price, rationalFromInt(redemptionIAssetAmt), ), ); const processingFee = calculateFeeFromRatio( wrongIasset.datum.redemptionProcessingFeeRatio, redemptionCollateralAmt, ); const reimburstmentFee = calculateFeeFromRatio( wrongIasset.datum.redemptionReimbursementRatio, redemptionCollateralAmt, ); const txValidity = oracleExpirationAwareValidity( context.emulator.slot, Number(sysParams.cdpCreatorParams.biasTime), Number(wrongPriceOracleDatum.expirationTime), network, ); const referenceInputs = [ wrongIasset.utxo, wrongCollateral.utxo, wrongPriceOracleUtxo, wrongInterestOracleUtxo, govUtxo, ]; const referenceScripts = [ cdpRefScriptUtxo, iAssetTokenPolicyRefScriptUtxo, cdpRedeemRefScriptUtxo, ]; const tx = context.lucid.newTx().readFrom(referenceScripts); const interestCollectorRefScriptUtxo = interestAmt > 0n ? await collectInterestTx( mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: cdpDatum.iasset, }, interestAmt, ), context.lucid, sysParams, tx, orefs.interestCollectorUtxo, ) : undefined; // We need to take into account the interest collector ref script as well. const priceOracleIdx = getInputIndices( [wrongPriceOracleUtxo], [ ...referenceInputs, ...referenceScripts, ...(interestCollectorRefScriptUtxo != null ? [interestCollectorRefScriptUtxo] : []), ], )[0]; tx.readFrom(referenceInputs) // Trigger CDP Redeem Withdrawal validator .withdraw( credentialToRewardAddress( context.lucid.config().network!, scriptHashToCredential(sysParams.cdpParams.cdpRedeemValHash), ), 0n, serialiseRedeemCdpWithdrawalRedeemer({ cdpOutReference: { txHash: fromHex(cdpUtxo.txHash), outputIndex: BigInt(cdpUtxo.outputIndex), }, currentTime: currentTime, priceOracleIdx: { OracleRefInputIdx: priceOracleIdx }, }), ) .validFrom(txValidity.validFrom) .validTo(txValidity.validTo) .collectFrom([cdpUtxo], serialiseCdpRedeemer('RedeemCdp')) .mintAssets( mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: cdpDatum.iasset, }, interestAmt - redemptionIAssetAmt, ), Data.void(), ) .pay.ToContract( cdpUtxo.address, { kind: 'inline', value: serialiseCdpDatum({ ...cdpDatum, mintedAmt: totalCdpDebt - redemptionIAssetAmt, cdpFees: { ActiveCDPInterestTracking: { lastSettled: currentTime, unitaryInterestSnapshot: wrongInterestOracleDatum.unitaryInterest + calculateUnitaryInterestSinceOracleLastUpdated( currentTime, wrongInterestOracleDatum, ), }, }, }), }, addAssets( cdpUtxo.assets, mkAssetsOf( cdpDatum.collateralAsset, -redemptionCollateralAmt + reimburstmentFee, ), ), ); const partialRedemptionFee = F.pipe( govDatum.protocolParams.cdpRedemptionRequiredSignature, O.fromNullable, O.match( // When public redemptions () => { return isPartial ? BigInt(sysParams.cdpRedeemParams.partialRedemptionExtraFeeLovelace) : 0n; }, // When private redemptions (requiredSignature) => { tx.addSignerKey(toHex(requiredSignature)); return 0n; }, ), ); //TODO: Use a treasury input to save on ADA. tx.pay.ToContract( credentialToAddress(context.lucid.config().network!, { hash: validatorToScriptHash( mkTreasuryValidatorFromSP(sysParams.treasuryParams), ), type: 'Script', }), { kind: 'inline', value: Data.void() }, addAssets( mkAssetsOf(cdpDatum.collateralAsset, processingFee), mkLovelacesOf(partialRedemptionFee), ), ); return tx; }