Context
The purpose of this question is to understand the process of adding a new asset type to the vaults and then opening a new vault of that collateral. More specifically, in the bootstrap test environment
, how are the smart wallet provisioned with the required amount of collateral to open a new vault?
As a guide for this question, lets use the test-liquidation-1.ts as an example, more specifically the last 2 tests related to the collateral STARS
.
Let's follow the execution flow of adding a new collateral to the vaults, and then open a new vault.
Add new collateral.
When the ensureVaultCollateral('STARS', t)
function is executed in the test environment, the addSTARsCollateral
function will be invoked. This function aims to build and deploy a proposal to add a new collateral to the vaults.
const addSTARsCollateral = async (t) => {
const { controller, buildProposal } = t.context;
t.log('building proposal');
const proposal = await buildProposal(
'@agoric/builders/scripts/inter-protocol/add-STARS.js',
);
ref: liquidation.ts
The starsVaultProposalBuilder
import the proposal builder and pass the interchainAssetOptions
. Note that the denom
is declared and not the issuerBoardId
. This will be relevant later.
/** @type {import('@agoric/deploy-script-support/src/externalTypes.js').ProposalBuilder} */
export const starsVaultProposalBuilder = async powers => {
return vaultProposalBuilder(powers, {
interchainAssetOptions: {
// Values for the Stargaze token on Osmosis
denom:
'ibc/987C17B11ABC2B20019178ACE62929FE9840202CE79498E29FE8E5CB02B7C0A4',
decimalPlaces: 6,
keyword: 'STARS',
oracleBrand: 'STARS',
proposedName: 'STARS',
},
});
};
ref: add-STARS.js
The defaultProposalBuilder
imports the getManifestForAddAssetToVault
and pass the required arguments.
ref: add-collateral-core.js
The getManifestForAddAssetToVault
will assert which manifest should be executed based on the interchainAssetOptions
, as mentioned above, and in this case, the publishInterchainAssetFromBank
is the one that we expect to run.
NOTE: at the produce
section we see that bankMints
and vBankKits
and being produced. I cannot find other references to these capabilities.
export const getManifestForAddAssetToVault = (
{ restoreRef },
{
debtLimitValue,
interestRateValue,
interchainAssetOptions,
scaledPriceAuthorityRef,
},
) => {
const publishIssuerFromBoardId =
typeof interchainAssetOptions.issuerBoardId === 'string';
const publishIssuerFromBank =
!publishIssuerFromBoardId &&
typeof interchainAssetOptions.denom === 'string';
return {
manifest: {
...
...(publishIssuerFromBank && {
[publishInterchainAssetFromBank.name]: {
consume: {
bankManager: true,
agoricNamesAdmin: true,
reserveKit: true,
startUpgradable: true,
},
produce: { bankMints: true, vBankKits: true },
installation: {
consume: { mintHolder: true },
},
},
ref: addAssetToVault.js
The publishInterchainAssetFromBank
will create a new issuerKit
with the mintHolder
contract and pass that kit as argument to the addAsset
method of bankManager
export const publishInterchainAssetFromBank = async (
{
consume: { bankManager, agoricNamesAdmin, reserveKit, startUpgradable },
installation: {
consume: { mintHolder },
},
},
{ options: { interchainAssetOptions } },
) => {
const {
denom,
decimalPlaces,
keyword,
issuerName = keyword,
proposedName = keyword,
} = interchainAssetOptions;
const terms = {
keyword: issuerName, // "keyword" is a misnomer in mintHolder terms
assetKind: AssetKind.NAT,
displayInfo: {
decimalPlaces,
assetKind: AssetKind.NAT,
},
};
const { creatorFacet: mint, publicFacet: issuer } = await E(startUpgradable)({
installation: mintHolder,
label: issuerName,
privateArgs: undefined,
terms,
});
const brand = await E(issuer).getBrand();
const kit = { mint, issuer, brand };
await E(E.get(reserveKit).creatorFacet).addIssuer(issuer, keyword);
await Promise.all([
E(E(agoricNamesAdmin).lookupAdmin('issuer')).update(issuerName, issuer),
E(E(agoricNamesAdmin).lookupAdmin('brand')).update(issuerName, brand),
E(bankManager).addAsset(denom, issuerName, proposedName, kit),
]);
};
ref: addAssetToVault.js
The addAsset
will create an escrow purse from the kit.issuer
provided above and deposits a payment into it.
Note: I cannot identify where was that payment created, and if I try to print its amount it will trigger an error. Although, when I print the purse balance after the deposit, it will show 0 STARS.
The addAsset
will then declare privateAssetRecord
and toPublish
objects. Where they will be used to initialise the brandToAssetRecord
and brandToAssetDescriptor
respectively.
Note: the brandToAssetRecord
is used at the getPurse
method of bank
.
Finally, if there's a nameAdmin, it updates with the settled issuer identity.
async addAsset(denom, issuerName, proposedName, kit) {
const {
assetPublisher,
brandToAssetDescriptor,
brandToAssetRecord,
denomToAddressUpdater,
nameAdmin,
} = this.state;
const brand = await kit.brand;
const assetKind = await E(kit.issuer).getAssetKind();
// Create an escrow purse for this asset, seeded with the payment.
const escrowPurse = E(kit.issuer).makeEmptyPurse();
const payment = await kit.payment;
await (payment && E(escrowPurse).deposit(payment));
const [privateAssetRecord, toPublish] = await deeplyFulfilledObject(
harden([
{
escrowPurse,
issuer: kit.issuer,
mint: kit.mint,
denom,
brand,
},
{
brand,
denom,
issuerName,
issuer: kit.issuer,
proposedName,
},
]),
);
brandToAssetRecord.init(brand, privateAssetRecord);
denomToAddressUpdater.init(
denom,
detachedZone.mapStore('addressToUpdater'),
);
brandToAssetDescriptor.init(brand, toPublish);
assetPublisher.publish(toPublish);
if (!nameAdmin) {
return;
}
// publish settled issuer identity
void Promise.all([kit.issuer, E(kit.brand).getDisplayInfo()]).then(
([issuer, displayInfo]) =>
E(nameAdmin).update(
denom,
/** @type {AssetInfo} */ (
harden({
brand,
issuer,
issuerName,
denom,
proposedName,
displayInfo,
})
),
),
);
},
ref: vat-bank.js
Open new vault
At the test environment, the makeLiquidationTestKit
export a method called setupVaults
. This method will use the consumed walletFactoryDriver
to create a SmartWallet
with the address agoric1minter.
Then It will use the executeOfferMaker
method of the smart wallet to give the instructions to open a new vault.
Note: for now, lets focus on how did this wallet got the funds to open the vault.
const setupVaults = async (
collateralBrandKey: string,
managerIndex: number,
setup: LiquidationSetup,
base: number = 0,
) => {
await setupStartingState({
collateralBrandKey,
managerIndex,
price: setup.price.starting,
});
const minter =
await walletFactoryDriver.provideSmartWallet('agoric1minter');
for (let i = 0; i < setup.vaults.length; i += 1) {
const offerId = `open-${collateralBrandKey}-vault${base + i}`;
await minter.executeOfferMaker(Offers.vaults.OpenVault, {
offerId,
collateralBrandKey,
wantMinted: setup.vaults[i].ist,
giveCollateral: setup.vaults[i].atom,
});
t.like(minter.getLatestUpdateRecord(), {
updated: 'offerStatus',
status: { id: offerId, numWantsSatisfied: 1 },
});
}
ref: liquidation.ts
The provideSmartWallet
at drivers.ts
a bank
will be created, passing as argument to getBankForAddress
the wallet address.
The getBankForAddress will create a new bank, initialise it in the addressToBank
and return it.
async provideSmartWallet(
walletAddress: string,
): Promise<ReturnType<typeof makeWalletDriver>> {
const bank = await EV(bankManager).getBankForAddress(walletAddress);
return EV(walletFactoryStartResult.creatorFacet)
.provideSmartWallet(walletAddress, bank, namesByAddressAdmin)
.then(([walletPresence, isNew]) =>
makeWalletDriver(walletAddress, walletPresence, isNew),
);
},
ref: drivers.ts
The method above will call provideSmartWallet
from the walletFactory
, which will use the bank object to make a new smart wallet and return it.
provideSmartWallet(address, bank, namesByAddressAdmin) {
let isNew = false;
/** @type {(address: string) => Promise<import('./smartWallet.js').SmartWallet>} */
const maker = async _address => {
const invitationPurse = await E(invitationIssuer).makeEmptyPurse();
const walletStorageNode = E(storageNode).makeChildNode(address);
const wallet = await makeSmartWallet(
harden({ address, walletStorageNode, bank, invitationPurse }),
);
// An await here would deadlock with invitePSMCommitteeMembers
void publishDepositFacet(address, wallet, namesByAddressAdmin);
isNew = true;
return wallet;
};
const finisher = walletReviver
? async (_address, _wallet) => {
const isRevive = await E(walletReviver).ackWallet(address);
isNew = !isRevive;
}
: undefined;
return provider
.provideAsync(address, maker, finisher)
.then(w => [w, isNew]);
},
ref: walletFactory.js
At makeSmartWallet
, the makeWalletWithResolvedStorageNodes
is an ExoClassKit
that holds all the facets belonging to the smart wallet and has stored in its state, the bank
passed before.
Note: this function will only return the self
facet.
const makeSmartWallet = async uniqueWithoutChildNodes => {
const [walletStorageNode, currentStorageNode] = await Promise.all([
uniqueWithoutChildNodes.walletStorageNode,
E(uniqueWithoutChildNodes.walletStorageNode).makeChildNode('current'),
]);
return makeWalletWithResolvedStorageNodes(
harden({
...uniqueWithoutChildNodes,
currentStorageNode,
walletStorageNode,
}),
).self;
};
ref: smartWallet.js
At provideSmartWallet
, after making the new smart wallet, the publishDepositFacet
is called. This method will use the namesByAddressAdmin
passed by the drivers
to associate the wallet address with its deposit
facet.
Note: did not identified yet the purpose of this step
export const publishDepositFacet = async (
address,
wallet,
namesByAddressAdmin,
) => {
const { nameAdmin: myAddressNameAdmin } = await E(
namesByAddressAdmin,
).provideChild(address, [WalletName.depositFacet]);
return E(myAddressNameAdmin).default(
WalletName.depositFacet,
wallet.getDepositFacet(),
);
};
ref: walletFactory.js
At the provideSmartWallet
of drivers.js, after receiving the new smart wallet, the makeWalletDriver method will wrap the wallet around new methods and return it.
Note: the only method that seems to be executed in this flow is executeOfferMaker
getPurse