Only-Paper Restricted Smart Contract Functions

There are some circumstances where you will want a smart contract function to be only callable by Paper. This is specifically useful when considering off-chain allowlists, or to centralize an NFT drop for more organized queueing of users and prevent gas wars.

We created PaperKeyManager to help simplify this process. Here's a link to the Github Repo for more details.

How it works

Under the hood, PaperKeyManager uses the signature pattern similar to EIP-1271 to help ensure that your data has not been spoofed.

Features

  • Makes implementing signature minting for your function trivial.
  • Paper automatically rotates and updates the key for you from time to time to following good security hygiene. Set once and forget!

Let's see how we can use it!

The PaperKeyManager Interface

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

/// @title Paper Key Manager
/// @author Winston Yeo
/// @notice PaperKeyManager makes it easy for developers to restrict certain functions to Paper.
/// @dev Developers are in charge of registering the contract with the initial Paper key.
///      Paper will then help you  automatically rotate and update your key in line with good security hygiene
interface IPaperKeyManager {
    /// @notice Registers a Paper Key to a contract
    /// @dev Registers the @param _paperKey with the caller of the function (your contract)
    /// @param _paperKey The Paper key that is associated with the checkout. 
    /// You should be able to find this in the response of the checkout API or on the checkout dashbaord.
    /// @return bool indicating if the @param _paperKey was successfully registered with the calling address
    function register(address _paperKey) external returns (bool);

    /// @notice Verifies if the given @param _data is from Paper and have not been used before
    /// @dev Called as the first line in your function or extracted in a modifier. Refer to the Documentation for more usage details.
    /// @param _hash The bytes32 encoding of the data passed into your function.
    /// This is done by calling keccak256(abi.encode(...your params in order))
    /// @param _nonce a random set of bytes Paper passes your function which you forward. This helps ensure that the @param _hash has not been used before.
    /// @param _signature used to verify that Paper was the one who sent the @param _hash
    /// @return bool indicating if the @param _hash was successfully verified
    function verify(
        bytes32 _hash,
        bytes32 _nonce,
        bytes calldata _signature
    ) external returns (bool);
}

Usage in Your Contract

From above we can see that PaperKeyManager is used in two stages, the registration and verification.

Let's start with the register function:

import "@paperxyz/contracts/keyManager/IPaperKeyManager.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

const YourContract {
    IPaperKeyManager paperKeyManager;

    // to set the initial paperKey for the contract
    constructor(..., address _paperKeyManagerAddress) {
        paperKeyManager = IPaperKeyManager(_paperKeyManagerAddress);
    }

    // onlyPaper modifier to easily restrict multiple different function
    modifier onlyPaper(bytes32 _hash, bytes32 _nonce, bytes calldata _signature) {
        bool success = paperKeyManager.verify(_hash, _nonce, _signature);
        require(success, "Failed to verify signature");
        _;
    }

    function registerPaperKey(address _paperKey) external onlyOwner {
        require(paperKeyManager.register(_paperKey), "Error registering key");
    }
  
    // using the modifier
    function yourFunction(... your params, bytes32 _nonce, bytes calldata _signature)
        onlyPaper(keccak256(abi.encode(...your params)), _nonce, _signature)
        ...
    {
        // your function
    }
}
  1. You'd want to first store the PaperKeyManager contract address as a state variable in your contract. (contract addresses here

  2. Deploy your contract to the chain of interest

  3. Register your contact on the dashboard as a CUSTOM_CONTRACT or through the API

  4. You should now be able to grab the PaperKeyManager Token from the dashboard.

  5. Call your register function ( paperKeyManager.register(_paperKey); where _paperKey is the PaperKeyManager Token you got from step 4. Note that you can only call register once.

  6. For functions that you want to restrict to only Paper, simply make sure that it:

    • Accepts all your parameters and bytes32 _nonce and bytes calldata _signature at the end.
    • Calls paperKeyManager.verify(keccak256(abi.encode(...you params in the order that you received it)), _nonce, _signature);. You can extract this out as a modifier if you plan to use it multiple times.

That's it! Below is a sample of what this could look like.

  1. Using With Checkout Intents

As mentioned, using paperKeyManager is only supported by CUSTOM_CONTRACT.

To use it, we introduce two more magic variables $NONCE and $SIGNATURE which you can pass in as params for your mintMethod

Below is an example of creating an SDK intent. Creating a link Intent is the same with additional link-related parameters

/**
 * We have two magic variables:
 * * `$WALLET` corresponds to the user's wallet
 * * `$QUANTITY`corresponds to the quantity the user wants to purchase.
 * * `$NONCE` is a unique string to prevent replay attack from attackers trying
 *    to use the same signature more than once  
 * * `$SIGNATURE`corresponds to the signature that the user wants to purchase.
 * It will resolve from {@param recipientWalletAddress} or {@param quantity} as expected.
 * Note that you can pass in the actual values themselves if you so choose.
 *
 */

let headersList = {
 "Authorization": "Bearer YOUR_API_KEY",
 "Content-Type": "application/json"
}

let bodyContent = JSON.stringify({
  "contractId": "YOUR_CONTRACT_ID",
  "mintMethod": {
    "args": {
      // this is just an exmaple call. corresponds to the following stub in solidity
      // function paperMint(uint256 _tokenId, uint256 _quantity, address _to, bytes32 _nonce, bytes _signature)
      "_tokenId": 1,
      "_quantity": "$QUANTITY",
      "_to": "$WALLET"
      "_nonce": "$NONCE",
      "_signature": "$SIGNATURE"
    },
    "name": "paperMint",
    "callOptions": {
      "gasPriority": "medium"
    },
    "payment": {
      "currency": "DERC20",
      "value": "0.001 * $QUANTITY"
    }
  },
  "walletAddress": "RECIPIENT_EMAIL_ADDRESS",
  "email": "RECIPIENT_EMAIL",
  "usePaperKey": true,
});

let response = await fetch("https://paper.xyz/api/2022-08-12/checkout-sdk-intent", { 
  method: "POST",
  body: bodyContent,
  headers: headersList
});

const { sdkClientSecret } = await response.json();

With the client secret, you can now use it with the Checkout Elements!

PaperKeyManager contract addresses

Reach out to us if the PaperKeyManager is not on the chain that you're on!