Skip to content

Conversation

grod220
Copy link
Member

@grod220 grod220 commented Mar 28, 2025

Enables offline multisig signature collection for the Wrap command. API inspiration: https://solana-program-docs.vercel.app/docs/token#example-offline-signing-with-multisig.

Requires a multi-step process in collecting signatures & then broadcasting.

Walkthrough for testing locally:

  1. Build program (generates .so file and builds binary)
cargo build-sbf
cargo build
  1. Start solana test validator
solana-test-validator \
  --bpf-program TwRapQCDhWkZRrDaHfZGuHxkZ91gHDRkyuzNqeU5MgR target/deploy/spl_token_wrap.so \
  --reset

Keep this running in a separate terminal.

  1. Generate Multisig Member Keypairs
for i in $(seq 3); do solana-keygen new --no-passphrase --outfile "signer-${i}.json"; done

SIGNER_1_KEYPAIR_PATH="signer-1.json"
SIGNER_2_KEYPAIR_PATH="signer-2.json"
SIGNER_3_KEYPAIR_PATH="signer-3.json"
SIGNER_1_PUBKEY=$(solana-keygen pubkey $SIGNER_1_KEYPAIR_PATH)
SIGNER_2_PUBKEY=$(solana-keygen pubkey $SIGNER_2_KEYPAIR_PATH)
SIGNER_3_PUBKEY=$(solana-keygen pubkey $SIGNER_3_KEYPAIR_PATH)

echo "Signer 1 Pubkey: $SIGNER_1_PUBKEY"
echo "Signer 2 Pubkey: $SIGNER_2_PUBKEY"
echo "Signer 3 Pubkey: $SIGNER_3_PUBKEY"
  1. Generate multisig account.
spl-token create-multisig 2 $SIGNER_1_PUBKEY $SIGNER_2_PUBKEY $SIGNER_3_PUBKEY

Save the result in a local var

MULTISIG_ADDRESS=FK4WwtdoNqK7TdRnzS94niQGeLsi8AVZrxUv34V2BE7n
  1. Save fee payer
FEE_PAYER_KEYPAIR_PATH="$HOME/.config/solana/id.json"
FEE_PAYER_PUBKEY=$(solana-keygen pubkey $FEE_PAYER_KEYPAIR_PATH)
echo "Fee Payer Pubkey: $FEE_PAYER_PUBKEY"
  1. Create Unwrapped Mint & Token Account (Owned by Multisig)
UNWRAPPED_MINT=$(spl-token create-token --fee-payer "$FEE_PAYER_KEYPAIR_PATH" --output json | jq -r '.commandOutput.address')
echo "Unwrapped Mint Address: $UNWRAPPED_MINT"

spl-token create-account "$UNWRAPPED_MINT" --owner "$MULTISIG_ADDRESS" --fee-payer "$FEE_PAYER_KEYPAIR_PATH"

Save the account result in a local var

UNWRAPPED_TOKEN_ACCOUNT_MULTISIG="6GnKWVx1VcY1G7cVQUSW42Do8RaEJ9EYkNUgQNFB5xHa"
echo "Using Token Account: $UNWRAPPED_TOKEN_ACCOUNT_MULTISIG"

Mint tokens to the multisig-owned account

spl-token mint "$UNWRAPPED_MINT" 1000 "$UNWRAPPED_TOKEN_ACCOUNT_MULTISIG" --fee-payer "$FEE_PAYER_KEYPAIR_PATH"
  1. Run CreateMint token-wrap command (saving results to vars
CREATE_MINT_JSON_OUTPUT=$(cargo run --bin spl-token-wrap create-mint "$UNWRAPPED_MINT" TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb --fee-payer "$FEE_PAYER_KEYPAIR_PATH" --output json)
WRAPPED_MINT=$(echo "$CREATE_MINT_JSON_OUTPUT" | jq -r '.wrappedMintAddress')
WRAPPED_BACKPOINTER=$(echo "$CREATE_MINT_JSON_OUTPUT" | jq -r '.wrappedBackpointerAddress')
echo "WRAPPED_MINT=$WRAPPED_MINT"
echo "WRAPPED_BACKPOINTER=$WRAPPED_BACKPOINTER"
  1. Find PDAs and save to local vars
PDAS_JSON_OUTPUT=$(cargo run --bin spl-token-wrap find-pdas "$UNWRAPPED_MINT" TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb --output json)
WRAPPED_MINT_AUTHORITY=$(echo "$PDAS_JSON_OUTPUT" | jq -r '.wrappedMintAuthority')
echo "WRAPPED_MINT_AUTHORITY=$WRAPPED_MINT_AUTHORITY"
  1. Create an escrow token account and set its owner to the mint_authority
spl-token create-account $UNWRAPPED_MINT --owner $WRAPPED_MINT_AUTHORITY --fee-payer $FEE_PAYER_KEYPAIR_PATH

Save result to vars

ESCROW_ACCOUNT=8V3Cn5rsvu9K9aCz6cjvW1m7jEJLSuqXc8yb6m7kkZhE
  1. Create a recipient token account of that new wrapped mint
spl-token create-account $WRAPPED_MINT --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb

Save result to vars

RECIPIENT_ACCOUNT=zfckZKmcqypDj5STrLN45jgA8aeLWdvvaDjYBhWmqHm
  1. Execute Multisig Wrap Command. Combined here in multiple steps to avoid block hash expiry.
WRAP_AMOUNT=50
BLOCKHASH=$(solana block --output json | jq -r '.blockhash')

SIGNER_1_OUTPUT=$(cargo run --bin spl-token-wrap wrap "$UNWRAPPED_TOKEN_ACCOUNT_MULTISIG" "$ESCROW_ACCOUNT" TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb "$WRAP_AMOUNT" --transfer-authority "$MULTISIG_ADDRESS" --recipient-token-account "$RECIPIENT_ACCOUNT" --unwrapped-mint "$UNWRAPPED_MINT" --unwrapped-token-program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA --fee-payer "$FEE_PAYER_PUBKEY" --multisig-signer "$SIGNER_1_KEYPAIR_PATH" --multisig-signer "$SIGNER_2_PUBKEY" --blockhash "$BLOCKHASH" --sign-only --output json)

PRESIGNER_1=$(echo "$SIGNER_1_OUTPUT" | jq -r '.signOnlyData.signers[0]')

SIGNER_2_OUTPUT=$(cargo run --bin spl-token-wrap wrap "$UNWRAPPED_TOKEN_ACCOUNT_MULTISIG" "$ESCROW_ACCOUNT" TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb "$WRAP_AMOUNT" --transfer-authority "$MULTISIG_ADDRESS" --recipient-token-account "$RECIPIENT_ACCOUNT" --unwrapped-mint "$UNWRAPPED_MINT" --unwrapped-token-program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA --fee-payer "$FEE_PAYER_PUBKEY" --multisig-signer "$SIGNER_1_PUBKEY" --multisig-signer "$SIGNER_2_KEYPAIR_PATH" --blockhash "$BLOCKHASH" --sign-only --output json)

PRESIGNER_2=$(echo "$SIGNER_2_OUTPUT" | jq -r '.signOnlyData.signers[0]')

echo "  Signer 1: $PRESIGNER_1"
echo "  Signer 2: $PRESIGNER_2"

cargo run --bin spl-token-wrap wrap \
    "$UNWRAPPED_TOKEN_ACCOUNT_MULTISIG" \
    "$ESCROW_ACCOUNT" \
    TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb \
    "$WRAP_AMOUNT" \
    --transfer-authority "$MULTISIG_ADDRESS" \
    --recipient-token-account "$RECIPIENT_ACCOUNT" \
    --unwrapped-mint "$UNWRAPPED_MINT" \
    --unwrapped-token-program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA \
    --fee-payer "$FEE_PAYER_KEYPAIR_PATH" \
    --multisig-signer "$SIGNER_1_PUBKEY" \
    --multisig-signer "$SIGNER_2_PUBKEY" \
    --blockhash "$BLOCKHASH" \
    --signer "$PRESIGNER_1" \
    --signer "$PRESIGNER_2"
  1. Verify balances. Results should be:
  • the multisig-owned unwrapped account should be reduced by 50
  • the escrow account should have 50 unwrapped tokens
  • the recipient wrapped account should have 50 wrapped tokens
spl-token balance --address $UNWRAPPED_TOKEN_ACCOUNT_MULTISIG

spl-token balance --address $ESCROW_ACCOUNT

spl-token balance --address $RECIPIENT_ACCOUNT --program-2022

@grod220 grod220 force-pushed the cli-wrap-multisig branch 3 times, most recently from 6f85ea4 to 59b308c Compare March 31, 2025 15:30
@grod220 grod220 marked this pull request as ready for review March 31, 2025 15:48
Copy link
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great overall! Just some small questions

Copy link

@buffalojoec buffalojoec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems to be in-step with the linked reference example, so looking good to me. Just small stuff.

@grod220 grod220 force-pushed the cli-wrap-multisig branch from 59b308c to 4db885c Compare April 3, 2025 14:36
@grod220 grod220 requested review from buffalojoec and joncinque April 3, 2025 14:58
Copy link

@buffalojoec buffalojoec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Contributor

@joncinque joncinque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@grod220 grod220 merged commit 14dc980 into main Apr 4, 2025
10 checks passed
@grod220 grod220 deleted the cli-wrap-multisig branch April 4, 2025 06:21
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

Successfully merging this pull request may close these issues.

3 participants