Optimize toString (#3573)

Co-authored-by: Hadrien Croubois <hadrien.croubois@gmail.com>
Co-authored-by: Francisco Giordano <frangio.1@gmail.com>
pull/3620/head^2
Igor Żuk 2 years ago committed by GitHub
parent 1eb55e2864
commit 160bf1a6eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 8
      contracts/mocks/StringsMock.sol
  3. 107
      contracts/utils/Strings.sol
  4. 65
      test/utils/Strings.test.js

@ -26,6 +26,7 @@
* `Checkpoints`: Use procedural generation to support multiple key/value lengths. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589)) * `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)) * `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)) * `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 ### Breaking changes

@ -5,19 +5,19 @@ pragma solidity ^0.8.0;
import "../utils/Strings.sol"; import "../utils/Strings.sol";
contract StringsMock { contract StringsMock {
function fromUint256(uint256 value) public pure returns (string memory) { function toString(uint256 value) public pure returns (string memory) {
return Strings.toString(value); 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); 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); 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); return Strings.toHexString(addr);
} }
} }

@ -7,48 +7,99 @@ pragma solidity ^0.8.0;
* @dev String operations. * @dev String operations.
*/ */
library Strings { library Strings {
bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; bytes16 private constant _SYMBOLS = "0123456789abcdef";
uint8 private constant _ADDRESS_LENGTH = 20; uint8 private constant _ADDRESS_LENGTH = 20;
/** /**
* @dev Converts a `uint256` to its ASCII `string` decimal representation. * @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/ */
function toString(uint256 value) internal pure returns (string memory) { function toString(uint256 value) internal pure returns (string memory) {
// Inspired by OraclizeAPI's implementation - MIT licence unchecked {
// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol uint256 length = 1;
if (value == 0) { // compute log10(value), and add it to length
return "0"; uint256 valueCopy = value;
} if (valueCopy >= 10**64) {
uint256 temp = value; valueCopy /= 10**64;
uint256 digits; length += 64;
while (temp != 0) { }
digits++; if (valueCopy >= 10**32) {
temp /= 10; valueCopy /= 10**32;
} length += 32;
bytes memory buffer = new bytes(digits); }
while (value != 0) { if (valueCopy >= 10**16) {
digits -= 1; valueCopy /= 10**16;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); length += 16;
value /= 10; }
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. * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/ */
function toHexString(uint256 value) internal pure returns (string memory) { function toHexString(uint256 value) internal pure returns (string memory) {
if (value == 0) { unchecked {
return "0x00"; uint256 length = 1;
}
uint256 temp = value; // compute log256(value), and add it to length
uint256 length = 0; uint256 valueCopy = value;
while (temp != 0) { if (valueCopy >= 1 << 128) {
length++; valueCopy >>= 128;
temp >>= 8; 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[0] = "0";
buffer[1] = "x"; buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) { for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _HEX_SYMBOLS[value & 0xf]; buffer[i] = _SYMBOLS[value & 0xf];
value >>= 4; value >>= 4;
} }
require(value == 0, "Strings: hex length insufficient"); require(value == 0, "Strings: hex length insufficient");

@ -1,71 +1,86 @@
const { constants, expectRevert } = require('@openzeppelin/test-helpers'); const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai'); const { expect } = require('chai');
const StringsMock = artifacts.require('StringsMock'); const StringsMock = artifacts.require('StringsMock');
contract('Strings', function (accounts) { contract('Strings', function (accounts) {
beforeEach(async function () { before(async function () {
this.strings = await StringsMock.new(); this.strings = await StringsMock.new();
}); });
describe('from uint256 - decimal format', function () { describe('toString', function () {
it('converts 0', async function () { for (const [ key, value ] of Object.entries([
expect(await this.strings.fromUint256(0)).to.equal('0'); '0',
}); '7',
'10',
it('converts a positive number', async function () { '99',
expect(await this.strings.fromUint256(4132)).to.equal('4132'); '100',
}); '101',
'123',
it('converts MAX_UINT256', async function () { '4132',
expect(await this.strings.fromUint256(constants.MAX_UINT256)).to.equal(constants.MAX_UINT256.toString()); '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 () { 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 () { 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 () { 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)); .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 () { 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'); .to.equal('0x0000000000000000000000000000000000000000000000000000000000004132');
}); });
it('converts a positive number (short)', async function () { it('converts a positive number (short)', async function () {
await expectRevert( await expectRevert(
this.strings.fromUint256HexFixed(0x4132, 1), this.strings.methods['toHexString(uint256,uint256)'](0x4132, 1),
'Strings: hex length insufficient', 'Strings: hex length insufficient',
); );
}); });
it('converts MAX_UINT256', async function () { 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)); .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 () { it('converts a random address', async function () {
const addr = '0xa9036907dccae6a1e0033479b12e837e5cf5a02f'; 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 () { it('converts an address with leading zeros', async function () {
const addr = '0x0000e0ca771e21bd00057f54a68c30d400000000'; const addr = '0x0000e0ca771e21bd00057f54a68c30d400000000';
expect(await this.strings.fromAddressHexFixed(addr)).to.equal(addr); expect(await this.strings.methods['toHexString(address)'](addr)).to.equal(addr);
}); });
}); });
}); });

Loading…
Cancel
Save