Skip to content

Commit 260e082

Browse files
Amxxfrangio
andauthored
Add a library for handling short strings in a gas efficient way (#4023)
Co-authored-by: Francisco <frangio.1@gmail.com>
1 parent 3b591a4 commit 260e082

File tree

6 files changed

+153
-8
lines changed

6 files changed

+153
-8
lines changed

.changeset/violet-frogs-hide.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ShortStrings`: Added a library for handling short strings in a gas efficient way, with fallback to storage for longer strings.

contracts/utils/README.adoc

+2
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ Note that, in all cases, accounts simply _declare_ their interfaces, but they ar
106106

107107
{{Strings}}
108108

109+
{{ShortStrings}}
110+
109111
{{StorageSlot}}
110112

111113
{{Multicall}}

contracts/utils/ShortStrings.sol

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.8;
4+
5+
import "./StorageSlot.sol";
6+
7+
type ShortString is bytes32;
8+
9+
/**
10+
* @dev This library provides functions to convert short memory strings
11+
* into a `ShortString` type that can be used as an immutable variable.
12+
* Strings of arbitrary length can be optimized if they are short enough by
13+
* the addition of a storage variable used as fallback.
14+
*
15+
* Usage example:
16+
*
17+
* ```solidity
18+
* contract Named {
19+
* using ShortStrings for *;
20+
*
21+
* ShortString private immutable _name;
22+
* string private _nameFallback;
23+
*
24+
* constructor(string memory contractName) {
25+
* _name = contractName.toShortStringWithFallback(_nameFallback);
26+
* }
27+
*
28+
* function name() external view returns (string memory) {
29+
* return _name.toStringWithFallback(_nameFallback);
30+
* }
31+
* }
32+
* ```
33+
*/
34+
library ShortStrings {
35+
error StringTooLong(string str);
36+
37+
/**
38+
* @dev Encode a string of at most 31 chars into a `ShortString`.
39+
*
40+
* This will trigger a `StringTooLong` error is the input string is too long.
41+
*/
42+
function toShortString(string memory str) internal pure returns (ShortString) {
43+
bytes memory bstr = bytes(str);
44+
if (bstr.length > 31) {
45+
revert StringTooLong(str);
46+
}
47+
return ShortString.wrap(bytes32(uint256(bytes32(bstr)) | bstr.length));
48+
}
49+
50+
/**
51+
* @dev Decode a `ShortString` back to a "normal" string.
52+
*/
53+
function toString(ShortString sstr) internal pure returns (string memory) {
54+
uint256 len = length(sstr);
55+
// using `new string(len)` would work locally but is not memory safe.
56+
string memory str = new string(32);
57+
/// @solidity memory-safe-assembly
58+
assembly {
59+
mstore(str, len)
60+
mstore(add(str, 0x20), sstr)
61+
}
62+
return str;
63+
}
64+
65+
/**
66+
* @dev Return the length of a `ShortString`.
67+
*/
68+
function length(ShortString sstr) internal pure returns (uint256) {
69+
return uint256(ShortString.unwrap(sstr)) & 0xFF;
70+
}
71+
72+
/**
73+
* @dev Encode a string into a `ShortString`, or write it to storage if it is too long.
74+
*/
75+
function toShortStringWithFallback(string memory value, string storage store) internal returns (ShortString) {
76+
if (bytes(value).length < 32) {
77+
return toShortString(value);
78+
} else {
79+
StorageSlot.getStringSlot(store).value = value;
80+
return ShortString.wrap(0);
81+
}
82+
}
83+
84+
/**
85+
* @dev Decode a string that was encoded to `ShortString` or written to storage using {setWithFallback}.
86+
*/
87+
function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) {
88+
if (length(value) > 0) {
89+
return toString(value);
90+
} else {
91+
return store;
92+
}
93+
}
94+
}

package-lock.json

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"glob": "^8.0.3",
7373
"graphlib": "^2.1.8",
7474
"hardhat": "^2.9.1",
75-
"hardhat-exposed": "^0.3.0",
75+
"hardhat-exposed": "^0.3.1",
7676
"hardhat-gas-reporter": "^1.0.4",
7777
"hardhat-ignore-warnings": "^0.2.0",
7878
"keccak256": "^1.0.2",

test/utils/ShortStrings.test.js

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
const { expect } = require('chai');
2+
const { expectRevertCustomError } = require('../helpers/customError');
3+
4+
const ShortStrings = artifacts.require('$ShortStrings');
5+
6+
function decode(sstr) {
7+
const length = parseInt(sstr.slice(64), 16);
8+
return web3.utils.toUtf8(sstr).slice(0, length);
9+
}
10+
11+
contract('ShortStrings', function () {
12+
before(async function () {
13+
this.mock = await ShortStrings.new();
14+
});
15+
16+
for (const str of [0, 1, 16, 31, 32, 64, 1024].map(length => 'a'.repeat(length))) {
17+
describe(`with string length ${str.length}`, function () {
18+
it('encode / decode', async function () {
19+
if (str.length < 32) {
20+
const encoded = await this.mock.$toShortString(str);
21+
expect(decode(encoded)).to.be.equal(str);
22+
23+
const length = await this.mock.$length(encoded);
24+
expect(length.toNumber()).to.be.equal(str.length);
25+
26+
const decoded = await this.mock.$toString(encoded);
27+
expect(decoded).to.be.equal(str);
28+
} else {
29+
await expectRevertCustomError(this.mock.$toShortString(str), `StringTooLong("${str}")`);
30+
}
31+
});
32+
33+
it('set / get with fallback', async function () {
34+
const { logs } = await this.mock.$toShortStringWithFallback(str, 0);
35+
const { ret0 } = logs.find(({ event }) => event == 'return$toShortStringWithFallback').args;
36+
37+
expect(await this.mock.$toString(ret0)).to.be.equal(str.length < 32 ? str : '');
38+
39+
const recovered = await this.mock.$toStringWithFallback(ret0, 0);
40+
expect(recovered).to.be.equal(str);
41+
});
42+
});
43+
}
44+
});

0 commit comments

Comments
 (0)