Skip to content

Commit 504d674

Browse files
authored
Merge branch 'main' into mariuszzak/7-remove-unused-ibc-app-authority-field
2 parents 6021f35 + e6e9a01 commit 504d674

File tree

1 file changed

+331
-17
lines changed

1 file changed

+331
-17
lines changed

programs/solana/programs/ift/src/instructions/create_and_initialize_spl_token.rs

Lines changed: 331 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use anchor_lang::prelude::*;
2+
use anchor_lang::solana_program::{program::invoke, system_instruction};
23
use anchor_spl::token_2022_extensions::{
34
metadata_pointer_initialize, token_metadata_initialize, MetadataPointerInitialize,
45
TokenMetadataInitialize,
@@ -126,6 +127,41 @@ pub fn create_and_initialize_spl_token(
126127
Ok(())
127128
}
128129

130+
/// Defensive account creation that tolerates pre-funded accounts.
131+
///
132+
/// Unlike `system_program::create_account`, which fails with `AccountAlreadyInUse`
133+
/// when the target already holds lamports, this function uses transfer + allocate +
134+
/// assign to safely initialize the account regardless of existing balance.
135+
fn create_account_defensively<'info>(
136+
payer: &AccountInfo<'info>,
137+
target: &AccountInfo<'info>,
138+
system_program: &AccountInfo<'info>,
139+
required_lamports: u64,
140+
space: u64,
141+
owner: &Pubkey,
142+
) -> Result<()> {
143+
let transfer_amount = required_lamports.saturating_sub(target.lamports());
144+
145+
if transfer_amount > 0 {
146+
invoke(
147+
&system_instruction::transfer(payer.key, target.key, transfer_amount),
148+
&[payer.clone(), target.clone(), system_program.clone()],
149+
)?;
150+
}
151+
152+
invoke(
153+
&system_instruction::allocate(target.key, space),
154+
&[target.clone(), system_program.clone()],
155+
)?;
156+
157+
invoke(
158+
&system_instruction::assign(target.key, owner),
159+
&[target.clone(), system_program.clone()],
160+
)?;
161+
162+
Ok(())
163+
}
164+
129165
/// Create a Token 2022 mint with `MetadataPointer` extension and on-chain metadata.
130166
fn create_token_2022_mint(
131167
ctx: &Context<CreateAndInitializeSplToken>,
@@ -167,14 +203,10 @@ fn create_token_2022_mint(
167203
let rent = Rent::get()?;
168204
let lamports = rent.minimum_balance(total_space);
169205

170-
anchor_lang::system_program::create_account(
171-
CpiContext::new(
172-
ctx.accounts.system_program.to_account_info(),
173-
anchor_lang::system_program::CreateAccount {
174-
from: ctx.accounts.payer.to_account_info(),
175-
to: ctx.accounts.mint.to_account_info(),
176-
},
177-
),
206+
create_account_defensively(
207+
&ctx.accounts.payer.to_account_info(),
208+
&ctx.accounts.mint.to_account_info(),
209+
&ctx.accounts.system_program.to_account_info(),
178210
lamports,
179211
extension_space as u64,
180212
&TOKEN_2022_PROGRAM_ID,
@@ -252,14 +284,10 @@ fn create_legacy_mint(
252284
let rent = Rent::get()?;
253285
let lamports = rent.minimum_balance(space);
254286

255-
anchor_lang::system_program::create_account(
256-
CpiContext::new(
257-
ctx.accounts.system_program.to_account_info(),
258-
anchor_lang::system_program::CreateAccount {
259-
from: ctx.accounts.payer.to_account_info(),
260-
to: ctx.accounts.mint.to_account_info(),
261-
},
262-
),
287+
create_account_defensively(
288+
&ctx.accounts.payer.to_account_info(),
289+
&ctx.accounts.mint.to_account_info(),
290+
&ctx.accounts.system_program.to_account_info(),
263291
lamports,
264292
space as u64,
265293
&token_program_id,
@@ -289,7 +317,8 @@ mod tests {
289317
use anchor_spl::token_2022_extensions::spl_token_metadata_interface::state::TokenMetadata;
290318
use anchor_spl::token_interface::spl_token_2022::{
291319
extension::{
292-
metadata_pointer::MetadataPointer, BaseStateWithExtensions as _, StateWithExtensions,
320+
metadata_pointer::MetadataPointer, BaseStateWithExtensions as _, ExtensionType,
321+
StateWithExtensions,
293322
},
294323
state::Mint as Token2022Mint,
295324
};
@@ -962,4 +991,289 @@ mod tests {
962991
"Token2022 variant with SPL Token program should fail"
963992
);
964993
}
994+
995+
// ─── Pre-funding griefing resistance tests ────────────────────
996+
997+
fn adversary_account(lamports: u64) -> solana_sdk::account::Account {
998+
solana_sdk::account::Account {
999+
lamports,
1000+
data: vec![],
1001+
owner: solana_sdk::system_program::ID,
1002+
executable: false,
1003+
rent_epoch: 0,
1004+
}
1005+
}
1006+
1007+
#[test]
1008+
fn test_prefunded_spl_token_mint_succeeds() {
1009+
let mollusk = setup_mollusk_with_token();
1010+
let (token_program_id, token_program_account) = token_program_keyed_account();
1011+
1012+
let setup = build_spl_token_setup(token_program_id, token_program_account, 6);
1013+
let mint_key = setup.instruction.accounts[2].pubkey;
1014+
1015+
let adversary = Pubkey::new_unique();
1016+
let prefund_amount: u64 = 500_000;
1017+
1018+
let prefund_ix =
1019+
solana_sdk::system_instruction::transfer(&adversary, &mint_key, prefund_amount);
1020+
1021+
let mut accounts = setup.accounts;
1022+
accounts.push((
1023+
adversary,
1024+
adversary_account(prefund_amount.saturating_add(1_000_000)),
1025+
));
1026+
1027+
let result = mollusk.process_instruction_chain(&[prefund_ix, setup.instruction], &accounts);
1028+
assert!(
1029+
!result.program_result.is_err(),
1030+
"Partially pre-funded SPL Token mint must not block creation: {:?}",
1031+
result.program_result
1032+
);
1033+
1034+
let (_, mint_acc) = &result.resulting_accounts[2];
1035+
let created_mint =
1036+
anchor_spl::token::spl_token::state::Mint::unpack(&mint_acc.data).expect("valid mint");
1037+
assert_eq!(created_mint.decimals, 6);
1038+
}
1039+
1040+
#[test]
1041+
fn test_prefunded_token_2022_mint_succeeds() {
1042+
let mollusk = setup_mollusk_with_token_2022();
1043+
let (token_program_id, token_program_account) = token_2022_keyed_account();
1044+
1045+
let setup = build_token_2022_setup(
1046+
token_program_id,
1047+
token_program_account,
1048+
9,
1049+
"Test Token",
1050+
"TST",
1051+
"https://example.com/metadata.json",
1052+
);
1053+
let mint_key = setup.instruction.accounts[2].pubkey;
1054+
1055+
let adversary = Pubkey::new_unique();
1056+
let prefund_amount: u64 = 500_000;
1057+
1058+
let prefund_ix =
1059+
solana_sdk::system_instruction::transfer(&adversary, &mint_key, prefund_amount);
1060+
1061+
let mut accounts = setup.accounts;
1062+
accounts.push((
1063+
adversary,
1064+
adversary_account(prefund_amount.saturating_add(1_000_000)),
1065+
));
1066+
1067+
let result = mollusk.process_instruction_chain(&[prefund_ix, setup.instruction], &accounts);
1068+
assert!(
1069+
!result.program_result.is_err(),
1070+
"Partially pre-funded Token 2022 mint must not block creation: {:?}",
1071+
result.program_result
1072+
);
1073+
1074+
let (_, mint_acc) = &result.resulting_accounts[2];
1075+
let state = StateWithExtensions::<Token2022Mint>::unpack(&mint_acc.data)
1076+
.expect("valid Token 2022 mint");
1077+
assert_eq!(state.base.decimals, 9);
1078+
1079+
let metadata = state
1080+
.get_variable_len_extension::<TokenMetadata>()
1081+
.expect("TokenMetadata should be present");
1082+
assert_eq!(metadata.name, "Test Token");
1083+
assert_eq!(metadata.symbol, "TST");
1084+
}
1085+
1086+
/// Compute the total lamports Token 2022 needs for a mint with metadata.
1087+
///
1088+
/// Mirrors the production calculation: `rent.minimum_balance(extension_space + metadata_space)`.
1089+
fn token_2022_required_lamports(name: &str, symbol: &str, uri: &str) -> u64 {
1090+
use anchor_spl::token_2022_extensions::spl_pod::optional_keys::OptionalNonZeroPubkey;
1091+
use anchor_spl::token_2022_extensions::spl_token_metadata_interface::state::TokenMetadata;
1092+
use anchor_spl::token_interface::spl_token_2022::state::Mint as T22Mint;
1093+
1094+
let metadata = TokenMetadata {
1095+
update_authority: OptionalNonZeroPubkey::default(),
1096+
mint: Pubkey::default(),
1097+
name: name.to_string(),
1098+
symbol: symbol.to_string(),
1099+
uri: uri.to_string(),
1100+
additional_metadata: vec![],
1101+
};
1102+
1103+
let extension_space =
1104+
ExtensionType::try_calculate_account_len::<T22Mint>(&[ExtensionType::MetadataPointer])
1105+
.unwrap();
1106+
let metadata_space = metadata.tlv_size_of().unwrap();
1107+
let total_space = extension_space.saturating_add(metadata_space);
1108+
Rent::default().minimum_balance(total_space)
1109+
}
1110+
1111+
#[test]
1112+
fn test_exact_prefunded_token_2022_mint_succeeds() {
1113+
let mollusk = setup_mollusk_with_token_2022();
1114+
let (token_program_id, token_program_account) = token_2022_keyed_account();
1115+
1116+
let name = "Exact Token";
1117+
let symbol = "EXT";
1118+
let uri = "https://example.com/exact.json";
1119+
1120+
let setup = build_token_2022_setup(
1121+
token_program_id,
1122+
token_program_account,
1123+
6,
1124+
name,
1125+
symbol,
1126+
uri,
1127+
);
1128+
let mint_key = setup.instruction.accounts[2].pubkey;
1129+
1130+
let exact_rent = token_2022_required_lamports(name, symbol, uri);
1131+
1132+
let adversary = Pubkey::new_unique();
1133+
let prefund_ix =
1134+
solana_sdk::system_instruction::transfer(&adversary, &mint_key, exact_rent);
1135+
1136+
let mut accounts = setup.accounts;
1137+
accounts.push((
1138+
adversary,
1139+
adversary_account(exact_rent.saturating_add(1_000_000)),
1140+
));
1141+
1142+
let result = mollusk.process_instruction_chain(&[prefund_ix, setup.instruction], &accounts);
1143+
assert!(
1144+
!result.program_result.is_err(),
1145+
"Exactly pre-funded Token 2022 mint must not block creation: {:?}",
1146+
result.program_result
1147+
);
1148+
1149+
let (_, mint_acc) = &result.resulting_accounts[2];
1150+
let state = StateWithExtensions::<Token2022Mint>::unpack(&mint_acc.data)
1151+
.expect("valid Token 2022 mint");
1152+
assert_eq!(state.base.decimals, 6);
1153+
1154+
let metadata = state
1155+
.get_variable_len_extension::<TokenMetadata>()
1156+
.expect("TokenMetadata should be present");
1157+
assert_eq!(metadata.name, name);
1158+
}
1159+
1160+
#[test]
1161+
fn test_overfunded_token_2022_mint_succeeds() {
1162+
let mollusk = setup_mollusk_with_token_2022();
1163+
let (token_program_id, token_program_account) = token_2022_keyed_account();
1164+
1165+
let name = "Over Token";
1166+
let symbol = "OVR";
1167+
let uri = "";
1168+
1169+
let setup = build_token_2022_setup(
1170+
token_program_id,
1171+
token_program_account,
1172+
6,
1173+
name,
1174+
symbol,
1175+
uri,
1176+
);
1177+
let mint_key = setup.instruction.accounts[2].pubkey;
1178+
1179+
let overfund_amount =
1180+
token_2022_required_lamports(name, symbol, uri).saturating_add(5_000_000);
1181+
1182+
let adversary = Pubkey::new_unique();
1183+
let prefund_ix =
1184+
solana_sdk::system_instruction::transfer(&adversary, &mint_key, overfund_amount);
1185+
1186+
let mut accounts = setup.accounts;
1187+
accounts.push((
1188+
adversary,
1189+
adversary_account(overfund_amount.saturating_add(1_000_000)),
1190+
));
1191+
1192+
let result = mollusk.process_instruction_chain(&[prefund_ix, setup.instruction], &accounts);
1193+
assert!(
1194+
!result.program_result.is_err(),
1195+
"Over-funded Token 2022 mint must not block creation: {:?}",
1196+
result.program_result
1197+
);
1198+
1199+
let (_, mint_acc) = &result.resulting_accounts[2];
1200+
let state = StateWithExtensions::<Token2022Mint>::unpack(&mint_acc.data)
1201+
.expect("valid Token 2022 mint");
1202+
assert_eq!(state.base.decimals, 6);
1203+
1204+
let metadata = state
1205+
.get_variable_len_extension::<TokenMetadata>()
1206+
.expect("TokenMetadata should be present");
1207+
assert_eq!(metadata.name, name);
1208+
}
1209+
1210+
#[test]
1211+
fn test_exact_prefunded_spl_token_mint_succeeds() {
1212+
let mollusk = setup_mollusk_with_token();
1213+
let (token_program_id, token_program_account) = token_program_keyed_account();
1214+
1215+
let setup = build_spl_token_setup(token_program_id, token_program_account, 6);
1216+
let mint_key = setup.instruction.accounts[2].pubkey;
1217+
1218+
let space = anchor_spl::token::spl_token::state::Mint::LEN;
1219+
let exact_rent = Rent::default().minimum_balance(space);
1220+
1221+
let adversary = Pubkey::new_unique();
1222+
let prefund_ix =
1223+
solana_sdk::system_instruction::transfer(&adversary, &mint_key, exact_rent);
1224+
1225+
let mut accounts = setup.accounts;
1226+
accounts.push((
1227+
adversary,
1228+
adversary_account(exact_rent.saturating_add(1_000_000)),
1229+
));
1230+
1231+
let result = mollusk.process_instruction_chain(&[prefund_ix, setup.instruction], &accounts);
1232+
assert!(
1233+
!result.program_result.is_err(),
1234+
"Exactly pre-funded SPL Token mint must not block creation: {:?}",
1235+
result.program_result
1236+
);
1237+
1238+
let (_, mint_acc) = &result.resulting_accounts[2];
1239+
let created_mint =
1240+
anchor_spl::token::spl_token::state::Mint::unpack(&mint_acc.data).expect("valid mint");
1241+
assert_eq!(created_mint.decimals, 6);
1242+
}
1243+
1244+
#[test]
1245+
fn test_overfunded_spl_token_mint_succeeds() {
1246+
let mollusk = setup_mollusk_with_token();
1247+
let (token_program_id, token_program_account) = token_program_keyed_account();
1248+
1249+
let setup = build_spl_token_setup(token_program_id, token_program_account, 6);
1250+
let mint_key = setup.instruction.accounts[2].pubkey;
1251+
1252+
let space = anchor_spl::token::spl_token::state::Mint::LEN;
1253+
let overfund_amount = Rent::default()
1254+
.minimum_balance(space)
1255+
.saturating_add(5_000_000);
1256+
1257+
let adversary = Pubkey::new_unique();
1258+
let prefund_ix =
1259+
solana_sdk::system_instruction::transfer(&adversary, &mint_key, overfund_amount);
1260+
1261+
let mut accounts = setup.accounts;
1262+
accounts.push((
1263+
adversary,
1264+
adversary_account(overfund_amount.saturating_add(1_000_000)),
1265+
));
1266+
1267+
let result = mollusk.process_instruction_chain(&[prefund_ix, setup.instruction], &accounts);
1268+
assert!(
1269+
!result.program_result.is_err(),
1270+
"Over-funded SPL Token mint must not block creation: {:?}",
1271+
result.program_result
1272+
);
1273+
1274+
let (_, mint_acc) = &result.resulting_accounts[2];
1275+
let created_mint =
1276+
anchor_spl::token::spl_token::state::Mint::unpack(&mint_acc.data).expect("valid mint");
1277+
assert_eq!(created_mint.decimals, 6);
1278+
}
9651279
}

0 commit comments

Comments
 (0)