Skip to content

Latest commit

 

History

History
308 lines (242 loc) · 10.5 KB

mep-802.md

File metadata and controls

308 lines (242 loc) · 10.5 KB
  MEP: 802
  Title: Provisioning Contract
  Status: Draft
  Type: Standards
  Created: 2023-04-19
  Updated: 2023-07-24

MEP-802: Provisioning Contract

Simple Summary

Specification allows user to create and manage the PID NFTs (Provisioning ID) which are the digital representation of the sensor devices.

Abstract

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.

Motivation

The goal is to allow user to create, add and manage sensor devices as NFTs.

Terminology

  • PID - Provision ID
  • BO - Business Owner; the entity that created the application
  • Sensor Owner - entity owning the physical sensor device

Specification

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;
}

Rationale

Sensor validity

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.

Front-running attack

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);
    ...
}