Skip to content

fix(sequencer): fix prefix removal when receiving source non-refund ics20 packet #1131

Closed
noot wants to merge 10 commits intomainfrom
noot/ibc-prefix
Closed

fix(sequencer): fix prefix removal when receiving source non-refund ics20 packet #1131
noot wants to merge 10 commits intomainfrom
noot/ibc-prefix

Conversation

@noot
Copy link
Copy Markdown
Contributor

@noot noot commented Jun 1, 2024

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/denom would have become just denom when it should have become transfer/channel-2/denom.

Background

audit fix

Changes

  • fix prefix removal when receiving source non-refund ics20 packet

Testing

unit tests

@noot noot requested a review from a team as a code owner June 1, 2024 01:53
@noot noot requested a review from SuperFluffy June 1, 2024 01:53
@github-actions github-actions bot added the sequencer pertaining to the astria-sequencer crate label Jun 1, 2024
Copy link
Copy Markdown
Contributor

@SuperFluffy SuperFluffy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a lazy constructor to avoid allocation on every single call (i.e. even if you don't end up hitting the error case):

Suggested change
.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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

@noot noot Jun 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add unit tests for this functionality?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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}.

Copy link
Copy Markdown
Contributor Author

@noot noot Jun 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@noot
Copy link
Copy Markdown
Contributor Author

noot commented Jun 5, 2024

@SuperFluffy refactored remove_prefix and added unit tests!

@noot noot requested a review from SuperFluffy June 5, 2024 16:39
@noot noot requested a review from Fraser999 June 5, 2024 21:30

let new_prefix = self
.prefix
.clone()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need the clone here

prefix: prefix.to_string(),
denom: self.to_string(),
})?
.trim_start_matches('/')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@SuperFluffy SuperFluffy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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",
}

@noot noot closed this Jun 7, 2024
github-merge-queue bot pushed a commit that referenced this pull request Jun 10, 2024
…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
@noot noot deleted the noot/ibc-prefix branch November 7, 2024 16:47
sgranfield4403-3 added a commit to sgranfield4403-3/astria that referenced this pull request Oct 2, 2025
…#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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sequencer pertaining to the astria-sequencer crate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants