UNPKG

@agoric/zoe

Version:

Zoe: the Smart Contract Framework for Offer Enforcement

153 lines (138 loc) 5.32 kB
import { Fail } from '@endo/errors'; import { E } from '@endo/eventual-send'; import { AmountMath } from '@agoric/ertp'; import { prepareExoClass } from '@agoric/vat-data'; import { coerceAmountKeywordRecord } from '../cleanProposal.js'; import { assertFullIssuerRecord, makeIssuerRecord } from '../issuerRecord.js'; import { addToAllocation, subtractFromAllocation } from './allocationMath.js'; import { ZcfMintI } from './typeGuards.js'; /** * @import {ZCFMint, ZCFSeat} from '@agoric/zoe'; */ /** * @param {AmountKeywordRecord} amr * @param {ZoeIssuerRecord} issuerRecord * @returns {Amount} */ export const sumAmountKeywordRecord = (amr, issuerRecord) => { const empty = AmountMath.makeEmpty( issuerRecord.brand, issuerRecord.assetKind, ); return Object.values(amr).reduce( (total, amountToAdd) => AmountMath.add(total, amountToAdd, issuerRecord.brand), empty, ); }; /** * @param {import('@agoric/vat-data').Baggage} zcfBaggage * @param {{ (keyword: string, issuerRecord: ZoeIssuerRecord): void }} recordIssuer * @param {GetAssetKindByBrand} getAssetKindByBrand * @param {(exit?: undefined) => { zcfSeat: any; userSeat: Promise<UserSeat> }} makeEmptySeatKit * @param {ZcfMintReallocator} reallocator */ export const prepareZcMint = ( zcfBaggage, recordIssuer, getAssetKindByBrand, makeEmptySeatKit, reallocator, ) => { const makeZcMintInternal = prepareExoClass( zcfBaggage, 'zcfMint', ZcfMintI, /** * @template {AssetKind} [K=AssetKind] * @param {string} keyword * @param {ZoeMint<K>} zoeMint * @param {Required<ZoeIssuerRecord<K>>} issuerRecord */ (keyword, zoeMint, issuerRecord) => { const { brand: mintyBrand, issuer: mintyIssuer, displayInfo: mintyDisplayInfo, } = issuerRecord; const mintyIssuerRecord = makeIssuerRecord( mintyBrand, mintyIssuer, mintyDisplayInfo, ); recordIssuer(keyword, mintyIssuerRecord); return { keyword, zoeMint, mintyIssuerRecord }; }, { getIssuerRecord() { return this.state.mintyIssuerRecord; }, /** @type {(gains: Record<string, Amount>, zcfSeat?: ZCFSeat) => ZCFSeat} */ mintGains(gains, zcfSeat = makeEmptySeatKit().zcfSeat) { const { mintyIssuerRecord, zoeMint } = this.state; gains = coerceAmountKeywordRecord(gains, getAssetKindByBrand); const totalToMint = sumAmountKeywordRecord(gains, mintyIssuerRecord); !zcfSeat.hasExited() || Fail`zcfSeat must be active to mint gains for the zcfSeat`; const allocationPlusGains = addToAllocation( zcfSeat.getCurrentAllocation(), gains, ); // Offer safety should never be able to be violated here, as // we are adding assets. However, we keep this check so that // all reallocations are covered by offer safety checks, and // that any bug within Zoe that may affect this is caught. zcfSeat.isOfferSafe(allocationPlusGains) || Fail`The allocation after minting gains ${allocationPlusGains} for the zcfSeat was not offer safe`; // No effects above, Note COMMIT POINT within // reallocator.reallocate(). The following two steps *should* be // committed atomically, but it is not a disaster if they are // not. If we minted only, no one would ever get those // invisibly-minted assets. void E(zoeMint).mintAndEscrow(totalToMint); reallocator.reallocate(zcfSeat, allocationPlusGains); return zcfSeat; }, /** * @param {AmountKeywordRecord} losses * @param {ZCFSeat} zcfSeat */ burnLosses(losses, zcfSeat) { const { mintyIssuerRecord, zoeMint } = this.state; losses = coerceAmountKeywordRecord(losses, getAssetKindByBrand); const totalToBurn = sumAmountKeywordRecord(losses, mintyIssuerRecord); !zcfSeat.hasExited() || Fail`zcfSeat must be active to burn losses from the zcfSeat`; const allocationMinusLosses = subtractFromAllocation( zcfSeat.getCurrentAllocation(), losses, ); // verifies offer safety zcfSeat.isOfferSafe(allocationMinusLosses) || Fail`The allocation after burning losses ${allocationMinusLosses} for the zcfSeat was not offer safe`; // No effects above, Note COMMIT POINT within // reallocator.reallocate(). The following two steps *should* be // committed atomically, but it is not a disaster if they are // not. If we only commit the allocationMinusLosses no one would // ever get the unburned assets. reallocator.reallocate(zcfSeat, allocationMinusLosses); void E(zoeMint).withdrawAndBurn(totalToBurn); }, }, ); /** * @template {AssetKind} K * @param {string} keyword * @param {ERef<ZoeMint<K>>} zoeMintP * @returns {Promise<ZCFMint<K>>} */ return async (keyword, zoeMintP) => { const [zoeMint, issuerRecord] = await Promise.all([ zoeMintP, E(zoeMintP).getIssuerRecord(), ]); assertFullIssuerRecord(issuerRecord); // @ts-expect-error cast, XXX AssetKind generic return makeZcMintInternal(keyword, zoeMint, issuerRecord); }; };