|
1 | 1 | use anchor_lang::prelude::*; |
| 2 | +use anchor_lang::solana_program::{program::invoke, system_instruction}; |
2 | 3 | use anchor_spl::token_2022_extensions::{ |
3 | 4 | metadata_pointer_initialize, token_metadata_initialize, MetadataPointerInitialize, |
4 | 5 | TokenMetadataInitialize, |
@@ -126,6 +127,41 @@ pub fn create_and_initialize_spl_token( |
126 | 127 | Ok(()) |
127 | 128 | } |
128 | 129 |
|
| 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 | + |
129 | 165 | /// Create a Token 2022 mint with `MetadataPointer` extension and on-chain metadata. |
130 | 166 | fn create_token_2022_mint( |
131 | 167 | ctx: &Context<CreateAndInitializeSplToken>, |
@@ -167,14 +203,10 @@ fn create_token_2022_mint( |
167 | 203 | let rent = Rent::get()?; |
168 | 204 | let lamports = rent.minimum_balance(total_space); |
169 | 205 |
|
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(), |
178 | 210 | lamports, |
179 | 211 | extension_space as u64, |
180 | 212 | &TOKEN_2022_PROGRAM_ID, |
@@ -252,14 +284,10 @@ fn create_legacy_mint( |
252 | 284 | let rent = Rent::get()?; |
253 | 285 | let lamports = rent.minimum_balance(space); |
254 | 286 |
|
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(), |
263 | 291 | lamports, |
264 | 292 | space as u64, |
265 | 293 | &token_program_id, |
@@ -289,7 +317,8 @@ mod tests { |
289 | 317 | use anchor_spl::token_2022_extensions::spl_token_metadata_interface::state::TokenMetadata; |
290 | 318 | use anchor_spl::token_interface::spl_token_2022::{ |
291 | 319 | extension::{ |
292 | | - metadata_pointer::MetadataPointer, BaseStateWithExtensions as _, StateWithExtensions, |
| 320 | + metadata_pointer::MetadataPointer, BaseStateWithExtensions as _, ExtensionType, |
| 321 | + StateWithExtensions, |
293 | 322 | }, |
294 | 323 | state::Mint as Token2022Mint, |
295 | 324 | }; |
@@ -962,4 +991,289 @@ mod tests { |
962 | 991 | "Token2022 variant with SPL Token program should fail" |
963 | 992 | ); |
964 | 993 | } |
| 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 | + } |
965 | 1279 | } |
0 commit comments