fix(sequencer): fix prefix removal when receiving source non-refund ics20 packet #1131
fix(sequencer): fix prefix removal when receiving source non-refund ics20 packet #1131
Conversation
SuperFluffy
left a comment
There was a problem hiding this comment.
There are some oddities in the code around removing the prefix. Can you add a few unit tests for the remove_prefix functionality (and for is_prefixed_with?)
| let denom_trace = self | ||
| .to_string() | ||
| .strip_prefix(prefix) | ||
| .ok_or(InvalidPrefixToRemove { |
There was a problem hiding this comment.
Use a lazy constructor to avoid allocation on every single call (i.e. even if you don't end up hitting the error case):
| .ok_or(InvalidPrefixToRemove { | |
| .ok_or_or_else(|| InvalidPrefixToRemove { |
| /// - if the denom does not have the given prefix. | ||
| pub fn remove_prefix(&self, prefix: &str) -> Result<Self, InvalidPrefixToRemove> { | ||
| let denom_trace = self | ||
| .to_string() |
There was a problem hiding this comment.
Why allocate a new string prior to removing strip_prefix? Because one sometimes wants to remove a prefix that reaches "into" the denom part?
Nomenclature wise this leaves me pretty confused. Just looking at the definition of Denom I see a prefix and a denom. So why would a method called remove_prefix also remove parts of the Denom?
There was a problem hiding this comment.
the Denom consists of a prefix and a base denom (I'll rename the denom field to base_denom), this is just a "denomination trace" broken down into parts (eg. transfer/channel-1/utia is broken down into transfer/channel-1 and utia). so removing a prefix is removing some prefix from the entire denomination trace. I'll change this to just remove from the denom.prefix field as I don't think there's ever a case we need to go into the base denom
| /// # Errors | ||
| /// | ||
| /// - if the denom does not have the given prefix. | ||
| pub fn remove_prefix(&self, prefix: &str) -> Result<Self, InvalidPrefixToRemove> { |
There was a problem hiding this comment.
Can you add unit tests for this functionality?
There was a problem hiding this comment.
I believe this function is fundamentally broken and very hard to fix. I have refactored denom parsing here: #1162
Context:
Assume I have this input "some/denomination/trace", then Denom::from("some/denomination/trace") will give me:
my_denom := Denom {
prefix: "some/denomination",
trace: "trace",
}
Now, my_denom.is_prefix_by("some/denom") == true. But even worse, I can get this:
stripped_denom
:= my_denom.remove_prefix("some/denom")
= Denom {
prefix: "ination",
trace: "trace",
}
| unprefixed_denom | ||
| .remove_prefix(&format!("{}/{}/", source_port, source_channel)) | ||
| .expect( | ||
| "denom must be prefixed by source_port/source_channel as it was checked in \ |
There was a problem hiding this comment.
Is this actually correct?
This is the definition of fn is_prefixed (after the changes)
fn is_prefixed(source_port: &PortId, source_channel: &ChannelId, asset: &Denom) -> bool {
let prefix = format!("{source_port}/{source_channel}/");
asset.is_prefixed_with(&prefix)
}But here is Asset::is_prefixed_with:
pub fn is_prefixed_with(&self, prefix: &str) -> bool {
self.prefix.starts_with(prefix)
}So this code is checking Asset::prefix if it starts with "{source_port}/{source_channel}", but it actually removing removing {source_port}/{source_channel} from the concatenation {Asset::prefix}/{Asset::denom}.
There was a problem hiding this comment.
yes this is the correct functionality, for example if you have a token {source_port}/{source_channel}/base_denom then is_prefixed is true, and if this is source we want to remove {source_port}/{source_channel} from the whole denom. this is the same functionality as before, except now we handle the case where a token has multiple IBC hops, and is something like {source_port}/{source_channel}/other_port/other_channel/base_denom. previously, the entire prefix {source_port}/{source_channel}/other_port/other_channel would have been removed, which is wrong, we only want to remove the {source_port}/{source_channel} prefix, so the resulting denom is other_port/other_channel/base_denom.
|
@SuperFluffy refactored |
|
|
||
| let new_prefix = self | ||
| .prefix | ||
| .clone() |
There was a problem hiding this comment.
I don't think we need the clone here
| prefix: prefix.to_string(), | ||
| denom: self.to_string(), | ||
| })? | ||
| .trim_start_matches('/') |
There was a problem hiding this comment.
Should we maybe check at this point that the stripped prefix is either empty or starts with / and return an error if not? Just now this would allow stripping e.g. p0/p from p0/p1 leaving 1 which probably isn't desirable?
Either way - I guess a test for that case would be useful.
SuperFluffy
left a comment
There was a problem hiding this comment.
I think the way the prefix construction works this code will always problematic. I have rewritten it here: #1162
| /// # Errors | ||
| /// | ||
| /// - if the denom does not have the given prefix. | ||
| pub fn remove_prefix(&self, prefix: &str) -> Result<Self, InvalidPrefixToRemove> { |
There was a problem hiding this comment.
I believe this function is fundamentally broken and very hard to fix. I have refactored denom parsing here: #1162
Context:
Assume I have this input "some/denomination/trace", then Denom::from("some/denomination/trace") will give me:
my_denom := Denom {
prefix: "some/denomination",
trace: "trace",
}
Now, my_denom.is_prefix_by("some/denom") == true. But even worse, I can get this:
stripped_denom
:= my_denom.remove_prefix("some/denom")
= Denom {
prefix: "ination",
trace: "trace",
}
…1162) ## Summary Rewrites the IBC denomation construction by parsing its prefix into segments. This fixes the incorrect removal of prefxies for source non-refund ics20 packets. ## Background follow up to #851 which was an audit fix - this fixes the fix by removing the source prefix correctly, in the case assets we're the source of are transferred in. previously, the entire prefix was removed, would would have been invalid if the asset was prefixed by multiple hops, eg. transfer/channel-1/transfer/channel-2/denom would have become just denom when it should have become transfer/channel-2/denom. ## Changes - refactor `astria_core::primitive::1::asset::Denom` to express its prefix as a list of segments - ensure that only full prefix segments are removed - make `Denom` construction a fallible operation by parsing it - removed a bunch of constructors as they were only used in tests and not otherwise ## Testing Provide exhaustive units. ## Related Issues Replaces #1131
…#1162) ## Summary Rewrites the IBC denomation construction by parsing its prefix into segments. This fixes the incorrect removal of prefxies for source non-refund ics20 packets. ## Background follow up to astriaorg/astria#851 which was an audit fix - this fixes the fix by removing the source prefix correctly, in the case assets we're the source of are transferred in. previously, the entire prefix was removed, would would have been invalid if the asset was prefixed by multiple hops, eg. transfer/channel-1/transfer/channel-2/denom would have become just denom when it should have become transfer/channel-2/denom. ## Changes - refactor `astria_core::primitive::1::asset::Denom` to express its prefix as a list of segments - ensure that only full prefix segments are removed - make `Denom` construction a fallible operation by parsing it - removed a bunch of constructors as they were only used in tests and not otherwise ## Testing Provide exhaustive units. ## Related Issues Replaces astriaorg/astria#1131
Summary
follow up to #851 which was an audit fix - this fixes the fix by removing the source prefix correctly, in the case assets we're the source of are transferred in. previously, the entire prefix was removed, would would have been invalid if the asset was prefixed by multiple hops, eg.
transfer/channel-1/transfer/channel-2/denomwould have become justdenomwhen it should have becometransfer/channel-2/denom.Background
audit fix
Changes
Testing
unit tests