Skip to content

Commit 164acda

Browse files
authored
BurnMint pool using multisig (#911)
1 parent dc68689 commit 164acda

File tree

9 files changed

+192
-51
lines changed

9 files changed

+192
-51
lines changed

chains/solana/contracts/programs/burnmint-token-pool/src/context.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,12 @@ pub struct DeleteChainConfig<'info> {
411411
pub authority: Signer<'info>,
412412
}
413413

414+
#[error_code]
415+
pub enum CcipBnMTokenPoolError {
416+
#[msg("Invalid Multisig Mint")]
417+
InvalidMultisig,
418+
}
419+
414420
// This account can not be declared in the common crate, the program ID for that Account would be incorrect.
415421
#[account]
416422
#[derive(InitSpace)]

chains/solana/contracts/programs/burnmint-token-pool/src/lib.rs

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,8 @@ pub mod burnmint_token_pool {
203203
Ok(())
204204
}
205205

206-
pub fn release_or_mint_tokens(
207-
ctx: Context<TokenOfframp>,
206+
pub fn release_or_mint_tokens<'info>(
207+
ctx: Context<'_, '_, 'info, 'info, TokenOfframp<'info>>,
208208
release_or_mint: ReleaseOrMintInV1,
209209
) -> Result<ReleaseOrMintOutV1> {
210210
let parsed_amount = to_svm_token_amount(
@@ -230,14 +230,38 @@ pub mod burnmint_token_pool {
230230
ctx.accounts.rmn_remote_config.to_account_info(),
231231
)?;
232232

233+
// For a burnmint pool, the mint authority should never be empty (that would mean a fixed supply and prevent minting)
234+
// If not set, then the mint operation will fail
235+
require!(
236+
ctx.accounts.mint.mint_authority.is_some(),
237+
CcipBnMTokenPoolError::InvalidMultisig
238+
);
239+
240+
let mint_authority = ctx.accounts.mint.mint_authority.unwrap();
241+
242+
let multisig = if mint_authority != ctx.accounts.pool_signer.key() {
243+
let multisig_account = ctx
244+
.remaining_accounts
245+
.iter()
246+
.find(|acc| acc.key() == mint_authority)
247+
.ok_or(CcipBnMTokenPoolError::InvalidMultisig)?;
248+
249+
Some(multisig_account)
250+
} else {
251+
None
252+
};
253+
233254
mint_tokens(
234255
ctx.accounts.token_program.key(),
235-
ctx.accounts.receiver_token_account.to_account_info(),
236-
ctx.accounts.mint.to_account_info(),
237-
ctx.accounts.pool_signer.to_account_info(),
238-
ctx.bumps.pool_signer,
239256
release_or_mint,
240257
parsed_amount,
258+
MintTokenAccountsInfo {
259+
receiver_token_account: &ctx.accounts.receiver_token_account.to_account_info(),
260+
mint: &ctx.accounts.mint.to_account_info(),
261+
pool_signer: &ctx.accounts.pool_signer.to_account_info(),
262+
pool_signer_bump: ctx.bumps.pool_signer,
263+
multisig,
264+
},
241265
)?;
242266

243267
Ok(ReleaseOrMintOutV1 {
@@ -338,43 +362,60 @@ pub fn burn_tokens<'a>(
338362
Ok(())
339363
}
340364

341-
pub fn mint_tokens<'a>(
365+
pub struct MintTokenAccountsInfo<'a, 'b> {
366+
pub receiver_token_account: &'a AccountInfo<'b>,
367+
pub mint: &'a AccountInfo<'b>,
368+
pub pool_signer: &'a AccountInfo<'b>,
369+
pub pool_signer_bump: u8,
370+
pub multisig: Option<&'a AccountInfo<'b>>,
371+
}
372+
373+
pub fn mint_tokens(
342374
token_program: Pubkey,
343-
receiver_token_account: AccountInfo<'a>,
344-
mint: AccountInfo<'a>,
345-
pool_signer: AccountInfo<'a>,
346-
pool_signer_bump: u8,
347375
release_or_mint: ReleaseOrMintInV1,
348376
parsed_amount: u64,
377+
accounts: MintTokenAccountsInfo,
349378
) -> Result<()> {
350379
// mint to receiver
351380
// https://docs.rs/spl-token-2022/latest/spl_token_2022/instruction/fn.mint_to.html
352381
let mut ix = mint_to(
353382
&spl_token_2022::ID, // use spl-token-2022 to compile instruction - change program later
354-
&mint.key(),
355-
&receiver_token_account.key(),
356-
&pool_signer.key(),
357-
&[],
383+
&accounts.mint.key(),
384+
&accounts.receiver_token_account.key(),
385+
&accounts.multisig.unwrap_or(accounts.pool_signer).key(),
386+
&[accounts.pool_signer.key],
358387
parsed_amount,
359388
)?;
360389
ix.program_id = token_program.key(); // set to user specified program
361390

362391
let seeds = &[
363392
POOL_SIGNER_SEED,
364-
&mint.key().to_bytes(),
365-
&[pool_signer_bump],
393+
&accounts.mint.key().to_bytes(),
394+
&[accounts.pool_signer_bump],
366395
];
367-
invoke_signed(
368-
&ix,
369-
&[receiver_token_account, mint.clone(), pool_signer.clone()],
370-
&[&seeds[..]],
371-
)?;
396+
397+
let account_infos: Vec<AccountInfo> = if accounts.multisig.is_some() {
398+
vec![
399+
accounts.receiver_token_account.clone(),
400+
accounts.mint.clone(),
401+
accounts.multisig.unwrap().clone(),
402+
accounts.pool_signer.clone(),
403+
]
404+
} else {
405+
vec![
406+
accounts.receiver_token_account.clone(),
407+
accounts.mint.clone(),
408+
accounts.pool_signer.clone(),
409+
]
410+
};
411+
412+
invoke_signed(&ix, &account_infos, &[&seeds[..]])?;
372413

373414
emit!(Minted {
374-
sender: pool_signer.key(),
415+
sender: accounts.pool_signer.key(),
375416
recipient: release_or_mint.receiver,
376417
amount: parsed_amount,
377-
mint: mint.key(),
418+
mint: accounts.mint.key(),
378419
});
379420

380421
Ok(())

chains/solana/contracts/programs/test-token-pool/src/lib.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::context::*;
99
#[program]
1010
pub mod test_token_pool {
1111
use anchor_lang::solana_program::{instruction::Instruction, program::invoke_signed};
12-
use burnmint_token_pool::{burn_tokens, mint_tokens};
12+
use burnmint_token_pool::{burn_tokens, mint_tokens, MintTokenAccountsInfo};
1313
use lockrelease_token_pool::{lock_tokens, release_tokens};
1414

1515
use super::*;
@@ -152,6 +152,8 @@ pub mod test_token_pool {
152152
ctx.accounts.rmn_remote_config.to_account_info(),
153153
)?;
154154

155+
let mint_authority = ctx.accounts.mint.mint_authority.unwrap_or_default();
156+
155157
match ctx.accounts.state.pool_type {
156158
PoolType::LockAndRelease => release_tokens(
157159
ctx.accounts.token_program.key(),
@@ -166,12 +168,24 @@ pub mod test_token_pool {
166168
)?,
167169
PoolType::BurnAndMint => mint_tokens(
168170
ctx.accounts.token_program.key(),
169-
ctx.accounts.receiver_token_account.to_account_info(),
170-
ctx.accounts.mint.to_account_info(),
171-
ctx.accounts.pool_signer.to_account_info(),
172-
ctx.bumps.pool_signer,
173171
release_or_mint,
174172
parsed_amount,
173+
MintTokenAccountsInfo {
174+
receiver_token_account: &ctx.accounts.receiver_token_account.to_account_info(),
175+
mint: &ctx.accounts.mint.to_account_info(),
176+
pool_signer: &ctx.accounts.pool_signer.to_account_info(),
177+
pool_signer_bump: ctx.bumps.pool_signer,
178+
multisig: if mint_authority != ctx.accounts.pool_signer.key() {
179+
Some(
180+
ctx.remaining_accounts
181+
.iter()
182+
.find(|acc| acc.key() == mint_authority)
183+
.ok_or(CcipTokenPoolError::InvalidInputs)?,
184+
)
185+
} else {
186+
None
187+
},
188+
},
175189
)?,
176190
PoolType::Wrapped => {
177191
// The External Execution Config Account is used to sign the CPI instruction

chains/solana/contracts/target/idl/burnmint_token_pool.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,5 +685,12 @@
685685
]
686686
}
687687
}
688+
],
689+
"errors": [
690+
{
691+
"code": 6000,
692+
"name": "InvalidMultisig",
693+
"msg": "Invalid Multisig Mint"
694+
}
688695
]
689696
}

chains/solana/contracts/target/types/burnmint_token_pool.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,13 @@ export type BurnmintTokenPool = {
685685
]
686686
}
687687
}
688+
],
689+
"errors": [
690+
{
691+
"code": 6000,
692+
"name": "InvalidMultisig",
693+
"msg": "Invalid Multisig Mint"
694+
}
688695
]
689696
};
690697

@@ -1375,5 +1382,12 @@ export const IDL: BurnmintTokenPool = {
13751382
]
13761383
}
13771384
}
1385+
],
1386+
"errors": [
1387+
{
1388+
"code": 6000,
1389+
"name": "InvalidMultisig",
1390+
"msg": "Invalid Multisig Mint"
1391+
}
13781392
]
13791393
};

chains/solana/contracts/tests/ccip/ccip_router_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ func TestCCIPRouter(t *testing.T) {
101101
// Random-generated key, but fixing it adds determinism to tests to make it easier to debug.
102102
linkMintPrivK := solana.MustPrivateKeyFromBase58("32YVeJArcWWWV96fztfkRQhohyFz5Hwno93AeGVrN4g2LuFyvwznrNd9A6tbvaTU6BuyBsynwJEMLre8vSy3CrVU")
103103

104+
token0Multisig := solana.MustPrivateKeyFromBase58("5BayUa1C1nfiSptuV521hYPKZZsjHJM5YDfWZJkyrDgnEhzvZS4x5Nuqn6n4E6anuqAc7dJpAr1faNUwyK99cf3C") // EkopXthh6nbLKkgEnACc94mygKsSfcaX4EjXLgt1LiR4
105+
104106
token0Mint := solana.MustPrivateKeyFromBase58("42uJJqZk4gFz6Q6ghMiaYrFdDapXhbufQdTCGJDMeyv2wN6wNBbXkBBPibF7xQQZemzRaDH66ouJmjfvWhPJKtQC")
105107
token0, gerr := tokens.NewTokenPool(config.Token2022Program, config.CcipTokenPoolProgram, token0Mint.PublicKey())
106108
require.NoError(t, gerr)
@@ -382,6 +384,10 @@ func TestCCIPRouter(t *testing.T) {
382384
ix0, ixErr0 := tokens.CreateToken(ctx, token0.Program, token0.Mint, token0PoolAdmin.PublicKey(), token0Decimals, solanaGoClient, config.DefaultCommitment)
383385
require.NoError(t, ixErr0)
384386

387+
ixMsig, ixErrMsig := tokens.CreateMultisig(ctx, user.PublicKey(), token0.Program, token0Multisig.PublicKey(), []solana.PublicKey{token0PoolAdmin.PublicKey(), token0.PoolSigner}, solanaGoClient, config.DefaultCommitment)
388+
require.NoError(t, ixErrMsig)
389+
testutils.SendAndConfirm(ctx, t, solanaGoClient, ixMsig, token0PoolAdmin, config.DefaultCommitment, common.AddSigners(token0Multisig, user))
390+
385391
ix1, ixErr1 := tokens.CreateToken(ctx, token1.Program, token1.Mint, token1PoolAdmin.PublicKey(), token1Decimals, solanaGoClient, config.DefaultCommitment)
386392
require.NoError(t, ixErr1)
387393

@@ -432,7 +438,7 @@ func TestCCIPRouter(t *testing.T) {
432438
})
433439

434440
t.Run("token-pool", func(t *testing.T) {
435-
token0.AdditionalAccounts = append(token0.AdditionalAccounts, solana.MemoProgramID) // add test additional accounts in pool interactions
441+
token0.AdditionalAccounts = append(token0.AdditionalAccounts, token0Multisig.PublicKey()) // add test additional accounts in pool interactions
436442

437443
type ProgramData struct {
438444
DataType uint32
@@ -560,7 +566,7 @@ func TestCCIPRouter(t *testing.T) {
560566
linkPool.PoolTokenAccount = addrLink
561567
linkPool.User[linkPool.PoolSigner] = linkPool.PoolTokenAccount
562568

563-
ixAuth, err := tokens.SetTokenMintAuthority(token0.Program, token0.PoolSigner, token0.Mint, token0PoolAdmin.PublicKey())
569+
ixAuth, err := tokens.SetTokenMintAuthority(token0.Program, token0Multisig.PublicKey(), token0.Mint, token0PoolAdmin.PublicKey())
564570
require.NoError(t, err)
565571

566572
testutils.SendAndConfirm(ctx, t, solanaGoClient, []solana.Instruction{ixInit0, ixInit1, ixInit2, ixInitLink}, legacyAdmin, config.DefaultCommitment)

chains/solana/contracts/tests/examples/pools_test.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,14 @@ var pools = []PoolInfo{
3333
type TokenInfo struct {
3434
tokenName string
3535
tokenProgram solana.PublicKey
36+
multisig bool
3637
}
3738

3839
var tokenPrograms = []TokenInfo{
39-
{tokenName: "spl-token", tokenProgram: solana.TokenProgramID},
40-
{tokenName: "spl-token-2022", tokenProgram: config.Token2022Program},
40+
{tokenName: "spl-token", tokenProgram: solana.TokenProgramID, multisig: false},
41+
{tokenName: "spl-token", tokenProgram: solana.TokenProgramID, multisig: true},
42+
{tokenName: "spl-token-2022", tokenProgram: config.Token2022Program, multisig: false},
43+
{tokenName: "spl-token-2022", tokenProgram: config.Token2022Program, multisig: true},
4144
}
4245

4346
type ProgramData struct {
@@ -203,8 +206,22 @@ func TestBaseTokenPoolHappyPath(t *testing.T) {
203206
poolInitI, err := tokenpool.NewInitializeInstruction(dumbRamp, config.RMNRemoteProgram, poolConfig, mint, admin.PublicKey(), solana.SystemProgramID, poolProgram, programData.Address, configPDA).ValidateAndBuild()
204207
require.NoError(t, err)
205208

209+
newMintAuthority := poolSigner
210+
211+
if v.multisig {
212+
// create multisig
213+
multisig, err := solana.NewRandomPrivateKey()
214+
require.NoError(t, err)
215+
ixMsig, ixErrMsig := tokens.CreateMultisig(ctx, admin.PublicKey(), v.tokenProgram, multisig.PublicKey(), []solana.PublicKey{admin.PublicKey(), poolSigner}, solanaGoClient, config.DefaultCommitment)
216+
require.NoError(t, ixErrMsig)
217+
testutils.SendAndConfirm(ctx, t, solanaGoClient, ixMsig, admin, config.DefaultCommitment, common.AddSigners(multisig, admin))
218+
219+
newMintAuthority = multisig.PublicKey()
220+
p.MintAuthorityMultisig = multisig.PublicKey() // set multisig as mint authority
221+
}
222+
206223
// make pool mint_authority for token (required for burn/mint)
207-
authI, err := tokens.SetTokenMintAuthority(v.tokenProgram, poolSigner, mint, admin.PublicKey())
224+
authI, err := tokens.SetTokenMintAuthority(v.tokenProgram, newMintAuthority, mint, admin.PublicKey())
208225
require.NoError(t, err)
209226

210227
// set pool config
@@ -310,7 +327,7 @@ func TestBaseTokenPoolHappyPath(t *testing.T) {
310327
t.Run("releaseOrMint", func(t *testing.T) {
311328
require.Equal(t, "0", getBalance(p.User[admin.PublicKey()]))
312329

313-
rmI, err := test_ccip_invalid_receiver.NewPoolProxyReleaseOrMintInstruction(
330+
raw := test_ccip_invalid_receiver.NewPoolProxyReleaseOrMintInstruction(
314331
test_ccip_invalid_receiver.ReleaseOrMintInV1{
315332
LocalToken: mint,
316333
SourcePoolAddress: remotePool.Address,
@@ -332,7 +349,13 @@ func TestBaseTokenPoolHappyPath(t *testing.T) {
332349
config.RMNRemoteCursesPDA,
333350
config.RMNRemoteConfigPDA,
334351
p.User[admin.PublicKey()],
335-
).ValidateAndBuild()
352+
)
353+
354+
if v.multisig {
355+
raw.AccountMetaSlice.Append(solana.Meta(p.MintAuthorityMultisig))
356+
}
357+
358+
rmI, err := raw.ValidateAndBuild()
336359
require.NoError(t, err)
337360

338361
cu := testutils.GetRequiredCU(ctx, t, solanaGoClient, []solana.Instruction{rmI}, admin, config.DefaultCommitment)

chains/solana/utils/tokens/token.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,34 @@ func CreateToken(ctx context.Context, program, mint, admin solana.PublicKey, dec
5555
return []solana.Instruction{initI, mintWrap}, nil
5656
}
5757

58+
func CreateMultisig(ctx context.Context, payer, program, multisig solana.PublicKey, signers []solana.PublicKey, client *rpc.Client, commitment rpc.CommitmentType) ([]solana.Instruction, error) {
59+
// get stake amount for init
60+
lamports, err := client.GetMinimumBalanceForRentExemption(ctx, 355, commitment)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
// initialize mint account
66+
initI, err := system.NewCreateAccountInstruction(lamports, 355, program, payer, multisig).ValidateAndBuild()
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
// Manually add the signer metas, as the SDK wrongly tries to set them as transaction signers
72+
// when they are just meant to be registered as part of the multisig
73+
raw := token.NewInitializeMultisig2Instruction(1, multisig, []solana.PublicKey{})
74+
for _, signer := range signers {
75+
raw.Signers = append(raw.Signers, solana.Meta(signer))
76+
}
77+
msigInitIx, err := raw.ValidateAndBuild()
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
msigWrap := &TokenInstruction{msigInitIx, program}
83+
return []solana.Instruction{initI, msigWrap}, nil
84+
}
85+
5886
var AssociatedTokenProgramID solana.PublicKey = ata.ProgramID
5987

6088
func CreateAssociatedTokenAccount(tokenProgram, mint, address, payer solana.PublicKey) (ins solana.Instruction, ataAddress solana.PublicKey, err error) {

0 commit comments

Comments
 (0)