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

Add Base64 library to utils #2884

Merged
merged 11 commits into from
Dec 29, 2021
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* `Votes`: Added a base contract for vote tracking with delegation. ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944))
* `ERC721Votes`: Added an extension of ERC721 enabled with vote tracking and delegation. ([#2944](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2944))
* `ERC2771Context`: use immutable storage to store the forwarder address, no longer an issue since Solidity >=0.8.8 allows reading immutable variables in the constructor. ([#2917](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2917))
* `Base64`: add a library to parse bytes into base64 strings using `encode(bytes memory)` function, and provide examples to show how to use to build URL-safe `tokenURIs` ([#2884](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/#2884))

## 4.4.1 (2021-12-14)

Expand Down
11 changes: 11 additions & 0 deletions contracts/mocks/Base64Mock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "../utils/Base64.sol";

contract Base64Mock {

Choose a reason for hiding this comment

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

just my 2 cents but as an alternative, you can have the following line at the top of the contract:
using Base64 for bytes;

and then in your encode:
value.encode();

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree it looks better just using the .encode but I'm following the same pattern used for Strings mock. I'll wait if there's somebody from the OZ team who has input on this and why to use one or the other.

function encode(bytes memory value) external pure returns (string memory) {
return Base64.encode(value);
}
}
88 changes: 88 additions & 0 deletions contracts/utils/Base64.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

/**
* @dev Provides a set of functions to operate with Base64 strings.
*/
library Base64 {
/**
* @dev Base64 Encoding/Decoding Table
*/
string internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/**
* @dev Converts a `bytes` to its Bytes64 `string` representation.
*/
function encode(bytes memory data) internal pure returns (string memory) {
/**
* Inspired by Brecht Devos (Brechtpd) implementation - MIT licence
* https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol
*/
if (data.length == 0) return "";

// Loads the table into memory
string memory table = _TABLE;

// Encoding takes 3 bytes chunks of binary data from `bytes` data parameter
// and split into 4 numbers of 6 bits.
// The final Base64 length should be `bytes` data length multiplied by 4/3 rounded up
// - `data.length + 2` -> Round up
// - `/ 3` -> Number of 3-bytes chunks
// - `4 *` -> 4 characters for each chunk
string memory result = new string(4 * ((data.length + 2) / 3));

assembly {
// Prepare the lookup table (skip the first "length" byte)
let tablePtr := add(table, 1)

// Prepare result pointer, jump over length
let resultPtr := add(result, 32)

// Run over the input, 3 bytes at a time
for {
let dataPtr := data
let endPtr := add(data, mload(data))
} lt(dataPtr, endPtr) {

} {
// Advance 3 bytes
dataPtr := add(dataPtr, 3)
let input := mload(dataPtr)

// To write each character, shift the 3 bytes (18 bits) chunk
// 4 times in blocks of 6 bits for each character (18, 12, 6, 0)
// and apply logical AND with 0x3F which is the number of
// the previous character in the ASCII table prior to the Base64 Table
// The result is then added to the table to get the character to write,
// and finally write it in the result pointer but with a left shift
// of 256 (1 byte) - 8 (1 ASCII char) = 248 bits

mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F))))
resultPtr := add(resultPtr, 1) // Advance

mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F))))
resultPtr := add(resultPtr, 1) // Advance

mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F))))
resultPtr := add(resultPtr, 1) // Advance

mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F))))
resultPtr := add(resultPtr, 1) // Advance
}

// When data `bytes` is not exactly 3 bytes long
// it is padded with `=` characters at the end
switch mod(mload(data), 3)
case 1 {
mstore8(sub(resultPtr, 1), 0x3d)
mstore8(sub(resultPtr, 2), 0x3d)
}
case 2 {
mstore8(sub(resultPtr, 1), 0x3d)
}
}

return result;
}
}
4 changes: 3 additions & 1 deletion contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/

Miscellaneous contracts and libraries containing utility functions you can use to improve security, work with new data types, or safely use low-level primitives.

The {Address}, {Arrays} and {Strings} libraries provide more operations related to these native data types, while {SafeCast} adds ways to safely convert between the different signed and unsigned numeric types.
The {Address}, {Arrays}, {Base64} and {Strings} libraries provide more operations related to these native data types, while {SafeCast} adds ways to safely convert between the different signed and unsigned numeric types.
{Multicall} provides a function to batch together multiple calls in a single external call.

For new data types:
Expand Down Expand Up @@ -96,6 +96,8 @@ Note that, in all cases, accounts simply _declare_ their interfaces, but they ar

{{Arrays}}

{{Base64}}

{{Counters}}

{{Strings}}
Expand Down
4 changes: 3 additions & 1 deletion docs/modules/ROOT/pages/erc1155.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ The JSON document for token ID 2 might look something like:

For more information about the metadata JSON Schema, check out the https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md#erc-1155-metadata-uri-json-schema[ERC-1155 Metadata URI JSON Schema].

NOTE: you'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game!
NOTE: You'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game!

TIP: If you'd like to put all item information on-chain, you can extend ERC721 to do so (though it will be rather costly) by providing a xref:utilities.adoc#base64[`Base64`] Data URI with the JSON schema encoded. You could also leverage IPFS to store the URI information, but these techniques are out of the scope of this overview guide

[[sending-to-contracts]]
== Sending Tokens to Contracts
Expand Down
4 changes: 3 additions & 1 deletion docs/modules/ROOT/pages/erc721.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ This `tokenURI` should resolve to a JSON document that might look something like

For more information about the `tokenURI` metadata JSON Schema, check out the https://eips.ethereum.org/EIPS/eip-721[ERC721 specification].

NOTE: you'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game! If you'd like to put all item information on-chain, you can extend ERC721 to do so (though it will be rather costly). You could also leverage IPFS to store the tokenURI information, but these techniques are out of the scope of this overview guide.
NOTE: You'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game!

TIP: If you'd like to put all item information on-chain, you can extend ERC721 to do so (though it will be rather costly) by providing a xref:utilities.adoc#base64[`Base64`] Data URI with the JSON schema encoded. You could also leverage IPFS to store the tokenURI information, but these techniques are out of the scope of this overview guide.

[[Presets]]
== Preset ERC721 contract
Expand Down
48 changes: 48 additions & 0 deletions docs/modules/ROOT/pages/utilities.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,54 @@ Want to check if an address is a contract? Use xref:api:utils.adoc#Address[`Addr

Want to keep track of some numbers that increment by 1 every time you want another one? Check out xref:api:utils.adoc#Counters[`Counters`]. This is useful for lots of things, like creating incremental identifiers, as shown on the xref:erc721.adoc[ERC721 guide].

=== Base64

xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data into its Base64 `string` representation.

This is specially useful to build URL-safe tokenURIs for both xref:api:token/ERC721.adoc#IERC721Metadata-tokenURI-uint256-[`ERC721`] or xref:api:token/ERC1155.adoc#IERC1155MetadataURI-uri-uint256-[`ERC1155`]. This library provides a clever way to serve URL-safe https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs/[Data URI] compliant strings to serve on-chain data structures.

Consider this is an example to send JSON Metadata through a Base64 Data URI using an ERC721:

[source, solidity]
----
// contracts/My721Token.sol
// SPDX-License-Identifier: MIT

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";

contract My721Token is ERC721 {
using Strings for uint256;
using Base64 for bytes;

constructor() ERC721("My721Token", "MTK") {}

...

function tokenURI(uint256 tokenId)
public
pure
override
returns (string memory)
{
bytes memory dataURI = abi.encodePacked(
'{
"name": "My721Token #', tokenId.toString(), '"',
// Replace with extra ERC721 Metadata properties
'}'
);

return string(
abi.encodePacked(
"data:application/json;base64,",
dataURI.encode()
)
);
}
}
----

=== Multicall

The `Multicall` abstract contract comes with a `multicall` function that bundles together multiple calls in a single external call. With it, external accounts may perform atomic operations comprising several function calls. This is not only useful for EOAs to make multiple calls in a single transaction, it's also a way to revert a previous call if a later one fails.
Expand Down
29 changes: 29 additions & 0 deletions test/utils/Base64.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const { expect } = require('chai');

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

contract('Strings', function () {
beforeEach(async function () {
this.base64 = await Base64Mock.new();
});

describe('from bytes - base64', function () {
it('converts to base64 encoded string with double padding', async function () {
const TEST_MESSAGE = 'test';
const input = web3.utils.asciiToHex(TEST_MESSAGE);
expect(await this.base64.encode(input)).to.equal('dGVzdA==');
});

it('converts to base64 encoded string with single padding', async function () {
const TEST_MESSAGE = 'test1';
const input = web3.utils.asciiToHex(TEST_MESSAGE);
expect(await this.base64.encode(input)).to.equal('dGVzdDE=');
});

it('converts to base64 encoded string without padding', async function () {
const TEST_MESSAGE = 'test12';
const input = web3.utils.asciiToHex(TEST_MESSAGE);
expect(await this.base64.encode(input)).to.equal('dGVzdDEy');
});
});
});