-
Notifications
You must be signed in to change notification settings - Fork 11.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Saturation Arithmetic Operations #5029
base: master
Are you sure you want to change the base?
Support Saturation Arithmetic Operations #5029
Conversation
🦋 Changeset detectedLatest commit: d513400 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
contracts/utils/math/Math.sol
Outdated
// equivalent to: a >= b ? a - b : 0 | ||
return (a - b) * SafeCast.toUint(a > b); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also do.
// equivalent to: a >= b ? a - b : 0 | |
return (a - b) * SafeCast.toUint(a > b); | |
return ternary(a > b, a - b, 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's actually a good point, because the ternary operator looks like:
b ^ ((a ^ b) * SafeCast.toUint(condition))
Whoever if a
or b
is zero, it can be simplified to:
// if `b` is zero, then `conditon ? a : 0` is equivalent to:
SafeCast.toUint(condition) * a;
// if `a` is zero, then `conditon ? 0 : b` is equivalent to:
SafeCast.toUint(!condition) * b;
So it works like an and
operator, I really wish the compiler could do this kind of optimization for us, I was wondering if creating a new operator would be confusing, like:
// if `condition` is true, returns `a`, otherwise return zero.
function and(bool condition, uint256 a) internal pure returns (uint256) {
unchecked {
return a * SafeCast.toUint(condition));
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise I can refactor it for use the ternary
at a little gas cost but increased code readability.
unchecked { | ||
uint256 c = a + b; | ||
// equivalent to: c < a ? type(uint256).max : c | ||
return c | (0 - SafeCast.toUint(c < a)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That may be cheaper than the ternary alternative, but its also way less readable. @ernestognw wdyt ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I designed all methods in this PR by first write everything in pure assembly using https://www.evm.codes/playground to find the most efficient solution possible, then I write the solidity code that generate the equivalent code. For this one specifically this is the most optimized way I've found:
PUSH2 0xdead // b
PUSH2 0xbeef // a b
// c = a + b
DUP2 // b a b
ADD // c b
// overflow = c > b
SWAP1 // b c
DUP2 // c b c
LT // overflow c
// limit = 0 - overflow
PUSH0 // 0 overflow c
SUB // limit c
// result = overflow ? limit : c
OR // result
// total gas used starting from DUP2 = 23 gas
/** | ||
* @dev Unsigned saturating multiplication, bounds to `2 ** 256 - 1` instead of overflowing. | ||
*/ | ||
function saturatingMul(uint256 a, uint256 b) internal pure returns (uint256) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use of assembly may be efficient, but readability is not great. I'd be more confortable with that function being
(bool success, uint256 result) = tryMul(a, b);
return ternary(success, result, type(uint256).max);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I had to use assembly because c / a
reverts, even inside a unchecked
statement, while in pure evm a division by zero always result in zero.
bool sign = b >= 0; | ||
bool overflow = a > c == sign; | ||
|
||
// Efficient branchless method to retrieve the boundary limit: | ||
// (1 << 255) == type(int256).min | ||
// (1 << 255) - 1 == type(int256).max | ||
uint256 limit = (SafeCast.toUint(overflow) << 255) - SafeCast.toUint(sign); | ||
|
||
return ternary(overflow, int256(limit), c); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this one, this is the assembly implementation, it uses 64
gas (discounting the a and b push).
// b = type(int256).min
PUSH32 0x8000000000000000000000000000000000000000000000000000000000000000
// a = -1
PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
// c = a + b
DUP2 // b a b
DUP2 // a b a b
ADD // c a b
// sign = b >= 0
SWAP2 // b a c
PUSH0 // 0 b a c
SGT // (0 > b) a c
ISZERO // sign a c
// overflow = (c < a) == sign
SWAP1 // a sign c
DUP3 // c a sign c
SLT // (c < a) sign c
DUP2 // sign (c < a) sign c
EQ // overflow sign c
// limit = (overflow << 255) - sign
SWAP1 // sign overflow c
DUP2 // overflow sign overflow c
PUSH1 0xff // 255 overflow sign overflow c
SHL // (overflow << 255) sign overflow c
SUB // limit overflow c
// result = overflow ? limit : c
DUP3 // c limit overflow c
XOR // (c ^ limit) overflow c
MUL // ((c ^ limit) * overflow) c
XOR // result
Description
Adds gas efficient saturating arithmetic operators, those operators are useful when both overflow and revert are not desired.
This PR also optimizes and remove unnecessary branching from various Math methods.
Motivation
If you want to write some formula, but don't want neither wrapping and reverts, the only option is using the
Math.try*
methods, which can't be used in chain and make the code less readable.Ex: Make sure there're at least
100_000
gas units left before calling an external contract, otherwise revert.The equivalent code using
Math.try*
is less readable and less efficient.