UNPKG

@indigo-labs/indigo-sdk

Version:

Indigo SDK for interacting with Indigo endpoints via lucid-evolution

1,606 lines (1,457 loc) 44.6 kB
import { addAssets, Assets, credentialToRewardAddress, Data, fromHex, getInputIndices, LucidEvolution, OutRef, scriptHashToCredential, slotToUnixTime, toHex, TxBuilder, UTxO, } from '@lucid-evolution/lucid'; import { fromSystemParamsAsset, fromSystemParamsScriptRef, SystemParams, } from '../../types/system-params'; import { addrDetails, createScriptAddress, getInlineDatumOrThrow, } from '../../utils/lucid-utils'; import { matchSingle } from '../../utils/utils'; import { calculateAccruedInterest, calculateUnitaryInterestSinceOracleLastUpdated, } from '../interest-oracle/helpers'; import { oracleExpirationAwareValidity } from '../price-oracle/helpers'; import { match, P } from 'ts-pattern'; import { calculateMinCollateralCappedIAssetRedemptionAmt } from './helpers'; import { bigintMax, bigintMin } from '../../utils/bigint-utils'; import { parseStabilityPoolDatumOrThrow, serialiseStabilityPoolDatum, serialiseStabilityPoolRedeemer, } from '../stability-pool/types-new'; import { liquidationHelper } from '../stability-pool/helpers'; import { array as A, function as F, option as O } from 'fp-ts'; import { calculateFeeFromRatio } from '../../utils/indigo-helpers'; import { collectInterestTx } from '../interest-collection/transactions'; import { CDPContent, parseCdpDatumOrThrow, serialiseCdpDatum, serialiseCdpRedeemer, serialiseRedeemCdpWithdrawalRedeemer, } from './types-new'; import { parseGovDatumOrThrow } from '../gov/types-new'; import { adaAssetClass, assetClassValueOf, mkAssetsOf, negateAssets, } from '@3rd-eye-labs/cardano-offchain-common'; import { parsePriceOracleDatum } from '../price-oracle/types-new'; import { parseInterestOracleDatum } from '../interest-oracle/types-new'; import { serialiseCDPCreatorDatum, serialiseCDPCreatorRedeemer, } from '../cdp-creator/types-new'; import { parseCollateralAssetDatumOrThrow, parseIAssetDatumOrThrow, } from '../iasset/types'; import { treasuryFeeTx } from '../treasury/transactions'; import { attachOracle } from '../iasset/helpers'; import { rationalFloor, rationalFromInt, rationalMul, } from '../../types/rational'; import { retrieveAdjustedPrice } from '../../utils/oracle-helpers'; export async function openCdp( collateralAmount: bigint, mintedAmount: bigint, sysParams: SystemParams, cdpCreatorOref: OutRef, iassetOref: OutRef, collateralAssetOref: OutRef, priceOracleOref: OutRef | undefined, interestOracleOref: OutRef, /** * `undefined` in case using direct treasury payment. */ treasuryOref: OutRef | undefined, lucid: LucidEvolution, currentSlot: number, pythMessage: string | undefined = undefined, pythStateOref: OutRef | undefined = undefined, ): Promise<TxBuilder> { const network = lucid.config().network!; const currentTime = BigInt(slotToUnixTime(network, currentSlot)); const [pkh, skh] = await addrDetails(lucid); const cdpCreatorRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.cdpCreatorValidatorRef, ), ]), (_) => new Error('Expected a single cdp creator 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 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 collateral asset UTXO'), ); const collateralAssetDatum = parseCollateralAssetDatumOrThrow( getInlineDatumOrThrow(collateralAssetUtxo), ); const interestOracleUtxo = matchSingle( await lucid.utxosByOutRef([interestOracleOref]), (_) => new Error('Expected a single interest oracle UTXO'), ); const interestOracleDatum = parseInterestOracleDatum( getInlineDatumOrThrow(interestOracleUtxo), ); const cdpCreatorUtxo = matchSingle( await lucid.utxosByOutRef([cdpCreatorOref]), (_) => new Error('Expected a single CDP creator UTXO'), ); match(collateralAssetDatum.priceInfo) .with({ Delisted: P.any }, () => { throw new Error("Can't open CDP of delisted asset"); }) .otherwise(() => {}); const cdpNftVal = mkAssetsOf( fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken), 1n, ); const iassetClass = { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }; const iassetTokensVal = mkAssetsOf(iassetClass, mintedAmount); const refScripts: UTxO[] = [ cdpCreatorRefScriptUtxo, cdpAuthTokenPolicyRefScriptUtxo, iAssetTokenPolicyRefScriptUtxo, ]; const referenceInputs: UTxO[] = [ interestOracleUtxo, iassetUtxo, collateralAssetUtxo, ]; const tx = lucid .newTx() // Ref scripts .readFrom(refScripts) .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: mintedAmount, cdpFees: { ActiveCDPInterestTracking: { lastSettled: currentTime, unitaryInterestSnapshot: calculateUnitaryInterestSinceOracleLastUpdated( currentTime, interestOracleDatum, ) + interestOracleDatum.unitaryInterest, }, }, }), }, addAssets( cdpNftVal, mkAssetsOf(collateralAssetDatum.collateralAsset, collateralAmount), ), ) .pay.ToContract( cdpCreatorUtxo.address, { kind: 'inline', value: serialiseCDPCreatorDatum({ creatorInputOref: { outputIndex: BigInt(cdpCreatorUtxo.outputIndex), txHash: fromHex(cdpCreatorUtxo.txHash), }, }), }, cdpCreatorUtxo.assets, ); const { interval, referenceInputs: refInputs } = await attachOracle( iassetDatum.assetName, collateralAssetDatum.collateralAsset, collateralAssetDatum.priceInfo, priceOracleOref, pythStateOref, pythMessage, sysParams.pythConfig, sysParams.cdpCreatorParams.biasTime, currentSlot, lucid, tx, ); // Set the validity interval for the transaction tx.validFrom(interval.validFrom).validTo(interval.validTo); // Read from the reference inputs referenceInputs.push(...refInputs); tx.readFrom(referenceInputs); const debtMintingFee = calculateFeeFromRatio( iassetDatum.debtMintingFeeRatio, mintedAmount, ); const treasuryRefScriptUtxo = debtMintingFee > 0 ? await treasuryFeeTx( iassetClass, debtMintingFee, 0n, lucid, sysParams, tx, cdpCreatorUtxo, treasuryOref, ) : 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: mintedAmount, collateralAmt: collateralAmount, currentTime: currentTime, creatorInputIdx: inputIdx, creatorOutputIdx: 1n, cdpOutputIdx: 0n, iassetRefInputIdx: refInputsIndices[1], collateralAssetRefInputIdx: refInputsIndices[2], interestOracleRefInputIdx: refInputsIndices[0], priceOracleIdx: priceOracleOref !== undefined ? { OracleRefInputIdx: refInputsIndices[3] } : 'OracleVoid', }, }); }, }); return tx; } export async function adjustCdp( collateralAdjustment: bigint, debtAdjustment: bigint, cdpOref: OutRef, iassetOref: OutRef, collateralAssetOref: OutRef, priceOracleOref: OutRef | undefined, interestOracleOref: OutRef, /** * `undefined` in case using direct treasury payment. */ treasuryOref: OutRef | undefined, interestCollectorOref: OutRef, sysParams: SystemParams, lucid: LucidEvolution, currentSlot: number, pythMessage: string | undefined = undefined, pythStateOref: OutRef | undefined = undefined, ): 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 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 collateral asset UTXO'), ); const collateralAssetDatum = parseCollateralAssetDatumOrThrow( getInlineDatumOrThrow(collateralAssetUtxo), ); const iAssetAc = { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }; const isMintOrWithdraw = debtAdjustment > 0n || collateralAdjustment < 0n; const interestOracleUtxo = matchSingle( await lucid.utxosByOutRef([interestOracleOref]), (_) => new Error('Expected a single interest oracle UTXO'), ); const interestOracleDatum = parseInterestOracleDatum( getInlineDatumOrThrow(interestOracleUtxo), ); 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 referenceScripts = [cdpRefScriptUtxo]; let referenceInputs = [iassetUtxo, collateralAssetUtxo, interestOracleUtxo]; let treasuryFee = 0n; const tx = lucid.newTx().readFrom(referenceScripts); if (isMintOrWithdraw) { const { interval, referenceInputs: oracleRefInputs } = await attachOracle( iassetDatum.assetName, collateralAssetDatum.collateralAsset, collateralAssetDatum.priceInfo, priceOracleOref, pythStateOref, pythMessage, sysParams.pythConfig, sysParams.cdpParams.biasTime, currentSlot, lucid, tx, ); referenceInputs = [...oracleRefInputs, ...referenceInputs]; // Set the validity interval for the transaction tx.validFrom(interval.validFrom).validTo(interval.validTo); } else { // Set the validity interval for the transaction tx.validFrom( Number(currentTime - BigInt(sysParams.cdpParams.biasTime) / 2n), ).validTo(Number(currentTime + BigInt(sysParams.cdpParams.biasTime) / 2n)); } // when mint if (debtAdjustment > 0n) { treasuryFee += calculateFeeFromRatio( iassetDatum.debtMintingFeeRatio, debtAdjustment, ); } const treasuryRefScriptUtxo = treasuryFee > 0 ? await treasuryFeeTx( iAssetAc, treasuryFee, 0n, lucid, sysParams, tx, cdpUtxo, treasuryOref, ) : undefined; const interestCollectorRefScriptUtxo = interestAmt > 0n ? await collectInterestTx( mkAssetsOf(iAssetAc, interestAmt), lucid, sysParams, tx, interestCollectorOref, ) : undefined; const iAssetTokenPolicyRefScriptUtxo = mintedAmountChange !== 0n ? matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.iAssetTokenPolicyRef, ), ]), (_) => new Error('Expected a single iasset token policy Ref Script UTXO'), ) : undefined; // We need to take into account the treasury, interest collector // and iAsset policy ref scripts as well. const refInputsIndices = getInputIndices(referenceInputs, [ ...referenceInputs, ...referenceScripts, ...(treasuryRefScriptUtxo != null ? [treasuryRefScriptUtxo] : []), ...(interestCollectorRefScriptUtxo != null ? [interestCollectorRefScriptUtxo] : []), ...(iAssetTokenPolicyRefScriptUtxo != null ? [iAssetTokenPolicyRefScriptUtxo] : []), ]); tx.readFrom(referenceInputs) .collectFrom( [cdpUtxo], serialiseCdpRedeemer({ AdjustCdp: { currentTime: currentTime, debtAdjustment: debtAdjustment, collateralAdjustment, priceOracleIdx: priceOracleOref !== undefined ? { OracleRefInputIdx: refInputsIndices[0] } : 'OracleVoid', }, }), ) .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 (mintedAmountChange !== 0n) { const iassetTokensVal = mkAssetsOf(iAssetAc, mintedAmountChange); tx.readFrom([iAssetTokenPolicyRefScriptUtxo as UTxO]).mintAssets( iassetTokensVal, Data.void(), ); } if (!cdpDatum.cdpOwner) { throw new Error('Expected active CDP'); } tx.addSignerKey(toHex(cdpDatum.cdpOwner)); return tx; } export async function depositCdp( amount: bigint, cdpOref: OutRef, iassetOref: OutRef, collateralAssetOref: OutRef, interestOracleOref: OutRef, /** * `undefined` in case using direct treasury payment. */ treasuryOref: OutRef | undefined, interestCollectorOref: OutRef, params: SystemParams, lucid: LucidEvolution, currentSlot: number, ): Promise<TxBuilder> { return adjustCdp( amount, 0n, cdpOref, iassetOref, collateralAssetOref, undefined, interestOracleOref, treasuryOref, interestCollectorOref, params, lucid, currentSlot, ); } export async function withdrawCdp( amount: bigint, cdpOref: OutRef, iassetOref: OutRef, collateralAssetOref: OutRef, priceOracleOref: OutRef | undefined, interestOracleOref: OutRef, /** * `undefined` in case using direct treasury payment. */ treasuryOref: OutRef | undefined, interestCollectorOref: OutRef, params: SystemParams, lucid: LucidEvolution, currentSlot: number, pythMessage: string | undefined = undefined, pythStateOref: OutRef | undefined = undefined, ): Promise<TxBuilder> { return adjustCdp( -amount, 0n, cdpOref, iassetOref, collateralAssetOref, priceOracleOref, interestOracleOref, treasuryOref, interestCollectorOref, params, lucid, currentSlot, pythMessage, pythStateOref, ); } export async function mintCdp( amount: bigint, cdpOref: OutRef, iassetOref: OutRef, collateralAssetOref: OutRef, priceOracleOref: OutRef | undefined, interestOracleOref: OutRef, /** * `undefined` in case using direct treasury payment. */ treasuryOref: OutRef | undefined, interestCollectorOref: OutRef, params: SystemParams, lucid: LucidEvolution, currentSlot: number, pythMessage: string | undefined = undefined, pythStateOref: OutRef | undefined = undefined, ): Promise<TxBuilder> { return adjustCdp( 0n, amount, cdpOref, iassetOref, collateralAssetOref, priceOracleOref, interestOracleOref, treasuryOref, interestCollectorOref, params, lucid, currentSlot, pythMessage, pythStateOref, ); } export async function burnCdp( amount: bigint, cdpOref: OutRef, iassetOref: OutRef, collateralAssetOref: OutRef, interestOracleOref: OutRef, /** * `undefined` in case using direct treasury payment. */ treasuryOref: OutRef | undefined, interestCollectorOref: OutRef, params: SystemParams, lucid: LucidEvolution, currentSlot: number, ): Promise<TxBuilder> { return adjustCdp( 0n, -amount, cdpOref, iassetOref, collateralAssetOref, undefined, interestOracleOref, treasuryOref, interestCollectorOref, params, lucid, currentSlot, ); } export async function closeCdp( cdpOref: OutRef, collateralAssetOref: OutRef, interestOracleOref: OutRef, interestCollectorOref: OutRef, sysParams: SystemParams, lucid: LucidEvolution, currentSlot: number, ): 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 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([cdpOref]), (_) => new Error('Expected a single cdp UTXO'), ); const cdpDatum = parseCdpDatumOrThrow(getInlineDatumOrThrow(cdpUtxo)); const collateralAssetUtxo = matchSingle( await lucid.utxosByOutRef([collateralAssetOref]), (_) => new Error('Expected a single collateral asset UTXO'), ); const collateralDatum = parseCollateralAssetDatumOrThrow( getInlineDatumOrThrow(collateralAssetUtxo), ); const interestOracleUtxo = matchSingle( await lucid.utxosByOutRef([interestOracleOref]), (_) => new Error('Expected a single interest oracle UTXO'), ); const interestOracleDatum = parseInterestOracleDatum( getInlineDatumOrThrow(interestOracleUtxo), ); const validateFrom = slotToUnixTime(network, currentSlot - 1); const validateTo = validateFrom + Number(sysParams.cdpCreatorParams.biasTime); const tx = lucid .newTx() .readFrom([ cdpRefScriptUtxo, iAssetTokenPolicyRefScriptUtxo, cdpAuthTokenPolicyRefScriptUtxo, ]) .readFrom([collateralAssetUtxo, interestOracleUtxo]) .validFrom(validateFrom) .validTo(validateTo) .mintAssets( mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: collateralDatum.iasset, }, -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, interestOracleDatum, ); }) .exhaustive(); if (interestAmt > 0n) { await collectInterestTx( mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: collateralDatum.iasset, }, interestAmt, ), lucid, sysParams, tx, interestCollectorOref, ); } return tx; } export async function redeemCdp( /** * 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, /** * `undefined` in case using direct treasury payment. */ treasuryOref: OutRef | undefined, govOref: OutRef, sysParams: SystemParams, lucid: LucidEvolution, currentSlot: number, pythMessage: string | undefined = undefined, _pythStateOref: OutRef | undefined = undefined, ): 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 collateral asset 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 processingFee = calculateFeeFromRatio( iassetDatum.redemptionProcessingFeeRatio, redemptionCollateralAmt, ); const reimburstmentFee = calculateFeeFromRatio( iassetDatum.redemptionReimbursementRatio, redemptionCollateralAmt, ); const referenceScripts = [ cdpRefScriptUtxo, iAssetTokenPolicyRefScriptUtxo, cdpRedeemRefScriptUtxo, ]; const referenceInputs = [ iassetUtxo, collateralAssetUtxo, interestOracleUtxo, govUtxo, ]; const tx = lucid.newTx().readFrom(referenceScripts); 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); } else { const validateFrom = slotToUnixTime(network, currentSlot - 1); const validateTo = validateFrom + Number(sysParams.cdpCreatorParams.biasTime); tx.validFrom(validateFrom).validTo(validateTo); } 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; }, ), ); const treasuryRefScriptUtxo = processingFee > 0n ? await treasuryFeeTx( cdpDatum.collateralAsset, processingFee, partialRedemptionFee, lucid, sysParams, tx, cdpOref, treasuryOref, ) : partialRedemptionFee > 0n ? await treasuryFeeTx( adaAssetClass, partialRedemptionFee, 0n, lucid, sysParams, tx, cdpOref, treasuryOref, ) : undefined; const interestCollectorRefScriptUtxo = interestAmt > 0n ? await collectInterestTx( mkAssetsOf( { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: iassetDatum.assetName, }, interestAmt, ), lucid, sysParams, tx, interestCollectorOref, ) : undefined; // We need to take into account the treasury and interest collector ref scripts as well. const refInputsIndices = getInputIndices(referenceInputs, [ ...referenceInputs, ...referenceScripts, ...(treasuryRefScriptUtxo != null ? [treasuryRefScriptUtxo] : []), ...(interestCollectorRefScriptUtxo != null ? [interestCollectorRefScriptUtxo] : []), ]); tx // Ref inputs .readFrom(referenceInputs) // Trigger CDP Redeem Withdrawal validator .withdraw( credentialToRewardAddress( lucid.config().network!, scriptHashToCredential(sysParams.cdpParams.cdpRedeemValHash), ), 0n, serialiseRedeemCdpWithdrawalRedeemer({ cdpOutReference: { txHash: fromHex(cdpUtxo.txHash), outputIndex: BigInt(cdpUtxo.outputIndex), }, currentTime: currentTime, priceOracleIdx: priceOracleOref !== undefined ? { OracleRefInputIdx: refInputsIndices[4] } : 'OracleVoid', }), ) .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, mkAssetsOf( cdpDatum.collateralAsset, -redemptionCollateralAmt + reimburstmentFee, ), ), ); return tx; } export async function freezeCdp( cdpOref: OutRef, iassetOref: OutRef, collateralAssetOref: OutRef, priceOracleOref: OutRef | undefined, interestOracleOref: OutRef, sysParams: SystemParams, lucid: LucidEvolution, currentSlot: number, pythMessage: string | undefined = undefined, pythStateOref: OutRef | undefined = undefined, ): 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 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 collateral asset UTXO'), ); const collateralAssetDatum = parseCollateralAssetDatumOrThrow( getInlineDatumOrThrow(collateralAssetUtxo), ); const [adjustedPrice, _] = 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 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 inputCollateral = assetClassValueOf( cdpUtxo.assets, cdpDatum.collateralAsset, ); const cdpDebtCollateralValue = rationalFloor( rationalMul( rationalFromInt(cdpDatum.mintedAmt + interestAmt), adjustedPrice, ), ); const liquidationProcessingFee = bigintMin( calculateFeeFromRatio( iassetDatum.liquidationProcessingFeeRatio, inputCollateral, ), bigintMax(0n, inputCollateral - cdpDebtCollateralValue), ); let referenceInputs = [iassetUtxo, collateralAssetUtxo, interestOracleUtxo]; const referenceScripts = [cdpRefScriptUtxo]; const tx = lucid.newTx(); const { interval, referenceInputs: oracleRefInputs } = await attachOracle( iassetDatum.assetName, collateralAssetDatum.collateralAsset, collateralAssetDatum.priceInfo, priceOracleOref, pythStateOref, pythMessage, sysParams.pythConfig, sysParams.cdpParams.biasTime, currentSlot, lucid, tx, ); referenceInputs = [...oracleRefInputs, ...referenceInputs]; const refInputsIndices = getInputIndices(referenceInputs, [ ...referenceInputs, ...referenceScripts, ]); return tx .readFrom(referenceScripts) .readFrom(referenceInputs) .validFrom(interval.validFrom) .validTo(interval.validTo) .collectFrom( [cdpUtxo], serialiseCdpRedeemer({ FreezeCdp: { currentTime: currentTime, priceOracleIdx: { OracleRefInputIdx: refInputsIndices[0] }, }, }), ) .pay.ToContract( createScriptAddress(network, sysParams.validatorHashes.cdpHash), { kind: 'inline', value: serialiseCdpDatum({ ...cdpDatum, cdpOwner: null, cdpFees: { FrozenCDPAccumulatedFees: { iassetInterest: interestAmt, collateralTreasury: liquidationProcessingFee, }, }, }), }, cdpUtxo.assets, ); } export async function liquidateCdp( cdpOref: OutRef, stabilityPoolOref: OutRef, interestCollectorOref: OutRef, /** * `undefined` in case using direct treasury payment. */ treasuryOref: OutRef | undefined, sysParams: SystemParams, lucid: LucidEvolution, ): Promise<TxBuilder> { const cdpRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef), ]), (_) => new Error('Expected a single cdp Ref Script UTXO'), ); const stabilityPoolRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.stabilityPoolValidatorRef, ), ]), (_) => new Error('Expected a single stability pool 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 cdpAuthTokenPolicyRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef( sysParams.scriptReferences.authTokenPolicies.cdpAuthTokenRef, ), ]), (_) => new Error('Expected a single cdp auth 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 spUtxo = matchSingle( await lucid.utxosByOutRef([stabilityPoolOref]), (_) => new Error('Expected a single stability pool UTXO'), ); const spDatum = parseStabilityPoolDatumOrThrow(getInlineDatumOrThrow(spUtxo)); const [frozenInterest, collateralForTreasury] = match(cdpDatum.cdpFees) .returnType<[bigint, bigint]>() .with({ FrozenCDPAccumulatedFees: P.select() }, (fees) => [ fees.iassetInterest, fees.collateralTreasury, ]) .with({ ActiveCDPInterestTracking: P.any }, () => { throw new Error('CDP fees wrong'); }) .exhaustive(); const cdpNftAc = fromSystemParamsAsset(sysParams.cdpParams.cdpAuthToken); const iassetsAc = { currencySymbol: fromHex( sysParams.cdpParams.cdpAssetSymbol.unCurrencySymbol, ), tokenName: cdpDatum.iasset, }; const spIassetAmt = assetClassValueOf(spUtxo.assets, iassetsAc); const totalCdpDebt = cdpDatum.mintedAmt + frozenInterest; // Take from the SP enough as much iAsset as possible to pay out the debt. const iassetUsedAmt = bigintMin(totalCdpDebt, spIassetAmt); const collateralAvailable = assetClassValueOf( cdpUtxo.assets, cdpDatum.collateralAsset, ); const collateralAvailMinusFees = collateralAvailable - collateralForTreasury; const collateralAbsorbed = (collateralAvailMinusFees * iassetUsedAmt) / totalCdpDebt; const isPartial = spIassetAmt < totalCdpDebt; // Interest partially paid if the liquidity in the SP is not enough to cover it. const remainingInterest = bigintMax(frozenInterest - spIassetAmt, 0n); const payableInterest = frozenInterest - remainingInterest; // All the iAsset taken from the SP and not used to pay out the interest must be burnt. const iassetBurnAmt = iassetUsedAmt - payableInterest; const tx = lucid .newTx() .readFrom([ cdpRefScriptUtxo, stabilityPoolRefScriptUtxo, iAssetTokenPolicyRefScriptUtxo, cdpAuthTokenPolicyRefScriptUtxo, ]) .collectFrom([spUtxo], { kind: 'selected', makeRedeemer: (inputIndices) => serialiseStabilityPoolRedeemer({ LiquidateCDP: { cdpIdx: inputIndices[0], }, }), inputs: [cdpUtxo], }) .collectFrom([cdpUtxo], serialiseCdpRedeemer('Liquidate')) .mintAssets(mkAssetsOf(iassetsAc, -iassetBurnAmt), Data.void()) .pay.ToContract( spUtxo.address, { kind: 'inline', value: serialiseStabilityPoolDatum({ StabilityPool: liquidationHelper( spDatum, cdpDatum.collateralAsset, iassetUsedAmt, collateralAbsorbed, ), }), }, addAssets( spUtxo.assets, mkAssetsOf(cdpDatum.collateralAsset, collateralAbsorbed), mkAssetsOf(iassetsAc, -iassetUsedAmt), ), ); if (collateralForTreasury > 0n) { await treasuryFeeTx( cdpDatum.collateralAsset, collateralForTreasury, 0n, lucid, sysParams, tx, cdpOref, treasuryOref, ); } if (isPartial) { tx.pay.ToContract( cdpUtxo.address, { kind: 'inline', value: serialiseCdpDatum({ ...cdpDatum, mintedAmt: cdpDatum.mintedAmt - iassetBurnAmt, cdpFees: { FrozenCDPAccumulatedFees: { iassetInterest: remainingInterest, collateralTreasury: 0n, }, }, }), }, addAssets( cdpUtxo.assets, negateAssets( mkAssetsOf( cdpDatum.collateralAsset, collateralForTreasury + collateralAbsorbed, ), ), ), ); } else { tx.mintAssets( mkAssetsOf(cdpNftAc, -assetClassValueOf(cdpUtxo.assets, cdpNftAc)), Data.void(), ); } if (payableInterest > 0) { await collectInterestTx( mkAssetsOf(iassetsAc, payableInterest), lucid, sysParams, tx, interestCollectorOref, ); } return tx; } export async function mergeCdps( cdpsToMergeUtxos: OutRef[], sysParams: SystemParams, lucid: LucidEvolution, ): Promise<TxBuilder> { const cdpRefScriptUtxo = matchSingle( await lucid.utxosByOutRef([ fromSystemParamsScriptRef(sysParams.scriptReferences.cdpValidatorRef), ]), (_) => new Error('Expected a single cdp Ref Script UTXO'), ); const cdpUtxos = await lucid.utxosByOutRef(cdpsToMergeUtxos); const cdpDatums = cdpUtxos.map((utxo) => parseCdpDatumOrThrow(getInlineDatumOrThrow(utxo)), ); if (cdpUtxos.length !== cdpsToMergeUtxos.length) { throw new Error('Expected certain number of CDPs'); } const aggregatedVal = F.pipe( cdpUtxos, A.reduce<UTxO, Assets>({}, (acc, utxo) => addAssets(acc, utxo.assets)), ); const aggregatedMintedAmt = F.pipe( cdpDatums, A.reduce<CDPContent, bigint>(0n, (acc, cdpDat) => acc + cdpDat.mintedAmt), ); type AggregatedFees = { aggregatedCollateralTreasury: bigint; aggregatedInterest: bigint; }; const { aggregatedInterest, aggregatedCollateralTreasury } = F.pipe( cdpDatums, A.reduce<CDPContent, AggregatedFees>( { aggregatedCollateralTreasury: 0n, aggregatedInterest: 0n }, (acc, cdpDat) => match(cdpDat.cdpFees) .returnType<AggregatedFees>() .with({ FrozenCDPAccumulatedFees: P.select() }, (fees) => ({ aggregatedCollateralTreasury: acc.aggregatedCollateralTreasury + fees.collateralTreasury, aggregatedInterest: acc.aggregatedInterest + fees.iassetInterest, })) .otherwise(() => acc), ), ); const [[mainMergeUtxo, mainCdpDatum], otherMergeUtxos] = match( A.zip(cdpUtxos, cdpDatums), ) .returnType<[[UTxO, CDPContent], UTxO[]]>() .with([P._, ...P.array()], ([main, ...other]) => [ main, other.map((a) => a[0]), ]) .otherwise(() => { throw new Error('Expects more CDPs for merging'); }); return lucid .newTx() .readFrom([cdpRefScriptUtxo]) .collectFrom([mainMergeUtxo], serialiseCdpRedeemer('MergeCdps')) .collectFrom( otherMergeUtxos, serialiseCdpRedeemer({ MergeAuxiliary: { mainMergeUtxo: { outputIndex: BigInt(mainMergeUtxo.outputIndex), txHash: fromHex(mainMergeUtxo.txHash), }, }, }), ) .pay.ToContract( mainMergeUtxo.address, { kind: 'inline', value: serialiseCdpDatum({ cdpOwner: null, iasset: mainCdpDatum.iasset, collateralAsset: mainCdpDatum.collateralAsset, mintedAmt: aggregatedMintedAmt, cdpFees: { FrozenCDPAccumulatedFees: { collateralTreasury: aggregatedCollateralTreasury, iassetInterest: aggregatedInterest, }, }, }), }, aggregatedVal, ); }