diff --git a/contracts/libraries/AssetsAccounting.sol b/contracts/libraries/AssetsAccounting.sol index c47af26..a57b176 100644 --- a/contracts/libraries/AssetsAccounting.sol +++ b/contracts/libraries/AssetsAccounting.sol @@ -167,8 +167,10 @@ library AssetsAccounting { _checkNonZeroShares(stETHSharesToWithdraw); assets.stETHLockedShares = SharesValues.ZERO; - ethWithdrawn = - SharesValues.calcETHValue(self.stETHTotals.claimedETH, stETHSharesToWithdraw, self.stETHTotals.lockedShares); + ethWithdrawn = ETHValues.from( + self.stETHTotals.claimedETH.toUint256() * stETHSharesToWithdraw.toUint256() + / self.stETHTotals.lockedShares.toUint256() + ); emit ETHWithdrawn(holder, stETHSharesToWithdraw, ethWithdrawn); } diff --git a/contracts/libraries/DualGovernanceConfig.sol b/contracts/libraries/DualGovernanceConfig.sol index 4c72389..f95f3d1 100644 --- a/contracts/libraries/DualGovernanceConfig.sol +++ b/contracts/libraries/DualGovernanceConfig.sol @@ -119,9 +119,9 @@ library DualGovernanceConfig { return vetoSignallingMinDuration + Durations.from( - PercentD16.unwrap(rageQuitSupport - firstSealRageQuitSupport) + (rageQuitSupport - firstSealRageQuitSupport).toUint256() * (vetoSignallingMaxDuration - vetoSignallingMinDuration).toSeconds() - / PercentD16.unwrap(secondSealRageQuitSupport - firstSealRageQuitSupport) + / (secondSealRageQuitSupport - firstSealRageQuitSupport).toUint256() ); } diff --git a/contracts/types/Duration.sol b/contracts/types/Duration.sol index f095cd7..768f669 100644 --- a/contracts/types/Duration.sol +++ b/contracts/types/Duration.sol @@ -3,20 +3,37 @@ pragma solidity 0.8.26; import {Timestamp, Timestamps} from "./Timestamp.sol"; +// --- +// Type Definition +// --- + type Duration is uint32; +// --- +// Assign Global Operations +// --- + +using {lt as <, lte as <=, eq as ==, neq as !=, gte as >=, gt as >} for Duration global; +using {addTo, plusSeconds, minusSeconds, multipliedBy, dividedBy, toSeconds} for Duration global; +using {plus as +, minus as -} for Duration global; + +// --- +// Errors +// --- + +error DivisionByZero(); error DurationOverflow(); error DurationUnderflow(); -// the max possible duration is ~ 106 years -uint256 constant MAX_VALUE = type(uint32).max; +// --- +// Constants +// --- -using {lt as <, lte as <=, gt as >, gte as >=, eq as ==, notEq as !=} for Duration global; -using {plus as +, minus as -} for Duration global; -using {addTo, plusSeconds, minusSeconds, multipliedBy, dividedBy, toSeconds} for Duration global; +/// @dev The maximum possible duration is approximately 136 years (assuming 365 days per year). +uint32 constant MAX_DURATION_VALUE = type(uint32).max; // --- -// Comparison Ops +// Comparison Operations // --- function lt(Duration d1, Duration d2) pure returns (bool) { @@ -27,20 +44,28 @@ function lte(Duration d1, Duration d2) pure returns (bool) { return Duration.unwrap(d1) <= Duration.unwrap(d2); } -function gt(Duration d1, Duration d2) pure returns (bool) { - return Duration.unwrap(d1) > Duration.unwrap(d2); +function eq(Duration d1, Duration d2) pure returns (bool) { + return Duration.unwrap(d1) == Duration.unwrap(d2); +} + +function neq(Duration d1, Duration d2) pure returns (bool) { + return Duration.unwrap(d1) != Duration.unwrap(d2); } function gte(Duration d1, Duration d2) pure returns (bool) { return Duration.unwrap(d1) >= Duration.unwrap(d2); } -function eq(Duration d1, Duration d2) pure returns (bool) { - return Duration.unwrap(d1) == Duration.unwrap(d2); +function gt(Duration d1, Duration d2) pure returns (bool) { + return Duration.unwrap(d1) > Duration.unwrap(d2); } -function notEq(Duration d1, Duration d2) pure returns (bool) { - return !(d1 == d2); +// --- +// Conversion Operations +// --- + +function toSeconds(Duration d) pure returns (uint256) { + return Duration.unwrap(d); } // --- @@ -48,67 +73,84 @@ function notEq(Duration d1, Duration d2) pure returns (bool) { // --- function plus(Duration d1, Duration d2) pure returns (Duration) { - return toDuration(Duration.unwrap(d1) + Duration.unwrap(d2)); + unchecked { + /// @dev Both `d1.toSeconds()` and `d2.toSeconds()` are <= type(uint32).max. Therefore, their + /// sum is <= type(uint256).max. + return Durations.from(d1.toSeconds() + d2.toSeconds()); + } } function minus(Duration d1, Duration d2) pure returns (Duration) { - if (d1 < d2) { + uint256 d1Seconds = d1.toSeconds(); + uint256 d2Seconds = d2.toSeconds(); + + if (d1Seconds < d2Seconds) { revert DurationUnderflow(); } - return Duration.wrap(Duration.unwrap(d1) - Duration.unwrap(d2)); + + unchecked { + /// @dev Subtraction is safe because `d1Seconds` >= `d2Seconds`. + /// Both `d1Seconds` and `d2Seconds` <= `type(uint32).max`, so the difference fits within `uint32`. + return Duration.wrap(uint32(d1Seconds - d2Seconds)); + } } -function plusSeconds(Duration d, uint256 seconds_) pure returns (Duration) { - return toDuration(Duration.unwrap(d) + seconds_); +// --- +// Custom Operations +// --- + +function plusSeconds(Duration d, uint256 secondsToAdd) pure returns (Duration) { + return Durations.from(d.toSeconds() + secondsToAdd); } -function minusSeconds(Duration d, uint256 seconds_) pure returns (Duration) { - uint256 durationValue = Duration.unwrap(d); - if (durationValue < seconds_) { +function minusSeconds(Duration d, uint256 secondsToSubtract) pure returns (Duration) { + uint256 durationSeconds = d.toSeconds(); + + if (durationSeconds < secondsToSubtract) { revert DurationUnderflow(); } - return Duration.wrap(uint32(durationValue - seconds_)); + + unchecked { + /// @dev Subtraction is safe because `durationSeconds` >= `secondsToSubtract`. + /// Both `durationSeconds` and `secondsToSubtract` <= `type(uint32).max`, + /// so the difference fits within `uint32`. + return Duration.wrap(uint32(durationSeconds - secondsToSubtract)); + } } function dividedBy(Duration d, uint256 divisor) pure returns (Duration) { - return Duration.wrap(uint32(Duration.unwrap(d) / divisor)); + if (divisor == 0) { + revert DivisionByZero(); + } + return Duration.wrap(uint32(d.toSeconds() / divisor)); } function multipliedBy(Duration d, uint256 multiplicand) pure returns (Duration) { - return toDuration(Duration.unwrap(d) * multiplicand); + return Durations.from(multiplicand * d.toSeconds()); } function addTo(Duration d, Timestamp t) pure returns (Timestamp) { - return Timestamps.from(t.toSeconds() + d.toSeconds()); + unchecked { + /// @dev Both `t.toSeconds()` <= `type(uint40).max` and `d.toSeconds()` <= `type(uint32).max`, so their + /// sum fits within `uint256`. + return Timestamps.from(t.toSeconds() + d.toSeconds()); + } } // --- -// Conversion Ops +// Namespaced Helper Methods // --- -function toDuration(uint256 value) pure returns (Duration) { - if (value > MAX_VALUE) { - revert DurationOverflow(); - } - return Duration.wrap(uint32(value)); -} - -function toSeconds(Duration d) pure returns (uint256) { - return Duration.unwrap(d); -} - library Durations { Duration internal constant ZERO = Duration.wrap(0); - Duration internal constant MIN = ZERO; - Duration internal constant MAX = Duration.wrap(uint32(MAX_VALUE)); - - function from(uint256 seconds_) internal pure returns (Duration res) { - res = toDuration(seconds_); - } - - function between(Timestamp t1, Timestamp t2) internal pure returns (Duration res) { - res = toDuration(t1.toSeconds() - t2.toSeconds()); + function from(uint256 durationInSeconds) internal pure returns (Duration res) { + if (durationInSeconds > MAX_DURATION_VALUE) { + revert DurationOverflow(); + } + /// @dev Casting `durationInSeconds` to `uint32` is safe as the check ensures it is less than or equal + /// to `MAX_DURATION_VALUE`, which fits within the `uint32`. + res = Duration.wrap(uint32(durationInSeconds)); } function min(Duration d1, Duration d2) internal pure returns (Duration res) { diff --git a/contracts/types/ETHValue.sol b/contracts/types/ETHValue.sol index 9a85a1b..9cf8f30 100644 --- a/contracts/types/ETHValue.sol +++ b/contracts/types/ETHValue.sol @@ -3,38 +3,41 @@ pragma solidity 0.8.26; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +// --- +// Type Definition +// --- + type ETHValue is uint128; +// --- +// Assign Global Operations +// --- + +using {lt as <, eq as ==, neq as !=, gt as >} for ETHValue global; +using {toUint256, sendTo} for ETHValue global; +using {plus as +, minus as -} for ETHValue global; + +// --- +// Errors +// --- + error ETHValueOverflow(); error ETHValueUnderflow(); -using {plus as +, minus as -, lt as <, gt as >, eq as ==, neq as !=} for ETHValue global; -using {toUint256} for ETHValue global; -using {sendTo} for ETHValue global; +// --- +// Constants +// --- -function sendTo(ETHValue value, address payable recipient) { - Address.sendValue(recipient, value.toUint256()); -} +uint128 constant MAX_ETH_VALUE = type(uint128).max; -function plus(ETHValue v1, ETHValue v2) pure returns (ETHValue) { - return ETHValues.from(ETHValue.unwrap(v1) + ETHValue.unwrap(v2)); -} - -function minus(ETHValue v1, ETHValue v2) pure returns (ETHValue) { - if (v1 < v2) { - revert ETHValueUnderflow(); - } - return ETHValues.from(ETHValue.unwrap(v1) - ETHValue.unwrap(v2)); -} +// --- +// Comparison Operations +// --- function lt(ETHValue v1, ETHValue v2) pure returns (bool) { return ETHValue.unwrap(v1) < ETHValue.unwrap(v2); } -function gt(ETHValue v1, ETHValue v2) pure returns (bool) { - return ETHValue.unwrap(v1) > ETHValue.unwrap(v2); -} - function eq(ETHValue v1, ETHValue v2) pure returns (bool) { return ETHValue.unwrap(v1) == ETHValue.unwrap(v2); } @@ -43,17 +46,66 @@ function neq(ETHValue v1, ETHValue v2) pure returns (bool) { return ETHValue.unwrap(v1) != ETHValue.unwrap(v2); } +function gt(ETHValue v1, ETHValue v2) pure returns (bool) { + return ETHValue.unwrap(v1) > ETHValue.unwrap(v2); +} + +// --- +// Conversion Operations +// --- + function toUint256(ETHValue value) pure returns (uint256) { return ETHValue.unwrap(value); } +// --- +// Arithmetic Operations +// --- + +function plus(ETHValue v1, ETHValue v2) pure returns (ETHValue) { + unchecked { + /// @dev Both `v1.toUint256()` and `v2.toUint256()` are <= type(uint128).max. Therefore, their + /// sum is <= type(uint256).max. + return ETHValues.from(v1.toUint256() + v2.toUint256()); + } +} + +function minus(ETHValue v1, ETHValue v2) pure returns (ETHValue) { + uint256 v1Value = v1.toUint256(); + uint256 v2Value = v2.toUint256(); + + if (v1Value < v2Value) { + revert ETHValueUnderflow(); + } + + unchecked { + /// @dev Subtraction is safe because `v1Value` >= `v2Value`. + /// Both `v1Value` and `v2Value` <= `type(uint128).max`, so the difference fits within `uint128`. + return ETHValue.wrap(uint128(v1Value - v2Value)); + } +} + +// --- +// Custom Operations +// --- + +function sendTo(ETHValue value, address payable recipient) { + Address.sendValue(recipient, value.toUint256()); +} + +// --- +// Namespaced Helper Methods +// --- + library ETHValues { ETHValue internal constant ZERO = ETHValue.wrap(0); function from(uint256 value) internal pure returns (ETHValue) { - if (value > type(uint128).max) { + if (value > MAX_ETH_VALUE) { revert ETHValueOverflow(); } + /// @dev Casting `value` to `uint128` is safe as the check ensures it is less than or equal + /// to `MAX_ETH_VALUE`, which fits within the `uint128`. return ETHValue.wrap(uint128(value)); } diff --git a/contracts/types/IndexOneBased.sol b/contracts/types/IndexOneBased.sol index b080af7..538c0b9 100644 --- a/contracts/types/IndexOneBased.sol +++ b/contracts/types/IndexOneBased.sol @@ -1,23 +1,50 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +// --- +// Type Definition +// --- + type IndexOneBased is uint32; +// --- +// Assign Global Operations +// --- + +using {neq as !=} for IndexOneBased global; +using {isEmpty, isNotEmpty, toZeroBasedValue} for IndexOneBased global; + +// --- +// Errors +// --- + error IndexOneBasedOverflow(); error IndexOneBasedUnderflow(); -using {neq as !=, isEmpty, isNotEmpty, toZeroBasedValue} for IndexOneBased global; +// --- +// Constants +// --- + +uint32 constant MAX_INDEX_ONE_BASED_VALUE = type(uint32).max; + +// --- +// Comparison Operations +// --- function neq(IndexOneBased i1, IndexOneBased i2) pure returns (bool) { return IndexOneBased.unwrap(i1) != IndexOneBased.unwrap(i2); } +// --- +// Custom Operations +// --- + function isEmpty(IndexOneBased index) pure returns (bool) { return IndexOneBased.unwrap(index) == 0; } function isNotEmpty(IndexOneBased index) pure returns (bool) { - return IndexOneBased.unwrap(index) != 0; + return IndexOneBased.unwrap(index) > 0; } function toZeroBasedValue(IndexOneBased index) pure returns (uint256) { @@ -25,15 +52,26 @@ function toZeroBasedValue(IndexOneBased index) pure returns (uint256) { revert IndexOneBasedUnderflow(); } unchecked { + /// @dev Subtraction is safe because `index` is not zero. + /// The result fits within `uint32`, so casting to `uint256` is safe. return IndexOneBased.unwrap(index) - 1; } } +// --- +// Namespaced Helper Methods +// --- + library IndicesOneBased { function fromOneBasedValue(uint256 oneBasedIndexValue) internal pure returns (IndexOneBased) { - if (oneBasedIndexValue > type(uint32).max) { + if (oneBasedIndexValue == 0) { + revert IndexOneBasedUnderflow(); + } + if (oneBasedIndexValue > MAX_INDEX_ONE_BASED_VALUE) { revert IndexOneBasedOverflow(); } + /// @dev Casting `oneBasedIndexValue` to `uint32` is safe as the check ensures it is less than or equal + /// to `MAX_INDEX_ONE_BASED_VALUE`, which fits within the `uint32`. return IndexOneBased.wrap(uint32(oneBasedIndexValue)); } } diff --git a/contracts/types/PercentD16.sol b/contracts/types/PercentD16.sol index c865cb9..162ca6e 100644 --- a/contracts/types/PercentD16.sol +++ b/contracts/types/PercentD16.sol @@ -1,13 +1,39 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -type PercentD16 is uint256; +// --- +// Type Definition +// --- -uint256 constant HUNDRED_PERCENTS_UINT256 = 100 * 10 ** 16; +type PercentD16 is uint128; -error Overflow(); +// --- +// Assign Global Operations +// --- -using {lt as <, lte as <=, gte as >=, gt as >, minus as -, plus as +} for PercentD16 global; +using {lt as <, lte as <=, eq as ==, gte as >=, gt as >} for PercentD16 global; +using {toUint256} for PercentD16 global; +using {minus as -, plus as +} for PercentD16 global; + +// --- +// Errors +// --- + +error DivisionByZero(); +error PercentD16Overflow(); +error PercentD16Underflow(); + +// --- +// Constants +// --- + +uint128 constant HUNDRED_PERCENT_BP = 100_00; +uint128 constant MAX_PERCENT_D16 = type(uint128).max; +uint128 constant HUNDRED_PERCENT_D16 = 100 * 10 ** 16; + +// --- +// Comparison Operations +// --- function lt(PercentD16 a, PercentD16 b) pure returns (bool) { return PercentD16.unwrap(a) < PercentD16.unwrap(b); @@ -17,31 +43,75 @@ function lte(PercentD16 a, PercentD16 b) pure returns (bool) { return PercentD16.unwrap(a) <= PercentD16.unwrap(b); } -function gt(PercentD16 a, PercentD16 b) pure returns (bool) { - return PercentD16.unwrap(a) > PercentD16.unwrap(b); +function eq(PercentD16 a, PercentD16 b) pure returns (bool) { + return PercentD16.unwrap(a) == PercentD16.unwrap(b); } function gte(PercentD16 a, PercentD16 b) pure returns (bool) { return PercentD16.unwrap(a) >= PercentD16.unwrap(b); } -function minus(PercentD16 a, PercentD16 b) pure returns (PercentD16) { - if (b > a) { - revert Overflow(); - } - return PercentD16.wrap(PercentD16.unwrap(a) - PercentD16.unwrap(b)); +function gt(PercentD16 a, PercentD16 b) pure returns (bool) { + return PercentD16.unwrap(a) > PercentD16.unwrap(b); +} + +// --- +// Conversion Operations +// --- + +function toUint256(PercentD16 value) pure returns (uint256) { + return PercentD16.unwrap(value); } +// --- +// Arithmetic Operations +// --- + function plus(PercentD16 a, PercentD16 b) pure returns (PercentD16) { - return PercentD16.wrap(PercentD16.unwrap(a) + PercentD16.unwrap(b)); + unchecked { + /// @dev Both `a.toUint256()` and `b.toUint256()` are <= type(uint128).max. Therefore, their + /// sum is <= type(uint256).max. + return PercentsD16.from(a.toUint256() + b.toUint256()); + } +} + +function minus(PercentD16 a, PercentD16 b) pure returns (PercentD16) { + uint256 aValue = a.toUint256(); + uint256 bValue = b.toUint256(); + + if (aValue < bValue) { + revert PercentD16Underflow(); + } + + unchecked { + /// @dev Subtraction is safe because `aValue` >= `bValue`. + /// Both `aValue` and `bValue` <= `type(uint128).max`, so the difference fits within `uint128`. + return PercentD16.wrap(uint128(aValue - bValue)); + } } +// --- +// Namespaced Helper Methods +// --- + library PercentsD16 { - function fromBasisPoints(uint256 bpValue) internal pure returns (PercentD16) { - return PercentD16.wrap(HUNDRED_PERCENTS_UINT256 * bpValue / 100_00); + function from(uint256 value) internal pure returns (PercentD16) { + if (value > MAX_PERCENT_D16) { + revert PercentD16Overflow(); + } + /// @dev Casting `value` to `uint128` is safe as the check ensures it is less than or equal + /// to `MAX_PERCENT_D16`, which fits within the `uint128`. + return PercentD16.wrap(uint128(value)); } function fromFraction(uint256 numerator, uint256 denominator) internal pure returns (PercentD16) { - return PercentD16.wrap(HUNDRED_PERCENTS_UINT256 * numerator / denominator); + if (denominator == 0) { + revert DivisionByZero(); + } + return from(HUNDRED_PERCENT_D16 * numerator / denominator); + } + + function fromBasisPoints(uint256 bpValue) internal pure returns (PercentD16) { + return from(HUNDRED_PERCENT_D16 * bpValue / HUNDRED_PERCENT_BP); } } diff --git a/contracts/types/SharesValue.sol b/contracts/types/SharesValue.sol index 1ff6045..a5aa466 100644 --- a/contracts/types/SharesValue.sol +++ b/contracts/types/SharesValue.sol @@ -1,22 +1,36 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {ETHValue, ETHValues} from "./ETHValue.sol"; +// --- +// Type Definition +// --- type SharesValue is uint128; -error SharesValueOverflow(); +// --- +// Assign Global Operations +// --- -using {plus as +, minus as -, eq as ==, lt as <} for SharesValue global; +using {lt as <, eq as ==} for SharesValue global; using {toUint256} for SharesValue global; +using {plus as +, minus as -} for SharesValue global; -function plus(SharesValue v1, SharesValue v2) pure returns (SharesValue) { - return SharesValue.wrap(SharesValue.unwrap(v1) + SharesValue.unwrap(v2)); -} +// --- +// Errors +// --- -function minus(SharesValue v1, SharesValue v2) pure returns (SharesValue) { - return SharesValue.wrap(SharesValue.unwrap(v1) - SharesValue.unwrap(v2)); -} +error SharesValueOverflow(); +error SharesValueUnderflow(); + +// --- +// Constants +// --- + +uint128 constant MAX_SHARES_VALUE = type(uint128).max; + +// --- +// Comparison Operations +// --- function lt(SharesValue v1, SharesValue v2) pure returns (bool) { return SharesValue.unwrap(v1) < SharesValue.unwrap(v2); @@ -26,25 +40,54 @@ function eq(SharesValue v1, SharesValue v2) pure returns (bool) { return SharesValue.unwrap(v1) == SharesValue.unwrap(v2); } +// --- +// Conversion Operations +// --- + function toUint256(SharesValue v) pure returns (uint256) { return SharesValue.unwrap(v); } +// --- +// Arithmetic Operations +// --- + +function plus(SharesValue v1, SharesValue v2) pure returns (SharesValue) { + unchecked { + /// @dev Both `v1.toUint256()` and `v2.toUint256()` are <= type(uint128).max. Therefore, their + /// sum is <= type(uint256).max. + return SharesValues.from(v1.toUint256() + v2.toUint256()); + } +} + +function minus(SharesValue v1, SharesValue v2) pure returns (SharesValue) { + uint256 v1Value = v1.toUint256(); + uint256 v2Value = v2.toUint256(); + + if (v1Value < v2Value) { + revert SharesValueUnderflow(); + } + + unchecked { + /// @dev Subtraction is safe because `v1Value` >= `v2Value`. + /// Both `v1Value` and `v2Value` <= `type(uint128).max`, so the difference fits within `uint128`. + return SharesValue.wrap(uint128(v1Value - v2Value)); + } +} + +// --- +// Namespaced Helper Methods +// --- + library SharesValues { SharesValue internal constant ZERO = SharesValue.wrap(0); function from(uint256 value) internal pure returns (SharesValue) { - if (value > type(uint128).max) { + if (value > MAX_SHARES_VALUE) { revert SharesValueOverflow(); } + /// @dev Casting `value` to `uint128` is safe as the check ensures it is less than or equal + /// to `MAX_SHARES_VALUE`, which fits within the `uint128`. return SharesValue.wrap(uint128(value)); } - - function calcETHValue( - ETHValue totalPooled, - SharesValue share, - SharesValue total - ) internal pure returns (ETHValue) { - return ETHValues.from(totalPooled.toUint256() * share.toUint256() / total.toUint256()); - } } diff --git a/contracts/types/Timestamp.sol b/contracts/types/Timestamp.sol index f153f71..7da5229 100644 --- a/contracts/types/Timestamp.sol +++ b/contracts/types/Timestamp.sol @@ -1,38 +1,38 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; +// --- +// Type Definition +// --- + type Timestamp is uint40; -error TimestampOverflow(); -error TimestampUnderflow(); +// --- +// Assign Global Operations +// --- -uint256 constant MAX_TIMESTAMP_VALUE = type(uint40).max; +using {lt as <, lte as <=, eq as ==, neq as !=, gte as >=, gt as >} for Timestamp global; +using {isZero, isNotZero, toSeconds} for Timestamp global; -using {lt as <} for Timestamp global; -using {gt as >} for Timestamp global; -using {gte as >=} for Timestamp global; -using {lte as <=} for Timestamp global; -using {eq as ==} for Timestamp global; -using {notEq as !=} for Timestamp global; +// --- +// Errors +// --- -using {isZero} for Timestamp global; -using {isNotZero} for Timestamp global; -using {toSeconds} for Timestamp global; +error TimestampOverflow(); // --- -// Comparison Ops +// Constants // --- -function lt(Timestamp t1, Timestamp t2) pure returns (bool) { - return Timestamp.unwrap(t1) < Timestamp.unwrap(t2); -} +/// @dev The maximum value for a `Timestamp`, corresponding to approximately the year 36812. +uint40 constant MAX_TIMESTAMP_VALUE = type(uint40).max; -function gt(Timestamp t1, Timestamp t2) pure returns (bool) { - return Timestamp.unwrap(t1) > Timestamp.unwrap(t2); -} +// --- +// Comparison Operations +// --- -function gte(Timestamp t1, Timestamp t2) pure returns (bool) { - return Timestamp.unwrap(t1) >= Timestamp.unwrap(t2); +function lt(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) < Timestamp.unwrap(t2); } function lte(Timestamp t1, Timestamp t2) pure returns (bool) { @@ -43,46 +43,62 @@ function eq(Timestamp t1, Timestamp t2) pure returns (bool) { return Timestamp.unwrap(t1) == Timestamp.unwrap(t2); } -function notEq(Timestamp t1, Timestamp t2) pure returns (bool) { - return !(t1 == t2); +function neq(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) != Timestamp.unwrap(t2); } -function isZero(Timestamp t) pure returns (bool) { - return Timestamp.unwrap(t) == 0; +function gte(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) >= Timestamp.unwrap(t2); } -function isNotZero(Timestamp t) pure returns (bool) { - return Timestamp.unwrap(t) != 0; +function gt(Timestamp t1, Timestamp t2) pure returns (bool) { + return Timestamp.unwrap(t1) > Timestamp.unwrap(t2); } // --- -// Conversion Ops +// Conversion Operations // --- function toSeconds(Timestamp t) pure returns (uint256) { return Timestamp.unwrap(t); } -uint256 constant MAX_VALUE = type(uint40).max; +// --- +// Custom Operations +// --- + +function isZero(Timestamp t) pure returns (bool) { + return Timestamp.unwrap(t) == 0; +} + +function isNotZero(Timestamp t) pure returns (bool) { + return Timestamp.unwrap(t) > 0; +} + +// --- +// Namespaced Helper Methods +// --- library Timestamps { Timestamp internal constant ZERO = Timestamp.wrap(0); - Timestamp internal constant MIN = ZERO; - Timestamp internal constant MAX = Timestamp.wrap(uint40(MAX_TIMESTAMP_VALUE)); + function from(uint256 timestampInSeconds) internal pure returns (Timestamp res) { + if (timestampInSeconds > MAX_TIMESTAMP_VALUE) { + revert TimestampOverflow(); + } - function max(Timestamp t1, Timestamp t2) internal pure returns (Timestamp) { - return t1 > t2 ? t1 : t2; + /// @dev Casting `timestampInSeconds` to `uint40` is safe as the check ensures it is less than or equal + /// to `MAX_TIMESTAMP_VALUE`, which fits within the `uint40`. + return Timestamp.wrap(uint40(timestampInSeconds)); } function now() internal view returns (Timestamp res) { + /// @dev Skipping the check that `block.timestamp` <= `MAX_TIMESTAMP_VALUE` for gas efficiency. + /// Overflow is possible only after approximately 34,000 years from the Unix epoch. res = Timestamp.wrap(uint40(block.timestamp)); } - function from(uint256 value) internal pure returns (Timestamp res) { - if (value > MAX_TIMESTAMP_VALUE) { - revert TimestampOverflow(); - } - return Timestamp.wrap(uint40(value)); + function max(Timestamp t1, Timestamp t2) internal pure returns (Timestamp) { + return t1 > t2 ? t1 : t2; } } diff --git a/test/unit/committees/EmergencyActivationCommittee.t.sol b/test/unit/committees/EmergencyActivationCommittee.t.sol index de423ce..31c9053 100644 --- a/test/unit/committees/EmergencyActivationCommittee.t.sol +++ b/test/unit/committees/EmergencyActivationCommittee.t.sol @@ -30,6 +30,7 @@ contract EmergencyActivationCommitteeUnitTest is UnitTest { uint256 _quorum, address _emergencyProtectedTimelock ) external { + vm.assume(_owner != address(0)); vm.assume(_quorum > 0 && _quorum <= committeeMembers.length); EmergencyActivationCommittee localCommittee = new EmergencyActivationCommittee(_owner, committeeMembers, _quorum, _emergencyProtectedTimelock); diff --git a/test/unit/committees/EmergencyExecutionCommittee.t.sol b/test/unit/committees/EmergencyExecutionCommittee.t.sol index 2f05da8..4b0325c 100644 --- a/test/unit/committees/EmergencyExecutionCommittee.t.sol +++ b/test/unit/committees/EmergencyExecutionCommittee.t.sol @@ -42,6 +42,7 @@ contract EmergencyExecutionCommitteeUnitTest is UnitTest { uint256 _quorum, address _emergencyProtectedTimelock ) external { + vm.assume(_owner != address(0)); vm.assume(_quorum > 0 && _quorum <= committeeMembers.length); EmergencyExecutionCommittee localCommittee = new EmergencyExecutionCommittee(_owner, committeeMembers, _quorum, _emergencyProtectedTimelock); diff --git a/test/unit/committees/TiebreakerSubCommittee.t.sol b/test/unit/committees/TiebreakerSubCommittee.t.sol index 774ae27..17a7575 100644 --- a/test/unit/committees/TiebreakerSubCommittee.t.sol +++ b/test/unit/committees/TiebreakerSubCommittee.t.sol @@ -43,6 +43,7 @@ contract TiebreakerSubCommitteeUnitTest is UnitTest { } function test_constructor_HappyPath(address _owner, uint256 _quorum, address _tiebreakerCore) external { + vm.assume(_owner != address(0)); vm.assume(_quorum > 0 && _quorum <= committeeMembers.length); new TiebreakerSubCommittee(_owner, committeeMembers, _quorum, _tiebreakerCore); } diff --git a/test/unit/libraries/AssetsAccounting.t.sol b/test/unit/libraries/AssetsAccounting.t.sol index 27c3642..b1a6af0 100644 --- a/test/unit/libraries/AssetsAccounting.t.sol +++ b/test/unit/libraries/AssetsAccounting.t.sol @@ -6,7 +6,7 @@ import {stdError} from "forge-std/StdError.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; import {ETHValue, ETHValues, ETHValueOverflow, ETHValueUnderflow} from "contracts/types/ETHValue.sol"; -import {SharesValue, SharesValues, SharesValueOverflow} from "contracts/types/SharesValue.sol"; +import {SharesValue, SharesValues, SharesValueOverflow, SharesValueUnderflow} from "contracts/types/SharesValue.sol"; import {IndicesOneBased} from "contracts/types/IndexOneBased.sol"; import {Durations} from "contracts/types/Duration.sol"; import {Timestamps} from "contracts/types/Timestamp.sol"; @@ -149,7 +149,7 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.stETHTotals.lockedShares = totalLockedShares; _accountingContext.assets[holder].stETHLockedShares = shares; - vm.expectRevert(stdError.arithmeticError); + vm.expectRevert(SharesValueUnderflow.selector); AssetsAccounting.accountStETHSharesUnlock(_accountingContext, holder, shares); } @@ -620,7 +620,7 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[0].isClaimed = false; _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; - vm.expectRevert(stdError.arithmeticError); + vm.expectRevert(SharesValueOverflow.selector); AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } @@ -644,7 +644,7 @@ contract AssetsAccountingUnitTests is UnitTest { withdrawalRequestStatuses[0].isClaimed = false; _accountingContext.unstETHRecords[unstETHIds[0]].status = UnstETHRecordStatus.NotLocked; - vm.expectRevert(stdError.arithmeticError); + vm.expectRevert(SharesValueOverflow.selector); AssetsAccounting.accountUnstETHLock(_accountingContext, holder, unstETHIds, withdrawalRequestStatuses); } @@ -880,7 +880,7 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(1); _accountingContext.assets[holder].unstETHIds.push(unstETHIds[0]); - vm.expectRevert(stdError.arithmeticError); + vm.expectRevert(SharesValueUnderflow.selector); AssetsAccounting.accountUnstETHUnlock(_accountingContext, holder, unstETHIds); } @@ -919,7 +919,7 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.unstETHRecords[unstETHIds[0]].index = IndicesOneBased.fromOneBasedValue(1); _accountingContext.assets[holder].unstETHIds.push(unstETHIds[0]); - vm.expectRevert(stdError.arithmeticError); + vm.expectRevert(SharesValueUnderflow.selector); AssetsAccounting.accountUnstETHUnlock(_accountingContext, holder, unstETHIds); } @@ -1111,7 +1111,7 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(123); claimableAmountsPrepared[0] = uint256(type(uint128).max - 2); - vm.expectRevert(stdError.arithmeticError); + vm.expectRevert(ETHValueOverflow.selector); AssetsAccounting.accountUnstETHFinalized(_accountingContext, unstETHIds, claimableAmountsPrepared); } @@ -1135,7 +1135,7 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.unstETHRecords[unstETHIds[0]].shares = SharesValues.from(type(uint64).max); claimableAmountsPrepared[0] = 1; - vm.expectRevert(stdError.arithmeticError); + vm.expectRevert(SharesValueUnderflow.selector); AssetsAccounting.accountUnstETHFinalized(_accountingContext, unstETHIds, claimableAmountsPrepared); } @@ -1410,7 +1410,7 @@ contract AssetsAccountingUnitTests is UnitTest { ETHValues.from(uint256(type(uint128).max) / 2 + 1); } - vm.expectRevert(stdError.arithmeticError); + vm.expectRevert(ETHValueOverflow.selector); AssetsAccounting.accountUnstETHWithdraw(_accountingContext, holder, unstETHIds); } @@ -1447,7 +1447,7 @@ contract AssetsAccountingUnitTests is UnitTest { _accountingContext.unstETHTotals.unfinalizedShares = totalUnfinalizedShares; _accountingContext.stETHTotals.lockedShares = totalLockedShares; - vm.expectRevert(stdError.arithmeticError); + vm.expectRevert(SharesValueOverflow.selector); AssetsAccounting.getLockedAssetsTotals(_accountingContext); } @@ -1495,14 +1495,6 @@ contract AssetsAccountingUnitTests is UnitTest { assertEq(_accountingContext.unstETHTotals.finalizedETH, finalizedETH); } - function assertEq(SharesValue a, SharesValue b) internal { - assertEq(a.toUint256(), b.toUint256()); - } - - function assertEq(ETHValue a, ETHValue b) internal { - assertEq(a.toUint256(), b.toUint256()); - } - function assertEq(UnstETHRecordStatus a, UnstETHRecordStatus b) internal { assertEq(uint256(a), uint256(b)); } diff --git a/test/unit/libraries/DualGovernanceConfig.t.sol b/test/unit/libraries/DualGovernanceConfig.t.sol index 5a17219..872567e 100644 --- a/test/unit/libraries/DualGovernanceConfig.t.sol +++ b/test/unit/libraries/DualGovernanceConfig.t.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.26; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; -import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Duration, Durations, MAX_DURATION_VALUE} from "contracts/types/Duration.sol"; import {PercentD16, PercentsD16} from "contracts/types/PercentD16.sol"; import {DualGovernanceConfig, PercentD16} from "contracts/libraries/DualGovernanceConfig.sol"; @@ -315,7 +315,7 @@ contract DualGovernanceConfigTest is UnitTest { vm.assume( config.rageQuitEthWithdrawalsMinDelay.toSeconds() - + config.rageQuitEthWithdrawalsDelayGrowth.toSeconds() * (rageQuitRound + 1) <= Durations.MAX.toSeconds() + + config.rageQuitEthWithdrawalsDelayGrowth.toSeconds() * (rageQuitRound + 1) <= MAX_DURATION_VALUE ); vm.assume(config.rageQuitEthWithdrawalsMinDelay <= config.rageQuitEthWithdrawalsMaxDelay); diff --git a/test/unit/libraries/EmergencyProtection.t.sol b/test/unit/libraries/EmergencyProtection.t.sol index 4789953..4fbb01a 100644 --- a/test/unit/libraries/EmergencyProtection.t.sol +++ b/test/unit/libraries/EmergencyProtection.t.sol @@ -34,7 +34,7 @@ contract EmergencyProtectionTest is UnitTest { function test_activateEmergencyMode_RevertOn_ProtectionExpired() external { Duration untilExpiration = - Durations.between(ctx.emergencyProtectionEndsAfter, Timestamps.from(block.timestamp)).plusSeconds(1); + Durations.from(ctx.emergencyProtectionEndsAfter.toSeconds() - Timestamps.now().toSeconds()).plusSeconds(1); _wait(untilExpiration); @@ -210,7 +210,7 @@ contract EmergencyProtectionTest is UnitTest { assertFalse(EmergencyProtection.isEmergencyModeDurationPassed(ctx)); Duration untilExpiration = - Durations.between(ctx.emergencyModeEndsAfter, Timestamps.from(block.timestamp)).plusSeconds(1); + Durations.from(ctx.emergencyModeEndsAfter.toSeconds() - Timestamps.now().toSeconds()).plusSeconds(1); _wait(untilExpiration); assertTrue(EmergencyProtection.isEmergencyModeDurationPassed(ctx)); @@ -220,7 +220,7 @@ contract EmergencyProtectionTest is UnitTest { assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); Duration untilExpiration = - Durations.between(ctx.emergencyProtectionEndsAfter, Timestamps.from(block.timestamp)).plusSeconds(1); + Durations.from(ctx.emergencyProtectionEndsAfter.toSeconds() - Timestamps.now().toSeconds()).plusSeconds(1); _wait(untilExpiration); assertFalse(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); @@ -232,13 +232,13 @@ contract EmergencyProtectionTest is UnitTest { assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); Duration untilExpiration = - Durations.between(ctx.emergencyModeEndsAfter, Timestamps.from(block.timestamp)).plusSeconds(1); + Durations.from(ctx.emergencyModeEndsAfter.toSeconds() - Timestamps.now().toSeconds()).plusSeconds(1); _wait(untilExpiration); assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); untilExpiration = - Durations.between(ctx.emergencyProtectionEndsAfter, Timestamps.from(block.timestamp)).plusSeconds(1); + Durations.from(ctx.emergencyProtectionEndsAfter.toSeconds() - Timestamps.now().toSeconds()).plusSeconds(1); assertTrue(EmergencyProtection.isEmergencyProtectionEnabled(ctx)); diff --git a/test/unit/libraries/EscrowState.t.sol b/test/unit/libraries/EscrowState.t.sol index 9018073..c768546 100644 --- a/test/unit/libraries/EscrowState.t.sol +++ b/test/unit/libraries/EscrowState.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.26; -import {Duration, Durations, MAX_VALUE as DURATION_MAX_VALUE} from "contracts/types/Duration.sol"; +import {Duration, Durations, MAX_DURATION_VALUE} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps, MAX_TIMESTAMP_VALUE, TimestampOverflow} from "contracts/types/Timestamp.sol"; import {EscrowState, State} from "contracts/libraries/EscrowState.sol"; @@ -195,14 +195,10 @@ contract EscrowStateUnitTests is UnitTest { _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; _context.rageQuitEthWithdrawalsDelay = rageQuitEthWithdrawalsDelay; - _wait( - Durations.between( - (rageQuitExtensionPeriodDuration + rageQuitEthWithdrawalsDelay).plusSeconds(1).addTo( - rageQuitExtensionPeriodStartedAt - ), - Timestamps.now() - ) - ); + Duration totalWithdrawalsDelay = rageQuitExtensionPeriodDuration + rageQuitEthWithdrawalsDelay; + Timestamp withdrawalsAllowedAt = totalWithdrawalsDelay.plusSeconds(1).addTo(rageQuitExtensionPeriodStartedAt); + + _wait(Durations.from(withdrawalsAllowedAt.toSeconds() - Timestamps.now().toSeconds())); EscrowState.checkEthWithdrawalsDelayPassed(_context); } @@ -226,12 +222,10 @@ contract EscrowStateUnitTests is UnitTest { _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; _context.rageQuitEthWithdrawalsDelay = rageQuitEthWithdrawalsDelay; - _wait( - Durations.between( - (rageQuitExtensionPeriodDuration + rageQuitEthWithdrawalsDelay).addTo(rageQuitExtensionPeriodStartedAt), - Timestamps.now() - ) - ); + Duration totalWithdrawalsDelay = rageQuitExtensionPeriodDuration + rageQuitEthWithdrawalsDelay; + Timestamp withdrawalsAllowedAfter = totalWithdrawalsDelay.addTo(rageQuitExtensionPeriodStartedAt); + + _wait(Durations.from(withdrawalsAllowedAfter.toSeconds() - Timestamps.now().toSeconds())); vm.expectRevert(EscrowState.EthWithdrawalsDelayNotPassed.selector); @@ -239,8 +233,8 @@ contract EscrowStateUnitTests is UnitTest { } function test_checkWithdrawalsDelayPassed_RevertWhen_EthWithdrawalsDelayOverflow() external { - Duration rageQuitExtensionPeriodDuration = Durations.from(DURATION_MAX_VALUE / 2); - Duration rageQuitEthWithdrawalsDelay = Durations.from(DURATION_MAX_VALUE / 2 + 1); + Duration rageQuitExtensionPeriodDuration = Durations.from(MAX_DURATION_VALUE / 2); + Duration rageQuitEthWithdrawalsDelay = Durations.from(MAX_DURATION_VALUE / 2 + 1); _context.rageQuitExtensionPeriodStartedAt = Timestamps.from(MAX_TIMESTAMP_VALUE - 1); _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; @@ -276,11 +270,10 @@ contract EscrowStateUnitTests is UnitTest { _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; - _wait( - Durations.between( - rageQuitExtensionPeriodDuration.plusSeconds(1).addTo(rageQuitExtensionPeriodStartedAt), Timestamps.now() - ) - ); + Timestamp rageQuitExtensionPeriodPassedAfter = + rageQuitExtensionPeriodDuration.plusSeconds(1).addTo(rageQuitExtensionPeriodStartedAt); + + _wait(Durations.from(rageQuitExtensionPeriodPassedAfter.toSeconds() - Timestamps.now().toSeconds())); bool res = EscrowState.isRageQuitExtensionPeriodPassed(_context); assertTrue(res); } @@ -296,9 +289,10 @@ contract EscrowStateUnitTests is UnitTest { _context.rageQuitExtensionPeriodStartedAt = rageQuitExtensionPeriodStartedAt; _context.rageQuitExtensionPeriodDuration = rageQuitExtensionPeriodDuration; - _wait( - Durations.between(rageQuitExtensionPeriodDuration.addTo(rageQuitExtensionPeriodStartedAt), Timestamps.now()) - ); + Timestamp rageQuitExtensionPeriodPassedAt = + rageQuitExtensionPeriodDuration.addTo(rageQuitExtensionPeriodStartedAt); + + _wait(Durations.from(rageQuitExtensionPeriodPassedAt.toSeconds() - Timestamps.now().toSeconds())); bool res = EscrowState.isRageQuitExtensionPeriodPassed(_context); assertFalse(res); } diff --git a/test/unit/libraries/ExecutableProposals.t.sol b/test/unit/libraries/ExecutableProposals.t.sol index 30dc25f..c67cff1 100644 --- a/test/unit/libraries/ExecutableProposals.t.sol +++ b/test/unit/libraries/ExecutableProposals.t.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.26; import {Vm} from "forge-std/Test.sol"; -import {Duration, Durations} from "contracts/types/Duration.sol"; +import {Duration, Durations, MAX_DURATION_VALUE} from "contracts/types/Duration.sol"; import {Timestamp, Timestamps} from "contracts/types/Timestamp.sol"; import {ITimelock} from "contracts/interfaces/ITimelock.sol"; @@ -71,7 +71,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function testFuzz_schedule_proposal(Duration delay) external { - vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); + vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); @@ -114,7 +114,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function testFuzz_cannot_schedule_proposal_before_delay_passed(Duration delay) external { - vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); + vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); @@ -137,7 +137,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function testFuzz_execute_proposal(Duration delay) external { - vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); + vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); @@ -203,7 +203,7 @@ contract ExecutableProposalsUnitTests is UnitTest { } function testFuzz_cannot_execute_before_delay_passed(Duration delay) external { - vm.assume(delay > Durations.ZERO && delay <= Durations.MAX); + vm.assume(delay > Durations.ZERO && delay.toSeconds() <= MAX_DURATION_VALUE); _proposals.submit(address(_executor), _getMockTargetRegularStaffCalls(address(_targetMock)), ""); uint256 proposalId = _proposals.getProposalsCount(); _proposals.schedule(proposalId, Durations.ZERO); diff --git a/test/unit/types/Duration.t.sol b/test/unit/types/Duration.t.sol new file mode 100644 index 0000000..d115173 --- /dev/null +++ b/test/unit/types/Duration.t.sol @@ -0,0 +1,336 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import { + Duration, + Durations, + MAX_DURATION_VALUE, + DivisionByZero, + DurationOverflow, + DurationUnderflow +} from "contracts/types/Duration.sol"; +import {Timestamp, TimestampOverflow, MAX_TIMESTAMP_VALUE} from "contracts/types/Timestamp.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract DurationTests is UnitTest { + // --- + // Comparison operations + // --- + + // --- + // lt() + // --- + + function test_lt_HappyPath_ReturnsTrue() external { + assertTrue(Duration.wrap(1) < Duration.wrap(2)); + assertTrue(Duration.wrap(0) < Durations.from(MAX_DURATION_VALUE)); + assertTrue(Durations.from(MAX_DURATION_VALUE - 1) < Durations.from(MAX_DURATION_VALUE)); + } + + function test_lt_HappyPath_ReturnsFalse() external { + assertFalse(Duration.wrap(0) < Duration.wrap(0)); + assertFalse(Duration.wrap(1) < Duration.wrap(0)); + assertFalse(Durations.from(MAX_DURATION_VALUE) < Durations.from(MAX_DURATION_VALUE - 1)); + } + + function testFuzz_lt_HappyPath(Duration d1, Duration d2) external { + assertEq(d1 < d2, d1.toSeconds() < d2.toSeconds()); + } + + // --- + // lte() + // --- + + function test_lte_HappyPath_ReturnsTrue() external { + assertTrue(Duration.wrap(0) <= Duration.wrap(0)); + assertTrue(Duration.wrap(1) <= Duration.wrap(2)); + assertTrue(Duration.wrap(2) <= Duration.wrap(2)); + assertTrue(Duration.wrap(0) <= Durations.from(MAX_DURATION_VALUE)); + assertTrue(Durations.from(MAX_DURATION_VALUE - 1) <= Durations.from(MAX_DURATION_VALUE)); + assertTrue(Durations.from(MAX_DURATION_VALUE) <= Durations.from(MAX_DURATION_VALUE)); + } + + function test_lte_HappyPath_ReturnsFalse() external { + assertFalse(Duration.wrap(1) <= Duration.wrap(0)); + assertFalse(Durations.from(MAX_DURATION_VALUE) <= Durations.from(MAX_DURATION_VALUE - 1)); + } + + function testFuzz_lte_HappyPath(Duration d1, Duration d2) external { + assertEq(d1 <= d2, d1.toSeconds() <= d2.toSeconds()); + } + + // --- + // eq() + // --- + + function test_eq_HappyPath_ReturnsTrue() external { + assertTrue(Duration.wrap(0) == Duration.wrap(0)); + assertTrue(Duration.wrap(MAX_DURATION_VALUE / 2) == Duration.wrap(MAX_DURATION_VALUE / 2)); + assertTrue(Duration.wrap(MAX_DURATION_VALUE) == Duration.wrap(MAX_DURATION_VALUE)); + } + + function test_eq_HappyPath_ReturnsFalse() external { + assertFalse(Duration.wrap(0) == Duration.wrap(1)); + assertFalse(Duration.wrap(MAX_DURATION_VALUE / 2) == Duration.wrap(MAX_DURATION_VALUE)); + assertFalse(Duration.wrap(0) == Duration.wrap(MAX_DURATION_VALUE)); + } + + function testFuzz_eq_HappyPath(Duration d1, Duration d2) external { + assertEq(d1 == d2, d1.toSeconds() == d2.toSeconds()); + } + + // --- + // neq() + // --- + + function test_neq_HappyPath_ReturnsTrue() external { + assertTrue(Duration.wrap(0) != Duration.wrap(1)); + assertTrue(Duration.wrap(MAX_DURATION_VALUE / 2) != Duration.wrap(MAX_DURATION_VALUE)); + assertTrue(Duration.wrap(0) != Duration.wrap(MAX_DURATION_VALUE)); + } + + function test_neq_HappyPath_ReturnsFalse() external { + assertFalse(Duration.wrap(0) != Duration.wrap(0)); + assertFalse(Duration.wrap(MAX_DURATION_VALUE / 2) != Duration.wrap(MAX_DURATION_VALUE / 2)); + assertFalse(Duration.wrap(MAX_DURATION_VALUE) != Duration.wrap(MAX_DURATION_VALUE)); + } + + function testFuzz_neq_HappyPath(Duration d1, Duration d2) external { + assertEq(d1 != d2, d1.toSeconds() != d2.toSeconds()); + } + + // --- + // gte + // --- + + function test_gte_HappyPath_ReturnsTrue() external { + assertTrue(Duration.wrap(0) >= Duration.wrap(0)); + assertTrue(Duration.wrap(5) >= Duration.wrap(3)); + assertTrue(Duration.wrap(MAX_DURATION_VALUE) >= Duration.wrap(MAX_DURATION_VALUE / 2)); + assertTrue(Duration.wrap(MAX_DURATION_VALUE) >= Duration.wrap(MAX_DURATION_VALUE)); + } + + function test_gte_HappyPath_ReturnsFalse() external { + assertFalse(Duration.wrap(0) >= Duration.wrap(1)); + assertFalse(Duration.wrap(5) >= Duration.wrap(9)); + assertFalse(Duration.wrap(MAX_DURATION_VALUE / 2) >= Duration.wrap(MAX_DURATION_VALUE)); + } + + function testFuzz_gte_HappyPath(Duration d1, Duration d2) external { + assertEq(d1 >= d2, d1.toSeconds() >= d2.toSeconds()); + } + + // --- + // gt + // --- + + function test_gt_HappyPath_ReturnsTrue() external { + assertTrue(Duration.wrap(1) > Duration.wrap(0)); + assertTrue(Duration.wrap(5) > Duration.wrap(3)); + assertTrue(Duration.wrap(MAX_DURATION_VALUE) > Duration.wrap(MAX_DURATION_VALUE / 2)); + } + + function test_gt_HappyPath_ReturnsFalse() external { + assertFalse(Duration.wrap(0) > Duration.wrap(0)); + assertFalse(Duration.wrap(5) > Duration.wrap(9)); + assertFalse(Duration.wrap(MAX_DURATION_VALUE / 2) > Duration.wrap(MAX_DURATION_VALUE)); + } + + function testFuzz_gt_HappyPath(Duration d1, Duration d2) external { + assertEq(d1 > d2, d1.toSeconds() > d2.toSeconds()); + } + + // --- + // Arithmetic operations + // --- + + // --- + // plus() + // --- + + function testFuzz_plus_HappyPath(Duration d1, Duration d2) external { + uint256 expectedResult = d1.toSeconds() + d2.toSeconds(); + vm.assume(expectedResult <= MAX_DURATION_VALUE); + + assertEq(d1 + d2, Duration.wrap(uint32(expectedResult))); + } + + function testFuzz_plus_Overflow(Duration d1, Duration d2) external { + vm.assume(d1.toSeconds() + d2.toSeconds() > MAX_DURATION_VALUE); + vm.expectRevert(DurationOverflow.selector); + this.external__plus(d1, d2); + } + + // --- + // minus() + // --- + + function testFuzz_minus_HappyPath(Duration d1, Duration d2) external { + vm.assume(d1 >= d2); + assertEq(d1 - d2, Durations.from(d1.toSeconds() - d2.toSeconds())); + } + + function testFuzz_minus_Underflow(Duration d1, Duration d2) external { + vm.assume(d1 < d2); + vm.expectRevert(DurationUnderflow.selector); + this.external__minus(d1, d2); + } + + // --- + // Custom operations + // --- + + // --- + // plusSeconds() + // --- + + function testFuzz_plusSeconds_HappyPath(Duration d, uint256 secondsToAdd) external { + vm.assume(secondsToAdd < type(uint256).max - MAX_DURATION_VALUE); + vm.assume(d.toSeconds() + secondsToAdd <= MAX_DURATION_VALUE); + + assertEq(d.plusSeconds(secondsToAdd), Duration.wrap(uint32(d.toSeconds() + secondsToAdd))); + } + + function testFuzz_plusSeconds_Overflow(Duration d, uint256 secondsToAdd) external { + vm.assume(secondsToAdd < type(uint256).max - MAX_DURATION_VALUE); + vm.assume(d.toSeconds() + secondsToAdd > MAX_DURATION_VALUE); + + vm.expectRevert(DurationOverflow.selector); + this.external__plusSeconds(d, secondsToAdd); + } + + // --- + // minusSeconds() + // --- + + function testFuzz_minusSeconds_HappyPath(Duration d, uint256 secondsToAdd) external { + vm.assume(secondsToAdd <= d.toSeconds()); + + assertEq(d.minusSeconds(secondsToAdd), Duration.wrap(uint32(d.toSeconds() - secondsToAdd))); + } + + function testFuzz_minusSeconds_Overflow(Duration d, uint256 secondsToSubtract) external { + vm.assume(secondsToSubtract > d.toSeconds()); + + vm.expectRevert(DurationUnderflow.selector); + this.external__minusSeconds(d, secondsToSubtract); + } + + // --- + // dividedBy() + // --- + + function testFuzz_dividedBy_HappyPath(Duration d, uint256 divisor) external { + vm.assume(divisor != 0); + assertEq(d.dividedBy(divisor), Duration.wrap(uint32(d.toSeconds() / divisor))); + } + + function testFuzz_dividedBy_RevertOn_DivisorIsZero(Duration d) external { + vm.expectRevert(DivisionByZero.selector); + this.external__dividedBy(d, 0); + } + + // --- + // multipliedBy() + // --- + + function testFuzz_multipliedBy_HappyPath(Duration d, uint256 multiplicand) external { + (bool isSuccess, uint256 expectedResult) = Math.tryMul(d.toSeconds(), multiplicand); + vm.assume(isSuccess && expectedResult <= MAX_DURATION_VALUE); + assertEq(d.multipliedBy(multiplicand), Duration.wrap(uint32(expectedResult))); + } + + function testFuzz_multipliedBy_RevertOn_ResultOverflow(Duration d, uint256 multiplicand) external { + (bool isSuccess, uint256 expectedResult) = Math.tryMul(d.toSeconds(), multiplicand); + vm.assume(isSuccess && expectedResult > MAX_DURATION_VALUE); + vm.expectRevert(DurationOverflow.selector); + this.external__multipliedBy(d, multiplicand); + } + + // --- + // addTo() + // --- + + function testFuzz_addTo_HappyPath(Duration d, Timestamp t) external { + (bool isSuccess, uint256 expectedResult) = Math.tryAdd(t.toSeconds(), d.toSeconds()); + vm.assume(isSuccess && expectedResult <= MAX_TIMESTAMP_VALUE); + assertEq(d.addTo(t), Timestamp.wrap(uint40(expectedResult))); + } + + function testFuzz_addTo_RevertOn_Overflow(Duration d, Timestamp t) external { + (bool isSuccess, uint256 expectedResult) = Math.tryAdd(t.toSeconds(), d.toSeconds()); + vm.assume(isSuccess && expectedResult > MAX_TIMESTAMP_VALUE); + vm.expectRevert(TimestampOverflow.selector); + this.external__addTo(d, t); + } + + // --- + // Conversion operations + // --- + + function testFuzz_toSeconds_HappyPath(Duration d) external { + assertEq(d.toSeconds(), Duration.unwrap(d)); + } + + // --- + // Namespaced helper methods + // --- + + function test_ZERO_CorrectValue() external { + assertEq(Durations.ZERO, Duration.wrap(0)); + } + + function testFuzz_from_HappyPath(uint256 durationInSeconds) external { + vm.assume(durationInSeconds <= MAX_DURATION_VALUE); + assertEq(Durations.from(durationInSeconds), Duration.wrap(uint32(durationInSeconds))); + } + + function testFuzz_from_RevertOn_Overflow(uint256 durationInSeconds) external { + vm.assume(durationInSeconds > MAX_DURATION_VALUE); + vm.expectRevert(DurationOverflow.selector); + this.external__from(durationInSeconds); + } + + function testFuzz_min_HappyPath(Duration d1, Duration d2) external { + assertEq(Durations.min(d1, d2), Durations.from(Math.min(d1.toSeconds(), d2.toSeconds()))); + } + + // --- + // Helper test methods + // --- + + function external__plus(Duration d1, Duration d2) external returns (Duration) { + return d1 + d2; + } + + function external__minus(Duration d1, Duration d2) external returns (Duration) { + return d1 - d2; + } + + function external__plusSeconds(Duration d, uint256 secondsToAdd) external returns (Duration) { + return d.plusSeconds(secondsToAdd); + } + + function external__minusSeconds(Duration d, uint256 secondsToSubtract) external returns (Duration) { + return d.minusSeconds(secondsToSubtract); + } + + function external__dividedBy(Duration d, uint256 divisor) external returns (Duration) { + return d.dividedBy(divisor); + } + + function external__multipliedBy(Duration d, uint256 multiplicand) external returns (Duration) { + return d.multipliedBy(multiplicand); + } + + function external__addTo(Duration d, Timestamp t) external returns (Timestamp) { + return d.addTo(t); + } + + function external__from(uint256 valueInSeconds) external returns (Duration) { + return Durations.from(valueInSeconds); + } +} diff --git a/test/unit/types/ETHValue.t.sol b/test/unit/types/ETHValue.t.sol new file mode 100644 index 0000000..2b887ff --- /dev/null +++ b/test/unit/types/ETHValue.t.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {ETHValue, ETHValues, ETHValueOverflow, ETHValueUnderflow, MAX_ETH_VALUE} from "contracts/types/ETHValue.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract ETHTransfersForbiddenStub { + error ETHTransfersForbidden(); + + receive() external payable { + revert ETHTransfersForbidden(); + } +} + +contract ETHValueTests is UnitTest { + uint256 internal constant _MAX_ETH_SEND = 1_000_000 ether; + address internal immutable _RECIPIENT = makeAddr("RECIPIENT"); + + // --- + // Comparison operations + // --- + + function testFuzz_lt_HappyPath(ETHValue v1, ETHValue v2) external { + assertEq(v1 < v2, ETHValue.unwrap(v1) < ETHValue.unwrap(v2)); + } + + function testFuzz_eq_HappyPath(ETHValue v1, ETHValue v2) external { + assertEq(v1 == v2, ETHValue.unwrap(v1) == ETHValue.unwrap(v2)); + } + + function testFuzz_neq_HappyPath(ETHValue v1, ETHValue v2) external { + assertEq(v1 != v2, ETHValue.unwrap(v1) != ETHValue.unwrap(v2)); + } + + function testFuzz_gt_HappyPath(ETHValue v1, ETHValue v2) external { + assertEq(v1 > v2, ETHValue.unwrap(v1) > ETHValue.unwrap(v2)); + } + + // --- + // Arithmetic operations + // --- + + function testFuzz_plus_HappyPath(ETHValue v1, ETHValue v2) external { + uint256 expectedResult = v1.toUint256() + v2.toUint256(); + vm.assume(expectedResult <= MAX_ETH_VALUE); + assertEq(v1 + v2, ETHValue.wrap(uint128(expectedResult))); + } + + function testFuzz_plus_Overflow(ETHValue v1, ETHValue v2) external { + uint256 expectedResult = v1.toUint256() + v2.toUint256(); + vm.assume(expectedResult > MAX_ETH_VALUE); + vm.expectRevert(ETHValueOverflow.selector); + this.external__plus(v1, v2); + } + + function testFuzz_minus_HappyPath(ETHValue v1, ETHValue v2) external { + vm.assume(v1 > v2); + uint256 expectedResult = v1.toUint256() - v2.toUint256(); + assertEq(v1 - v2, ETHValue.wrap(uint128(expectedResult))); + } + + function testFuzz_minus_Overflow(ETHValue v1, ETHValue v2) external { + vm.assume(v1 < v2); + vm.expectRevert(ETHValueUnderflow.selector); + this.external__minus(v1, v2); + } + + // --- + // Custom operations + // --- + + function testFuzz_sendTo_HappyPath(ETHValue amount, uint256 balance) external { + vm.assume(balance <= _MAX_ETH_SEND); + vm.assume(amount.toUint256() <= balance); + + vm.deal(address(this), balance); + + assertEq(_RECIPIENT.balance, 0); + + amount.sendTo(payable(_RECIPIENT)); + assertEq(_RECIPIENT.balance, amount.toUint256()); + } + + function testFuzz_sendTo_RevertOn_InsufficientBalance(ETHValue amount, uint256 balance) external { + vm.assume(balance <= _MAX_ETH_SEND); + vm.assume(amount.toUint256() > balance); + + vm.deal(address(this), balance); + + vm.expectRevert(abi.encodeWithSelector(Address.AddressInsufficientBalance.selector, address(this))); + this.external__sendTo(amount, payable(_RECIPIENT)); + } + + function testFuzz_sendTo_RevertOn_ETHTransfersForbidden(ETHValue amount, uint256 balance) external { + vm.assume(balance <= _MAX_ETH_SEND); + vm.assume(amount.toUint256() <= balance); + + vm.deal(address(this), balance); + + assertEq(_RECIPIENT.balance, 0); + + ETHTransfersForbiddenStub ethTransfersForbiddenStub = new ETHTransfersForbiddenStub(); + vm.expectRevert(Address.FailedInnerCall.selector); + this.external__sendTo(amount, payable(address(ethTransfersForbiddenStub))); + } + + function testFuzz_toUint256_HappyPath(ETHValue amount) external { + assertEq(amount.toUint256(), ETHValue.unwrap(amount)); + } + + function testFuzz_from_HappyPath(uint256 amount) external { + vm.assume(amount <= MAX_ETH_VALUE); + assertEq(ETHValues.from(amount), ETHValue.wrap(uint128(amount))); + } + + function testFuzz_from_RevertOn_Overflow(uint256 amount) external { + vm.assume(amount > MAX_ETH_VALUE); + vm.expectRevert(ETHValueOverflow.selector); + this.external__from(amount); + } + + function testFuzz_fromAddressBalance_HappyPath(ETHValue balance) external { + vm.assume(balance.toUint256() <= _MAX_ETH_SEND); + vm.deal(address(this), balance.toUint256()); + assertEq(balance, ETHValue.wrap(uint128(address(this).balance))); + } + + function external__sendTo(ETHValue amount, address payable recipient) external { + amount.sendTo(recipient); + } + + function external__plus(ETHValue v1, ETHValue v2) external returns (ETHValue) { + return v1 + v2; + } + + function external__minus(ETHValue v1, ETHValue v2) external returns (ETHValue) { + return v1 - v2; + } + + function external__from(uint256 amount) external returns (ETHValue) { + return ETHValues.from(amount); + } +} diff --git a/test/unit/types/IndexOneBased.t.sol b/test/unit/types/IndexOneBased.t.sol new file mode 100644 index 0000000..3ce7e88 --- /dev/null +++ b/test/unit/types/IndexOneBased.t.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { + IndicesOneBased, + IndexOneBased, + IndexOneBasedOverflow, + IndexOneBasedUnderflow, + MAX_INDEX_ONE_BASED_VALUE +} from "contracts/types/IndexOneBased.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract IndexOneBasedUnitTests is UnitTest { + // --- + // Comparison operations + // --- + + function testFuzz_neq_HappyPath(IndexOneBased i1, IndexOneBased i2) external { + assertEq(i1 != i2, IndexOneBased.unwrap(i1) != IndexOneBased.unwrap(i2)); + } + + // --- + // Custom operations + // --- + + // --- + // isEmpty() + // --- + + function test_isEmpty_HappyPath() external { + assertTrue(IndexOneBased.wrap(0).isEmpty()); + assertFalse(IndicesOneBased.fromOneBasedValue(1).isEmpty()); + } + + function testFuzz_isEmpty_HappyPath(IndexOneBased i1) external { + assertEq(i1.isEmpty(), IndexOneBased.unwrap(i1) == 0); + } + + // --- + // isNotEmpty() + // --- + + function test_isNotEmpty_HappyPath() external { + assertTrue(IndicesOneBased.fromOneBasedValue(1).isNotEmpty()); + assertFalse(IndexOneBased.wrap(0).isNotEmpty()); + } + + function testFuzz_isNotEmpty_HappyPath(IndexOneBased i1) external { + assertEq(i1.isNotEmpty(), IndexOneBased.unwrap(i1) != 0); + } + + // --- + // toZeroBasedValue() + // --- + + function testFuzz_toZeroBasedValue_HappyPath(IndexOneBased index) external { + vm.assume(IndexOneBased.unwrap(index) > 0); + assertEq(index.toZeroBasedValue(), IndexOneBased.unwrap(index) - 1); + } + + function test_toZeroBasedValue_RevertOn_EmptyIndex(IndexOneBased index) external { + IndexOneBased emptyIndex = IndexOneBased.wrap(0); + vm.expectRevert(IndexOneBasedUnderflow.selector); + this.external__toZeroBasedValue(emptyIndex); + } + + // --- + // Namespaced helper methods + // --- + + // --- + // fromOneBasedValue() + // --- + + function testFuzz_fromOneBasedValue_HappyPath(uint256 oneBasedIndexValue) external { + vm.assume(oneBasedIndexValue > 0); + vm.assume(oneBasedIndexValue <= MAX_INDEX_ONE_BASED_VALUE); + + assertEq(IndicesOneBased.fromOneBasedValue(oneBasedIndexValue), IndexOneBased.wrap(uint32(oneBasedIndexValue))); + } + + function test_fromOneBasedValue_RevertOn_ZeroIndex() external { + vm.expectRevert(IndexOneBasedUnderflow.selector); + this.external__fromOneBasedValue(0); + } + + function testFuzz_fromOneBasedValue_RevertOn_Overflow(uint256 oneBasedIndexValue) external { + vm.assume(oneBasedIndexValue > MAX_INDEX_ONE_BASED_VALUE); + + vm.expectRevert(IndexOneBasedOverflow.selector); + this.external__fromOneBasedValue(oneBasedIndexValue); + } + + // --- + // Helper test methods + // --- + + function external__toZeroBasedValue(IndexOneBased oneBasedIndex) external pure returns (uint256) { + return oneBasedIndex.toZeroBasedValue(); + } + + function external__fromOneBasedValue(uint256 oneBasedIndexValue) external pure returns (IndexOneBased) { + return IndicesOneBased.fromOneBasedValue(oneBasedIndexValue); + } +} diff --git a/test/unit/types/PercentD16.t.sol b/test/unit/types/PercentD16.t.sol new file mode 100644 index 0000000..c07558d --- /dev/null +++ b/test/unit/types/PercentD16.t.sol @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import { + PercentD16, + PercentsD16, + DivisionByZero, + PercentD16Underflow, + PercentD16Overflow, + MAX_PERCENT_D16, + HUNDRED_PERCENT_BP, + HUNDRED_PERCENT_D16 +} from "contracts/types/PercentD16.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; +import {stdError} from "forge-std/StdError.sol"; + +contract PercentD16UnitTests is UnitTest { + // --- + // Comparison operations + // --- + + // --- + // lt() + // --- + + function testFuzz_lt_HappyPath(PercentD16 a, PercentD16 b) external { + assertEq(a < b, a.toUint256() < b.toUint256()); + } + + // --- + // lte() + // --- + + function testFuzz_lte_HappyPath(PercentD16 a, PercentD16 b) external { + assertEq(a <= b, a.toUint256() <= b.toUint256()); + } + + // --- + // eq() + // --- + + function testFuzz_eq_HappyPath(PercentD16 a, PercentD16 b) external { + assertEq(a == b, a.toUint256() == b.toUint256()); + } + + // --- + // gt() + // --- + + function testFuzz_gt_HappyPath(PercentD16 a, PercentD16 b) external { + assertEq(a > b, a.toUint256() > b.toUint256()); + } + + // --- + // gte() + // --- + + function testFuzz_gte_HappyPath(PercentD16 a, PercentD16 b) external { + assertEq(a >= b, a.toUint256() >= b.toUint256()); + } + + // --- + // Arithmetic operations + // --- + + // --- + // plus + // --- + + function test_plus_HappyPath() external { + assertEq(PercentsD16.from(0) + PercentsD16.from(0), PercentsD16.from(0)); + assertEq(PercentsD16.from(0) + PercentsD16.from(1), PercentsD16.from(1)); + assertEq(PercentsD16.from(0) + PercentsD16.from(1), PercentsD16.from(1)); + assertEq(PercentsD16.from(500) + PercentsD16.from(20), PercentsD16.from(520)); + assertEq(PercentsD16.from(0) + PercentsD16.from(MAX_PERCENT_D16), PercentsD16.from(MAX_PERCENT_D16)); + assertEq(PercentsD16.from(MAX_PERCENT_D16) + PercentsD16.from(0), PercentsD16.from(MAX_PERCENT_D16)); + + assertEq( + PercentsD16.from(MAX_PERCENT_D16 / 2) + PercentsD16.from(MAX_PERCENT_D16 / 2 + 1), + PercentsD16.from(MAX_PERCENT_D16) + ); + } + + function test_plus_RevertOn_Overflow() external { + vm.expectRevert(PercentD16Overflow.selector); + this.external__plus(PercentsD16.from(MAX_PERCENT_D16), PercentsD16.from(1)); + + vm.expectRevert(PercentD16Overflow.selector); + this.external__plus(PercentsD16.from(MAX_PERCENT_D16), PercentsD16.from(MAX_PERCENT_D16)); + + vm.expectRevert(PercentD16Overflow.selector); + this.external__plus(PercentsD16.from(MAX_PERCENT_D16 / 2 + 1), PercentsD16.from(MAX_PERCENT_D16 / 2 + 1)); + } + + function testFuzz_plus_HappyPath(PercentD16 a, PercentD16 b) external { + vm.assume(a.toUint256() + b.toUint256() <= MAX_PERCENT_D16); + assertEq(a + b, PercentD16.wrap(uint128(a.toUint256() + b.toUint256()))); + } + + function testFuzz_plus_RevertOn_Overflow(PercentD16 a, PercentD16 b) external { + vm.assume(a.toUint256() + b.toUint256() > MAX_PERCENT_D16); + vm.expectRevert(PercentD16Overflow.selector); + this.external__plus(a, b); + } + + // --- + // minus + // --- + + function test_minus_HappyPath() external { + assertEq(PercentsD16.from(5) - PercentsD16.from(2), PercentsD16.from(3)); + assertEq(PercentsD16.from(0) - PercentsD16.from(0), PercentsD16.from(0)); + assertEq(PercentsD16.from(1) - PercentsD16.from(0), PercentsD16.from(1)); + } + + function test_minus_RevertOn_Underflow() external { + vm.expectRevert(PercentD16Underflow.selector); + this.external__minus(PercentsD16.from(0), PercentsD16.from(1)); + + vm.expectRevert(PercentD16Underflow.selector); + this.external__minus(PercentsD16.from(4), PercentsD16.from(5)); + } + + function testFuzz_minus_HappyPath(PercentD16 a, PercentD16 b) external { + vm.assume(a >= b); + assertEq(a - b, PercentD16.wrap(uint128(a.toUint256() - b.toUint256()))); + } + + function testFuzz_minus_RevertOn_Underflow(PercentD16 a, PercentD16 b) external { + vm.assume(a < b); + vm.expectRevert(PercentD16Underflow.selector); + this.external__minus(a, b); + } + + // --- + // Conversion operations + // --- + + // --- + // toUint256() + // --- + + function test_toUint256_HappyPath() external { + assertEq(PercentsD16.from(0).toUint256(), 0); + assertEq(PercentsD16.from(1).toUint256(), 1); + assertEq(PercentsD16.from(MAX_PERCENT_D16 / 2).toUint256(), MAX_PERCENT_D16 / 2); + assertEq(PercentsD16.from(MAX_PERCENT_D16 - 1).toUint256(), MAX_PERCENT_D16 - 1); + assertEq(PercentsD16.from(MAX_PERCENT_D16).toUint256(), MAX_PERCENT_D16); + } + + function testFuzz_toUint256_HappyPath(PercentD16 a) external { + assertEq(a.toUint256(), PercentD16.unwrap(a)); + } + + // --- + // Namespaced helper methods + // --- + + // --- + // from() + // --- + + function test_from_HappyPath() external { + assertEq(PercentsD16.from(0), PercentD16.wrap(0)); + assertEq(PercentsD16.from(1), PercentD16.wrap(1)); + assertEq(PercentsD16.from(MAX_PERCENT_D16 / 2), PercentD16.wrap(uint128(MAX_PERCENT_D16 / 2))); + assertEq(PercentsD16.from(MAX_PERCENT_D16 - 1), PercentD16.wrap(uint128(MAX_PERCENT_D16 - 1))); + assertEq(PercentsD16.from(MAX_PERCENT_D16), PercentD16.wrap(uint128(MAX_PERCENT_D16))); + } + + function test_from_RevertOn_Overflow() external { + vm.expectRevert(PercentD16Overflow.selector); + this.external__from(uint256(MAX_PERCENT_D16) + 1); + + vm.expectRevert(PercentD16Overflow.selector); + this.external__from(type(uint256).max); + } + + function testFuzz_from_HappyPath(uint256 a) external { + vm.assume(a <= MAX_PERCENT_D16); + assertEq(PercentsD16.from(a), PercentD16.wrap(uint128(a))); + } + + function testFuzz_from_RevertOn_Overflow(uint256 a) external { + vm.assume(a > MAX_PERCENT_D16); + vm.expectRevert(PercentD16Overflow.selector); + this.external__from(a); + } + + // --- + // fromFraction() + // --- + + function test_fromFraction() external { + assertEq(PercentsD16.fromFraction({numerator: 0, denominator: 1}), PercentsD16.from(0)); + assertEq(PercentsD16.fromFraction({numerator: 0, denominator: 33}), PercentsD16.from(0)); + assertEq(PercentsD16.fromFraction({numerator: 1, denominator: 1}), PercentsD16.from(100 * 10 ** 16)); + assertEq(PercentsD16.fromFraction({numerator: 1, denominator: 2}), PercentsD16.from(50 * 10 ** 16)); + assertEq(PercentsD16.fromFraction({numerator: 5, denominator: 2}), PercentsD16.from(250 * 10 ** 16)); + assertEq(PercentsD16.fromFraction({numerator: 2, denominator: 5}), PercentsD16.from(40 * 10 ** 16)); + assertEq(PercentsD16.fromFraction({numerator: 1, denominator: 100}), PercentsD16.from(1 * 10 ** 16)); + assertEq(PercentsD16.fromFraction({numerator: 2, denominator: 1000}), PercentsD16.from(0.2 * 10 ** 16)); + + assertEq( + PercentsD16.fromFraction({numerator: MAX_PERCENT_D16 / HUNDRED_PERCENT_D16, denominator: 1}), + PercentsD16.from(MAX_PERCENT_D16 / HUNDRED_PERCENT_D16 * HUNDRED_PERCENT_D16) + ); + + assertEq( + PercentsD16.fromFraction({ + numerator: MAX_PERCENT_D16 / HUNDRED_PERCENT_D16, + denominator: HUNDRED_PERCENT_D16 + }), + PercentsD16.from(MAX_PERCENT_D16 / HUNDRED_PERCENT_D16) + ); + } + + function test_fromFraction_RevertOn_DenominatorIsZero() external { + vm.expectRevert(DivisionByZero.selector); + this.external__fromFraction({numerator: 1, denominator: 0}); + } + + function test_fromFraction_RevertOn_ArithmeticErrors() external { + vm.expectRevert(stdError.arithmeticError); + this.external__fromFraction({numerator: type(uint256).max, denominator: 1}); + + vm.expectRevert(stdError.arithmeticError); + this.external__fromFraction({numerator: type(uint256).max, denominator: type(uint256).max}); + + vm.expectRevert(stdError.arithmeticError); + this.external__fromFraction({numerator: type(uint256).max / HUNDRED_PERCENT_D16 + 1, denominator: 1}); + } + + function test_fromFraction_RevertOn_PercentD16Overflow() external { + vm.expectRevert(PercentD16Overflow.selector); + this.external__fromFraction({numerator: uint256(MAX_PERCENT_D16) + 1, denominator: 1}); + + vm.expectRevert(PercentD16Overflow.selector); + this.external__fromFraction({numerator: MAX_PERCENT_D16 / 1000, denominator: 100}); + } + + function testFuzz_fromFraction_HappyPath(uint256 numerator, uint256 denominator) external { + vm.assume(numerator <= MAX_PERCENT_D16 / HUNDRED_PERCENT_D16); + vm.assume(denominator > 0); + assertEq( + PercentsD16.fromFraction(numerator, denominator), + PercentD16.wrap(uint128(HUNDRED_PERCENT_D16 * numerator / denominator)) + ); + } + + function testFuzz_fromFraction_RevertOn_ArithmeticErrors(uint256 numerator, uint256 denominator) external { + (bool isSuccess,) = Math.tryMul(numerator, HUNDRED_PERCENT_D16); + vm.assume(!isSuccess); + vm.assume(denominator > 0); + + vm.expectRevert(stdError.arithmeticError); + this.external__fromFraction(numerator, denominator); + } + + function testFuzz_fromFraction_RevertOn_PercentD16Overflow(uint256 numerator, uint256 denominator) external { + (bool isSuccess,) = Math.tryMul(numerator, HUNDRED_PERCENT_D16); + vm.assume(isSuccess); + + vm.assume(denominator > 0); + vm.assume(HUNDRED_PERCENT_D16 * numerator / denominator > MAX_PERCENT_D16); + + vm.expectRevert(PercentD16Overflow.selector); + this.external__fromFraction(numerator, denominator); + } + + // --- + // fromBasisPoints() + // --- + + function test_fromBasisPoints_HappyPath() external { + assertEq(PercentsD16.fromBasisPoints(0), PercentsD16.from(0)); + assertEq(PercentsD16.fromBasisPoints(42_42), PercentsD16.from(42.42 * 10 ** 16)); + assertEq(PercentsD16.fromBasisPoints(100_00), PercentsD16.from(100 * 10 ** 16)); + assertEq(PercentsD16.fromBasisPoints(3000_00), PercentsD16.from(3000 * 10 ** 16)); + assertEq( + PercentsD16.fromBasisPoints(uint256(HUNDRED_PERCENT_BP) * MAX_PERCENT_D16 / HUNDRED_PERCENT_D16), + PercentsD16.from( + uint256(MAX_PERCENT_D16) * HUNDRED_PERCENT_BP / HUNDRED_PERCENT_D16 * HUNDRED_PERCENT_D16 + / HUNDRED_PERCENT_BP + ) + ); + } + + function test_fromBasisPoints_RevertOn_ArithmeticErrors() external { + vm.expectRevert(stdError.arithmeticError); + this.external__fromBasisPoints(type(uint256).max); + + vm.expectRevert(stdError.arithmeticError); + this.external__fromBasisPoints(type(uint256).max / HUNDRED_PERCENT_D16 * HUNDRED_PERCENT_BP); + + vm.expectRevert(stdError.arithmeticError); + this.external__fromBasisPoints(type(uint256).max / HUNDRED_PERCENT_D16 + 1); + } + + function test_fromBasisPoints_RevertOn_PercentD16Overflow() external { + vm.expectRevert(PercentD16Overflow.selector); + this.external__fromBasisPoints(MAX_PERCENT_D16); + + vm.expectRevert(PercentD16Overflow.selector); + this.external__fromBasisPoints(MAX_PERCENT_D16 / HUNDRED_PERCENT_D16 * HUNDRED_PERCENT_BP * 10); + } + + function testFuzz_fromBasisPoints(uint256 value) external { + vm.assume(value <= MAX_PERCENT_D16 / HUNDRED_PERCENT_D16); + assertEq( + PercentsD16.fromBasisPoints(value), + PercentD16.wrap(uint128(value * HUNDRED_PERCENT_D16 / HUNDRED_PERCENT_BP)) + ); + } + + // --- + // Helper test methods + // --- + + function external__fromBasisPoints(uint256 bpValue) external returns (PercentD16) { + return PercentsD16.fromBasisPoints(bpValue); + } + + function external__fromFraction(uint256 numerator, uint256 denominator) external returns (PercentD16) { + return PercentsD16.fromFraction(numerator, denominator); + } + + function external__plus(PercentD16 a, PercentD16 b) external returns (PercentD16) { + return a + b; + } + + function external__minus(PercentD16 a, PercentD16 b) external returns (PercentD16) { + return a - b; + } + + function external__from(uint256 value) external returns (PercentD16) { + return PercentsD16.from(value); + } +} diff --git a/test/unit/types/SharesValue.t.sol b/test/unit/types/SharesValue.t.sol new file mode 100644 index 0000000..3c7becf --- /dev/null +++ b/test/unit/types/SharesValue.t.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { + SharesValue, + SharesValues, + SharesValueOverflow, + SharesValueUnderflow, + MAX_SHARES_VALUE +} from "contracts/types/SharesValue.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract ETHTransfersForbiddenStub { + error ETHTransfersForbidden(); + + receive() external payable { + revert ETHTransfersForbidden(); + } +} + +contract SharesValueTests is UnitTest { + // --- + // Comparison operations + // --- + + // --- + // lt() + // --- + + function testFuzz_lt_HappyPath(SharesValue v1, SharesValue v2) external { + assertEq(v1 < v2, SharesValue.unwrap(v1) < SharesValue.unwrap(v2)); + } + + // --- + // eq() + // --- + + function testFuzz_eq_HappyPath(SharesValue v1, SharesValue v2) external { + assertEq(v1 == v2, SharesValue.unwrap(v1) == SharesValue.unwrap(v2)); + } + + // --- + // Arithmetic operations + // --- + + // --- + // plus() + // --- + + function test_plus_HappyPath() external { + assertEq(SharesValues.from(0) + SharesValues.from(0), SharesValue.wrap(0)); + assertEq(SharesValues.from(1) + SharesValues.from(0), SharesValue.wrap(1)); + assertEq(SharesValues.from(0) + SharesValues.from(1), SharesValue.wrap(1)); + assertEq(SharesValues.from(0) + SharesValues.from(1), SharesValue.wrap(1)); + assertEq( + SharesValues.from(MAX_SHARES_VALUE / 2) + SharesValues.from(MAX_SHARES_VALUE / 2), + SharesValue.wrap(type(uint128).max - 1) + ); + assertEq(SharesValues.from(MAX_SHARES_VALUE) + SharesValues.from(0), SharesValue.wrap(type(uint128).max)); + } + + function test_plus_RevertOn_Overflow() external { + vm.expectRevert(SharesValueOverflow.selector); + this.external__plus(SharesValues.from(MAX_SHARES_VALUE), SharesValues.from(1)); + + vm.expectRevert(SharesValueOverflow.selector); + this.external__plus(SharesValues.from(MAX_SHARES_VALUE / 2 + 1), SharesValues.from(MAX_SHARES_VALUE / 2 + 1)); + } + + function testFuzz_plus_HappyPath(SharesValue v1, SharesValue v2) external { + uint256 expectedResult = v1.toUint256() + v2.toUint256(); + vm.assume(expectedResult <= MAX_SHARES_VALUE); + assertEq(v1 + v2, SharesValue.wrap(uint128(expectedResult))); + } + + function testFuzz_plus_RevertOn_Overflow(SharesValue v1, SharesValue v2) external { + uint256 expectedResult = v1.toUint256() + v2.toUint256(); + vm.assume(expectedResult > MAX_SHARES_VALUE); + vm.expectRevert(SharesValueOverflow.selector); + this.external__plus(v1, v2); + } + + // --- + // minus() + // --- + + function test_minus_HappyPath() external { + assertEq(SharesValues.from(0) - SharesValues.from(0), SharesValue.wrap(0)); + assertEq(SharesValues.from(1) - SharesValues.from(0), SharesValue.wrap(1)); + assertEq(SharesValues.from(1) - SharesValues.from(1), SharesValue.wrap(0)); + assertEq( + SharesValues.from(MAX_SHARES_VALUE) - SharesValues.from(1), SharesValue.wrap(uint128(MAX_SHARES_VALUE - 1)) + ); + + assertEq(SharesValues.from(MAX_SHARES_VALUE) - SharesValues.from(MAX_SHARES_VALUE), SharesValue.wrap(0)); + } + + function test_minus_RevertOn_SharesValueUnderflow() external { + vm.expectRevert(SharesValueUnderflow.selector); + this.external__minus(SharesValues.from(0), SharesValues.from(1)); + + vm.expectRevert(SharesValueUnderflow.selector); + this.external__minus(SharesValues.from(0), SharesValues.from(MAX_SHARES_VALUE)); + } + + function testFuzz_minus_HappyPath(SharesValue v1, SharesValue v2) external { + vm.assume(SharesValue.unwrap(v1) > SharesValue.unwrap(v2)); + uint256 expectedResult = v1.toUint256() - v2.toUint256(); + assertEq(v1 - v2, SharesValue.wrap(uint128(expectedResult))); + } + + function testFuzz_minus_Overflow(SharesValue v1, SharesValue v2) external { + vm.assume(v1 < v2); + vm.expectRevert(SharesValueUnderflow.selector); + this.external__minus(v1, v2); + } + + // --- + // Custom operations + // --- + + // --- + // toUint256() + // --- + + function test_toUint256_HappyPath() external { + assertEq(SharesValues.from(0).toUint256(), 0); + assertEq(SharesValues.from(MAX_SHARES_VALUE / 2).toUint256(), MAX_SHARES_VALUE / 2); + assertEq(SharesValues.from(MAX_SHARES_VALUE).toUint256(), MAX_SHARES_VALUE); + } + + function testFuzz_toUint256_HappyPath(SharesValue amount) external { + assertEq(amount.toUint256(), SharesValue.unwrap(amount)); + } + + // --- + // from() + // --- + + function testFuzz_from_HappyPath(uint256 amount) external { + vm.assume(amount <= MAX_SHARES_VALUE); + assertEq(SharesValues.from(amount), SharesValue.wrap(uint128(amount))); + } + + function testFuzz_from_RevertOn_Overflow(uint256 amount) external { + vm.assume(amount > MAX_SHARES_VALUE); + vm.expectRevert(SharesValueOverflow.selector); + this.external__from(amount); + } + + // --- + // Helper test methods + // --- + + function external__plus(SharesValue a, SharesValue b) external { + a + b; + } + + function external__minus(SharesValue a, SharesValue b) external { + a - b; + } + + function external__from(uint256 amount) external returns (SharesValue) { + SharesValues.from(amount); + } +} diff --git a/test/unit/types/Timestamp.t.sol b/test/unit/types/Timestamp.t.sol new file mode 100644 index 0000000..f12d02c --- /dev/null +++ b/test/unit/types/Timestamp.t.sol @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; + +import {Timestamp, Timestamps, TimestampOverflow, MAX_TIMESTAMP_VALUE} from "contracts/types/Timestamp.sol"; + +import {UnitTest} from "test/utils/unit-test.sol"; + +contract TimestampTests is UnitTest { + // --- + // Constants + // --- + + function test_MAX_TIMESTAMP_VALUE_HappyPath() external { + assertEq(MAX_TIMESTAMP_VALUE, type(uint40).max); + } + + // --- + // Comparison operations + // --- + + // --- + // lt() + // --- + + function testFuzz_lt_HappyPath(Timestamp t1, Timestamp t2) external { + assertEq(t1 < t2, t1.toSeconds() < t2.toSeconds()); + } + + // --- + // lte() + // --- + + function testFuzz_lte_HappyPath(Timestamp t1, Timestamp t2) external { + assertEq(t1 <= t2, t1.toSeconds() <= t2.toSeconds()); + } + + // --- + // eq() + // --- + + function testFuzz_eq_HappyPath(Timestamp t1, Timestamp t2) external { + assertEq(t1 == t2, t1.toSeconds() == t2.toSeconds()); + } + + // --- + // neq() + // --- + + function testFuzz_neq_HappyPath(Timestamp t1, Timestamp t2) external { + assertEq(t1 != t2, t1.toSeconds() != t2.toSeconds()); + } + + // --- + // gte() + // --- + + function testFuzz_gte_HappyPath(Timestamp t1, Timestamp t2) external { + assertEq(t1 >= t2, t1.toSeconds() >= t2.toSeconds()); + } + + // --- + // gt() + // --- + + function testFuzz_gt_HappyPath(Timestamp t1, Timestamp t2) external { + assertEq(t1 > t2, t1.toSeconds() > t2.toSeconds()); + } + + // --- + // Conversion operations + // --- + + function testFuzz_toSeconds_HappyPath(Timestamp t) external { + assertEq(t.toSeconds(), Timestamp.unwrap(t)); + } + + // --- + // Custom operations + // --- + + // --- + // isZero() + // --- + + function test_isZero_HappyPath_ReturnsTrue() external { + assertTrue(Timestamp.wrap(0).isZero()); + } + + function test_isZero_HappyPath_ReturnFalse() external { + assertFalse(Timestamp.wrap(1).isZero()); + assertFalse(Timestamp.wrap(MAX_TIMESTAMP_VALUE / 2).isZero()); + assertFalse(Timestamp.wrap(MAX_TIMESTAMP_VALUE).isZero()); + } + + function testFuzz_isZero_HappyPath(Timestamp t) external { + assertEq(t.isZero(), t == Timestamps.ZERO); + } + + // --- + // isNotZero() + // --- + + function test_isNotZero_HappyPath_ReturnFalse() external { + assertTrue(Timestamp.wrap(1).isNotZero()); + assertTrue(Timestamp.wrap(MAX_TIMESTAMP_VALUE / 2).isNotZero()); + assertTrue(Timestamp.wrap(MAX_TIMESTAMP_VALUE).isNotZero()); + } + + function test_isNotZero_HappyPath_ReturnsFalse() external { + assertFalse(Timestamp.wrap(0).isNotZero()); + } + + function testFuzz_isNotZero_HappyPath(Timestamp t) external { + assertEq(t.isNotZero(), t != Timestamps.ZERO); + } + + // --- + // Namespaced helper methods + // --- + + // --- + // now() + // --- + + function testFuzz_max_HappyPath(Timestamp t1, Timestamp t2) external { + assertEq(Timestamps.max(t1, t2), Timestamps.from(Math.max(t1.toSeconds(), t2.toSeconds()))); + } + + function test_now_HappyPath() external { + assertEq(Timestamps.now(), Timestamps.from(block.timestamp)); + + vm.warp(12 hours); + assertEq(Timestamps.now(), Timestamps.from(block.timestamp)); + + vm.warp(30 days); + assertEq(Timestamps.now(), Timestamps.from(block.timestamp)); + + vm.warp(365 days); + assertEq(Timestamps.now(), Timestamps.from(block.timestamp)); + + vm.warp(100 * 365 days); + assertEq(Timestamps.now(), Timestamps.from(block.timestamp)); + + vm.warp(1_000 * 365 days); + assertEq(Timestamps.now(), Timestamps.from(block.timestamp)); + + vm.warp(10_000 * 365 days); + assertEq(Timestamps.now(), Timestamps.from(block.timestamp)); + + vm.warp(34_000 * 365 days); + assertEq(Timestamps.now(), Timestamps.from(block.timestamp)); + } + + function test_now_InvalidValueAfterApprox34000Years() external { + vm.warp(MAX_TIMESTAMP_VALUE); // MAX_TIMESTAMP_VALUE is ~ 36812 year + assertEq(Timestamps.now().toSeconds(), block.timestamp); + + // After the ~34800 years the uint40 timestamp value will overflow and conversion + // of block.timestamp to uint40 will start return incorrect values. + vm.warp(uint256(MAX_TIMESTAMP_VALUE) + 1); + assertEq(Timestamps.now().toSeconds(), 0); + } + + // --- + // from() + // --- + + function testFuzz_from_HappyPath(uint256 value) external { + vm.assume(value <= MAX_TIMESTAMP_VALUE); + assertEq(Timestamps.from(value), Timestamp.wrap(uint40(value))); + } + + function testFuzz_from_RevertOn_Overflow(uint256 value) external { + vm.assume(value > MAX_TIMESTAMP_VALUE); + + vm.expectRevert(TimestampOverflow.selector); + this.external__from(value); + } + + // --- + // Helper test methods + // --- + + function external__from(uint256 value) external returns (Timestamp) { + return Timestamps.from(value); + } +} diff --git a/test/utils/testing-assert-eq-extender.sol b/test/utils/testing-assert-eq-extender.sol index 5d350c7..fcac37d 100644 --- a/test/utils/testing-assert-eq-extender.sol +++ b/test/utils/testing-assert-eq-extender.sol @@ -3,9 +3,12 @@ pragma solidity 0.8.26; import {Test} from "forge-std/Test.sol"; +import {ETHValue} from "contracts/types/ETHValue.sol"; +import {SharesValue} from "contracts/types/SharesValue.sol"; import {Duration} from "contracts/types/Duration.sol"; import {Timestamp} from "contracts/types/Timestamp.sol"; import {PercentD16} from "contracts/types/PercentD16.sol"; +import {IndexOneBased} from "contracts/types/IndexOneBased.sol"; import {Status as ProposalStatus} from "contracts/libraries/ExecutableProposals.sol"; import {State as DualGovernanceState} from "contracts/DualGovernance.sol"; @@ -49,4 +52,16 @@ contract TestingAssertEqExtender is Test { function assertEq(PercentD16 a, PercentD16 b) internal { assertEq(PercentD16.unwrap(a), PercentD16.unwrap(b)); } + + function assertEq(ETHValue a, ETHValue b) internal { + assertEq(ETHValue.unwrap(a), ETHValue.unwrap(b)); + } + + function assertEq(SharesValue a, SharesValue b) internal { + assertEq(SharesValue.unwrap(a), SharesValue.unwrap(b)); + } + + function assertEq(IndexOneBased a, IndexOneBased b) internal { + assertEq(IndexOneBased.unwrap(a), IndexOneBased.unwrap(b)); + } }