Skip to content

Commit

Permalink
Optimize toString (#3573)
Browse files Browse the repository at this point in the history
Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
  • Loading branch information
3 people committed Aug 31, 2022
1 parent 1eb55e2 commit 160bf1a
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 57 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
* `Checkpoints`: Use procedural generation to support multiple key/value lengths. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
* `Checkpoints`: Add new lookup mechanisms. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
* `Array`: Add `unsafeAccess` functions that allow reading and writing to an element in a storage array bypassing Solidity's "out-of-bounds" check. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
* `Strings`: optimize `toString`. ([#3573](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3573))

### Breaking changes

Expand Down
8 changes: 4 additions & 4 deletions contracts/mocks/StringsMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ pragma solidity ^0.8.0;
import "../utils/Strings.sol";

contract StringsMock {
function fromUint256(uint256 value) public pure returns (string memory) {
function toString(uint256 value) public pure returns (string memory) {
return Strings.toString(value);
}

function fromUint256Hex(uint256 value) public pure returns (string memory) {
function toHexString(uint256 value) public pure returns (string memory) {
return Strings.toHexString(value);
}

function fromUint256HexFixed(uint256 value, uint256 length) public pure returns (string memory) {
function toHexString(uint256 value, uint256 length) public pure returns (string memory) {
return Strings.toHexString(value, length);
}

function fromAddressHexFixed(address addr) public pure returns (string memory) {
function toHexString(address addr) public pure returns (string memory) {
return Strings.toHexString(addr);
}
}
107 changes: 79 additions & 28 deletions contracts/utils/Strings.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,99 @@ pragma solidity ^0.8.0;
* @dev String operations.
*/
library Strings {
bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
bytes16 private constant _SYMBOLS = "0123456789abcdef";
uint8 private constant _ADDRESS_LENGTH = 20;

/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) internal pure returns (string memory) {
// Inspired by OraclizeAPI's implementation - MIT licence
// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
unchecked {
uint256 length = 1;

if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
// compute log10(value), and add it to length
uint256 valueCopy = value;
if (valueCopy >= 10**64) {
valueCopy /= 10**64;
length += 64;
}
if (valueCopy >= 10**32) {
valueCopy /= 10**32;
length += 32;
}
if (valueCopy >= 10**16) {
valueCopy /= 10**16;
length += 16;
}
if (valueCopy >= 10**8) {
valueCopy /= 10**8;
length += 8;
}
if (valueCopy >= 10**4) {
valueCopy /= 10**4;
length += 4;
}
if (valueCopy >= 10**2) {
valueCopy /= 10**2;
length += 2;
}
if (valueCopy >= 10**1) {
length += 1;
}
// now, length is log10(value) + 1

string memory buffer = new string(length);
uint256 ptr;
/// @solidity memory-safe-assembly
assembly {
ptr := add(buffer, add(32, length))
}
while (true) {
ptr--;
/// @solidity memory-safe-assembly
assembly {
mstore8(ptr, byte(mod(value, 10), _SYMBOLS))
}
value /= 10;
if (value == 0) break;
}
return buffer;
}
return string(buffer);
}

/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) internal pure returns (string memory) {
if (value == 0) {
return "0x00";
}
uint256 temp = value;
uint256 length = 0;
while (temp != 0) {
length++;
temp >>= 8;
unchecked {
uint256 length = 1;

// compute log256(value), and add it to length
uint256 valueCopy = value;
if (valueCopy >= 1 << 128) {
valueCopy >>= 128;
length += 16;
}
if (valueCopy >= 1 << 64) {
valueCopy >>= 64;
length += 8;
}
if (valueCopy >= 1 << 32) {
valueCopy >>= 32;
length += 4;
}
if (valueCopy >= 1 << 16) {
valueCopy >>= 16;
length += 2;
}
if (valueCopy >= 1 << 8) {
valueCopy >>= 8;
length += 1;
}
// now, length is log256(value) + 1

return toHexString(value, length);
}
return toHexString(value, length);
}

/**
Expand All @@ -59,7 +110,7 @@ library Strings {
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _HEX_SYMBOLS[value & 0xf];
buffer[i] = _SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
Expand Down
65 changes: 40 additions & 25 deletions test/utils/Strings.test.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,86 @@
const { constants, expectRevert } = require('@openzeppelin/test-helpers');
const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');

const { expect } = require('chai');

const StringsMock = artifacts.require('StringsMock');

contract('Strings', function (accounts) {
beforeEach(async function () {
before(async function () {
this.strings = await StringsMock.new();
});

describe('from uint256 - decimal format', function () {
it('converts 0', async function () {
expect(await this.strings.fromUint256(0)).to.equal('0');
});

it('converts a positive number', async function () {
expect(await this.strings.fromUint256(4132)).to.equal('4132');
});

it('converts MAX_UINT256', async function () {
expect(await this.strings.fromUint256(constants.MAX_UINT256)).to.equal(constants.MAX_UINT256.toString());
});
describe('toString', function () {
for (const [ key, value ] of Object.entries([
'0',
'7',
'10',
'99',
'100',
'101',
'123',
'4132',
'12345',
'1234567',
'1234567890',
'123456789012345',
'12345678901234567890',
'123456789012345678901234567890',
'1234567890123456789012345678901234567890',
'12345678901234567890123456789012345678901234567890',
'123456789012345678901234567890123456789012345678901234567890',
'1234567890123456789012345678901234567890123456789012345678901234567890',
].reduce((acc, value) => Object.assign(acc, { [value]: new BN(value) }), {
MAX_UINT256: constants.MAX_UINT256.toString(),
}))) {
it(`converts ${key}`, async function () {
expect(await this.strings.methods['toString(uint256)'](value)).to.equal(value.toString(10));
});
}
});

describe('from uint256 - hex format', function () {
describe('toHexString', function () {
it('converts 0', async function () {
expect(await this.strings.fromUint256Hex(0)).to.equal('0x00');
expect(await this.strings.methods['toHexString(uint256)'](0)).to.equal('0x00');
});

it('converts a positive number', async function () {
expect(await this.strings.fromUint256Hex(0x4132)).to.equal('0x4132');
expect(await this.strings.methods['toHexString(uint256)'](0x4132)).to.equal('0x4132');
});

it('converts MAX_UINT256', async function () {
expect(await this.strings.fromUint256Hex(constants.MAX_UINT256))
expect(await this.strings.methods['toHexString(uint256)'](constants.MAX_UINT256))
.to.equal(web3.utils.toHex(constants.MAX_UINT256));
});
});

describe('from uint256 - fixed hex format', function () {
describe('toHexString fixed', function () {
it('converts a positive number (long)', async function () {
expect(await this.strings.fromUint256HexFixed(0x4132, 32))
expect(await this.strings.methods['toHexString(uint256,uint256)'](0x4132, 32))
.to.equal('0x0000000000000000000000000000000000000000000000000000000000004132');
});

it('converts a positive number (short)', async function () {
await expectRevert(
this.strings.fromUint256HexFixed(0x4132, 1),
this.strings.methods['toHexString(uint256,uint256)'](0x4132, 1),
'Strings: hex length insufficient',
);
});

it('converts MAX_UINT256', async function () {
expect(await this.strings.fromUint256HexFixed(constants.MAX_UINT256, 32))
expect(await this.strings.methods['toHexString(uint256,uint256)'](constants.MAX_UINT256, 32))
.to.equal(web3.utils.toHex(constants.MAX_UINT256));
});
});

describe('from address - fixed hex format', function () {
describe('toHexString address', function () {
it('converts a random address', async function () {
const addr = '0xa9036907dccae6a1e0033479b12e837e5cf5a02f';
expect(await this.strings.fromAddressHexFixed(addr)).to.equal(addr);
expect(await this.strings.methods['toHexString(address)'](addr)).to.equal(addr);
});

it('converts an address with leading zeros', async function () {
const addr = '0x0000e0ca771e21bd00057f54a68c30d400000000';
expect(await this.strings.fromAddressHexFixed(addr)).to.equal(addr);
expect(await this.strings.methods['toHexString(address)'](addr)).to.equal(addr);
});
});
});

0 comments on commit 160bf1a

Please sign in to comment.