Skip to content

Commit 4c801d7

Browse files
committed
fix: better error messages
1 parent f021332 commit 4c801d7

8 files changed

Lines changed: 67 additions & 99 deletions

File tree

rust/sealevel/programs/ism/composite-ism/src/account_metas.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use solana_program::{account_info::AccountInfo, instruction::AccountMeta, pubkey
44

55
use crate::{
66
accounts::{derive_domain_pda, DomainIsmAccount, IsmNode},
7-
metadata::{parse_aggregation_ranges, sub_metadata},
7+
metadata::{parse_aggregation_ranges, parse_routing_amount, sub_metadata},
88
};
99

1010
/// Returns `true` if the ISM tree contains any `RateLimited` node.
@@ -90,14 +90,7 @@ pub fn required_accounts_for_node(
9090
lower,
9191
upper,
9292
} => {
93-
const AMOUNT_OFFSET: usize = 32;
94-
const AMOUNT_END: usize = 64;
95-
if message.body.len() < AMOUNT_END {
96-
return vec![];
97-
}
98-
let Ok(amount): Result<[u8; 32], _> =
99-
message.body[AMOUNT_OFFSET..AMOUNT_END].try_into()
100-
else {
93+
let Some(amount) = parse_routing_amount(&message.body) else {
10194
return vec![];
10295
};
10396
let sub_ism = if amount >= *threshold { upper } else { lower };

rust/sealevel/programs/ism/composite-ism/src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ pub enum Error {
4343
RateLimitedInDomainIsm = 19,
4444
#[error("Routing ISM is not allowed inside a domain PDA")]
4545
RoutingInDomainIsm = 20,
46+
#[error("Domain PDA must be writable when ISM tree contains a RateLimited node")]
47+
DomainPdaNotWritable = 21,
48+
#[error("Expected system program account")]
49+
InvalidSystemProgram = 22,
50+
#[error("Storage PDA account does not match expected derived address")]
51+
InvalidStoragePda = 23,
52+
#[error("Domain PDA account does not match expected derived address for this origin")]
53+
InvalidDomainPda = 24,
4654
}
4755

4856
impl From<Error> for ProgramError {

rust/sealevel/programs/ism/composite-ism/src/metadata.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ pub fn sub_metadata(metadata: &[u8], range: MetadataRange) -> &[u8] {
7676
&metadata[range.start as usize..range.end as usize]
7777
}
7878

79+
/// Parses the 32-byte big-endian U256 amount from a warp-route message body.
80+
///
81+
/// Warp-route (TokenMessage) layout: `[recipient (32 bytes)][amount (32 bytes)][metadata]`.
82+
/// Returns `None` if the body is too short.
83+
pub(crate) fn parse_routing_amount(body: &[u8]) -> Option<[u8; 32]> {
84+
const AMOUNT_OFFSET: usize = 32;
85+
const AMOUNT_END: usize = 64;
86+
body.get(AMOUNT_OFFSET..AMOUNT_END)?.try_into().ok()
87+
}
88+
7989
#[cfg(test)]
8090
mod test {
8191
use super::*;
@@ -154,4 +164,26 @@ mod test {
154164
metadata.extend_from_slice(&[0u8; 4]);
155165
assert!(parse_aggregation_ranges(&metadata, 1).is_err());
156166
}
167+
168+
#[test]
169+
fn test_parse_routing_amount_exact_body() {
170+
let mut body = [0u8; 64];
171+
body[63] = 42; // amount = 42 in the low byte
172+
let amount = parse_routing_amount(&body).unwrap();
173+
assert_eq!(amount[31], 42);
174+
}
175+
176+
#[test]
177+
fn test_parse_routing_amount_body_longer_than_64() {
178+
let mut body = vec![0u8; 100];
179+
body[32] = 1; // high byte of amount
180+
let amount = parse_routing_amount(&body).unwrap();
181+
assert_eq!(amount[0], 1);
182+
}
183+
184+
#[test]
185+
fn test_parse_routing_amount_body_too_short() {
186+
assert!(parse_routing_amount(&[0u8; 63]).is_none());
187+
assert!(parse_routing_amount(&[]).is_none());
188+
}
157189
}

rust/sealevel/programs/ism/composite-ism/src/metadata_spec.rs

Lines changed: 4 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use solana_program::{account_info::AccountInfo, pubkey::Pubkey};
55
use crate::{
66
accounts::{derive_domain_pda, load_domain_ism, IsmNode},
77
error::Error,
8+
metadata::parse_routing_amount,
89
};
910

1011
/// Describes the metadata a relayer must supply for this composite ISM tree.
@@ -32,68 +33,6 @@ pub enum MetadataSpec {
3233

3334
/// Resolves the [`MetadataSpec`] for an ISM node given the message.
3435
///
35-
/// Routing/AmountRouting are resolved inline. `Routing` nodes require a
36-
/// domain PDA account to be provided; use [`spec_for_node_with_pdas`] when
37-
/// `Routing` nodes may be present in the tree.
38-
#[allow(dead_code)]
39-
pub(crate) fn spec_for_node(
40-
node: &IsmNode,
41-
message: &HyperlaneMessage,
42-
) -> Result<MetadataSpec, Error> {
43-
match node {
44-
IsmNode::MultisigMessageId {
45-
validators,
46-
threshold,
47-
} => Ok(MetadataSpec::MultisigMessageId {
48-
validators: validators.clone(),
49-
threshold: *threshold,
50-
}),
51-
52-
IsmNode::Aggregation {
53-
threshold,
54-
sub_isms,
55-
} => {
56-
let sub_specs = sub_isms
57-
.iter()
58-
.map(|sub| spec_for_node(sub, message))
59-
.collect::<Result<Vec<_>, _>>()?;
60-
Ok(MetadataSpec::Aggregation {
61-
threshold: *threshold,
62-
sub_specs,
63-
})
64-
}
65-
66-
IsmNode::AmountRouting {
67-
threshold,
68-
lower,
69-
upper,
70-
} => {
71-
const AMOUNT_OFFSET: usize = 32;
72-
const AMOUNT_END: usize = 64;
73-
if message.body.len() < AMOUNT_END {
74-
return Err(Error::InvalidMessageBody);
75-
}
76-
let amount: [u8; 32] = message.body[AMOUNT_OFFSET..AMOUNT_END]
77-
.try_into()
78-
.map_err(|_| Error::InvalidMessageBody)?;
79-
let sub_ism = if amount >= *threshold { upper } else { lower };
80-
spec_for_node(sub_ism, message)
81-
}
82-
83-
// Routing requires domain PDA accounts — use spec_for_node_with_pdas instead.
84-
IsmNode::Routing { .. } => Err(Error::NoRouteForDomain),
85-
86-
// Leaf nodes that need no metadata from the relayer.
87-
IsmNode::TrustedRelayer { .. }
88-
| IsmNode::Test { .. }
89-
| IsmNode::Pausable { .. }
90-
| IsmNode::RateLimited { .. } => Ok(MetadataSpec::Null),
91-
}
92-
}
93-
94-
/// Like [`spec_for_node`], but resolves `Routing` nodes by reading accounts
95-
/// from `accounts_iter`.
96-
///
9736
/// For each `Routing` encountered during depth-first traversal, the next
9837
/// account from `accounts_iter` must be the domain PDA for `message.origin`
9938
/// (or any PDA — the key is verified against the derived address).
@@ -110,12 +49,12 @@ where
11049
match node {
11150
IsmNode::Routing { default_ism } => {
11251
// Expect the domain PDA as the next account.
113-
let domain_pda_info = accounts_iter.next().ok_or(Error::AccountOutOfOrder)?;
52+
let domain_pda_info = accounts_iter.next().ok_or(Error::InvalidDomainPda)?;
11453

11554
// Verify correct account.
11655
let (expected_key, _) = derive_domain_pda(program_id, message.origin);
11756
if *domain_pda_info.key != expected_key {
118-
return Err(Error::AccountOutOfOrder);
57+
return Err(Error::InvalidDomainPda);
11958
}
12059

12160
// Load sub-ISM.
@@ -160,14 +99,7 @@ where
16099
lower,
161100
upper,
162101
} => {
163-
const AMOUNT_OFFSET: usize = 32;
164-
const AMOUNT_END: usize = 64;
165-
if message.body.len() < AMOUNT_END {
166-
return Err(Error::InvalidMessageBody);
167-
}
168-
let amount: [u8; 32] = message.body[AMOUNT_OFFSET..AMOUNT_END]
169-
.try_into()
170-
.map_err(|_| Error::InvalidMessageBody)?;
102+
let amount = parse_routing_amount(&message.body).ok_or(Error::InvalidMessageBody)?;
171103
let sub_ism = if amount >= *threshold { upper } else { lower };
172104
spec_for_node_with_pdas(sub_ism, message, program_id, accounts_iter)
173105
}

rust/sealevel/programs/ism/composite-ism/src/processor.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], mut root: IsmNode)
197197
let (storage_pda_key, storage_pda_bump) =
198198
Pubkey::find_program_address(storage_pda_seeds!(), program_id);
199199
if *storage_pda_account.key != storage_pda_key {
200-
return Err(Error::AccountOutOfOrder.into());
200+
return Err(Error::InvalidStoragePda.into());
201201
}
202202

203203
if let Ok(Some(_)) =
@@ -208,7 +208,7 @@ fn initialize(program_id: &Pubkey, accounts: &[AccountInfo], mut root: IsmNode)
208208

209209
let system_program_account = next_account_info(accounts_iter)?;
210210
if system_program_account.key != &system_program::ID {
211-
return Err(Error::AccountOutOfOrder.into());
211+
return Err(Error::InvalidSystemProgram.into());
212212
}
213213

214214
let storage = CompositeIsmAccount::from(CompositeIsmStorage {
@@ -290,7 +290,11 @@ fn set_domain_ism(
290290
let (expected_pda, bump) =
291291
Pubkey::find_program_address(&[DOMAIN_ISM_SEED, &domain_bytes], program_id);
292292
if *domain_pda_account.key != expected_pda {
293-
return Err(Error::AccountOutOfOrder.into());
293+
return Err(Error::InvalidDomainPda.into());
294+
}
295+
296+
if system_program_account.key != &system_program::ID {
297+
return Err(Error::InvalidSystemProgram.into());
294298
}
295299

296300
let domain_storage = DomainIsmAccount::from(DomainIsmStorage {
@@ -344,7 +348,7 @@ fn remove_domain_ism(program_id: &Pubkey, accounts: &[AccountInfo], domain: u32)
344348
// Verify domain PDA address.
345349
let (expected_pda, _) = derive_domain_pda(program_id, domain);
346350
if *domain_pda_account.key != expected_pda {
347-
return Err(Error::AccountOutOfOrder.into());
351+
return Err(Error::InvalidDomainPda.into());
348352
}
349353

350354
if domain_pda_account.owner != program_id {
@@ -549,7 +553,7 @@ fn load_storage(
549553
let storage_pda_key =
550554
Pubkey::create_program_address(storage_pda_seeds!(storage.bump_seed), program_id)?;
551555
if *storage_pda_account.key != storage_pda_key {
552-
return Err(Error::AccountOutOfOrder.into());
556+
return Err(Error::InvalidStoragePda.into());
553557
}
554558

555559
Ok(storage)

rust/sealevel/programs/ism/composite-ism/src/verify.rs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::{
1111
account_metas::contains_rate_limited,
1212
accounts::{derive_domain_pda, load_domain_ism_storage, DomainIsmAccount, IsmNode},
1313
error::Error,
14+
metadata::parse_routing_amount,
1415
metadata::{parse_aggregation_ranges, sub_metadata},
1516
multisig_metadata::MultisigIsmMessageIdMetadata,
1617
rate_limit::calculate_current_level,
@@ -115,15 +116,7 @@ where
115116
lower,
116117
upper,
117118
} => {
118-
const AMOUNT_OFFSET: usize = 32;
119-
const AMOUNT_END: usize = 64;
120-
if message.body.len() < AMOUNT_END {
121-
return Err(Error::InvalidMessageBody.into());
122-
}
123-
let amount: [u8; 32] = message.body[AMOUNT_OFFSET..AMOUNT_END]
124-
.try_into()
125-
.map_err(|_| Error::InvalidMessageBody)?;
126-
119+
let amount = parse_routing_amount(&message.body).ok_or(Error::InvalidMessageBody)?;
127120
let sub_ism: &mut IsmNode = if amount >= *threshold { upper } else { lower };
128121
verify_node(sub_ism, metadata, message, accounts_iter, program_id)
129122
}
@@ -135,7 +128,7 @@ where
135128
// Verify the caller passed the correct domain PDA for this origin.
136129
let (expected_key, _) = derive_domain_pda(program_id, message.origin);
137130
if *domain_pda_info.key != expected_key {
138-
return Err(Error::AccountOutOfOrder.into());
131+
return Err(Error::InvalidDomainPda.into());
139132
}
140133

141134
// Load the full domain PDA storage (None if not owned by this program).
@@ -147,7 +140,7 @@ where
147140
// RateLimited state must be persisted; require a writable domain PDA so a
148141
// hand-crafted transaction cannot bypass the rate limit by passing it readonly.
149142
if contains_rate_limited(&ism) && !domain_pda_info.is_writable {
150-
return Err(Error::AccountOutOfOrder.into());
143+
return Err(Error::DomainPdaNotWritable.into());
151144
}
152145
verify_node(&mut ism, metadata, message, accounts_iter, program_id)?;
153146
// Write updated state back to the domain PDA (e.g. RateLimited counters).
@@ -196,6 +189,12 @@ where
196189
}
197190
}
198191

192+
// Warp-route message body layout (TokenMessage): [recipient (32 bytes)][amount (32 bytes, big-endian U256)][metadata].
193+
// max_capacity is u64, so we read the amount as the low 8 bytes of the U256
194+
// (body[56..64]) and require the high 24 bytes (body[32..56]) to be zero.
195+
// A transfer whose amount doesn't fit in u64 would exceed any realistic
196+
// capacity and is rejected — AmountRouting reads the full 32 bytes because
197+
// it only needs ordering, not arithmetic.
199198
if message.body.len() < 64 {
200199
return Err(Error::InvalidMessageBody.into());
201200
}

rust/sealevel/programs/ism/composite-ism/tests/functional_nested.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,7 @@ async fn test_rate_limited_in_domain_pda_readonly_bypass_rejected() {
859859
&result,
860860
TransactionError::InstructionError(
861861
0,
862-
InstructionError::Custom(Error::AccountOutOfOrder as u32),
862+
InstructionError::Custom(Error::DomainPdaNotWritable as u32),
863863
),
864864
);
865865
}

typescript/svm-sdk/src/hyperlane/program-bytes.ts

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)