Skip to content

Commit 44117a7

Browse files
authored
Merge branch 'main' into decoding-unification
2 parents a75f960 + 575181b commit 44117a7

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed

src/libraries/Percentage.sol

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// SPDX-License-Identifier: Apache 2
2+
pragma solidity ^0.8.19;
3+
4+
// ╭────────────────────────────────────────────────────────────╮
5+
// │ Library for compact (uint16) representation of percentages │
6+
// ╰────────────────────────────────────────────────────────────╯
7+
8+
// Represent percentages with 4 decimal digits of precision up to a maximum of 1000 %
9+
//
10+
// Uses a 14 bit mantissa / 2 bit (decimal!) exponent split:
11+
// value = mantissa / 10^(1 + exponent)
12+
//
13+
// 2^14 = 16384, i.e. we get 4 full digits of precision and a can also represent 1000 %
14+
// 2 bits of the exponent are used to shift our decimal point *downwards*(!)
15+
// thus giving us a range of 0.abcd % to abc.d % (or 1000.00 %)
16+
// This format is somewhat idiosyncratic and some values have multiple representations:
17+
// Using (mantissa, exponent) notation:
18+
// 0.1 % = (1, 0) = 0b00000001100100_00 (or (10, 1) or (100, 2))
19+
// 10 % = (100, 0) = 0b00000001100100_00 (or (1000, 1) or (10000, 2))
20+
// 432.1 % = (4321, 0) = 0b01000011100001_00
21+
// 0.4321 % = (4321, 3) = 0b01000011100001_11
22+
// 1000.0 % = (10000, 0) = 0b10011100010000_00
23+
24+
type Percentage is uint16;
25+
library PercentageLib {
26+
uint internal constant BYTE_SIZE = 2;
27+
28+
uint private constant EXPONENT_BITS = 2;
29+
uint private constant EXPONENT_BASE = 1;
30+
uint private constant EXPONENT_BITS_MASK = (1 << EXPONENT_BITS) - 1;
31+
uint private constant MAX_MANTISSA = 1e4; //= 1000 % (if exponent = 0)
32+
//we essentially use a uint128 like an array of 4 uint24s containing [1e6, 1e5, 1e4, 1e3] as a
33+
// simple way to save some gas over using EVM exponentiation
34+
uint private constant BITS_PER_POWER = 3*8; //4 powers, 3 bytes per power of ten, 8 bits per byte
35+
uint private constant POWERS_OF_TEN =
36+
(1e6 << 3*BITS_PER_POWER) +
37+
(1e5 << 2*BITS_PER_POWER) +
38+
(1e4 << 1*BITS_PER_POWER) +
39+
(1e3 << 0*BITS_PER_POWER);
40+
uint private constant POWERS_OF_TEN_MASK = (1 << BITS_PER_POWER) - 1;
41+
42+
error InvalidPercentage(uint16 percentage);
43+
error InvalidArguments(uint mantissa, uint fractionalDigits);
44+
45+
//to(3141, 3) = 3.141 %
46+
function to(
47+
uint value,
48+
uint fractionalDigits
49+
) internal pure returns (Percentage) { unchecked {
50+
if (value == 0)
51+
return Percentage.wrap(0);
52+
53+
if (fractionalDigits > 4)
54+
revert InvalidArguments(value, fractionalDigits);
55+
56+
if (fractionalDigits == 0) {
57+
value *= 10;
58+
fractionalDigits = 1;
59+
}
60+
61+
if (value > MAX_MANTISSA)
62+
revert InvalidArguments(value, fractionalDigits);
63+
64+
value = (value << EXPONENT_BITS) | (fractionalDigits - 1);
65+
66+
uint16 ret;
67+
//skip unneccessary cleanup
68+
assembly ("memory-safe") { ret := value }
69+
70+
return Percentage.wrap(ret);
71+
}}
72+
73+
function checkedWrap(uint16 percentage) internal pure returns (Percentage) { unchecked {
74+
if ((percentage >> EXPONENT_BITS) > MAX_MANTISSA)
75+
revert InvalidPercentage(percentage);
76+
77+
return Percentage.wrap(percentage);
78+
}}
79+
80+
//we can silently overflow if value > 2^256/MAX_MANTISSA - not worth wasting gas to check
81+
// if you have values this large you should know what you're doing regardless and can just
82+
// check that the result is greater than or equal to the input value to detect overflows
83+
function mulUnchecked(
84+
Percentage percentage_,
85+
uint value
86+
) internal pure returns (uint) { unchecked {
87+
uint percentage = Percentage.unwrap(percentage_);
88+
//negative exponent = 0 -> denominator = 100, ..., negative exponent = 3 -> denominator = 1e5
89+
uint negativeExponent = percentage & EXPONENT_BITS_MASK;
90+
uint shift = negativeExponent * BITS_PER_POWER;
91+
uint denominator = (POWERS_OF_TEN >> shift) & POWERS_OF_TEN_MASK;
92+
uint numerator = value * (percentage >> EXPONENT_BITS);
93+
//the + here can overflow if value is within 2 orders of magnitude of 2^256
94+
return numerator/denominator;
95+
}}
96+
}
97+
using PercentageLib for Percentage global;

test/Percentage.t.sol

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// SPDX-License-Identifier: Apache 2
2+
pragma solidity ^0.8.19;
3+
4+
import "forge-std/Test.sol";
5+
6+
import {Percentage, PercentageLib} from "wormhole-sdk/libraries/Percentage.sol";
7+
8+
contract TypeLibsTest is Test {
9+
function testPercentageFixed() public {
10+
Percentage pi = PercentageLib.to(3141, 3);
11+
assertEq(pi.mulUnchecked(1e0), 0);
12+
assertEq(pi.mulUnchecked(1e1), 0);
13+
assertEq(pi.mulUnchecked(1e2), 3);
14+
assertEq(pi.mulUnchecked(1e3), 31);
15+
assertEq(pi.mulUnchecked(1e4), 314);
16+
assertEq(pi.mulUnchecked(1e5), 3141);
17+
18+
assertEq(PercentageLib.to(3141, 4).mulUnchecked(1e6), 3141);
19+
}
20+
21+
function testPercentageDigit() public {
22+
for (uint digit = 0; digit < 10; ++digit) {
23+
assertEq(PercentageLib.to(digit * 100, 0).mulUnchecked(1e0), digit);
24+
assertEq(PercentageLib.to(digit * 10, 0).mulUnchecked(1e1), digit);
25+
assertEq(PercentageLib.to(digit , 0).mulUnchecked(1e2), digit);
26+
assertEq(PercentageLib.to(digit , 1).mulUnchecked(1e3), digit);
27+
assertEq(PercentageLib.to(digit , 2).mulUnchecked(1e4), digit);
28+
assertEq(PercentageLib.to(digit , 3).mulUnchecked(1e5), digit);
29+
assertEq(PercentageLib.to(digit , 4).mulUnchecked(1e6), digit);
30+
}
31+
}
32+
33+
function testPercentageFuzz(uint value, uint rngSeed_) public {
34+
uint[] memory rngSeed = new uint[](1);
35+
rngSeed[0] = rngSeed_;
36+
vm.assume(value < type(uint256).max/1e4);
37+
Percentage percentage = fuzzPercentage(rngSeed);
38+
uint unwrapped = Percentage.unwrap(percentage);
39+
uint mantissa = unwrapped >> 2;
40+
uint fractDigits = (unwrapped & 3) + 1;
41+
uint denominator = 10**(fractDigits + 2); //+2 to adjust for percentage to floating point conv
42+
assertEq(percentage.mulUnchecked(value), value * mantissa / denominator);
43+
}
44+
45+
function nextRn(uint[] memory rngSeed) private pure returns (uint) {
46+
rngSeed[0] = uint(keccak256(abi.encode(rngSeed[0])));
47+
return rngSeed[0];
48+
}
49+
50+
function fuzzPercentage(uint[] memory rngSeed) private pure returns (Percentage) {
51+
uint fractionalDigits = uint8(nextRn(rngSeed) % 5); //at most 4 fractional digits
52+
uint mantissa = uint16(nextRn(rngSeed) >> 8) % 1e4; //4 digit mantissa
53+
54+
if (mantissa > 100 && fractionalDigits == 0)
55+
++fractionalDigits;
56+
if (mantissa > 1000 && fractionalDigits < 2)
57+
++fractionalDigits;
58+
59+
return PercentageLib.to(mantissa, fractionalDigits);
60+
}
61+
}

0 commit comments

Comments
 (0)