diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dc4ec6faab..8f99f35cac3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/contracts/mocks/StringsMock.sol b/contracts/mocks/StringsMock.sol index b8622680bab..90a6c94b34b 100644 --- a/contracts/mocks/StringsMock.sol +++ b/contracts/mocks/StringsMock.sol @@ -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); } } diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 39e0cf124f8..b7127f2264f 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -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); } /** @@ -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"); diff --git a/test/utils/Strings.test.js b/test/utils/Strings.test.js index 8dda829ea96..7abc859ff3a 100644 --- a/test/utils/Strings.test.js +++ b/test/utils/Strings.test.js @@ -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); }); }); });