Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

After adding a new asset to Vaults, how is the collateral payment extracted from smart wallet when opening a new vault? #32

Open
Jorge-Lopes opened this issue Mar 4, 2024 · 1 comment

Comments

@Jorge-Lopes
Copy link
Owner

Jorge-Lopes commented Mar 4, 2024

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

@Jorge-Lopes
Copy link
Owner Author

Jorge-Lopes commented Mar 5, 2024

Unit test to track bank purse balance during the flow above

import { test as anyTest } from '@agoric/zoe/tools/prepare-test-env-ava.js';
import { Offers } from '@agoric/inter-protocol/src/clientSupport.js';
import {
  makeAgoricNamesRemotesFromFakeStorage,
  boardSlottingMarshaller,
} from '@agoric/vats/tools/board-utils.js';
import { slotToRemotable } from '@agoric/internal/src/storage-test-utils.js';
import {
  ensureVaultCollateral,
  LiquidationTestContext,
  makeLiquidationTestContext,
} from '../../tools/liquidation.ts';

const test = anyTest as TestFn<LiquidationTestContext>;

const collateralBrandKey = 'STARS';
const setup = {
  vaults: [
    {
      atom: 15,
      ist: 100,
      debt: 100.5,
    },
  ],
  price: {
    starting: 12.34,
    trigger: 9.99,
  },
};

const marshaller = boardSlottingMarshaller(slotToRemotable);

const printAssets = async (EV, bankManager) => {
  const assetSubscriber = await EV(bankManager).getAssetSubscription();
  const assetIterator = await EV(assetSubscriber)[Symbol.asyncIterator]();

  let isDone = false;
  while (!isDone) {
    const { value, done } = await EV(assetIterator).next();
    console.log('LOG: bank assets ', value.issuerName);
    if (done) {
      isDone = true;
    }
  }
};

test.before(async t => {
  t.context = await makeLiquidationTestContext(t);
});
test.after.always(t => {
  return t.context.shutdown && t.context.shutdown();
});

test.serial('display vbank balance', async t => {
  const {
    runUtils: { EV },
    storage,
    setupStartingState,
  } = t.context;

  // consume required modules from bootstrap
  const bankManager = await EV.vat('bootstrap').consumeItem('bankManager');
  const agoricNames = await EV.vat('bootstrap').consumeItem('agoricNames');
  const namesByAddressAdmin = await EV.vat('bootstrap').consumeItem(
    'namesByAddressAdmin',
  );
  const walletFactoryStartResult = await EV.vat('bootstrap').consumeItem(
    'walletFactoryStartResult',
  );
  const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage(storage);

  printAssets(EV, bankManager);

  await ensureVaultCollateral('STARS', t);

  // printAssets(EV, bankManager);
  // ToDo: the method above fails 
  // ERROR: db not initialized

  // update STARS priceFeed
  await setupStartingState({
    collateralBrandKey,
    managerIndex: 1,
    price: setup.price.starting,
  });

  // create a personal bank and get STARS virtual purse
  const starsBrand = await EV(agoricNames).lookup('brand', 'STARS');
  
  const bank = await EV(bankManager).getBankForAddress('agoric1dummy');
  const purse = await EV(bank).getPurse(starsBrand);

  // create smartWallet
  const [walletPresence, isNew] = await EV(
    walletFactoryStartResult.creatorFacet,
  ).provideSmartWallet('agoric1dummy', bank, namesByAddressAdmin);

  // print STARS balance from bank purse and smart wallet
  console.log('LOG: purse STARS balance ', await EV(purse).getCurrentAmount());

  const walletPublisher = await EV(walletPresence).getPublicTopics();
  const subscriber = walletPublisher.current.subscriber;
  console.log(
    'Log: smartWallet purses',
    (await EV(subscriber).getUpdateSince()).value.purses,
  );

  // build offer to open a new STARS vault
  const opts = {
    offerId: `open-${collateralBrandKey}-vault1}`,
    collateralBrandKey,
    wantMinted: setup.vaults[0].ist,
    giveCollateral: setup.vaults[0].atom,
  };
  const offer = Offers.vaults.OpenVault(agoricNamesRemotes, opts);

  const offerCapData = marshaller.toCapData(
    harden({
      method: 'executeOffer',
      offer,
    }),
  );

  await EV(walletPresence).handleBridgeAction(offerCapData, true);

  // verify Vstorage Data

  console.log(
    'LOG: Vstorage vaults of manager1',
    storage.toStorage({
      method: 'children',
      args: ['published.vaultFactory.managers.manager1.vaults'],
    }),
  );

  console.log(
    'LOG: Vstorage metrics of manager1',
    storage.toStorage({
      method: 'entries',
      args: ['published.vaultFactory.managers.manager1.metrics'],
    }),
  );

  t.pass();
});

@Jorge-Lopes Jorge-Lopes changed the title When adding a new asset to Vaults, from were is the payment extracted when opening a new vault? After adding a new asset to Vaults, how is the collateral payment extracted from smart wallet when opening a new vault? Mar 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant