0.1.0 • Published 2 years ago

solidity-math v0.1.0

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

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
  • ✅ 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.

UnsignedSigned
uint8int8
uint16int16
uint24int24
uint32int32
uint40int40
uint48int48
uint56int56
uint64int64
uint72int72
uint80int80
uint88int88
uint96int96
uint104int104
uint112int112
uint120int120
uint128int128
uint136int136
uint144int144
uint152int152
uint160int160
uint168int168
uint176int176
uint184int184
uint192int192
uint200int200
uint208int208
uint216int216
uint224int224
uint232int232
uint240int240
uint248int248
uint256int256

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)