From e3926758131c3f63a99546f33645579814385094 Mon Sep 17 00:00:00 2001 From: Arthur Guillon Date: Mon, 22 Jan 2024 16:23:47 +0100 Subject: [PATCH] Add a chapter on TZIP 17 permits and permit-cameligo --- docs/src/SUMMARY.md | 3 +- docs/src/permits.md | 138 +++++++++++++++++++++++++++++++++++++++++++ docs/src/tutorial.md | 13 ++-- docs/src/welcome.md | 2 +- permit-cameligo | 2 +- 5 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 docs/src/permits.md diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index a74f741..f8c9774 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -4,4 +4,5 @@ [Gas Station Webapp](./webapp.md) [SDKs](./library.md) [API](./api.md) -[Tutorial](./tutorial.md) \ No newline at end of file +[Tutorial](./tutorial.md) +[Permits](./permits.md) diff --git a/docs/src/permits.md b/docs/src/permits.md new file mode 100644 index 0000000..8cd0290 --- /dev/null +++ b/docs/src/permits.md @@ -0,0 +1,138 @@ +# An introduction to off-chain permits + +In the [Tutorial](./tutorial.md) chapter, we show how to transfer a NFT to a smart contract +through the Gas Station. As the corresponding operation is ultimately going to get posted by the Gas +Station account, there is an issue: how is NFT contract going to allow this transfer on behalf of +the user? While FA2 contracts, which are used to implement NFTs, support the concept of *operators* +accounts acting on the behalf of the user, only the original owner of the NFT can allow a new +operator to do so. The simplest way would be to modify the NFT contract to + +* make the Gas Station account a super-user of the contract, and +* let this account [register third-party contracts as + operators](https://tezostaquito.io/docs/fa2_parameters/#the-update_operators-entrypoint), which + would allow the transfer to happen + +This, of course, creates a security (and, potentially, legal) issue: if the key of Marigold Gas +Station account gets stolen, then several FA 2 contracts could be compromised as well. On the other +hand, users whose operations get sponsored are not supposed to have any tez in their wallet, and +thus cannot post the `update_operator` call on-chain themselves. + +What is the solution, then? + +## Off-chain permits + +To solve this problem in a secure way, the notion of *off-chain permits* was introduced by [TZIP +17](https://tzip.tezosagora.org/proposal/tzip-17/). It extends the FA 2 standard with a few new +entrypoints. The most interesting one, itself called `permit`, can be called by anyone, and +expects a list of authorizations for transfers signed by the owners. Those transfers are signed +off-chain: this means that the application has to ask the users for their signature through the +usual ways (e.g. a Beacon-compatible wallet), but this signature has then to be stored and/or +sent to this entrypoint by another account. + +Most of the time, however, these permits can be sent in the same transaction as the call to the +other contract, as we did in the previous chapter. When a permit is registered by the contract, it +acts as a one-time authorization for a transfer to a specific address, which can either be a +contract or a implicit account. The `transfer` entrypoint has the same interface as an ordinary FA 2 +contract and of course supports the same usage as before, including regular operators. This means +that regular users, who don't need their transactions to be relayed by the gas station, can always +use their assets in a normal, permissionless way. + +Let's define permits: they are signed bytes, formed from 4 parameters: + +* the chain identifier, such that a permit signed for a given chain (such as Ghostnet) cannot be + used on a different chain; +* the address of the permit FA2 contract, such that a permit signed for a given NFT collection + cannot be used on another one; +* a counter (nonce) defined inside the contract, such as a permit can only be used once; +* and, finally, a hash of the allowed operation, which is going to be checked when the + transfer takes place. + +If you recall [the previous chapter](./tutorial.md), this byte string was computed by the library +with the following call: + +```ts +const permitData = await permitContract.generatePermit({ + from_: userAddress, + txs: [{ + to_: RECIPIENT, + token_id, + amount: 1 + }] +}); +``` + +Indeed, it can be a little bit complicated to form by hand, and the slightest error makes the permit +fail silently. + +Once it is signed by the user, the permit can be registered in the contract by calling the `permit` +entrypoint, which expects a list of parameters of the form `(public_key, signature, transfer_hash)` +where `public_key` is the user's public key, which is necessary to check the `signature`. This +`signature` is computed from the whole byte string, not just the `transfer_hash`. + +i If you choose to compute permits by hand, be mindful that they are actually computed by forming +the following couple: `((chain_identifier, contract_address), (contract_counter, transfer_hash))`. +Check the documentation of the contract library that you are using to be sure. + +## How to deploy a permit contract + +The most up-to-date implementation of TZIP 17-style permits is [the permit-cameligo Ligo +package](https://github.com/aguillon/permit-cameligo/), which is currently maintained outside of +Ligo Package Registry website. To use it, it is recommended to clone the following repository and +use the Ligo compiler to install the dependencies: + +```bash +$ git clone https://github.com/aguillon/permit-cameligo +$ cd permit-cameligo/ +$ make install +$ make compile +``` + +Note that the Makefile assumes that you run the dockerized version of Ligo. To use another one, for +instance a local one, you can prefix the `make` commands with `ligo_compiler=ligo `. For instance: + +```bash +$ ligo_compiler=ligo make install +$ ligo_compiler=ligo make compile +``` + +This installs the dependencies in `.ligo/`, and compiles the code to produce two files in `compiled/`. +The first of those files is a JSONized version of the second, which is ready to be deployed by the +scripts in `deploy/`. In addition to the compiled code, this script requires two files: +`deploy/metadata.json` that contains the contract's metadata, and `deploy/.env` which contains the +secret key and the RPC node. + +Let's create a minimal `deploy/metadata.json` file: + +```json +{ + "name":"Example", + "interfaces":[ + "TZIP-12" + ] +} +``` + +Change this file according to your needs. If you just want to test the deployment script, you can +also use the pre-generated `deploy/metadata.json.dist` file and rename it to `deploy/metadata.json`. +In the same spirit, copy `deploy/.env.dist` to `deploy/.env` and edit the file to put your secret +key: + +```bash +# Required: Your private key +PK=edsk... +# Required: see https://tezostaquito.io/docs/rpc_nodes/ +RPC_URL=https://ghostnet.tezos.marigold.dev/ +``` + +Finally, you should be able to + +``` +$ cd deploy/ +$ npm i +$ npm run start +``` + +This workflow assumes that you're going to mint each token individually by calling the +`create_token` entrypoint. If you want to pre-mint some tokens, you need to edit the +`deploy/deploy.ts` script to start with a non-empty `token_metadata` map. The script should print +the address of the contract after origination. diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md index f7857fe..45de68d 100644 --- a/docs/src/tutorial.md +++ b/docs/src/tutorial.md @@ -1,4 +1,4 @@ -# Tutorial +# Developing a web application with the Gas Station This chapter walks through a simple example of a dapp using the Gas Station. You can [try it online at this address](https://ghostnet.gas-station-nft-example.marigold.dev). @@ -26,7 +26,7 @@ Let's go ! 💪 ## Minting an NFT -We'll start with minting an NFT by a user. The contract we'll use is available at this address on Ghostnet: [`KT199yuNkHQKpy331A6fvWJtQ1uan9uya2jx`](https://ghostnet.tzkt.io/KT199yuNkHQKpy331A6fvWJtQ1uan9uya2jx/operations). +We'll start with minting an NFT by a user. The contract we'll use is available at this address on Ghostnet: [`KT1HUdxmgZUw21ED9gqELVvCty5d1ff41p7J`](https://ghostnet.tzkt.io/KT1HUdxmgZUw21ED9gqELVvCty5d1ff41p7J/operations). This contract has an admin, which is the only account allowed to mint NFTs. This is the same settings as you would have in a video game, where the game decides when to mint an NFT for a user. In this case, the contract admin has been set to be the Gas Station account, because the `mint` @@ -49,7 +49,8 @@ const contract = await Tezos.wallet.at(PUBLIC_PERMIT_CONTRACT); ℹ️ The `Tezos` instance of Taquito is already initialized in the `tezos.ts` file, so it can be directly imported. -ℹ️ `PUBLIC_PERMIT_CONTRACT` is also an environment variable corresponding to the address of your NFT contract. +ℹ️ `PUBLIC_PERMIT_CONTRACT` is an environment variable corresponding to the address of your NFT +contract. It is defined in the `.env` file. Afterward, we will forge our operation to send to the Gas Station: ```ts @@ -86,7 +87,7 @@ final users do not have tez in their wallet, all the transactions are posted by Despite this centralization, it is still possible to maintain security and non-custodiality using permits. In this section, we call _staking_ the operation of sending an NFT to a contract. As the -user ownsthe NFT, it is appropriate to sign a permit (authorization) to perform this transfer. +user owns the NFT, it is appropriate to sign a permit (authorization) to perform this transfer. To facilitate the development of this new feature, we will also use the TypeScript SDK (for reference, you have all the information [here](./library.md)) @@ -111,7 +112,7 @@ const permitData = await permitContract.generatePermit({ }); ``` Some explanations: -- The variable `PUBLIC_STAKING_CONTRACT` contains the address of the staking contract (available at this address [`KT1MLMXwFEMcfByGbGcQ9ow3nsrQCkLbcRAu`](https://ghostnet.tzkt.io/KT1MLMXwFEMcfByGbGcQ9ow3nsrQCkLbcRAu/operations) on Ghostnet). +- The variable `PUBLIC_STAKING_CONTRACT` contains the address of the staking contract (available at this address [`KT1VVotciVbvz1SopVfoXsxXcpyBBSryQgEn`](https://ghostnet.tzkt.io/KT1VVotciVbvz1SopVfoXsxXcpyBBSryQgEn/operations) on Ghostnet). - The `token_id` corresponds to the ID of the token you want to stake. `permitData` then contains the hash of the permit `bytes` and the hash of transfer operation `transfer_hash`: @@ -159,7 +160,7 @@ const stakingOperation = await stakingContract.methods.stake( userAddress ).toTransferParams(); ``` -ℹ️ `PUBLIC_STAKING_CONTRACT` is an environment variable containing the staking contract's address. +ℹ️ `PUBLIC_STAKING_CONTRACT` is also an environment variable containing the staking contract's address. All that remains is to send the operation to the Gas Station to have the gas fees covered: diff --git a/docs/src/welcome.md b/docs/src/welcome.md index 920b743..796e7ae 100644 --- a/docs/src/welcome.md +++ b/docs/src/welcome.md @@ -1,4 +1,4 @@ -# Welcome on Gas Station +# Marigold Gas Station documentation ## Introduction diff --git a/permit-cameligo b/permit-cameligo index 9aabf86..6c7fb30 160000 --- a/permit-cameligo +++ b/permit-cameligo @@ -1 +1 @@ -Subproject commit 9aabf8600979dbe2bc85b21e02c080dcca679d00 +Subproject commit 6c7fb3065ac9f917d9c51532dceba583587a240c