Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize toString #3573

Merged
merged 18 commits into from
Aug 31, 2022
Merged
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";
frangio marked this conversation as resolved.
Show resolved Hide resolved
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))
frangio marked this conversation as resolved.
Show resolved Hide resolved
}
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;
}
Comment on lines 112 to 115
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be written in the same or similar way that the decimal toString.

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);
});
});
});