From 157c32b65a15cb0b58257543643cafa1cebf883a Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 25 Sep 2021 15:25:57 -0500 Subject: [PATCH 1/8] Add Base64 library to utils --- contracts/mocks/Base64Mock.sol | 11 ++++ contracts/utils/Base64.sol | 102 +++++++++++++++++++++++++++++++++ test/utils/Base64.test.js | 29 ++++++++++ 3 files changed, 142 insertions(+) create mode 100644 contracts/mocks/Base64Mock.sol create mode 100644 contracts/utils/Base64.sol create mode 100644 test/utils/Base64.test.js diff --git a/contracts/mocks/Base64Mock.sol b/contracts/mocks/Base64Mock.sol new file mode 100644 index 00000000000..b255487bc5c --- /dev/null +++ b/contracts/mocks/Base64Mock.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Base64.sol"; + +contract Base64Mock { + function encode(bytes memory value) external pure returns (string memory) { + return Base64.encode(value); + } +} diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol new file mode 100644 index 00000000000..4cd2ec4ecee --- /dev/null +++ b/contracts/utils/Base64.sol @@ -0,0 +1,102 @@ +// 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. + * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence + * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol + */ + function encode(bytes memory data) internal pure returns (string memory) { + 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 + uint256 encodedLen = 4 * ((data.length + 2) / 3); + + // Add some extra buffer at the end required for the writing + string memory result = new string(encodedLen + 32); + + assembly { + // Store the actual result length in memory + mstore(result, encodedLen) + + // Prepare the lookup table + let tablePtr := add(table, 1) + + // Prepare input pointer + let dataPtr := data + let endPtr := add(dataPtr, mload(data)) + + // Prepare result pointer, jump over length + let resultPtr := add(result, 32) + + // Run over the input, 3 bytes at a time + for { } 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 + + mstore( + resultPtr, + shl(248, mload(add(tablePtr, and(shr(18, input), 0x3F)))) + ) + resultPtr := add(resultPtr, 1) // Advance + + mstore( + resultPtr, + shl(248, mload(add(tablePtr, and(shr(12, input), 0x3F)))) + ) + resultPtr := add(resultPtr, 1) // Advance + + mstore( + resultPtr, + shl(248, mload(add(tablePtr, and(shr(6, input), 0x3F)))) + ) + resultPtr := add(resultPtr, 1) // Advance + + mstore( + resultPtr, + shl(248, 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 beginning + switch mod(mload(data), 3) + case 1 { + mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) + } + case 2 { + mstore(sub(resultPtr, 1), shl(248, 0x3d)) + } + } + + return result; + } +} diff --git a/test/utils/Base64.test.js b/test/utils/Base64.test.js new file mode 100644 index 00000000000..5eb4f74b289 --- /dev/null +++ b/test/utils/Base64.test.js @@ -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'); + }); + }); +}); From df3d838388812d00f757cd3988f22611eb8cc98b Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 25 Sep 2021 15:43:53 -0500 Subject: [PATCH 2/8] Fix typo on Base64 padding --- contracts/utils/Base64.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 4cd2ec4ecee..7d79986aaaf 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -87,7 +87,7 @@ library Base64 { } // When data `bytes` is not exactly 3 bytes long - // it is padded with `=` characters at the beginning + // it is padded with `=` characters at the end switch mod(mload(data), 3) case 1 { mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) From 7c187e0b12c40e76e1de4e9e745f9cb1df4a3ddf Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 8 Oct 2021 01:09:05 -0500 Subject: [PATCH 3/8] Added documentation for Base64 and references from ERC1155 and ERC721 --- contracts/utils/Base64.sol | 46 +++++++++++------------- contracts/utils/README.adoc | 4 ++- docs/modules/ROOT/pages/erc1155.adoc | 4 ++- docs/modules/ROOT/pages/erc721.adoc | 4 ++- docs/modules/ROOT/pages/utilities.adoc | 48 ++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 29 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 7d79986aaaf..fb4c78f822f 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -6,17 +6,19 @@ pragma solidity ^0.8.0; * @dev Provides a set of functions to operate with Base64 strings. */ library Base64 { - /** - * @dev Base64 Encoding/Decoding Table - */ + /** + * @dev Base64 Encoding/Decoding Table + */ string internal constant _TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; /** * @dev Converts a `bytes` to its Bytes64 `string` representation. - * Inspired by Brecht Devos (Brechtpd) implementation - MIT licence - * https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol */ 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 @@ -25,7 +27,7 @@ library Base64 { // 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 + // - `data.length + 2` -> Round up // - `/ 3` -> Number of 3-bytes chunks // - `4 *` -> 4 characters for each chunk uint256 encodedLen = 4 * ((data.length + 2) / 3); @@ -48,7 +50,11 @@ library Base64 { let resultPtr := add(result, 32) // Run over the input, 3 bytes at a time - for { } lt(dataPtr, endPtr) { } { + for { + + } lt(dataPtr, endPtr) { + + } { // Advance 3 bytes dataPtr := add(dataPtr, 3) let input := mload(dataPtr) @@ -61,28 +67,16 @@ library Base64 { // and finally write it in the result pointer but with a left shift // of 256 (1 byte) - 8 (1 ASCII char) = 248 bits - mstore( - resultPtr, - shl(248, mload(add(tablePtr, and(shr(18, input), 0x3F)))) - ) + mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(18, input), 0x3F))))) resultPtr := add(resultPtr, 1) // Advance - - mstore( - resultPtr, - shl(248, mload(add(tablePtr, and(shr(12, input), 0x3F)))) - ) + + mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(12, input), 0x3F))))) resultPtr := add(resultPtr, 1) // Advance - - mstore( - resultPtr, - shl(248, mload(add(tablePtr, and(shr(6, input), 0x3F)))) - ) + + mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(6, input), 0x3F))))) resultPtr := add(resultPtr, 1) // Advance - - mstore( - resultPtr, - shl(248, mload(add(tablePtr, and(input, 0x3F)))) - ) + + mstore(resultPtr, shl(248, mload(add(tablePtr, and(input, 0x3F))))) resultPtr := add(resultPtr, 1) // Advance } diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 4edcf923bb1..0a501e922c4 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -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: @@ -94,6 +94,8 @@ Note that, in all cases, accounts simply _declare_ their interfaces, but they ar {{Arrays}} +{{Base64}} + {{Counters}} {{Strings}} diff --git a/docs/modules/ROOT/pages/erc1155.adoc b/docs/modules/ROOT/pages/erc1155.adoc index c9d7b9f8077..ee2e3d2e984 100644 --- a/docs/modules/ROOT/pages/erc1155.adoc +++ b/docs/modules/ROOT/pages/erc1155.adoc @@ -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 diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 8d28fad2e6e..d13f4539249 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -78,7 +78,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 diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 09c6b029c51..cc9b5365dfb 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -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 in 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. From c8a317cd54fa14b0ce05037010ea6a5fcdd133d7 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 8 Oct 2021 01:19:03 -0500 Subject: [PATCH 4/8] Updated Changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb03e735fa..40ea69c41c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ * Add internal `_setApprovalForAll` to `ERC721` and `ERC1155`. ([#2834](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2834)) * `Governor`: shift vote start and end by one block to better match Compound's GovernorBravo and prevent voting at the Governor level if the voting snapshot is not ready. ([#2892](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/#2892)) * `PaymentSplitter`: now supports ERC20 assets in addition to Ether. ([#2858](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/#2858)) - + * `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.3.2 (2021-09-14) * `UUPSUpgradeable`: Add modifiers to prevent `upgradeTo` and `upgradeToAndCall` being executed on any contract that is not the active ERC1967 proxy. This prevents these functions being called on implementation contracts or minimal ERC1167 clones, in particular. From 9094dfa5d377f778d769179919b766cd146a77b2 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 8 Oct 2021 01:22:16 -0500 Subject: [PATCH 5/8] Fix typo in utilities doc --- docs/modules/ROOT/pages/utilities.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index cc9b5365dfb..bfd2d772875 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -105,7 +105,7 @@ xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data 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 in a Base64 Data URI using an ERC721: +Consider this is an example to send JSON Metadata through a Base64 Data URI using an ERC721: [source, solidity] ---- From c1060ca493d30676973a83eadbff698accdc7ed3 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 21 Dec 2021 15:34:43 +0100 Subject: [PATCH 6/8] use mstore8 to improve memory accesses --- contracts/utils/Base64.sol | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index fb4c78f822f..f5e0b17220c 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -30,28 +30,19 @@ library Base64 { // - `data.length + 2` -> Round up // - `/ 3` -> Number of 3-bytes chunks // - `4 *` -> 4 characters for each chunk - uint256 encodedLen = 4 * ((data.length + 2) / 3); - - // Add some extra buffer at the end required for the writing - string memory result = new string(encodedLen + 32); + string memory result = new string(4 * ((data.length + 2) / 3)); assembly { - // Store the actual result length in memory - mstore(result, encodedLen) - - // Prepare the lookup table + // Prepare the lookup table (skip the first "length" byte) let tablePtr := add(table, 1) - // Prepare input pointer - let dataPtr := data - let endPtr := add(dataPtr, mload(data)) - // 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) { } { @@ -67,16 +58,16 @@ library Base64 { // and finally write it in the result pointer but with a left shift // of 256 (1 byte) - 8 (1 ASCII char) = 248 bits - mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(18, input), 0x3F))))) + mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F)))) resultPtr := add(resultPtr, 1) // Advance - mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(12, input), 0x3F))))) + mstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F)))) resultPtr := add(resultPtr, 1) // Advance - mstore(resultPtr, shl(248, mload(add(tablePtr, and(shr(6, input), 0x3F))))) + mstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F)))) resultPtr := add(resultPtr, 1) // Advance - mstore(resultPtr, shl(248, mload(add(tablePtr, and(input, 0x3F))))) + mstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F)))) resultPtr := add(resultPtr, 1) // Advance } @@ -84,10 +75,11 @@ library Base64 { // it is padded with `=` characters at the end switch mod(mload(data), 3) case 1 { - mstore(sub(resultPtr, 2), shl(240, 0x3d3d)) + mstore8(sub(resultPtr, 1), 0x3d) + mstore8(sub(resultPtr, 2), 0x3d) } case 2 { - mstore(sub(resultPtr, 1), shl(248, 0x3d)) + mstore8(sub(resultPtr, 1), 0x3d) } } From e48057c0573c3e9c3acf50d8fae772530f53ae11 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Wed, 29 Dec 2021 14:16:36 -0300 Subject: [PATCH 7/8] use shorter strings with encodePacked --- docs/modules/ROOT/pages/utilities.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 710ce3d12db..50d778cfec8 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -131,8 +131,8 @@ contract My721Token is ERC721 { returns (string memory) { bytes memory dataURI = abi.encodePacked( - '{ - "name": "My721Token #', tokenId.toString(), '"', + '{', + '"name": "My721Token #', tokenId.toString(), '"', // Replace with extra ERC721 Metadata properties '}' ); From f0fc4592f0841bc62c84aafd71484103c1e799bc Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Wed, 29 Dec 2021 14:17:39 -0300 Subject: [PATCH 8/8] do not use using-for syntax, for clarity --- docs/modules/ROOT/pages/utilities.adoc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 50d778cfec8..2f23ceb70b0 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -118,7 +118,6 @@ import "@openzeppelin/contracts/utils/Base64.sol"; contract My721Token is ERC721 { using Strings for uint256; - using Base64 for bytes; constructor() ERC721("My721Token", "MTK") {} @@ -140,7 +139,7 @@ contract My721Token is ERC721 { return string( abi.encodePacked( "data:application/json;base64,", - dataURI.encode() + Base64.encode(dataURI) ) ); }