DIY Physical Backed NFTs (EIP-5791) using ESP32 and BLE
The concept of Physical Backed Tokens (PBTs) allows one to bind a NFT permanently to a physical object, using a small chip that can generate cryptographic keys and sign messages. The signature obtained from a device like this can be used to interact with a smart contract to transfer the corresponding NFT to one's wallet.
Some folks smarter than me put together the specification for EIP-5791 on how to make this work both on the smart contract- and hardware-side, in a way that it's safe to use and no one involved in the process can cheat. As opposed to the few commercial solutions out there employing custom NFC chips, the ESP32 microcontroller is freely available for everyone for ~$5-10 (probably even cheaper in bulk), with a wide selection of different board designs and form factors. It comes with hardware flash-encryption and Bluetooth Low Energy, and is fully Arduino IDE-compatible, which makes it easy to get started with for almost everyone, and a viable candidate for both smaller scale PBT projects and learning about the technology to build more professional solutions.
The ESP-5791 project complies with the EIP-5791 standard and facilitates
- creating a hardware device using an ESP32 microcontroller that
- securely generates and stores private keys on first boot
- exposes chip public keys and address via BLE to verify it's authenticity
- signs messages sent via BLE using the stored private keys
- deploying a "PBT-enabled" NFT smart contract (ERC-721) to be used with the device
- interacting with both the device and the smart contract to transfer the NFT to the user's wallet
Arduino
contains the microcontroller software and tweaked libraries to be used with Arduino IDEhardhat
contains Solidity smart contracts implementing EIP-5791, based on the PBT projectclient
contains a browser based GUI to interact with the hardware using Web Bluetooth API. Check out the ESP-5791 GUI on Github Pages.
- ESP32 Dev board + USB cable
- supported chips: v1 (ESP32, ESP32D, ESP32-S1, ...) and v3 (ESP32-S3, ESP32-C3, ...) - v2 chips are Wifi-only
- Arduino IDE Desktop App to flash the device
- (for production use) esptool.py for encrypting the device
- Go to
Tools > Board > Boards Manager
, search for and install esp32 board definitions - Symlink or copy libraries and sketch from the repo to the Arduino sketch folder
# example for symlinking on macOS
ln -s ~/path_to_repo/esp5791/Arduino/libraries/Web3E ~/Documents/Arduino/libraries/Web3E
ln -s ~/path_to_repo/esp5791/Arduino/libraries/NimBLE-Arduino ~/Documents/Arduino/libraries/NimBLE-Arduino
ln -s ~/path_to_repo/esp5791/Arduino/ESP5791 ~/Documents/Arduino/ESP5791
(Please use the libs included in the repo. You can find the original libs here for reference: Web3E / NimBLE)
- Connect ESP32 Dev board via USB, choose correct settings in
Tools > Board
andTools > Port
for your setup- there are some boards out there with cheapo USB controllers (CH340X / CH9102X) - if your board isn't picked up over USB at all, you might have to find and install additional USB drivers to make it work
- Click Upload to compile and flash the software
- Depending on your ESP32 board, you may have to hold down the boot button while flashing or you'll get an error
- On first boot, the ESP32 securely generates a random private key itself, and stores it in flash memory
If you're only experimenting, you're done here! Connect your ESP-5791 to a power supply and start using the BLE interface. The next step encrypts the ESP32, which is irreversible and makes it impossible to flash new sketches going forward, so it's only relevant if you plan to use your board in production.
!! Danger zone: this may brick your board and void your warranty !! changes done here are irreversible, thread carefully !! the author assumes no responsibility !!
These are the main steps involved to secure your ESP32 for production. Be aware that flash encryption on v1 chips has been exploited before, use v3 only for production.
- Some ESP32 eFuse basics first
- Enabling Flash encryption.
- Also disable UART ROM download mode here permanently
- Be aware of the differences between Development vs. Release modes, the latter is irreversible
- Enabling Secure Boot V2. This also disables JTAG access
ESP-5791 comes with a BLE interface to obtain signatures from the device. The general workflow looks like this:
- Start a BLE scan, look for devices exposing a GATT service with the UUID
0x5791
- Connect to the device (pairing not necessary; only one device can be connected at a time)
- The service exposes the following GATT characteristics:
0xA001
let's you read the device's public key0xA002
let's you read the device's wallet address- write a plaintext message to
0xB001
or a keccak256-hashed message to0xB002
for the device to sign - read the signature from
0xC001
.0xC002
returns the hash of the corresponding prefixed Ethereum signed message,0xC008
gives you the keccak256-hash of the original plaintext message.
You can use the ESP-5791 GUI on Github Pages to interact with your device (source can be found in the client
folder), or an app like nRF Connect (mobile/desktop) for more generic testing and debugging.
The message you have to sign on your device for the provided EIP-5791 PBT smart contract consists of
- your wallet address (e.g. 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045)
- the blockhash of a recent block (e.g. 0x9ad2aa4bdfea57c247ef4202f08b5dec795fa6fb04f94773222b22d18199d9c7)
- look up e.g. on Etherscan, note down the corresponding block number too (our GUI takes care of this for you)
You need to concatenate both these hex strings before signing them. In a simplified scenario, if your wallet would e.g. be 0x42069 and the blockhash of block #777 is 0xBBBBB, then your message to sign should look like this: 0x42069BBBBB
. After obtaining the signature from the device, you can call the smart contract like this: ESP5791.transferTokenWithChip(mySignatureHexString = 0x..., blockNumberUsedInSig = 777)
to claim the connected NFT. Generated signatures expire within 250 blocks.
To edit the provided GUI, you'll need to run a server locally, as MetaMask and other wallets don't work locally (i.e. using file://
protocol). To quickly spin up a dev server, you could use
# install server
npm i -g http-server
# go to public web directory
cd ./client
# start dev server on http://localhost:8080
npx http-server
This package includes a ready-to-deploy Hardhat project and a PBT contract based on Chiru Labs' PBT implementation and OpenZeppelin's ERC721 NFT contracts with additional management capabilities. Also check out our demo contract on Etherscan, or open the contracts in Remix IDE to get a first impression.
The general interaction flow looks like this:
- deploy token contract
- setup chip address to token id mapping using
seedChipToTokenMapping(address[] chipAddresses, uint256[] tokenIds)
- transfer or mint a token with a chip signature using
transferTokenWithChip(bytes signatureFromChip, uint256 blockNumberUsedInSig)
- switch to
hardhat
directory:cd hardhat
- install dependencies:
yarn
- copy
.env.example
to.env
and put your own values in - compile contracts:
yarn compile
ornpx hardhat compile
- deploy token contract:
npx hardhat deploy --network goerli --name "My Token" --symbol "ABC" --uri "https://example.com/path-to-token-metadata/"
- if you've changed the contract name, you also need to pass in
--contract MyCustomContract
- use your desired network instead of
goerli
. Checkhardhat.config.ts
for all available network keys
- if you've changed the contract name, you also need to pass in
- (recommended) verify contract source code on Etherscan:
npx hardhat verify --network goerli YOUR_CONTRACT_ADDRESS "My Token" "ABC" "https://example.com/path-to-token-metadata/"
- you need to use the exact same input as when deploying here
- setup chip address to token id mapping:
npx hardhat seed --network goerli --address YOUR_CONTRACT_ADDRESS --chips "0x123,0x234" --tokenids "1,2"
- additional commands to manage the token contract:
- transfer or mint a token using a chip signature:
npx hardhat transfer --network goerli --address YOUR_CONTRACT_ADDRESS --signature 0xABC --block 42069
- get token mapping data for a given chip address:
npx hardhat token --network goerli --address YOUR_CONTRACT_ADDRESS --chip 0x123
- add new admin wallet to token contract:
npx hardhat admin --network goerli --address YOUR_CONTRACT_ADDRESS --admin 0x777
- update token metadata uri:
npx hardhat uri --network goerli --address YOUR_CONTRACT_ADDRESS --uri "https://example.com/new-path-to-metadata/"
- replace existing defective chips:
npx hardhat update --network goerli --address YOUR_CONTRACT_ADDRESS --oldchips "0x123,0x234" --newchips "0x888,0x999"
- transfer or mint a token using a chip signature:
This repo does not include packages to generate public token metadata or provide respective endpoints for your ERC721 PBT. You can find some inspiration on in my research repo for interactive NFTs snakes-on-a-chain, or just make your metadata available as JSON files on a static file host or IPFS. See here for more details on the structure of NFT metadata.
Hardhat dev environment based on PaulRBerg's Hardhat Template, refer to that repo's README for specific questions on how to use the tools.
- The authors of EIP-5791
- Chiru Labs for the PBT smart contract reference implementation
- Espressif Systems for the ESP :)
- Firefly Wallet for the inspiration
- AlphaWallet for the Web3E ESP32 Ethereum lib (includes the trezor-crypto lib)
- Mbed TLS (cryptographic lib for embedded systems)
- NimBLE (lightweight BLE lib for Arduino/ESP32)
MIT
Copyright © 2023 xtools-at
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.