solidity-math
This package extends bn.js to implement Solidity integer types and operations. It is useful for replicating public Solidity contract interactions, particularly when developing autonomous agents and DeFi programs.
Features
✅ Compatible with Solidity 0.8.17✅ Comprehensive Solidity integer types & operators- Unsigned integers:
uint8
,uint16
, ...,uint256
- Signed integers:
int8
,int16
, ...,int256
- Unsigned integers:
✅ Inline assembly functions:addmod()
&mulmod()
✅ Unchecked arithmetic✅ Type safety checks✅ Type casting✅ Flexible right operand type (e.g.uint256(20).add(10)
)✅ Support for CommonJS & ES6
Table of Contents
Installation
npm i solidity-math
Usage
Named import:
import { uint256, uint128, type, unchecked } from "solidity-math";
const a = uint256(10);
const b = uint128(20);
console.log(a.add(b)); // uint256(30)
console.log(a.add(20)); // uint256(30)
console.log(a.add("20")); // uint256(30)
console.log(a.lte(0)); // false
unchecked(() => {
const c = type(uint256).max.add(1).add(a);
console.log(a.eq(c)); // true
});
const c = type(uint256).max.add(1).add(a); // RangeError: Value overflow: uint256(115792089237316195423570985008687907853269984665640564039457584007913129639946)
Default import:
import SM from "solidity-math";
const a = SM.uint256(10);
const b = SM.uint128(20);
const c = SM.unchecked(() => SM.type(SM.uint256).max.add(11));
Motivation
Certain decentralized applications require external actors to regulraly interact with on-chain contracts to ensure normal operations. DEXs like Uniswap rely on arbitrageurs and JIT LP to maintain market prices. Jobs on Keep3r Network will only be executed if they are profitable. These external actors need to pre-compute the rewards of their work, often in a very short amount of time to compete with other actors. It is infeasible to do all calculations on a smart contract as the connection overhead will be too slow. This package is an excellent tool to perform such calculations.
Comprehensive integer sizes
Packages like fixed-bn and uint256 offer either only uint256 or limited integer sizes. This package provides all integer sizes suppported by Solidity.
Unchecked arithmetic
To the best of the author's knowledge, there is no Javascript package that allows users to
toggle on and off unchecked { ... }
mode. This package does it in the closest possible syntax.
Right shift operator
Solidity's right shift operator (after v0.5.0) has a different implementation than bn.js.
x >> y is equivalent to the mathematical expression x / 2**y, rounded towards negative infinity.
For example, in Solidity, -204812 >> 10 == -201
,
whereas in bn.js, (new BN(-204812)).ushrn(10)
returns -200
, i.e., rounded towards zero.
Bitwise operators
To replicate Solidity x & y
in bn.js, one must explicity convert to two's complement representation
and do the verbose x.toTwos(256).uand(y.toTwos(256)).fromTwos(256)
.
Documentation
Types
Note 1: uint
& int
aliases are not implemented as they are redundant and confusing.
Note 2: Fixed point numbers are not implemented because it's not fully supported by Solidity yet as of 0.8.17.
Unsigned | Signed |
---|---|
uint8 |
int8 |
uint16 |
int16 |
uint24 |
int24 |
uint32 |
int32 |
uint40 |
int40 |
uint48 |
int48 |
uint56 |
int56 |
uint64 |
int64 |
uint72 |
int72 |
uint80 |
int80 |
uint88 |
int88 |
uint96 |
int96 |
uint104 |
int104 |
uint112 |
int112 |
uint120 |
int120 |
uint128 |
int128 |
uint136 |
int136 |
uint144 |
int144 |
uint152 |
int152 |
uint160 |
int160 |
uint168 |
int168 |
uint176 |
int176 |
uint184 |
int184 |
uint192 |
int192 |
uint200 |
int200 |
uint208 |
int208 |
uint216 |
int216 |
uint224 |
int224 |
uint232 |
int232 |
uint240 |
int240 |
uint248 |
int248 |
uint256 |
int256 |
The base class of all classes is an abstract class BaseInteger
.
All unsigned integers are of a single subclass Uint
, and all signed integers Int
.
These "types" are not Javascript classes, but merely functions to create new Solidity numbers.
Operations
There are restrictions on the types of operands, as enforced by Solidity.
uint256(1).add(uint256(2)); // valid
uint256(1).add(int256(2)); // TypeError: Operator "add" not compatible with types uint256 and int256.
uint64(1).iadd(uint256(2)) // TypeError: Operator "iadd" not compatible with uint64 and a larger type uint256
int256(1).pow(int256(-1)); // TypeError: Operator "pow" not compatible with signed type int256
uint256(-1); // RangeError: Value overflow: uint256(-1)
The right operand can also be a regular JS number, string, or another BN. However, it must fit into the range of left operand type, and must stay compliant of Solidity restrictions.
uint256(1).add(2); // uint256(3)
uint256(1).add("3"); // uint256(4)
uint256(1).add(new BN(4)); // uint256(5)
uint256(1).add(-1); // TypeError: Right operand -1 does not fit into type uint256
int256(1).pow(-1); // TypeError: Operator "pow" not compatible with negative value -1
Restrictions:
Symbol | Description |
---|---|
A |
a must be unsigned |
B |
b must be unsigned |
≌ |
a & b must have the same signedness |
≥ |
a must have same or larger type than b
|
List of Solidity operations supported:
Method | In-place method | Solidity Equivalent | Restriction | In-place restriction |
---|---|---|---|---|
a.add(b) |
a.iadd(b) |
a + b |
≌ | ≌, ≥ |
a.sub(b) |
a.isub(b) |
a - b |
≌ | ≌, ≥ |
a.mul(b) |
a.imul(b) |
a * b |
≌ | ≌, ≥ |
a.div(b) |
a.idiv(b) |
a / b |
≌ | ≌, ≥ |
a.mod(b) |
a.imod(b) |
a % b |
≌ | ≌, ≥ |
a.pow(b) |
a ** b |
B | ||
a.neg() |
-a |
A | ||
a.addmod(b, m) |
assembly { addmod(a, b, m) } |
≌ | ||
a.mulmod(b, m) |
assembly { mulmod(a, b, m) } |
≌ | ||
a.shln(b) |
a.ishln(b) |
a << b |
B | B |
a.shrn(b) |
a.ishrn(b) |
a >> b |
B | B |
a.and(b) |
a.iand(b) |
a & b |
≌ | ≌, ≥ |
a.or(b) |
a.ior(b) |
a | b |
≌ | ≌, ≥ |
a.xor(b) |
a.ixor(b) |
a ^ b |
≌ | ≌, ≥ |
a.not() |
~a |
|||
a.gt(b) |
a > b |
≌ | ||
a.lt(b) |
a < b |
≌ | ||
a.gte(b) |
a >= b |
≌ | ||
a.lte(b) |
a <= b |
≌ | ||
a.eq(b) |
a == b |
≌ | ||
a.neq(b) |
a != b |
≌ |
Note that for out-of-place arithmetic and bitwise operators, the output will always have the larger type among
a
and b
. For example, int112(0).add(int64(0))
will have type int112
.
The below comparison methods will return an Uint
or Int
instance (either 1
or 0
), depending on a
, instead of boolean:
Method | Restriction |
---|---|
a.gt_(b) |
≌ |
a.lt_(b) |
≌ |
a.gte_(b) |
≌ |
a.lte_(b) |
≌ |
a.eq_(b) |
≌ |
a.neq_(b) |
≌ |
uint256(10).gt(uint256(2)); // true
uint256(10).gt(uint256(20)); // false
uint256(10).gt_(uint256(2)); // uint256(1)
uint256(10).gt_(uint256(20)); // uint256(0)
Other supported functions:
Method | Return type | Description |
---|---|---|
a.clone() |
typeof a |
Returns a clone of a. |
a.cast(_type) |
_type |
Returns a new instance of type _type and the value of a. |
a.like(b) |
typeof b |
Returns a new instance of same type as b and the value of a. |
a.toString(base: number) |
string |
Returns the base-string and pad with zeroes. |
Maximum and Minimum
For any type, e.g. uint256
, you can use type(uint256).min
and type(uint256).max
to access the minimum and maximum value representable by the type.
import { uint256, type } from "solidity-math";
const a = type(uint256).max; // uint256(115792089237316195423570985008687907853269984665640564039457584007913129639935)
Overflow
Same as in Solidity, by default, all arithmetic operations are checked for overflow:
import { uint256, type } from "solidity-math";
const a = type(uint256).max;
a.add(1); // RangeError: Value overflow: uint256(115792089237316195423570985008687907853269984665640564039457584007913129639936)
Unchecked Mode
You can replicate Solidity's unchecked behaviour.
Simply put your calculations as a callback function inside unchecked()
:
// Solidity code
uint256 a;
unchecked {
a = type(uint256).max + 1; // 0
}
// Typescript equivalent
import { uint256, type, unchecked } from "solidity-math";
let a = uint256(0);
unchecked(() => {
a = type(uint256).max.add(1); // uint256(0)
})
You can also directly access the return value of your callback function:
import { uint256, type, unchecked } from "solidity-math";
const a = unchecked(() => type(uint256).max.add(1)); // uint256(0)
For the purpose of this package, you should also perform Solidity inline assembly assembly { ... }
in unchecked
mode.
Casting
Casting between unsigned & signed types are not allowed.
const a = uint256(10);
// Cast a to type uint64
const b = a.cast(uint64);
const c = uint64(a);
// Cast a to same type as b
const d = a.like(b);
Example
Muldiv
muldiv is an algorithm that calculates floor(a * b / denominator)
.
It is also included in Uniswap V3 FullMath.sol.
Below is the Typescript equivalent function. Note that the original code is in Solidity <0.8.0,
which allows -uint256(denominator)
. To use this package,
we need to perform uint256(0).sub(denominator)
in unchecked mode.
import { unchecked, uint256, Uint, type } from "solidity-math";
function muldiv(a: Uint, b: Uint, denominator: Uint) {
if (!denominator.gt(0)) {
throw new Error;
}
const mm = unchecked(() => a.mulmod(b, type(uint256).max));
let prod0 = a.mul(b);
let prod1 = mm.sub(prod0).sub(a.lt_(b));
if (prod1.eq(0)) {
return prod0.div(denominator);
}
if (!prod1.lt(denominator)) {
throw new Error;
}
const remainder = unchecked(() => a.mulmod(b, denominator));
prod1 = prod1.sub(remainder.gt_(prod0));
prod0 = prod0.sub(remainder);
let twos = uint256(0);
// -x for uint256 is disabled since 0.8.0
// so we need unchecked mode
unchecked(() => {
twos = uint256(0).sub(denominator).and(denominator);
denominator = denominator.div(twos);
prod0 = prod0.div(twos);
twos = uint256(0).sub(twos).div(twos).add(1);
});
prod0.ior(prod1.mul(twos));
const inv = denominator.xor(2).mul(3);
inv.imul(uint256(2).sub(denominator.mul(inv)));
inv.imul(uint256(2).sub(denominator.mul(inv)));
inv.imul(uint256(2).sub(denominator.mul(inv)));
inv.imul(uint256(2).sub(denominator.mul(inv)));
inv.imul(uint256(2).sub(denominator.mul(inv)));
inv.imul(uint256(2).sub(denominator.mul(inv)));
const result = prod0.mul(inv);
return result;
}
const a = uint256(14718);
const b = uint256(13812);
const denominator = uint256(151231);
console.log(muldiv(a, b, denominator)); // uint256(1344)