Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions crates/float/abi/DecimalFloat.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions crates/float/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ pub enum DecimalFloatErrorSelector {
CoefficientOverflow,
ExponentOverflow,
ExponentUnderflow,
FixedDecimalOverflow,
Log10Negative,
Log10Zero,
LossyConversionFromFloat,
Expand All @@ -61,6 +62,9 @@ impl TryFrom<FixedBytes<4>> for DecimalFloatErrorSelector {
}
<DecimalFloat::ExponentOverflow as SolError>::SELECTOR => Ok(Self::ExponentOverflow),
<DecimalFloat::ExponentUnderflow as SolError>::SELECTOR => Ok(Self::ExponentUnderflow),
<DecimalFloat::FixedDecimalOverflow as SolError>::SELECTOR => {
Ok(Self::FixedDecimalOverflow)
}
<DecimalFloat::Log10Negative as SolError>::SELECTOR => Ok(Self::Log10Negative),
<DecimalFloat::Log10Zero as SolError>::SELECTOR => Ok(Self::Log10Zero),
<DecimalFloat::LossyConversionFromFloat as SolError>::SELECTOR => {
Expand Down
7 changes: 7 additions & 0 deletions src/error/ErrDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ error ExponentUnderflow(int256 signedCoefficient, int256 exponent);
/// fixed-point number.
error NegativeFixedDecimalConversion(int256 signedCoefficient, int256 exponent);

/// @dev Thrown when converting a Float to a fixed-decimal uint256 and the
/// scaled value exceeds `uint256.max`. Returning a silent zero with
/// `lossless=false` would decapitate the high bits of the value;
/// reverting surfaces the overflow with the original inputs so callers can
/// rescale or reject.
error FixedDecimalOverflow(int256 signedCoefficient, int256 exponent, uint8 decimals);

/// @dev Thrown when attempting to calculate the log of 0.
error Log10Zero();

Expand Down
27 changes: 20 additions & 7 deletions src/lib/LibDecimalFloat.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ExponentOverflow,
ExponentUnderflow,
CoefficientOverflow,
FixedDecimalOverflow,
NegativeFixedDecimalConversion,
LossyConversionFromFloat,
LossyConversionToFloat,
Expand Down Expand Up @@ -242,14 +243,26 @@ library LibDecimalFloat {
return (fixedDecimal, fixedDecimal * scale == unsignedCoefficient);
}
} else if (finalExponent > 0) {
// finalExponent is positive here.
// forge-lint: disable-next-line(unsafe-typecast)
scale = 10 ** uint256(finalExponent);
fixedDecimal = unsignedCoefficient * scale;
unchecked {
// This is always lossless because we're scaling up.
// If the value is too large to fit in a uint256, we'll
// revert above due to overflow.
// The smallest non-zero coefficient times 10^78 already
// exceeds uint256.max, so any finalExponent > 77 cannot
// be represented as a uint256.
if (finalExponent > 77) {
revert FixedDecimalOverflow(signedCoefficient, exponent, decimals);
}

// finalExponent in [1, 77] keeps 10 ** finalExponent
// within uint256.
// forge-lint: disable-next-line(unsafe-typecast)
scale = 10 ** uint256(finalExponent);

// Pre-check the multiplication; the alternative is a
// bare Panic(0x11) from checked-math, which carries no
// structured information about which inputs overflowed.
if (unsignedCoefficient > type(uint256).max / scale) {
revert FixedDecimalOverflow(signedCoefficient, exponent, decimals);
}
fixedDecimal = unsignedCoefficient * scale;
return (fixedDecimal, true);
}
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/deploy/LibDecimalFloatDeploy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ library LibDecimalFloatDeploy {
/// @dev Address of the DecimalFloat contract deployed via Zoltu's
/// deterministic deployment proxy.
/// This address is the same across all EVM-compatible networks.
address constant ZOLTU_DEPLOYED_DECIMAL_FLOAT_ADDRESS = address(0x588F097a34D611D358c923087cBA5CB75165336A);
address constant ZOLTU_DEPLOYED_DECIMAL_FLOAT_ADDRESS = address(0xBee0eEFaffD046c9602109eB30A858Be301CC926);

/// @dev The expected codehash of the DecimalFloat contract deployed via
/// Zoltu's deterministic deployment proxy.
bytes32 constant DECIMAL_FLOAT_CONTRACT_HASH = 0xa44a59b43daa055502bcea92033fdaf7754a7b6ac1cccf4eb5ccbc0d04e9fb28;
bytes32 constant DECIMAL_FLOAT_CONTRACT_HASH = 0x7a93d0311f7782b44157ba40e94ec936085ebe001c7893bdd74911c8351d3def;

/// Combines all log and anti-log tables into a single bytes array for
/// deployment. These are using packed encoding to minimize size and remove
Expand Down
21 changes: 21 additions & 0 deletions test/src/concrete/DecimalFloat.toFixedDecimalLossy.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pragma solidity =0.8.25;
import {LogTest} from "test/abstract/LogTest.sol";
import {DecimalFloat} from "src/concrete/DecimalFloat.sol";
import {LibDecimalFloat, Float} from "src/lib/LibDecimalFloat.sol";
import {FixedDecimalOverflow} from "src/error/ErrDecimalFloat.sol";

contract DecimalFloatToFixedDecimalLossyTest is LogTest {
using LibDecimalFloat for Float;
Expand All @@ -13,6 +14,26 @@ contract DecimalFloatToFixedDecimalLossyTest is LogTest {
return LibDecimalFloat.toFixedDecimalLossy(packed, decimals);
}

/// `toFixedDecimalLossy` reverts with `FixedDecimalOverflow` instead of
/// `Panic(0x11)` when the positive-exponent scaling would overflow
/// uint256. Two distinct overflow paths: `10 ** finalExponent` itself
/// (exponent > 77) and `coefficient * scale` (coefficient too large for
/// the chosen scale). Reverting rather than returning a silent zero
/// avoids decapitating the high bits of a real value.
function testToFixedDecimalLossyPositiveExponentScaleOverflow() external {
Float aboveScaleLimit = LibDecimalFloat.packLossless(1, 78);
vm.expectRevert(abi.encodeWithSelector(FixedDecimalOverflow.selector, int256(1), int256(78), uint8(0)));
this.toFixedDecimalLossyExternal(aboveScaleLimit, 0);
}

function testToFixedDecimalLossyPositiveExponentMulOverflow() external {
Float coefficientOverflowsScale = LibDecimalFloat.packLossless(type(int224).max, 50);
vm.expectRevert(
abi.encodeWithSelector(FixedDecimalOverflow.selector, int256(type(int224).max), int256(50), uint8(0))
);
this.toFixedDecimalLossyExternal(coefficientOverflowsScale, 0);
}

function testToFixedDecimalLossyDeployed(Float packed, uint8 decimals) external {
DecimalFloat deployed = new DecimalFloat();

Expand Down
5 changes: 3 additions & 2 deletions test/src/lib/LibDecimalFloat.decimal.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
pragma solidity =0.8.25;

import {LibDecimalFloat, ExponentOverflow, NegativeFixedDecimalConversion, Float} from "src/lib/LibDecimalFloat.sol";
import {Test, stdError} from "forge-std-1.16.1/src/Test.sol";
import {FixedDecimalOverflow} from "src/error/ErrDecimalFloat.sol";
import {Test} from "forge-std-1.16.1/src/Test.sol";

contract LibDecimalFloatDecimalTest is Test {
using LibDecimalFloat for Float;
Expand Down Expand Up @@ -305,7 +306,7 @@ contract LibDecimalFloatDecimalTest is Test {
uint256 c = scale * unsignedCoefficient;
vm.assume(c / scale != unsignedCoefficient);
}
vm.expectRevert(stdError.arithmeticError);
vm.expectRevert(abi.encodeWithSelector(FixedDecimalOverflow.selector, signedCoefficient, exponent, decimals));
(uint256 value, bool lossless) = this.toFixedDecimalLossyExternal(signedCoefficient, exponent, decimals);
(value, lossless);
}
Expand Down
Loading