The Fastest Way To Convert uint256 to String in Solidity

Rob
3 min readJul 31, 2023

Solidity isn’t built for string manipulation, and until recently (solc 0.8.12) we didn’t even have native concatenation. Transforming primitives, like integers, addresses, and raw bytes into something readable is mostly handled on the client end by your js library. But sometimes you need to stringify in situ, and there are a few different ways to do it.

Something about cyclomatic complexity probably

The Algorithms

Provable (*Updated)

The most frequently cited answer is some variation of the algorithm from Provable Things (formerly Oraclize). About 80% of the solutions I’ve seen stem from this code. I’m testing the updated variant from Barnabas Ujvari, which runs on solc 0.8.x.

OpenZeppelin Strings.sol

Of course OpenZeppelin has code for this, and presumably it’s well tested. This one is also the most sophisticated I’ve seen, with logarithm tricks and inline assembly, hopefully resulting in lower gas use.

Mikhail Vladimirov’s method

I like this one because it comes from that place where cleverness wraps around and gets a little crazy. The code is undeniably ugly, but it’s a unique approach and I like an underdog so I’m including this algo.

Just ABI.encode it!

Most threads on this topic have someone claiming you can just encode() or encodePacked() your uint to a byte array and then cast the result to a string. Something like this string(abi.encode(myUint)). I’m skeptical, but let’s include it for completeness.

Methodology

We’ll use gasleft() to benchmark the gas before and after calling each algorithm. To make sure this work is being done in a real environment, we’ll put the code into non-pure contract methods and call them through transactions, so they are run by real EVM validator nodes. Each test looks roughly like this:

function uintToStringTest(uint256 i) external returns (string memory result, uint256 gasUsed) {
uint256 startingGas = gasleft();
result = Algos.uint2str(i); // <<-- algorithm
uint256 endingGas = gasleft();
gasUsed = (startingGas - endingGas);
emit TestResult(i, result, gasUsed);
}

A node.js script will send our transactions, capturing their gas spend and double-checking the veracity of the outputs to see if we detect any conversion bugs.

The contract code is compiled and deployed using Remix, on solc 0.8.21+commit.d9974bed, targeting the london EVM fork, and using 200 optimization runs. The compiled contract is deployed on Mumbai testnet at 0xb51a3175aCcE7D01bFD0717f9C4BD69a13dF6D3C.

Results

Oh boy, data! First, the obvious stuff. ABI.encode doesn’t convert uint256 to string. The output is a byte array that needs additional processing.

Next, inputs above type(uint256).max throw an error, as expected. Other than that, all three of the remaining methods returned faithful base-10 conversions.

The Provable Things method is slow across the board. This might have been a better option in earlier versions of Solidity, but going forward, this code should be deprecated.

OpenZeppelin performs well, as anticipated, but Mikhail Vladimirov’s dark horse algorithm edges into the lead when stringifying huge numbers, somewhere between 2⁴⁸ and 2⁶⁴. This suggests that OpenZeppelin’s core is faster but Mikhail’s binary sort approach scales better. Neat!

Raw data spreadsheet here:

Code for these tests is available on Github:

TL;DR

For massive numbers, like wallet addresses, you’ll get a little more performance from Mikhail Vladimirov’s algorithm.

For anything smaller, OpenZeppelin’s Strings library is a bit faster.

--

--

Rob

Leader of software engineers. Crypto enthusiast. Amateur bicycler.