MEP: 802 Title: Provisioning Contract Status: Draft Type: Standards Created: 2023-04-19 Updated: 2023-07-24
Specification allows user to create and manage the PID NFTs (Provisioning ID) which are the digital representation of the sensor devices.
Each sensor device is provisioned on the ChirpVM with a help of a NFT. The NFT is a digital representation of the device and contains all the information needed by the ChirpVM to successufly establish a LPWAN communication channel. The ownership of the device is minted by the ownership of the NFT.
In regards to the above, the MEP-802 proposal will extend the ERC721 interface.
The goal is to allow user to create, add and manage sensor devices as NFTs.
- PID - Provision ID
- BO - Business Owner; the entity that created the application
- Sensor Owner - entity owning the physical sensor device
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
Every MEP-802 compliant contract must implement the the following interface and the ERC721 interface:
pragma solidity ^0.8.0;
/// @title MEP802 (ISO Sensor NFT)
interface MEP802 /* is IERC721 */ {
/// @dev This event gets emitted when requesting to produce PIDs
/// The parameters is the index, email, number of PID, amount paid and profile index.
event ProducePidRequested(
uint256 idx,
address buyer,
string email,
uint256 numOfPid,
uint256 amountPaid,
uint256 profileIdx
);
/// @dev This event gets emitted when a NFT price info added.
/// The parameters is the index, price and number of block.
event PriceInfoAdded(uint256 idx, uint256 _price, uint _numOfBlock);
/// @dev This event gets emitted when a sensor NFT minted
/// The parameters is the tokein ID, PID hash, paid amount and number of block.
event SensorNFTMinted(
uint256 tokenId,
uint256 pidZkevmHash,
address owner,
uint256 amountPaid,
uint numOfBlock
);
/// @dev This event gets emitted when a sensor NFT burned
/// The parameters is the index and PID hash.
event SensorNFTBurned(uint256 tokenId, uint256 pidZkevmHash);
/// @dev This event gets emitted when the application index of a sensor NFT changed
/// The parameters is the tokein ID, PID hash and the new applicaton index
event ApplicationIdxChanged(
uint256 tokenId,
uint256 pidZkevmHash,
uint256 applicationIdx
);
/// @dev This event gets emitted when a downlink enqueued
/// The parameters is the tokein ID, PID hash, data, option flags.
event DownlinkEnqueued(
uint256 tokenId,
uint256 pidZkevmHash,
string data,
uint8 flushOld,
uint8 isJson
);
/// @notice Return the name of contract, use to identiry the version of the contract
function name2() external view returns (string memory);
/// @notice Set PID unit price (onlyOwner)
/// @param _price The unit price of PID
function setPidUnitPrice(uint256 _price) external;
/// @notice Request to produce PIDs and send to recipient
/// @param _email The recipient email address for the PID list
/// @param _numOfPid Number of PIDs to purchase
/// @param _profileIdx The Device Profile index of MEP803
/// @return uint256 The new created record index (_pidRecordList)
function producePid(
string memory _email,
uint256 _numOfPid,
uint256 _profileIdx
) external payable returns (uint256);
/// @notice Get number of PID purchase record (onlyOwner)
/// @return uint256 Number of PID record
function getPidRecordCount() external view returns (uint256);
/// @notice Get a PID purchase record (onlyOwner)
/// @param _idx The index of the record
/// @return PidRecord The PID purchase record
function getPidRecord(
uint256 _idx
) external view returns (PidRecord memory);
/// @notice Mint a Sensor NFT
/// @param _pidZkevmHash The PID hash (for zkevm) of the sensor
/// @param _priceIdx The price index (priceInfoList) for the price/block
/// @return uint256 The new minted token index
/// @param _uri The Token URI
function mintSensorNFT(
uint256 _pidZkevmHash,
uint256 _priceIdx,
string memory _uri
) external payable returns (uint256);
/// @notice Renew Sensor NFT (only Token owner)
/// @param _pidZkevmHash The PID hash (for zkevm) of the sensor
/// @param _priceIdx The price index (priceInfoList) for the price/block
function renewSensorNFT(
uint256 _pidZkevmHash,
uint256 _priceIdx
) external payable;
/// @notice Check Sensor NFT minted or not
/// @param _pidZkevmHash The PID hash (for zkevm) of the sensor
function isMinted(uint256 _pidZkevmHash) external view returns (bool);
/// @notice Burn a Sensor NFT (onlyOwner)
/// @param _pidZkevmHash The PID hash (for zkevm) of the sensor
function burnSensorNFT(uint256 _pidZkevmHash) external;
/// @notice Set Application Index (only Token owner)
/// @param _pidZkevmHash The PID hash (for zkevm) of the sensor
/// @param _applicationIdx The application index of MEP801
function setApplicationIdx(
uint256 _pidZkevmHash,
uint256 _applicationIdx
) external;
/// @notice Get Application Index
/// @param _pidZkevmHash The PID hash (for zkevm) of the sensor
function getApplicationIdx(
uint256 _pidZkevmHash
) external view returns (uint256);
/// @notice Enqueue a downlink to the sensor (only Token owner)
/// @param _pidZkevmHash The PID hash (for zkevm) of the sensor
/// @param _data The data.
/// @param _flushOld Set to 1 to flush the queue before enqueue
/// @param _isJson 0: data is base64 encoded raw data, 1: JSON object
/// @notice When using JSON, it require a Downlink encoder at Device Profile codec.
function enqueueDownlink(
uint256 _pidZkevmHash,
string memory _data,
uint8 _flushOld,
uint8 _isJson
) external;
/// @notice Get pidZkevmHash by Token ID
/// @param _tokenId Token ID
function getPidZkevmHash(
uint256 _tokenId
) external returns (uint256);
/// @notice Get Token ID by pidZkevmHash
/// @param _pidZkevmHash The PID hash (for zkevm) of the sensor
function getTokenId(
uint256 _pidZkevmHash
) external returns (uint256);
/// @notice Get token owner by pidZkevmHash
/// @param _pidZkevmHash The PID hash (for zkevm) of the sensor
function pidZkevmHashOwner(
uint256 _pidZkevmHash
) external returns (address);
/// @notice Set Token URI
/// @param _pidZkevmHash The PID hash (for zkevm) of the sensor
/// @param _uri The Token URI
function setTokenURI(uint256 _pidZkevmHash, string memory _uri) external;
/// @notice Set the base URI (onlyOwner)
/// @param _uri The Base URI
function setBaseURI(string memory _uri) external;
/// @notice Get the base URI (onlyOwner)
function getBaseURI() external view returns (string memory);
/// @notice Add price info for NFT minting (onlyOwner)
/// @param _price The Price
/// @param _numOfBlock Number of block to expiration
/// @return uint256 The new created price index
function addPriceInfo(
uint256 _price,
uint _numOfBlock
) external returns (uint256);
/// @notice Set price info for NFT minting (onlyOwner)
/// @param _idx The index of the price info at priceInfoList
/// @param _price The Price
/// @param _numOfBlock Number of block to expiration
/// @param _active The price info is active or not.
function setPriceInfo(
uint256 _idx,
uint256 _price,
uint _numOfBlock,
bool _active
) external;
}
And this public accessible state values:
struct Sensor {
uint creationBlock;
uint expirationBlock;
uint256 lastAmount;
uint256 pidZkevmHash;
uint256 applicationIdx;
string uri;
}
struct NtfPrice {
uint256 price;
uint numOfBlock;
bool active;
}
struct PidRecord {
string email;
uint256 numOfPid;
uint256 amountPaid;
uint256 profileIdx;
}
contract Storage1 {
using Counters for Counters.Counter;
/// @notice PID unit price
uint256 public pidUnitPrice;
/// @notice Price for mint a Sensor NFT
mapping(uint256 => NtfPrice) public priceInfoList;
Counters.Counter public numOfPriceInfo;
/// @notice Sensors
Counters.Counter internal _sensorCount;
mapping(uint256 => Sensor) internal _sensorList;
mapping(uint256 => uint256) internal _pidToNftIdx;
/// @notice PID production history
Counters.Counter internal _pidRecordCount;
mapping(uint256 => PidRecord) internal _pidRecordList;
/// @notice URI
string internal _currentBaseURI;
}
The sensor NFT validity acts as a way of constraining outdated (or not active) sensors. Each sensor has to have a expiration time (calculated in block height). When the current block height reaches the expiration block, the sensor is considered to be inactive. The end-user can renew the sensor by paying a fee proportional to the specified expirationBlock
parameter.
When the device owner gets the sensor physically, he needs to mint the Sensor NFT by calling the mintSensorNFT
method with the hash of the PID from the device's QR code.
A malicious observer looking at the pending TX pool can spot the claim transaction with the correct PID and replay the transaction with a higher gas price, effectively stealing the NFT from the original owner.
One potential way of preventing this is to implement a proper pre-commit scheme.
An implementer can introduce a commitment
method which maps the msg.sender
to a precommit hash:
mapping(address => bytes) commitments;
The precommit hash could be keccak256(abi.encodePacked(PID, msg.sender))
.
Afterwards, when claim
gets called, the method checks if there exists a matching hash for the given msg.sender.
function claimDevice(uint256 _tokenId, uint256 _expirationBlock, bytes _pid) {
bytes commitment = commitments[msg.sender];
bytes hash = keccak256(abi.encodePacked(_pid, msg.sender));
require(commitment == hash);
...
}