From 8ceb4b26f0d18a4991b0aa0307a72288d198f460 Mon Sep 17 00:00:00 2001 From: Dmytro Horbatenko Date: Tue, 2 Dec 2025 15:55:44 +0200 Subject: [PATCH 01/55] feat: maxSupplyCap minter vault --- Anchor.toml | 8 +- programs/access-control/src/lib.rs | 2 +- programs/data-feed/src/lib.rs | 2 +- programs/midas-vault/src/errors.rs | 2 + programs/midas-vault/src/events.rs | 2 + .../minter_vault/approve_mint_request.rs | 10 +- .../instructions/minter_vault/mint_instant.rs | 9 +- .../minter_vault/new_minter_vault.rs | 11 ++- .../minter_vault/update_minter_vault.rs | 9 +- programs/midas-vault/src/lib.rs | 7 +- .../src/state/minter_vault_state.rs | 2 + programs/midas-vault/src/utils.rs | 21 ++++ programs/token-authority/src/lib.rs | 2 +- test/constants/vaults.constants.ts | 1 + test/fixture/vaults.fixture.ts | 4 +- test/minter-vault.test.ts | 98 +++++++++++++++++++ test/testers/minter-vault.testers.ts | 30 +++++- 17 files changed, 199 insertions(+), 21 deletions(-) diff --git a/Anchor.toml b/Anchor.toml index 5a3ae4e..3be1e01 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -5,10 +5,10 @@ resolution = true skip-lint = false [programs.localnet] -access_control = "GQp4fJwxmLF7vL7uJ4jpS3uRz96qrb7MfoLKMnJoeE3Z" -data_feed = "7dTNTpTqbHCLxc1FtpCRAq5d4u1Y6WVqrAc1znVGQDxV" -midas_vaults = "6eFgYZCZZFTe61T4YxWsiHHAunCLTh9V7TAjj8DxuZwm" -token_authority = "6XqSwGFEuadyqXC9vBLYGJhvQsEVjPdCrtvN6inAb4z3" +access_control = "7fmev1BQVc9LEXsMDoJs63muHR1B2uSAw1963zfVrpJP" +data_feed = "6vrYAbfttohJKguPRzspHUW6fn61KDyjzog3q8YRG6yq" +midas_vaults = "DHfwFSG3JQ2qdX1Ub2QPuDsk9FMQUwyXyZqAs4gGeLnQ" +token_authority = "GYmnAy5UiKMYuuwJubKKC5WLnWSW3qLo4JFqX4rBqHKE" [registry] url = "https://api.apr.dev" diff --git a/programs/access-control/src/lib.rs b/programs/access-control/src/lib.rs index e5ec90c..9486108 100644 --- a/programs/access-control/src/lib.rs +++ b/programs/access-control/src/lib.rs @@ -7,7 +7,7 @@ pub mod instructions; pub mod state; use instructions::*; -declare_id!("GQp4fJwxmLF7vL7uJ4jpS3uRz96qrb7MfoLKMnJoeE3Z"); +declare_id!("7fmev1BQVc9LEXsMDoJs63muHR1B2uSAw1963zfVrpJP"); #[program] pub mod access_control { diff --git a/programs/data-feed/src/lib.rs b/programs/data-feed/src/lib.rs index ac98b67..37dfad3 100644 --- a/programs/data-feed/src/lib.rs +++ b/programs/data-feed/src/lib.rs @@ -9,7 +9,7 @@ pub mod utils; use instructions::*; -declare_id!("7dTNTpTqbHCLxc1FtpCRAq5d4u1Y6WVqrAc1znVGQDxV"); +declare_id!("6vrYAbfttohJKguPRzspHUW6fn61KDyjzog3q8YRG6yq"); #[program] pub mod data_feed { diff --git a/programs/midas-vault/src/errors.rs b/programs/midas-vault/src/errors.rs index dd9e463..84cab01 100644 --- a/programs/midas-vault/src/errors.rs +++ b/programs/midas-vault/src/errors.rs @@ -42,4 +42,6 @@ pub enum MidasVaultsError { InvalidVaultProvided, #[msg("The new value is the same as the old one")] ValueDidntChange, + #[msg("Max supply cap exceeded")] + MaxSupplyCapExceeded, } diff --git a/programs/midas-vault/src/events.rs b/programs/midas-vault/src/events.rs index 023992e..8cbd940 100644 --- a/programs/midas-vault/src/events.rs +++ b/programs/midas-vault/src/events.rs @@ -113,6 +113,8 @@ pub struct MinterVaultUpdatedEvent { pub first_deposit_min_m_tokens: Option, /// mint authority pda (token-authority program) pub mint_authority_pda: Option, + /// max supply cap for mToken minting + pub max_supply_cap: Option, } #[event] diff --git a/programs/midas-vault/src/instructions/minter_vault/approve_mint_request.rs b/programs/midas-vault/src/instructions/minter_vault/approve_mint_request.rs index 7d2c755..f061dd7 100644 --- a/programs/midas-vault/src/instructions/minter_vault/approve_mint_request.rs +++ b/programs/midas-vault/src/instructions/minter_vault/approve_mint_request.rs @@ -10,7 +10,9 @@ use crate::{ constants::{ac_roles, ONE}, events::MinterVaultRequestApprovedEvent, state::{MintVaultRequestState, MinterVaultState, VaultCommonState}, - utils::{close_account, mint_token, require_variation_tolerance, Closable}, + utils::{ + close_account, mint_token, require_variation_tolerance, validate_max_supply_cap, Closable, + }, }; #[derive(Accounts)] @@ -143,6 +145,12 @@ pub fn handle( .checked_div(new_out_rate.into()) .unwrap(); + validate_max_supply_cap( + &ctx.accounts.m_mint, + &ctx.accounts.minter_vault, + amount_to_mint.try_into().unwrap(), + )?; + mint_token( &ctx.accounts.vault_common.key(), &ctx.accounts.minter_vault.to_account_info(), diff --git a/programs/midas-vault/src/instructions/minter_vault/mint_instant.rs b/programs/midas-vault/src/instructions/minter_vault/mint_instant.rs index 464d15d..0f57401 100644 --- a/programs/midas-vault/src/instructions/minter_vault/mint_instant.rs +++ b/programs/midas-vault/src/instructions/minter_vault/mint_instant.rs @@ -20,7 +20,8 @@ use crate::{ utils::{ mint_token, minter::{self}, - require_and_update_limit, transfer_token, validate_common, Validate, VaultActionId, + require_and_update_limit, transfer_token, validate_common, validate_max_supply_cap, + Validate, VaultActionId, }, }; @@ -259,6 +260,12 @@ pub fn handle( )?; } + validate_max_supply_cap( + &ctx.accounts.m_mint, + &ctx.accounts.minter_vault, + params.m_token_amount.try_into().unwrap(), + )?; + mint_token( &ctx.accounts.vault_common.key(), &ctx.accounts.minter_vault.to_account_info(), diff --git a/programs/midas-vault/src/instructions/minter_vault/new_minter_vault.rs b/programs/midas-vault/src/instructions/minter_vault/new_minter_vault.rs index b84a107..c3ca641 100644 --- a/programs/midas-vault/src/instructions/minter_vault/new_minter_vault.rs +++ b/programs/midas-vault/src/instructions/minter_vault/new_minter_vault.rs @@ -57,15 +57,22 @@ pub struct NewMinterVault<'info> { /// # Arguments /// /// - `first_deposit_min_m_tokens` - minimum amount of mTokens required for the first deposit -pub fn handle(ctx: Context, first_deposit_min_m_tokens: u64) -> Result<()> { +/// - `max_supply_cap` - maximum supply cap for mToken minting (use u64::MAX for no cap) +pub fn handle( + ctx: Context, + first_deposit_min_m_tokens: u64, + max_supply_cap: u64, +) -> Result<()> { ctx.accounts.minter_vault.common_vault = ctx.accounts.vault_common.key(); ctx.accounts.minter_vault.first_deposit_min_m_tokens = first_deposit_min_m_tokens; ctx.accounts.minter_vault.mint_authority_pda = ctx.accounts.token_authority.key(); + ctx.accounts.minter_vault.max_supply_cap = max_supply_cap; emit!(MinterVaultUpdatedEvent { common_vault: ctx.accounts.vault_common.key(), first_deposit_min_m_tokens: Some(first_deposit_min_m_tokens), - mint_authority_pda: Some(ctx.accounts.token_authority.key()) + mint_authority_pda: Some(ctx.accounts.token_authority.key()), + max_supply_cap: Some(max_supply_cap), }); Ok(()) diff --git a/programs/midas-vault/src/instructions/minter_vault/update_minter_vault.rs b/programs/midas-vault/src/instructions/minter_vault/update_minter_vault.rs index 72ba897..7cc10f6 100644 --- a/programs/midas-vault/src/instructions/minter_vault/update_minter_vault.rs +++ b/programs/midas-vault/src/instructions/minter_vault/update_minter_vault.rs @@ -44,10 +44,12 @@ pub struct UpdateMinterVault<'info> { /// /// - `first_deposit_min_m_tokens` - new value for `first_deposit_min_m_tokens` /// - `mint_authority_pda` - new value for `mint_authority_pda` +/// - `max_supply_cap` - new value for `max_supply_cap` (use u64::MAX for no cap) pub fn handle( ctx: Context, first_deposit_min_m_tokens: Option, mint_authority_pda: Option, + max_supply_cap: Option, ) -> Result<()> { if let Some(first_deposit_min_m_tokens) = first_deposit_min_m_tokens { ctx.accounts.minter_vault.first_deposit_min_m_tokens = first_deposit_min_m_tokens; @@ -57,10 +59,15 @@ pub fn handle( ctx.accounts.minter_vault.mint_authority_pda = mint_authority_pda; } + if let Some(max_supply_cap) = max_supply_cap { + ctx.accounts.minter_vault.max_supply_cap = max_supply_cap; + } + emit!(MinterVaultUpdatedEvent { common_vault: ctx.accounts.vault_common.key(), first_deposit_min_m_tokens, - mint_authority_pda + mint_authority_pda, + max_supply_cap, }); Ok(()) diff --git a/programs/midas-vault/src/lib.rs b/programs/midas-vault/src/lib.rs index 86b1354..b5fa9e9 100644 --- a/programs/midas-vault/src/lib.rs +++ b/programs/midas-vault/src/lib.rs @@ -9,7 +9,7 @@ pub mod utils; use crate::utils::Validate; use instructions::*; -declare_id!("6eFgYZCZZFTe61T4YxWsiHHAunCLTh9V7TAjj8DxuZwm"); +declare_id!("DHfwFSG3JQ2qdX1Ub2QPuDsk9FMQUwyXyZqAs4gGeLnQ"); #[program] pub mod midas_vaults { @@ -20,19 +20,22 @@ pub mod midas_vaults { pub fn new_minter_vault( ctx: Context, first_deposit_min_m_tokens: u64, + max_supply_cap: u64, ) -> Result<()> { - minter_vault::new_minter_vault::handle(ctx, first_deposit_min_m_tokens) + minter_vault::new_minter_vault::handle(ctx, first_deposit_min_m_tokens, max_supply_cap) } pub fn update_minter_vault( ctx: Context, new_first_deposit_min_m_tokens: Option, mint_authority_pda: Option, + max_supply_cap: Option, ) -> Result<()> { minter_vault::update_minter_vault::handle( ctx, new_first_deposit_min_m_tokens, mint_authority_pda, + max_supply_cap, ) } diff --git a/programs/midas-vault/src/state/minter_vault_state.rs b/programs/midas-vault/src/state/minter_vault_state.rs index e5592c3..5cd0834 100644 --- a/programs/midas-vault/src/state/minter_vault_state.rs +++ b/programs/midas-vault/src/state/minter_vault_state.rs @@ -14,6 +14,8 @@ pub struct MinterVaultState { pub common_vault: Pubkey, /// mint authority pda (token-authority program) pub mint_authority_pda: Pubkey, + /// max supply cap for mToken minting + pub max_supply_cap: u64, } impl MinterVaultState { diff --git a/programs/midas-vault/src/utils.rs b/programs/midas-vault/src/utils.rs index 27cd661..80bf41c 100644 --- a/programs/midas-vault/src/utils.rs +++ b/programs/midas-vault/src/utils.rs @@ -230,6 +230,27 @@ pub fn require_variation_tolerance( Ok(()) } +/// Validates that minting `mint_amount` tokens would not exceed `max_supply_cap`. +/// To disable the cap (unlimited), set `max_supply_cap` to `u64::MAX`. +pub fn validate_max_supply_cap( + m_mint: &Mint, + minter: &MinterVaultState, + mint_amount: u64, +) -> Result<()> { + let new_supply = m_mint + .supply + .checked_add(mint_amount) + .ok_or(MidasVaultsError::MaxSupplyCapExceeded)?; + + require_gte!( + minter.max_supply_cap, + new_supply, + MidasVaultsError::MaxSupplyCapExceeded + ); + + Ok(()) +} + /// Calculates fee for a given amount /// /// # Arguments diff --git a/programs/token-authority/src/lib.rs b/programs/token-authority/src/lib.rs index 0a60948..955c053 100644 --- a/programs/token-authority/src/lib.rs +++ b/programs/token-authority/src/lib.rs @@ -4,7 +4,7 @@ pub mod instructions; use instructions::*; pub mod state; -declare_id!("6XqSwGFEuadyqXC9vBLYGJhvQsEVjPdCrtvN6inAb4z3"); +declare_id!("GYmnAy5UiKMYuuwJubKKC5WLnWSW3qLo4JFqX4rBqHKE"); #[program] pub mod token_authority { diff --git a/test/constants/vaults.constants.ts b/test/constants/vaults.constants.ts index a6d4f09..717008d 100644 --- a/test/constants/vaults.constants.ts +++ b/test/constants/vaults.constants.ts @@ -49,4 +49,5 @@ export enum VaultError { InvalidSeedProvided, InvalidVaultProvided, ValueDidtnChange, + MaxSupplyCapExceeded, } diff --git a/test/fixture/vaults.fixture.ts b/test/fixture/vaults.fixture.ts index 3c1ecd5..378046f 100644 --- a/test/fixture/vaults.fixture.ts +++ b/test/fixture/vaults.fixture.ts @@ -13,7 +13,7 @@ import { MidasVaults } from 'target/types/midas_vaults'; import { createMTokenMint } from '../../common/create-mtoken-mint'; import MIDAS_VAULTS_IDL from '../../target/idl/midas_vaults.json' with { type: 'json' }; import { AC_ROLES } from '../constants/ac.constants'; -import { MAX_U128 } from '../constants/common.constants'; +import { MAX_U64, MAX_U128 } from '../constants/common.constants'; import { TOKEN_AUTHORITY_ROLES } from '../constants/token-authority.constants'; import { VAULT_AC_ROLES, VaultActionIds } from '../constants/vaults.constants'; import { acRoleToBuffer, getAccountAcRoleStatePda } from '../helpers/ac.helpers'; @@ -272,7 +272,7 @@ export const vaultsFixture = async (initSlot?: bigint) => { }) .instruction(), await vaultsProgram.methods - .newMinterVault(toBN(0)) + .newMinterVault(toBN(0), toBN(MAX_U64)) .accountsPartial({ vaultCommon: minterCommonVault.publicKey, authority: authority.publicKey, diff --git a/test/minter-vault.test.ts b/test/minter-vault.test.ts index 048d6b0..3159d52 100644 --- a/test/minter-vault.test.ts +++ b/test/minter-vault.test.ts @@ -84,6 +84,17 @@ describe('minter-vault', () => { }); }); + it('update max_supply_cap', async () => { + const fixture = await vaultsFixture(); + + const commonVault = await newVaultCommon(fixture, {}); + await newMinterVault(fixture, { commonVault }); + await updateMinterVault(fixture, { + commonVault, + maxSupplyCap: parseUnits('1000'), + }); + }); + it('should fail; call from non-authority', async () => { const fixture = await vaultsFixture(); @@ -710,6 +721,69 @@ describe('minter-vault', () => { }, ); }); + + it('should fail: max supply cap exceeded', async () => { + const fixture = await vaultsFixture(); + + await prepareCommonMintTest(fixture); + await updateVaultCommonAccount(fixture, { waivedFee: true }); + await updateMinterVault(fixture, { maxSupplyCap: parseUnits('99') }); + + await mintInstant( + fixture, + { + amountToken: 100, + minReceiveAmount: parseUnits('100'), + }, + {}, + {}, + { + revertedWith: VaultError.MaxSupplyCapExceeded, + }, + ); + }); + + it('mint instant with exact cap match', async () => { + const fixture = await vaultsFixture(); + + await prepareCommonMintTest(fixture); + await updateVaultCommonAccount(fixture, { waivedFee: true }); + await updateMinterVault(fixture, { maxSupplyCap: parseUnits('100') }); + + await mintInstant( + fixture, + { + amountToken: 100, + minReceiveAmount: parseUnits('100'), + }, + {}, + { + fee: 0, + tokensMinted: parseUnits('100'), + }, + ); + }); + + it('mint instant with room under cap', async () => { + const fixture = await vaultsFixture(); + + await prepareCommonMintTest(fixture); + await updateVaultCommonAccount(fixture, { waivedFee: true }); + await updateMinterVault(fixture, { maxSupplyCap: parseUnits('100') }); + + await mintInstant( + fixture, + { + amountToken: 90, + minReceiveAmount: parseUnits('90'), + }, + {}, + { + fee: 0, + tokensMinted: parseUnits('90'), + }, + ); + }); }); describe('mint_request', () => { @@ -1289,6 +1363,30 @@ describe('minter-vault', () => { }, ); }); + + it('should fail: max supply cap exceeded on approve', async () => { + const fixture = await vaultsFixture(); + + await prepareCommonMintTest(fixture); + await updateVaultCommonAccount(fixture, { waivedFee: true }); + + // Create request with 100 tokens (cap is MAX_U64 by default, so this succeeds) + await mintRequest(fixture, { amountToken: 100 }, {}, { fee: 0 }); + + // Reduce cap before approval + await updateMinterVault(fixture, { maxSupplyCap: parseUnits('50') }); + + // Approve should fail because cap is now 50 but request wants to mint 100 + await approveMintRequest( + fixture, + { requestId: 0n }, + {}, + {}, + { + revertedWith: VaultError.MaxSupplyCapExceeded, + }, + ); + }); }); describe('reject_mint_request', () => { diff --git a/test/testers/minter-vault.testers.ts b/test/testers/minter-vault.testers.ts index 937acff..b1f8aeb 100644 --- a/test/testers/minter-vault.testers.ts +++ b/test/testers/minter-vault.testers.ts @@ -2,7 +2,7 @@ import { getMint, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-to import { PublicKey } from '@solana/web3.js'; import { expect } from 'vitest'; -import { MAX_U128, ONE } from '../constants/common.constants'; +import { MAX_U64, MAX_U128, ONE } from '../constants/common.constants'; import { TOKEN_AUTHORITY_ROLES } from '../constants/token-authority.constants'; import { VAULT_AC_ROLES } from '../constants/vaults.constants'; import { VaultsFixtureReturnType } from '../fixture/vaults.fixture'; @@ -43,10 +43,12 @@ export const newMinterVault = async ( commonVault, tokenAuthority, firstDepositMinMTokens, + maxSupplyCap, }: { commonVault?: PublicKey; tokenAuthority?: PublicKey; firstDepositMinMTokens?: bigint; + maxSupplyCap?: bigint; }, opt?: OptionalCommonParams, @@ -63,6 +65,7 @@ export const newMinterVault = async ( commonVault ??= minterCommonVault.publicKey; tokenAuthority ??= getTokenAuthorityPda(mTBillMinterAuthoritySeed); firstDepositMinMTokens ??= parseUnits('10'); + maxSupplyCap ??= MAX_U64; // u64::MAX = no cap const fetchState = async () => { const common = await fetchVaultCommonState(vaultsProgram, commonVault); @@ -77,7 +80,7 @@ export const newMinterVault = async ( const stateBefore = await fetchState(); const tx = await vaultsProgram.methods - .newMinterVault(toBN(firstDepositMinMTokens)) + .newMinterVault(toBN(firstDepositMinMTokens), toBN(maxSupplyCap)) .accountsPartial({ authority: from.publicKey, vaultCommon: commonVault, @@ -105,6 +108,7 @@ export const newMinterVault = async ( expect(stateAfter.minter.commonVault.equals(commonVault)).toBe(true); expect(stateAfter.minter.mintAuthorityPda.equals(tokenAuthority)).toBe(true); expect(fromBN(stateAfter.minter.firstDepositMinMTokens)).toEqual(firstDepositMinMTokens); + expect(fromBN(stateAfter.minter.maxSupplyCap)).toEqual(maxSupplyCap); }; export const updateMinterVault = async ( @@ -113,10 +117,12 @@ export const updateMinterVault = async ( commonVault, tokenAuthority, firstDepositMinMTokens, + maxSupplyCap, }: { commonVault?: PublicKey; tokenAuthority?: PublicKey; firstDepositMinMTokens?: bigint; + maxSupplyCap?: bigint; }, opt?: OptionalCommonParams, @@ -127,6 +133,7 @@ export const updateMinterVault = async ( commonVault ??= minterCommonVault.publicKey; tokenAuthority ??= null; firstDepositMinMTokens ??= null; + maxSupplyCap ??= null; const fetchState = async () => { const common = await fetchVaultCommonState(vaultsProgram, commonVault); @@ -141,7 +148,11 @@ export const updateMinterVault = async ( const stateBefore = await fetchState(); const tx = await vaultsProgram.methods - .updateMinterVault(toBNNullable(firstDepositMinMTokens), tokenAuthority) + .updateMinterVault( + toBNNullable(firstDepositMinMTokens), + tokenAuthority, + toBNNullable(maxSupplyCap), + ) .accountsPartial({ authority: from.publicKey, vaultCommon: commonVault, @@ -171,6 +182,10 @@ export const updateMinterVault = async ( if (firstDepositMinMTokens !== null) { expect(fromBN(stateAfter.minter.firstDepositMinMTokens)).toEqual(firstDepositMinMTokens); } + + if (maxSupplyCap !== null) { + expect(fromBN(stateAfter.minter.maxSupplyCap)).toEqual(maxSupplyCap); + } }; export const mintInstant = async ( @@ -889,9 +904,14 @@ export const prepareCommonMintTest = async ( fee?: bigint; }; } = {}, + accounts?: { + commonVault?: PublicKey; + }, opt?: OptionalCommonParams, ) => { - await addPaymentToken(fixture, params.addPaymentToken ?? {}, undefined); - await newVaultCommonAccount(fixture, {}, undefined, opt); + await addPaymentToken(fixture, params.addPaymentToken ?? {}, { + commonVault: accounts?.commonVault, + }); + await newVaultCommonAccount(fixture, {}, { commonVault: accounts?.commonVault }, opt); await newAccountAc(fixture, {}, undefined, opt); }; From f83e35c3824a2496c9b9af548c0c329270414f35 Mon Sep 17 00:00:00 2001 From: Dmytro Horbatenko Date: Tue, 2 Dec 2025 18:20:09 +0200 Subject: [PATCH 02/55] feat: implement maxSupplyCap in minter vault configuration and migration scripts --- package.json | 2 +- programs/midas-vault/src/utils.rs | 5 +- scripts/configs/types.ts | 1 + scripts/deploy/vaults.ts | 5 +- .../verify-upgrade-authority.ts | 82 +++++++++++++++++++ scripts/tasks/deploy/deploy-minter-vault.ts | 1 + scripts/tasks/manage/migrate-minter-vault.ts | 68 +++++++++++++++ 7 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 scripts/local-test-utils/verify-upgrade-authority.ts create mode 100644 scripts/tasks/manage/migrate-minter-vault.ts diff --git a/package.json b/package.json index c455d74..f5e665d 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "update:data-feed": "tsx scripts/tasks/manage/update-data-feed.ts", "update:manual-feed-price": "tsx scripts/tasks/manage/update-manual-feed-price.ts", "delegate": "tsx scripts/tasks/manage/delegate.ts", - "verify:deployment": "tsx scripts/tasks/verify/verify-deployment.ts", + "verify:upgrade-authority": "tsx scripts/local-test-utils/verify-upgrade-authority.ts", "verify:roles": "tsx scripts/local-test-utils/verify-roles.ts", "export:addresses": "tsx scripts/tasks/verify/export-addresses.ts" }, diff --git a/programs/midas-vault/src/utils.rs b/programs/midas-vault/src/utils.rs index 80bf41c..58ce39e 100644 --- a/programs/midas-vault/src/utils.rs +++ b/programs/midas-vault/src/utils.rs @@ -237,10 +237,7 @@ pub fn validate_max_supply_cap( minter: &MinterVaultState, mint_amount: u64, ) -> Result<()> { - let new_supply = m_mint - .supply - .checked_add(mint_amount) - .ok_or(MidasVaultsError::MaxSupplyCapExceeded)?; + let new_supply = m_mint.supply.checked_add(mint_amount).unwrap(); require_gte!( minter.max_supply_cap, diff --git a/scripts/configs/types.ts b/scripts/configs/types.ts index fbcb377..ef62f4d 100644 --- a/scripts/configs/types.ts +++ b/scripts/configs/types.ts @@ -99,6 +99,7 @@ export const minterVaultConfigSchema = z.object({ variationTolerance: z.string(), minAmount: z.string(), firstMintMinMTokens: z.string(), + maxSupplyCap: z.string().optional(), // If not set, defaults to unlimited (u64::MAX) greenListEnforced: z.boolean().default(false), tokensReceiver: publicKeySchema, feeReceiver: publicKeySchema, diff --git a/scripts/deploy/vaults.ts b/scripts/deploy/vaults.ts index 775fa9c..99cce82 100644 --- a/scripts/deploy/vaults.ts +++ b/scripts/deploy/vaults.ts @@ -4,6 +4,7 @@ import { Keypair, PublicKey, Transaction } from '@solana/web3.js'; import { sendAndWaitForCustomSolanaTxSign } from '@/common/solanaTxHelper'; import { MidasVaults } from '@/target/types/midas_vaults'; +import { MAX_U64 } from '@/test/constants/common.constants'; import { TOKEN_AUTHORITY_ROLES } from '@/test/constants/token-authority.constants'; import { VAULT_AC_ROLES, VaultActionIds } from '@/test/constants/vaults.constants'; import { createAtaIfNotExistsInx, toBN } from '@/test/helpers/common.helpers'; @@ -36,6 +37,7 @@ export interface DeployMinterVaultConfig { minAmount: bigint; tokenAuthority: PublicKey; firstMintMinMTokens: bigint; + maxSupplyCap?: bigint; // If not set, defaults to unlimited (MAX_U64) } export interface DeployRedeemerVaultConfig { @@ -66,6 +68,7 @@ export const deployMinterVault = async ( mToken, tokenAuthority, firstMintMinMTokens, + maxSupplyCap, greenListEnforced, instantDailyLimit, instantFee, @@ -155,7 +158,7 @@ export const deployMinterVault = async ( }) .instruction(), await vaultsProgram.methods - .newMinterVault(toBN(firstMintMinMTokens)) + .newMinterVault(toBN(firstMintMinMTokens), toBN(maxSupplyCap ?? MAX_U64)) .accountsPartial({ vaultCommon: commonVault.publicKey, authority: common.payer.publicKey, diff --git a/scripts/local-test-utils/verify-upgrade-authority.ts b/scripts/local-test-utils/verify-upgrade-authority.ts new file mode 100644 index 0000000..9d8197c --- /dev/null +++ b/scripts/local-test-utils/verify-upgrade-authority.ts @@ -0,0 +1,82 @@ +import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; +import { PublicKey } from '@solana/web3.js'; + +import { executeNetworkScript } from '@/common/scriptRunner'; + +import { getNetwork } from '../utils/argumentParser'; + +// Program IDs from Anchor.toml +const PROGRAMS = { + access_control: new PublicKey('7fmev1BQVc9LEXsMDoJs63muHR1B2uSAw1963zfVrpJP'), + data_feed: new PublicKey('6vrYAbfttohJKguPRzspHUW6fn61KDyjzog3q8YRG6yq'), + midas_vaults: new PublicKey('DHfwFSG3JQ2qdX1Ub2QPuDsk9FMQUwyXyZqAs4gGeLnQ'), + token_authority: new PublicKey('GYmnAy5UiKMYuuwJubKKC5WLnWSW3qLo4JFqX4rBqHKE'), +}; + +const BPF_UPGRADEABLE_LOADER = new PublicKey('BPFLoaderUpgradeab1e11111111111111111111111'); + +async function getUpgradeAuthority( + provider: AnchorProvider, + programId: PublicKey, +): Promise<{ authority: PublicKey | null; programDataAddress: PublicKey } | null> { + try { + const programAccount = await provider.connection.getAccountInfo(programId); + if (!programAccount) { + return null; + } + + // Check if owned by BPF Upgradeable Loader + if (!programAccount.owner.equals(BPF_UPGRADEABLE_LOADER)) { + console.log(` Program not owned by BPF Upgradeable Loader`); + return null; + } + + // First 4 bytes are account type, next 32 bytes are programdata address + const programDataAddress = new PublicKey(programAccount.data.slice(4, 36)); + + const programDataAccount = await provider.connection.getAccountInfo(programDataAddress); + if (!programDataAccount) { + return null; + } + + // ProgramData account: first 4 bytes type, next 8 bytes slot, then 1 byte option, then 32 bytes authority + const hasAuthority = programDataAccount.data[12] === 1; + const authority = hasAuthority ? new PublicKey(programDataAccount.data.slice(13, 45)) : null; + + return { authority, programDataAddress }; + } catch (error) { + console.error(` Error fetching program info: ${error}`); + return null; + } +} + +async function main(provider: AnchorProvider, payer: Wallet) { + const network = getNetwork(); + console.log(`\nVerifying upgrade authorities on ${network}`); + console.log(`Wallet: ${payer.publicKey.toString()}\n`); + + let allMatch = true; + + for (const [name, programId] of Object.entries(PROGRAMS)) { + const result = await getUpgradeAuthority(provider, programId); + + if (!result) { + console.log(`${name}: not found`); + continue; + } + + if (!result.authority) { + console.log(`${name}: no authority (immutable)`); + allMatch = false; + } else { + const matches = result.authority.equals(payer.publicKey); + console.log(`${name}: ${result.authority.toString()} ${matches ? '✓' : '✗'}`); + if (!matches) allMatch = false; + } + } + + console.log(allMatch ? '\n✅ All programs upgradeable' : '\n❌ Some programs not upgradeable'); +} + +const network = getNetwork(); +executeNetworkScript(network, main); diff --git a/scripts/tasks/deploy/deploy-minter-vault.ts b/scripts/tasks/deploy/deploy-minter-vault.ts index 6f794a0..73de9fa 100644 --- a/scripts/tasks/deploy/deploy-minter-vault.ts +++ b/scripts/tasks/deploy/deploy-minter-vault.ts @@ -93,6 +93,7 @@ async function main(provider: AnchorProvider, payer: Wallet, network: string) { variationTolerance: parsePercent(parseFloat(config.minter.variationTolerance)), minAmount: parseUnits(config.minter.minAmount), firstMintMinMTokens: parseUnits(config.minter.firstMintMinMTokens), + maxSupplyCap: config.minter.maxSupplyCap ? parseUnits(config.minter.maxSupplyCap) : undefined, greenListEnforced: config.minter.greenListEnforced, tokensReceiver: new PublicKey(config.minter.tokensReceiver), feeReceiver: new PublicKey(config.minter.feeReceiver), diff --git a/scripts/tasks/manage/migrate-minter-vault.ts b/scripts/tasks/manage/migrate-minter-vault.ts new file mode 100644 index 0000000..ef9d699 --- /dev/null +++ b/scripts/tasks/manage/migrate-minter-vault.ts @@ -0,0 +1,68 @@ +import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; +import { Transaction } from '@solana/web3.js'; + +import { createUserError } from '@/common/errorHandler'; +import { executeNetworkScript } from '@/common/scriptRunner'; +import { sendAndWaitForCustomSolanaTxSign } from '@/common/solanaTxHelper'; +import { VAULT_AC_ROLES } from '@/test/constants/vaults.constants'; +import { getAccountAcRoleStatePda } from '@/test/helpers/ac.helpers'; +import { fetchMinterVaultState, fetchVaultCommonState } from '@/test/helpers/vaults.helpers'; + +import { getVaultsProgram } from '../../deploy/vaults'; +import { getTokenAddresses } from '../../utils/addressQueries'; +import { getMtoken, getNetwork } from '../../utils/argumentParser'; + +async function main(provider: AnchorProvider, payer: Wallet) { + const mtoken = getMtoken(); + const network = getNetwork(); + + console.log(`Migrating minter vault for ${mtoken} on ${network}`); + + const tokenAddrs = getTokenAddresses(network, mtoken); + if (!tokenAddrs?.minter?.commonVault) { + throw createUserError(`Minter vault not found for ${mtoken} on ${network}`, [ + `Run: yarn deploy:minter-vault --mtoken ${mtoken} --network ${network}`, + ]); + } + + const vaultsProgram = getVaultsProgram(provider); + const commonVault = tokenAddrs.minter.commonVault; + + const commonState = await fetchVaultCommonState(vaultsProgram, commonVault); + const minterState = await fetchMinterVaultState(vaultsProgram, tokenAddrs.minter.account); + + console.log(`Current minter vault state:`); + console.log(` - commonVault: ${minterState.commonVault.toString()}`); + console.log(` - mintAuthorityPda: ${minterState.mintAuthorityPda.toString()}`); + console.log(` - firstDepositMinMTokens: ${minterState.firstDepositMinMTokens.toString()}`); + + const tx = new Transaction().add( + await vaultsProgram.methods + .migrateMinterVault() + .accountsPartial({ + authority: payer.publicKey, + vaultCommon: commonVault, + minterVault: tokenAddrs.minter.account, + authorityAcRole: getAccountAcRoleStatePda( + commonState.acRole, + payer.publicKey, + VAULT_AC_ROLES.VAULT_ADMIN, + ), + }) + .instruction(), + ); + + const result = await sendAndWaitForCustomSolanaTxSign(provider, tx, [], { + mToken: mtoken, + }); + + console.log(`✅ Minter vault migrated successfully!`); + console.log(`Transaction: ${result.signature}`); + + // Fetch updated state + const updatedState = await fetchMinterVaultState(vaultsProgram, tokenAddrs.minter.account); + console.log(`Updated max_supply_cap: ${updatedState.maxSupplyCap.toString()}`); +} + +const network = getNetwork(); +executeNetworkScript(network, main); From 289872b475fba39efeaf9151f705ec59f556967d Mon Sep 17 00:00:00 2001 From: Dmytro Horbatenko Date: Tue, 2 Dec 2025 18:20:44 +0200 Subject: [PATCH 03/55] refactor: remove migrate-minter-vault script --- scripts/tasks/manage/migrate-minter-vault.ts | 68 -------------------- 1 file changed, 68 deletions(-) delete mode 100644 scripts/tasks/manage/migrate-minter-vault.ts diff --git a/scripts/tasks/manage/migrate-minter-vault.ts b/scripts/tasks/manage/migrate-minter-vault.ts deleted file mode 100644 index ef9d699..0000000 --- a/scripts/tasks/manage/migrate-minter-vault.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; -import { Transaction } from '@solana/web3.js'; - -import { createUserError } from '@/common/errorHandler'; -import { executeNetworkScript } from '@/common/scriptRunner'; -import { sendAndWaitForCustomSolanaTxSign } from '@/common/solanaTxHelper'; -import { VAULT_AC_ROLES } from '@/test/constants/vaults.constants'; -import { getAccountAcRoleStatePda } from '@/test/helpers/ac.helpers'; -import { fetchMinterVaultState, fetchVaultCommonState } from '@/test/helpers/vaults.helpers'; - -import { getVaultsProgram } from '../../deploy/vaults'; -import { getTokenAddresses } from '../../utils/addressQueries'; -import { getMtoken, getNetwork } from '../../utils/argumentParser'; - -async function main(provider: AnchorProvider, payer: Wallet) { - const mtoken = getMtoken(); - const network = getNetwork(); - - console.log(`Migrating minter vault for ${mtoken} on ${network}`); - - const tokenAddrs = getTokenAddresses(network, mtoken); - if (!tokenAddrs?.minter?.commonVault) { - throw createUserError(`Minter vault not found for ${mtoken} on ${network}`, [ - `Run: yarn deploy:minter-vault --mtoken ${mtoken} --network ${network}`, - ]); - } - - const vaultsProgram = getVaultsProgram(provider); - const commonVault = tokenAddrs.minter.commonVault; - - const commonState = await fetchVaultCommonState(vaultsProgram, commonVault); - const minterState = await fetchMinterVaultState(vaultsProgram, tokenAddrs.minter.account); - - console.log(`Current minter vault state:`); - console.log(` - commonVault: ${minterState.commonVault.toString()}`); - console.log(` - mintAuthorityPda: ${minterState.mintAuthorityPda.toString()}`); - console.log(` - firstDepositMinMTokens: ${minterState.firstDepositMinMTokens.toString()}`); - - const tx = new Transaction().add( - await vaultsProgram.methods - .migrateMinterVault() - .accountsPartial({ - authority: payer.publicKey, - vaultCommon: commonVault, - minterVault: tokenAddrs.minter.account, - authorityAcRole: getAccountAcRoleStatePda( - commonState.acRole, - payer.publicKey, - VAULT_AC_ROLES.VAULT_ADMIN, - ), - }) - .instruction(), - ); - - const result = await sendAndWaitForCustomSolanaTxSign(provider, tx, [], { - mToken: mtoken, - }); - - console.log(`✅ Minter vault migrated successfully!`); - console.log(`Transaction: ${result.signature}`); - - // Fetch updated state - const updatedState = await fetchMinterVaultState(vaultsProgram, tokenAddrs.minter.account); - console.log(`Updated max_supply_cap: ${updatedState.maxSupplyCap.toString()}`); -} - -const network = getNetwork(); -executeNetworkScript(network, main); From 74cc2b028222f109ac4b585e1f5e3feb0601d1ba Mon Sep 17 00:00:00 2001 From: Dmytro Horbatenko Date: Mon, 8 Dec 2025 14:51:45 +0200 Subject: [PATCH 04/55] fix: move local scripts to verify --- package.json | 6 +----- scripts/README.md | 12 ++++-------- .../{local-test-utils => verify}/get-all-requests.ts | 6 +++--- .../mint-payment-token.ts | 4 ++-- scripts/{local-test-utils => verify}/verify-feed.ts | 4 ++-- .../verify-mint-state.ts | 2 +- .../verify-payment-tokens.ts | 2 +- .../verify-redeem-request.ts | 2 +- scripts/{local-test-utils => verify}/verify-roles.ts | 4 ++-- .../verify-upgrade-authority.ts | 3 +++ 10 files changed, 20 insertions(+), 25 deletions(-) rename scripts/{local-test-utils => verify}/get-all-requests.ts (96%) rename scripts/{local-test-utils => verify}/mint-payment-token.ts (94%) rename scripts/{local-test-utils => verify}/verify-feed.ts (97%) rename scripts/{local-test-utils => verify}/verify-mint-state.ts (96%) rename scripts/{local-test-utils => verify}/verify-payment-tokens.ts (98%) rename scripts/{local-test-utils => verify}/verify-redeem-request.ts (98%) rename scripts/{local-test-utils => verify}/verify-roles.ts (93%) rename scripts/{local-test-utils => verify}/verify-upgrade-authority.ts (97%) diff --git a/package.json b/package.json index f5e665d..d532d5b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,6 @@ { "type": "module", "scripts": { - "anchor:script": "anchor run script --", "build": "RUSTUP_TOOLCHAIN=\"nightly-2024-05-09\" anchor build", "test": "yarn test:anchor && yarn test:cargo", "test:cargo": "anchor run test-cargo", @@ -29,10 +28,7 @@ "transfer:authority": "tsx scripts/tasks/manage/transfer-authority.ts", "update:data-feed": "tsx scripts/tasks/manage/update-data-feed.ts", "update:manual-feed-price": "tsx scripts/tasks/manage/update-manual-feed-price.ts", - "delegate": "tsx scripts/tasks/manage/delegate.ts", - "verify:upgrade-authority": "tsx scripts/local-test-utils/verify-upgrade-authority.ts", - "verify:roles": "tsx scripts/local-test-utils/verify-roles.ts", - "export:addresses": "tsx scripts/tasks/verify/export-addresses.ts" + "delegate": "tsx scripts/tasks/manage/delegate.ts" }, "dependencies": { "@coral-xyz/anchor": "0.30.1", diff --git a/scripts/README.md b/scripts/README.md index 3ba1e47..8333ee7 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -88,17 +88,13 @@ Roles: `admin`, `update_account_ac`, `vault_admin`, `vault_pauser`, `m_minter`, - `yarn update:data-feed --mtoken --network --new-mode ` - `yarn update:manual-feed-price --mtoken --network --price [--decimals ]` -## Verification +**Local test utilities** (run with `tsx scripts/verify/