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