Skip to content

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

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

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.

Copy link
Contributor Author

@rflechtner rflechtner Nov 14, 2024

Choose a reason for hiding this comment

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

UPDATE: Since 08b71c4 we no longer have new cases of overflow introduced with this fix.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With 08b71c4 overflow issues are now a thing of the past.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since 08b71c4 large denominations do not result in an overflow any more. Note that of course truncation can still happen with large denominations if the fixed type is not able to offer enough digits behind the comma.

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
@rflechtner rflechtner requested a review from ntn-x2 November 12, 2024 14:16
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
Contributor

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
Contributor

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.

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 new solution does not really put a limit on the maximum denomination - as long as 10^denomination can be represented in a U256 - but if the denomination is larger than the precision of the fixed, we will experience truncation of balance amounts during scaling, which I documented here.

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 also added an integrity test to check this, as suggested: 3a2306b

@rflechtner rflechtner merged commit 6b3dcf5 into pallet_bonded_coins Nov 14, 2024
1 of 7 checks passed
@rflechtner rflechtner deleted the fix-denomination-arithmetic branch November 14, 2024 14:12
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.

4 participants