diff --git a/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script-permit.json b/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script-permit.json
new file mode 100644
index 00000000000..27ba77ddaf6
--- /dev/null
+++ b/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script-permit.json
@@ -0,0 +1 @@
+true
diff --git a/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script.tjs b/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script.tjs
new file mode 100644
index 00000000000..eb1a4815470
--- /dev/null
+++ b/a3p-integration/proposals/p:upgrade-19/mint-payment/send-script.tjs
@@ -0,0 +1,56 @@
+/* global E */
+
+///
+///
+
+/**
+ * The primary purpose of this script is to mint a payment of a certain
+ * bankAsset and deposit in an user wallet.
+ *
+ * The receiverAddress and label placeholders should be replaced with
+ * the desired address and asset name during the execution of each test case.
+ *
+ * See z:acceptance/mintHolder.test.js
+ *
+ * @param {BootstrapPowers} powers
+ */
+const sendBankAsset = async powers => {
+ const {
+ consume: { namesByAddress, contractKits: contractKitsP },
+ } = powers;
+
+ const receiverAddress = '{{ADDRESS}}';
+ const label = '{{LABEL}}';
+ const valueStr = '{{VALUE}}';
+ const value = BigInt(valueStr)
+
+ console.log(`Start sendBankAsset for ${label}`);
+
+ const contractKits = await contractKitsP;
+ const mintHolderKit = Array.from(contractKits.values()).filter(
+ kit => kit.label && kit.label === label,
+ );
+
+ const { creatorFacet: mint, publicFacet: issuer } = mintHolderKit[0];
+
+ /*
+ * Ensure that publicFacet holds an issuer by verifying that has
+ * the makeEmptyPurse method.
+ */
+ await E(issuer).makeEmptyPurse()
+
+ const brand = await E(issuer).getBrand();
+ const amount = harden({ value, brand });
+ const payment = await E(mint).mintPayment(amount);
+
+ const receiverDepositFacet = E(namesByAddress).lookup(
+ receiverAddress,
+ 'depositFacet',
+ );
+
+ await E(receiverDepositFacet).receive(payment);
+
+ console.log(`Finished sendBankAsset for ${label}`);
+};
+
+sendBankAsset;
diff --git a/a3p-integration/proposals/p:upgrade-19/mintHolder.test.js b/a3p-integration/proposals/p:upgrade-19/mintHolder.test.js
new file mode 100644
index 00000000000..b47a499411e
--- /dev/null
+++ b/a3p-integration/proposals/p:upgrade-19/mintHolder.test.js
@@ -0,0 +1,32 @@
+/* eslint-env node */
+
+import '@endo/init';
+import test from 'ava';
+import { addUser, provisionSmartWallet } from '@agoric/synthetic-chain';
+import {
+ mintPayment,
+ getAssetList,
+ swap,
+ getPSMChildren,
+ upgradeMintHolder,
+} from './test-lib/mintHolder-helpers.js';
+
+const networkConfig = {
+ rpcAddrs: ['http://0.0.0.0:26657'],
+ chainName: 'agoriclocal',
+};
+
+test('mintHolder contract is upgraded', async t => {
+ const receiver = await addUser('receiver');
+ await provisionSmartWallet(receiver, `20000000ubld`);
+
+ let assetList = await getAssetList();
+ t.log('List of mintHolder vats being upgraded: ', assetList);
+ await upgradeMintHolder(`upgrade-mintHolder`, assetList);
+ await mintPayment(t, receiver, assetList, 10);
+
+ const psmLabelList = await getPSMChildren(fetch, networkConfig);
+ assetList = await getAssetList(psmLabelList);
+ t.log('List of assets being swapped with IST via PSM: ', assetList);
+ await swap(t, receiver, assetList, 5);
+});
diff --git a/a3p-integration/proposals/p:upgrade-19/test-lib/mintHolder-helpers.js b/a3p-integration/proposals/p:upgrade-19/test-lib/mintHolder-helpers.js
new file mode 100644
index 00000000000..131de77d30e
--- /dev/null
+++ b/a3p-integration/proposals/p:upgrade-19/test-lib/mintHolder-helpers.js
@@ -0,0 +1,156 @@
+/* eslint-env node */
+
+import {
+ agoric,
+ evalBundles,
+ getDetailsMatchingVats,
+ getISTBalance,
+} from '@agoric/synthetic-chain';
+import { makeVstorageKit, retryUntilCondition } from '@agoric/client-utils';
+import { readFile, writeFile } from 'node:fs/promises';
+import { sendOfferAgd, psmSwap, snapshotAgoricNames } from './psm-helpers.js';
+
+/**
+ * @param {string} fileName base file name without .tjs extension
+ * @param {Record} replacements
+ */
+export const replaceTemplateValuesInFile = async (fileName, replacements) => {
+ let script = await readFile(`${fileName}.tjs`, 'utf-8');
+ for (const [template, value] of Object.entries(replacements)) {
+ script = script.replaceAll(`{{${template}}}`, value);
+ }
+ await writeFile(`${fileName}.js`, script);
+};
+
+export const getPSMChildren = async (fetch, networkConfig) => {
+ const {
+ vstorage: { keys },
+ } = await makeVstorageKit({ fetch }, networkConfig);
+
+ const children = await keys('published.psm.IST');
+
+ return children;
+};
+
+export const getAssetList = async labelList => {
+ const assetList = [];
+ const { vbankAssets } = await snapshotAgoricNames();
+
+ // Determine the assets to consider based on labelList
+ const assetsToConsider =
+ labelList || Object.values(vbankAssets).map(asset => asset.issuerName);
+
+ for (const label of assetsToConsider) {
+ if (label === 'IST') {
+ break;
+ }
+
+ const vbankAsset = Object.values(vbankAssets).find(
+ asset => asset.issuerName === label,
+ );
+ assert(vbankAsset, `vbankAsset not found for ${label}`);
+
+ const { denom } = vbankAsset;
+ const mintHolderVat = `zcf-mintHolder-${label}`;
+
+ assetList.push({ label, denom, mintHolderVat });
+ }
+
+ return assetList;
+};
+
+export const mintPayment = async (t, address, assetList, value) => {
+ const SUBMISSION_DIR = 'mint-payment';
+
+ for (const asset of assetList) {
+ const { label, denom } = asset;
+ const scaled = BigInt(parseInt(value, 10) * 1_000_000).toString();
+
+ await replaceTemplateValuesInFile(`${SUBMISSION_DIR}/send-script`, {
+ ADDRESS: address,
+ LABEL: label,
+ VALUE: scaled,
+ });
+
+ await evalBundles(SUBMISSION_DIR);
+
+ const balance = await getISTBalance(address, denom);
+
+ // Add to value the BLD provisioned to smart wallet
+ if (label === 'BLD') {
+ value += 10;
+ }
+
+ t.is(
+ balance,
+ value,
+ `receiver ${denom} balance ${balance} is not ${value}`,
+ );
+ }
+};
+
+export const swap = async (t, address, assetList, want) => {
+ for (const asset of assetList) {
+ const { label, denom } = asset;
+
+ // TODO: remove condition after fixing issue #10655
+ if (/^DAI/.test(label)) {
+ break;
+ }
+
+ const pair = `IST.${label}`;
+
+ const istBalanceBefore = await getISTBalance(address, 'uist');
+ const anchorBalanceBefore = await getISTBalance(address, denom);
+
+ await psmSwap(address, ['swap', '--pair', pair, '--wantMinted', want], {
+ now: Date.now,
+ follow: agoric.follow,
+ setTimeout,
+ sendOffer: sendOfferAgd,
+ });
+
+ const istBalanceAfter = await getISTBalance(address, 'uist');
+ const anchorBalanceAfter = await getISTBalance(address, denom);
+
+ t.is(istBalanceAfter, istBalanceBefore + want);
+ t.is(anchorBalanceAfter, anchorBalanceBefore - want);
+ }
+};
+
+const getIncarnationForAllVats = async assetList => {
+ const vatsIncarnation = {};
+
+ for (const asset of assetList) {
+ const { label, mintHolderVat } = asset;
+ const matchingVats = await getDetailsMatchingVats(label);
+ const expectedVat = matchingVats.find(vat => vat.vatName === mintHolderVat);
+ vatsIncarnation[label] = expectedVat.incarnation;
+ }
+ assert(Object.keys(vatsIncarnation).length === assetList.length);
+
+ return vatsIncarnation;
+};
+
+const checkVatsUpgraded = (before, current) => {
+ for (const vatLabel in before) {
+ if (current[vatLabel] !== before[vatLabel] + 1) {
+ console.log(`${vatLabel} upgrade failed. `);
+ return false;
+ }
+ }
+ return true;
+};
+
+export const upgradeMintHolder = async (submissionPath, assetList) => {
+ const before = await getIncarnationForAllVats(assetList);
+
+ await evalBundles(submissionPath);
+
+ return retryUntilCondition(
+ async () => getIncarnationForAllVats(assetList),
+ current => checkVatsUpgraded(before, current),
+ `mintHolder upgrade not processed yet`,
+ { setTimeout, retryIntervalMs: 5000, maxRetries: 15 },
+ );
+};
diff --git a/a3p-integration/proposals/p:upgrade-19/test-lib/psm-helpers.js b/a3p-integration/proposals/p:upgrade-19/test-lib/psm-helpers.js
new file mode 100644
index 00000000000..954e954941c
--- /dev/null
+++ b/a3p-integration/proposals/p:upgrade-19/test-lib/psm-helpers.js
@@ -0,0 +1,192 @@
+/* eslint-env node */
+
+import { execa } from 'execa';
+import {
+ boardSlottingMarshaller,
+ makeFromBoard,
+ waitUntilOfferResult,
+ fetchEnvNetworkConfig,
+} from '@agoric/client-utils';
+import {
+ agopsLocation,
+ agoric,
+ executeCommand,
+ mkTemp,
+} from '@agoric/synthetic-chain';
+import fsp from 'node:fs/promises';
+
+/** @import {Result as ExecaResult, ExecaError} from 'execa'; */
+/**
+ * @typedef {ExecaResult & { all: string } & (
+ * | { failed: false }
+ * | Pick<
+ * ExecaError & { failed: true },
+ * | 'failed'
+ * | 'shortMessage'
+ * | 'cause'
+ * | 'exitCode'
+ * | 'signal'
+ * | 'signalDescription'
+ * >
+ * )} SendOfferResult
+ */
+
+/**
+ * @typedef {object} PsmMetrics
+ * @property {import('@agoric/ertp').Amount<'nat'>} anchorPoolBalance
+ * @property {import('@agoric/ertp').Amount<'nat'>} feePoolBalance
+ * @property {import('@agoric/ertp').Amount<'nat'>} mintedPoolBalance
+ * @property {import('@agoric/ertp').Amount<'nat'>} totalAnchorProvided
+ * @property {import('@agoric/ertp').Amount<'nat'>} totalMintedProvided
+ */
+
+const fromBoard = makeFromBoard();
+const marshaller = boardSlottingMarshaller(fromBoard.convertSlotToVal);
+
+/**
+ * @param {string} path
+ */
+const objectFromVstorageEntries = async path => {
+ const rawEntries = await agoric.follow('-lF', `:${path}`, '-o', 'text');
+ return Object.fromEntries(marshaller.fromCapData(JSON.parse(rawEntries)));
+};
+
+export const snapshotAgoricNames = async () => {
+ const [brands, instances, vbankAssets] = await Promise.all([
+ objectFromVstorageEntries('published.agoricNames.brand'),
+ objectFromVstorageEntries('published.agoricNames.instance'),
+ objectFromVstorageEntries('published.agoricNames.vbankAsset'),
+ ]);
+ return { brands, instances, vbankAssets };
+};
+
+/**
+ * Similar to
+ * https://github.com/Agoric/agoric-3-proposals/blob/422b163fecfcf025d53431caebf6d476778b5db3/packages/synthetic-chain/src/lib/commonUpgradeHelpers.ts#L123-L139
+ * However, for an address that is not provisioned, `agoric wallet send` is
+ * needed because `agops perf satisfaction` hangs when trying to follow
+ * nonexistent vstorage path ":published.wallet.${address}".
+ *
+ * @param {string} address
+ * @param {Promise} offerPromise
+ * @returns {Promise}
+ */
+export const sendOfferAgoric = async (address, offerPromise) => {
+ const offerPath = await mkTemp('agops.XXX');
+ const offer = await offerPromise;
+ await fsp.writeFile(offerPath, offer);
+
+ const [settlement] = await Promise.allSettled([
+ execa({
+ all: true,
+ })`agoric wallet --keyring-backend=test send --offer ${offerPath} --from ${address} --verbose`,
+ ]);
+ return settlement.status === 'fulfilled'
+ ? settlement.value
+ : settlement.reason;
+};
+
+/**
+ * A variant of {@link sendOfferAgoric} that uses `agd` directly to e.g.
+ * control gas calculation.
+ *
+ * @param {string} address
+ * @param {Promise} offerPromise
+ * @returns {Promise}
+ */
+export const sendOfferAgd = async (address, offerPromise) => {
+ const offer = await offerPromise;
+ const networkConfig = await fetchEnvNetworkConfig({
+ env: process.env,
+ fetch,
+ });
+ const { chainName, rpcAddrs } = networkConfig;
+ const args = /** @type {string[]} */ (
+ // @ts-expect-error heterogeneous concat
+ [].concat(
+ [`--node=${rpcAddrs[0]}`, `--chain-id=${chainName}`],
+ [`--keyring-backend=test`, `--from=${address}`],
+ ['tx', 'swingset', 'wallet-action', '--allow-spend', offer],
+ '--yes',
+ '-bblock',
+ '-ojson',
+ )
+ );
+
+ const [settlement] = await Promise.allSettled([
+ execa('agd', args, { all: true }),
+ ]);
+
+ // Upon successful exit, verify that the *output* also indicates success.
+ // cf. https://github.com/Agoric/agoric-sdk/blob/master/packages/agoric-cli/src/lib/wallet.js
+ if (settlement.status === 'fulfilled') {
+ const result = settlement.value;
+ try {
+ const tx = JSON.parse(result.stdout);
+ if (tx.code !== 0) {
+ return { ...result, failed: true, shortMessage: `code ${tx.code}` };
+ }
+ } catch (err) {
+ return {
+ ...result,
+ failed: true,
+ shortMessage: 'unexpected output',
+ cause: err,
+ };
+ }
+ }
+
+ return settlement.status === 'fulfilled'
+ ? settlement.value
+ : settlement.reason;
+};
+
+/**
+ * @param {string} address
+ * @param {Array} params
+ * @param {{
+ * follow: (...params: string[]) => Promise