Skip to content

Commit

Permalink
feat: add fast uint to string conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
dk1a committed Dec 15, 2022
1 parent dfb9916 commit 5a975fe
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 0 deletions.
76 changes: 76 additions & 0 deletions src/utils/toString.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

uint256 constant ASCII_DIGIT_OFFSET = 0x30;
// 96 = 78 rounded up to a multiple of 32
// 78 = ceil(log10(2**256))
uint256 constant MAX_UINT256_STRING_LENGTH = 96;

/**
* @dev uint256 to string (decimal).
* WARNING: this function is very optimized for gas, it's almost pure assembly.
* Just use OpenZeppelin's toString for safety and readability.
*
* (this is ~100 gas/digit, OZ is ~1000)
*
* Derived from https://github.com/moodlezoup/sol2string
*/
function toString(uint256 value) pure returns (string memory str) {
if (value <= 9) {
// very fast path for 1 digit
/// @solidity memory-safe-assembly
assembly {
// allocate memory (0x20 for length, 0x20 for content)
str := mload(0x40)
mstore(0x40, add(str, 0x40))
// store length
mstore(str, 1)
// store content
mstore8(add(str, 0x20), add(value, ASCII_DIGIT_OFFSET))
}
return str;
}

uint256 startPtr;
uint256 slidingPtr;
/// @solidity memory-safe-assembly
assembly {
// slidingPtr is confusing, here's an example if MAX_UINT256_STRING_LENGTH were equal 5:
// length (0x20) (5)
// |0000000000000000000000000000000000000000000000000000000000000000|0000000000|
// ^startPtr ^slidingPtr; mstore will write to the 32 bytes which end here ^
// <== and the pointer slides from right to left, filling each LSB

startPtr := mload(0x40)
// note how slidingPtr doesn't include 0x20 for length
slidingPtr := add(startPtr, MAX_UINT256_STRING_LENGTH)
// overallocate memory
// 0x20 for length, MAX_UINT256_STRING_LENGTH for content
mstore(0x40, add(0x20, slidingPtr))
}

// populate from right to left (lsb to msb)
while (value != 0) {
/// @solidity memory-safe-assembly
assembly {
let char := add(
mod(value, 10),
ASCII_DIGIT_OFFSET
)
mstore(slidingPtr, char)
slidingPtr := sub(slidingPtr, 1)
value := div(value, 10)
}
}

/// @solidity memory-safe-assembly
assembly {
let realLen := sub(MAX_UINT256_STRING_LENGTH, sub(slidingPtr, startPtr))
// move `str` pointer to the start of the string
str := slidingPtr
// store the real length
mstore(str, realLen)
}
return str;
}
25 changes: 25 additions & 0 deletions test/Utils.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

import { PRBTest } from "@prb/test/src/PRBTest.sol";

import { toString } from "../src/utils/toString.sol";

contract UtilsTest is PRBTest {
function testUintToString() public {
for (uint256 value; value < 10000; value++) {
assertEq(toString(value), vm.toString(value));
}
for (uint256 value; value < 10000; value++) {
assertEq(toString(10**77 - value), vm.toString(10**77 - value));
assertEq(toString(10**77 + value), vm.toString(10**77 + value));
}
assertEq(toString(type(uint256).max - 1), vm.toString(type(uint256).max - 1));
assertEq(toString(type(uint256).max), vm.toString(type(uint256).max));
}

function testUintToString__Fuzz(uint256 value) public {
assertEq(toString(value), vm.toString(value));
}
}

0 comments on commit 5a975fe

Please sign in to comment.