diff --git a/programs/conditional_vault/src/error.rs b/programs/conditional_vault/src/error.rs index bf0d04c7..7e5ea5bf 100644 --- a/programs/conditional_vault/src/error.rs +++ b/programs/conditional_vault/src/error.rs @@ -30,4 +30,6 @@ pub enum VaultError { ConditionalTokenMintMismatch, #[msg("Payouts must sum to 1 or more")] PayoutZero, + #[msg("Conditional token metadata already set")] + ConditionalTokenMetadataAlreadySet, } diff --git a/programs/conditional_vault/src/instructions/add_metadata_to_conditional_tokens.rs b/programs/conditional_vault/src/instructions/add_metadata_to_conditional_tokens.rs index 2e1e7eb3..dd603f23 100644 --- a/programs/conditional_vault/src/instructions/add_metadata_to_conditional_tokens.rs +++ b/programs/conditional_vault/src/instructions/add_metadata_to_conditional_tokens.rs @@ -8,7 +8,6 @@ pub mod proph3t_deployer { #[derive(AnchorSerialize, AnchorDeserialize)] pub struct AddMetadataToConditionalTokensArgs { - // pub uri: String, pub name: String, pub symbol: String, pub image: String, @@ -24,7 +23,6 @@ pub struct AddMetadataToConditionalTokens<'info> { #[account( mut, mint::authority = vault, - mint::freeze_authority = vault, )] pub conditional_token_mint: Account<'info, Mint>, /// CHECK: verified via cpi into token metadata @@ -42,6 +40,11 @@ impl AddMetadataToConditionalTokens<'_> { // VaultError::VaultAlreadySettled // ); + require!( + self.conditional_token_metadata.data_is_empty(), + VaultError::ConditionalTokenMetadataAlreadySet + ); + #[cfg(feature = "production")] require_eq!( self.payer.key(), proph3t_deployer::ID diff --git a/programs/conditional_vault/src/lib.rs b/programs/conditional_vault/src/lib.rs index 1c3c3a14..839bcb74 100644 --- a/programs/conditional_vault/src/lib.rs +++ b/programs/conditional_vault/src/lib.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use anchor_spl::metadata::{ create_metadata_accounts_v3, mpl_token_metadata::types::DataV2, CreateMetadataAccountsV3, - Metadata, MetadataAccount, + Metadata, }; use anchor_spl::{ associated_token::AssociatedToken, diff --git a/sdk/src/ConditionalVaultClient.ts b/sdk/src/ConditionalVaultClient.ts index c8ef7cdd..d1464ea6 100644 --- a/sdk/src/ConditionalVaultClient.ts +++ b/sdk/src/ConditionalVaultClient.ts @@ -422,45 +422,49 @@ export class ConditionalVaultClient { addMetadataToConditionalTokensIx( vault: PublicKey, - underlyingTokenMint: PublicKey, - proposalNumber: number, - onFinalizeUri: string, - onRevertUri: string + index: number, + name: string, + symbol: string, + image: string + // underlyingTokenMint: PublicKey, + // proposalNumber: number, + // onFinalizeUri: string, + // onRevertUri: string ) { - const [underlyingTokenMetadata] = getMetadataAddr(underlyingTokenMint); + // const [underlyingTokenMetadata] = getMetadataAddr(underlyingTokenMint); - const [conditionalOnFinalizeTokenMint] = getVaultFinalizeMintAddr( - this.vaultProgram.programId, - vault - ); - const [conditionalOnRevertTokenMint] = getVaultRevertMintAddr( + const [conditionalTokenMint] = getConditionalTokenMintAddr( this.vaultProgram.programId, - vault + vault, + index ); - const [conditionalOnFinalizeTokenMetadata] = getMetadataAddr( - conditionalOnFinalizeTokenMint - ); + // const [conditionalOnFinalizeTokenMint] = getVaultFinalizeMintAddr( + // this.vaultProgram.programId, + // vault + // ); + // const [conditionalOnRevertTokenMint] = getVaultRevertMintAddr( + // this.vaultProgram.programId, + // vault + // ); - const [conditionalOnRevertTokenMetadata] = getMetadataAddr( - conditionalOnRevertTokenMint - ); + const [conditionalTokenMetadata] = getMetadataAddr(conditionalTokenMint); + + // const [conditionalOnRevertTokenMetadata] = getMetadataAddr( + // conditionalOnRevertTokenMint + // ); return this.vaultProgram.methods .addMetadataToConditionalTokens({ - proposalNumber: new BN(proposalNumber), - onFinalizeUri, - onRevertUri, + name, + symbol, + image, }) .accounts({ payer: this.provider.publicKey, vault, - underlyingTokenMint, - underlyingTokenMetadata, - conditionalOnFinalizeTokenMint, - conditionalOnRevertTokenMint, - conditionalOnFinalizeTokenMetadata, - conditionalOnRevertTokenMetadata, + conditionalTokenMint, + conditionalTokenMetadata, tokenMetadataProgram: MPL_TOKEN_METADATA_PROGRAM_ID, }); } diff --git a/sdk/src/types/conditional_vault.ts b/sdk/src/types/conditional_vault.ts index a0e38449..fd19a968 100644 --- a/sdk/src/types/conditional_vault.ts +++ b/sdk/src/types/conditional_vault.ts @@ -497,6 +497,11 @@ export type ConditionalVault = { code: 6013; name: "PayoutZero"; msg: "Payouts must sum to 1 or more"; + }, + { + code: 6014; + name: "ConditionalTokenMetadataAlreadySet"; + msg: "Conditional token metadata already set"; } ]; }; @@ -1001,5 +1006,10 @@ export const IDL: ConditionalVault = { name: "PayoutZero", msg: "Payouts must sum to 1 or more", }, + { + code: 6014, + name: "ConditionalTokenMetadataAlreadySet", + msg: "Conditional token metadata already set", + }, ], }; diff --git a/tests/conditionalVault/integration/binaryPredictionMarket.test.ts b/tests/conditionalVault/integration/binaryPredictionMarket.test.ts index 48be6f42..985adc18 100644 --- a/tests/conditionalVault/integration/binaryPredictionMarket.test.ts +++ b/tests/conditionalVault/integration/binaryPredictionMarket.test.ts @@ -61,6 +61,9 @@ export default async function test() { const vault = await vaultClient.initializeVault(question, USDC, 2); const storedVault = await vaultClient.fetchVault(vault); + await vaultClient.addMetadataToConditionalTokensIx(vault, 0, "Trump Share", "TRUMP", "https://example.com/trump.png").rpc(); + await vaultClient.addMetadataToConditionalTokensIx(vault, 1, "Harris Share", "HARRIS", "https://example.com/harris.png").rpc(); + await vaultClient .splitTokensIx(question, vault, USDC, new BN(100), 2, alice.publicKey) .signers([alice]) diff --git a/tests/conditionalVault/main.test.ts b/tests/conditionalVault/main.test.ts index 67cc857a..10240843 100644 --- a/tests/conditionalVault/main.test.ts +++ b/tests/conditionalVault/main.test.ts @@ -4,6 +4,7 @@ import resolveQuestion from "./unit/resolveQuestion.test"; import splitTokens from "./unit/splitTokens.test"; import mergeTokens from "./unit/mergeTokens.test"; import redeemTokens from "./unit/redeemTokens.test"; +import addMetadataToConditionalTokens from "./unit/addMetadataToConditionalTokens.test"; import binaryPredictionMarket from "./integration/binaryPredictionMarket.test"; import scalarGrantMarket from "./integration/scalarGrantMarket.test"; @@ -16,4 +17,5 @@ export default function suite() { describe("#split_tokens", splitTokens); describe("#merge_tokens", mergeTokens); describe("#redeem_tokens", redeemTokens); + describe("#add_metadata_to_conditional_tokens", addMetadataToConditionalTokens); } diff --git a/tests/conditionalVault/unit/addMetadataToConditionalTokens.test.ts b/tests/conditionalVault/unit/addMetadataToConditionalTokens.test.ts new file mode 100644 index 00000000..4730fb83 --- /dev/null +++ b/tests/conditionalVault/unit/addMetadataToConditionalTokens.test.ts @@ -0,0 +1,109 @@ +import { sha256, ConditionalVaultClient, getConditionalTokenMintAddr, getMetadataAddr } from "@metadaoproject/futarchy"; +import { Keypair, PublicKey } from "@solana/web3.js"; +import { assert } from "chai"; +import { createMint } from "spl-token-bankrun"; +import * as anchor from "@coral-xyz/anchor"; +import { expectError } from "../../utils"; +import { Metadata, deserializeMetadata, getMetadataAccountDataSerializer } from "@metaplex-foundation/mpl-token-metadata"; + +export default function suite() { + let vaultClient: ConditionalVaultClient; + let question: PublicKey; + let vault: PublicKey; + let underlyingTokenMint: PublicKey; + + const metadataSerializer = getMetadataAccountDataSerializer(); + + before(function () { + vaultClient = this.vaultClient; + }); + + async function setupVault(outcomes: number) { + let questionId = sha256(new Uint8Array([1, 2, 3])); + let oracle = Keypair.generate(); + + question = await vaultClient.initializeQuestion( + questionId, + oracle.publicKey, + outcomes + ); + + underlyingTokenMint = await createMint( + this.banksClient, + this.payer, + this.payer.publicKey, + null, + 8 + ); + + vault = await vaultClient.initializeVault(question, underlyingTokenMint, outcomes); + } + + async function addMetadataToConditionalTokens(outcomes: number) { + for (let i = 0; i < outcomes; i++) { + await vaultClient.addMetadataToConditionalTokensIx( + vault, + i, + `Outcome ${i}`, + `OUT${i}`, + `https://example.com/image${i}.png` + ).rpc(); + } + } + + async function verifyMetadata(outcomes: number) { + for (let i = 0; i < outcomes; i++) { + const [conditionalTokenMint] = getConditionalTokenMintAddr( + vaultClient.vaultProgram.programId, + vault, + i + ); + + const storedMetadata = await this.banksClient.getAccount(getMetadataAddr(conditionalTokenMint)[0]); + assert.isNotNull(storedMetadata); + const metadata = metadataSerializer.deserialize(storedMetadata.data)[0]; + assert.equal(metadata.name, `Outcome ${i}`); + assert.equal(metadata.symbol, `OUT${i}`); + const expectedUri = `data:,{"name":"${metadata.name}","symbol":"${metadata.symbol}","image":"https://example.com/image${i}.png"}`; + assert.equal(metadata.uri, expectedUri); + } + } + + it("adds metadata to 2-token vault", async function () { + await setupVault.call(this, 2); + await addMetadataToConditionalTokens(2); + await verifyMetadata.call(this, 2); + }); + + it("adds metadata to 3-token vault", async function () { + await setupVault.call(this, 3); + await addMetadataToConditionalTokens(3); + await verifyMetadata.call(this, 3); + }); + + it("adds metadata to 10-token vault", async function () { + await setupVault.call(this, 10); + await addMetadataToConditionalTokens(10); + await verifyMetadata.call(this, 10); + }); + + it("cannot add metadata twice for the same conditional token", async function () { + await setupVault.call(this, 2); + await addMetadataToConditionalTokens(2); + + const callbacks = expectError( + "ConditionalTokenMetadataAlreadySet", + "added metadata to a conditional token that already had metadata" + ); + + await vaultClient.addMetadataToConditionalTokensIx( + vault, + 0, + "New Outcome", + "NEW", + "https://example.com/new.png" + ) + .rpc() + .then(callbacks[0], callbacks[1]); + }); +} \ No newline at end of file