Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: denomination arithmetic #794

Open
wants to merge 6 commits into
base: pallet_bonded_coins
Choose a base branch
from

Conversation

rflechtner
Copy link
Contributor

@rflechtner rflechtner commented Nov 11, 2024

Adresses issues with precision stemming from integer division in convert_to_fixed and suggests a strategy to mitigate overflow problems on refund_account.

Checklist:

  • I have verified that the code works
    • No panics! (checked arithmetic ops, no indexing array[3] use get(3), ...)
  • I have verified that the code is easy to understand
    • If not, I have left a well-balanced amount of inline comments
  • I have left the code in a better state
  • I have documented the changes (where applicable)
    • Either PR or Ticket to update the Docs
    • Link the PR/Ticket here

@rflechtner rflechtner mentioned this pull request Nov 11, 2024
6 tasks
@rflechtner rflechtner self-assigned this Nov 11, 2024
Comment on lines 132 to 133
// This WILL overflow if 10^denomination > capacity(float)
.checked_div(decimals.checked_to_fixed().ok_or(ArithmeticError::Overflow)?)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The solution comes at the cost of one additional case where overflow can happen:

  • Previously, overflow could happen where capacity(balance) / 10^denomination > capacity(fixed), which for a u128 balance (capacity ~3.4e+38) and a I75F53 (capacity ~1.8e+22) meant that any denomination >= 17 is safe.
  • With the current solution, overflow will occur where 10^denomination > capacity(float), which for an I75F53 means that we have a hard cap at a denomination of 22.

We can possibly soften the hard cap, e.g. by iteratively dividing the remainder by 10 (or some other multiplicative component of decimals which can be represented), but denominations larger than the soft cap would progressively increase the probability of an overflow occurring.

For example, with an I75F53 and a denomination of 23, any amount ~1.8e+22 < x < e+23 would result in a remainder that cannot be represented and thus in an overflow error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Softened the cap on 694334c, which I think is a better way of scaling a fixed by an integer.
Still, I introduced a constant to limit allowable denominations on e94104e.

Comment on lines +701 to +703
.checked_mul(&burnt)
// This can indeed overflow if (sum_of_issuances - 1) * burnt > capacity(u128)
.ok_or(ArithmeticError::Overflow)?
Copy link
Contributor Author

@rflechtner rflechtner Nov 11, 2024

Choose a reason for hiding this comment

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

Very large sum_of_issuances make large remainders more likely, which can in turn result in a product that is too large.
We can accept that refunds would sometimes fail, which would require accounts with lower balances to refund first (after which the situation may have changed).
Or we can look for fallback strategies, such as:

  • saturating multiplication, which could mean that people receive only a fraction of the funds they would be entitled to
  • scaling, which sacrifices precision
  • partial refunds, where we only burn the amount of funds for which we can properly compute the collateral to be refunded, and leave the rest untouched.

Comment on lines 103 to 110
#[test]
fn test_convert_to_fixed_too_large_denomination() {
let x = u128::MAX; // around 3.4e+38
let denomination = 30u8; // I75F53 should handle around 1.8e+22, this can't be represented

let result = convert_to_fixed::<Test>(x, denomination);
assert_ok!(result);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this test fails, as a reminder that the proposed solution is still imperfect

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Due to 694334c this test now passes; but not for u128::MAX, which results in an overflowing remainder.

Comment on lines +689 to +690
// divide first to avoid overflow, remainder will be handled seperately
let divisible_part = total_collateral_issuance
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I realised that the approach of handling remainders separately can also improve the calculations of how much collateral is refunded, preventing overflow in many (but not all) cases.

@rflechtner rflechtner marked this pull request as ready for review November 12, 2024 14:16

#[pallet::constant]
type MaxStringLength: Get<u32>;

/// The maximum denomination that bonded currencies can use. To ensure
/// safe operation, this should be set so that 10^MaxDenomination <
Copy link
Member

Choose a reason for hiding this comment

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

If you want to make sure that only these values are specified here, you can add an integrity test (available with the try-runtime feature) that only checks the values of the Config implementations for any given runtime, according to the specified rules. You can check an example here: https://github.com/paritytech/polkadot-sdk/blob/a875f1803253e469288f1a905e73861fb600bb28/polkadot/runtime/parachains/src/configuration.rs#L1258.

Copy link
Member

Choose a reason for hiding this comment

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

This is to protect against malformed runtime-injected values inside a config trait, not too much about user-provided values.

pallets/pallet-bonded-coins/src/lib.rs Show resolved Hide resolved
pallets/pallet-bonded-coins/src/lib.rs Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants