Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions scripts/utils/adminApproval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { AnchorProvider } from "@coral-xyz/anchor";
import * as multisig from "@sqds/multisig";
import BN from "bn.js";
import {
PublicKey,
Transaction,
TransactionInstruction,
TransactionMessage,
} from "@solana/web3.js";
import { FutarchyClient } from "@metadaoproject/programs/futarchy/v0.6";
import { METADAO_MULTISIG_VAULT } from "@metadaoproject/programs";
import {
createSquadsVaultTxAndProposal,
getSquadsPdasFromDao,
} from "./squads.js";

// MetaDAO operational multisig - its vault 0 (METADAO_MULTISIG_VAULT) is the futarchy admin
export const METADAO_MULTISIG = new PublicKey(
"8N3Tvc6B1wEVKVC6iD4s6eyaCNqX2ovj2xze2q3Q9DWH",

Check warning on line 19 in scripts/utils/adminApproval.ts

View workflow job for this annotation

GitHub Actions / repository-guard

Hardcoded Solana address literal: + "8N3Tvc6B1wEVKVC6iD4s6eyaCNqX2ovj2xze2q3Q9DWH",
);

const SEED_ENQUEUED_APPROVAL = Buffer.from("enqueued_approval");

/**
* Routes a set of instructions the DAO's squads vault should execute through
* the admin approval system, as two lazily built transactions:
*
* 1. `buildDaoTransaction()` builds the transaction that creates the squads
* vault transaction + proposal holding `instructions` on the DAO's
* multisig. Sign with the payer and PERMISSIONLESS_ACCOUNT (the creator).
* 2. `buildMetadaoTransaction()` (returned by `buildDaoTransaction`) builds
* the transaction that creates the squads vault transaction + proposal on
* the MetaDAO operational multisig enqueueing the futarchy admin approval
* of the DAO proposal. Sign with the payer, which MUST be a member of the
* operational multisig with permission to propose transactions.
*
* Each builder reads its multisig's next transaction index at call time, so
* call it right before sending its transaction - an index read earlier can be
* consumed by someone else's proposal while previous transactions in the flow
* confirm, making vaultTransactionCreate fail.
*
* Once the operational multisig approves + executes its transaction, the DAO
* proposal can be approved + executed permissionlessly via
* executeMultisigProposalApproval.
*/
export const buildAdminApprovalTransactions = async ({
provider,
futarchy,
dao,
instructions,
payer,
}: {
provider: AnchorProvider;
futarchy: FutarchyClient;
dao: PublicKey;
instructions: TransactionInstruction[];
payer: PublicKey;
}) => {
const { multisigPda: daoMultisig, vaultPda: daoMultisigVault } =
await getSquadsPdasFromDao(dao);

const buildDaoTransaction = async () => {
const daoMultisigAccount =
await multisig.accounts.Multisig.fromAccountAddress(
provider.connection,
daoMultisig,
);

const daoTransactionIndex =
BigInt(daoMultisigAccount.transactionIndex.toString()) + 1n;

const daoMessage = new TransactionMessage({
payerKey: daoMultisigVault,
recentBlockhash: (await provider.connection.getLatestBlockhash())
.blockhash,
instructions,
});

const {
vaultTxCreateIx: daoVaultTxCreateIx,
proposalCreateIx: daoProposalCreateIx,
} = await createSquadsVaultTxAndProposal(
daoMultisig,
daoTransactionIndex,
daoMessage,
payer,
);

const [daoVaultTransactionPda] = multisig.getTransactionPda({
multisigPda: daoMultisig,
index: daoTransactionIndex,
});

const [daoProposalPda] = multisig.getProposalPda({
multisigPda: daoMultisig,
transactionIndex: daoTransactionIndex,
});

const daoTransaction = new Transaction().add(
daoVaultTxCreateIx,
daoProposalCreateIx,
);
daoTransaction.recentBlockhash = (
await provider.connection.getLatestBlockhash()
).blockhash;
daoTransaction.feePayer = payer;

const [enqueuedApprovalPda] = PublicKey.findProgramAddressSync(
[
SEED_ENQUEUED_APPROVAL,
dao.toBuffer(),
new BN(daoTransactionIndex.toString()).toArrayLike(Buffer, "le", 8),
],
futarchy.futarchy.programId,
);

const buildMetadaoTransaction = async () => {
const metadaoMultisigAccount =
await multisig.accounts.Multisig.fromAccountAddress(
provider.connection,
METADAO_MULTISIG,
);

const metadaoTransactionIndex =
BigInt(metadaoMultisigAccount.transactionIndex.toString()) + 1n;

// The vault signs as the futarchy admin and pays rent for the enqueued
// approval account, so it needs to hold a small amount of SOL
const enqueueApprovalIx = await futarchy.futarchy.methods
.adminEnqueueMultisigProposalApproval({
transactionIndex: new BN(daoTransactionIndex.toString()),
})
.accounts({
dao,
admin: METADAO_MULTISIG_VAULT,
squadsMultisig: daoMultisig,
squadsMultisigProposal: daoProposalPda,
enqueuedApproval: enqueuedApprovalPda,
})
.instruction();

const enqueueApprovalMessage = new TransactionMessage({
payerKey: METADAO_MULTISIG_VAULT,
recentBlockhash: (await provider.connection.getLatestBlockhash())
.blockhash,
instructions: [enqueueApprovalIx],
});

const {
vaultTxCreateIx: enqueueApprovalVaultTxCreateIx,
proposalCreateIx: enqueueApprovalProposalCreateIx,
} = await createSquadsVaultTxAndProposal(
METADAO_MULTISIG,
metadaoTransactionIndex,
enqueueApprovalMessage,
payer,
payer,
);

const [metadaoVaultTransactionPda] = multisig.getTransactionPda({
multisigPda: METADAO_MULTISIG,
index: metadaoTransactionIndex,
});

const [metadaoProposalPda] = multisig.getProposalPda({
multisigPda: METADAO_MULTISIG,
transactionIndex: metadaoTransactionIndex,
});

const metadaoTransaction = new Transaction().add(
enqueueApprovalVaultTxCreateIx,
enqueueApprovalProposalCreateIx,
);
metadaoTransaction.recentBlockhash = (
await provider.connection.getLatestBlockhash()
).blockhash;
metadaoTransaction.feePayer = payer;

return {
metadaoTransaction,
metadaoTransactionIndex,
metadaoVaultTransactionPda,
metadaoProposalPda,
};
};

return {
daoTransaction,
daoTransactionIndex,
daoVaultTransactionPda,
daoProposalPda,
enqueuedApprovalPda,
buildMetadaoTransaction,
};
};

return { buildDaoTransaction };
};
Loading
Loading