-
Notifications
You must be signed in to change notification settings - Fork 29
CEP-2612: Permit Extension #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| # Casper Payments: Permit Extension (CEP-2612) | ||
|
|
||
| ## Summary | ||
| A standard interface for off-chain approval of CEP-18 token allowances via | ||
| signed messages. The standard is inspired by the ERC-2612 standard from Ethereum | ||
| and is adapted to Casper's native multi-scheme signatures (ed25519, secp256k1). | ||
| A token holder signs a typed message authorizing a `spender` to receive an | ||
| allowance, and any party may submit that signature to the contract to set the | ||
| allowance on the holder's behalf — enabling gas-less approvals for the holder. | ||
|
|
||
| This CEP extends [CEP-18](0018-token-standard.md). It does not change CEP-18's | ||
| `approve` / `allowance` / `transfer_from` semantics; it adds a new entry point | ||
| that produces the same allowance state change as `approve` while being callable | ||
| by a third party. | ||
|
|
||
| ## Prior art | ||
| The main source of influence is [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612). | ||
|
|
||
| The signed payload follows [EIP-712](https://eips.ethereum.org/EIPS/eip-712) | ||
| typed data hashing. EIP-712 is treated here as a dependency rather than | ||
| re-specified — implementations are expected to use [EIP-712 toolkit for | ||
| Casper](https://github.com/casper-ecosystem/casper-eip-712). | ||
|
|
||
| ## Specification | ||
|
|
||
| The CEP-2612 extension is defined by: | ||
| - one new entry point (`permit`), | ||
| - the EIP-712 typed data definition for the `Permit` struct and its domain separator, | ||
| - error codes, | ||
| - storage structure for replay-protection nonces and the EIP-712 chain | ||
| identifier. | ||
|
|
||
| The extension reuses CEP-18's `SetAllowance` event — `permit` produces the | ||
| same allowance state change as `approve` and emits the same event. No new | ||
| events are introduced by this CEP. | ||
|
|
||
| Below definitions use Rust syntax, but they are not Rust specific. | ||
|
|
||
| ### Entry point interface | ||
|
|
||
| Contracts implementing this standard must expose the following entry point | ||
| in addition to the CEP-18 interface: | ||
|
|
||
| ```rust | ||
| pub trait CEP2612Interface { | ||
| /// Sets `value` as the allowance of `spender` over `owner`'s tokens, | ||
| /// given `owner`'s signed approval. | ||
| /// | ||
| /// - `owner` is the token holder whose tokens are being approved. | ||
| /// - `spender` is the address that will be allowed to spend the tokens. | ||
| /// - `value` is the allowance amount. | ||
| /// - `deadline` is a block-time timestamp after which the permit is no | ||
| /// longer accepted, expressed in the same units as the host | ||
| /// environment's `get_block_time()`. The sentinel value `u64::MAX` | ||
| /// disables the expiry check. | ||
| /// - `public_key` is the signer's Casper public key. | ||
| /// - `signature` is `public_key`'s signature over the EIP-712 digest | ||
| /// of the `Permit` typed data. | ||
| /// | ||
| /// On success: | ||
| /// - the allowance of `spender` over `owner` is set to `value` (i.e. | ||
| /// the same effect as `owner` calling `approve(spender, value)`), | ||
| /// - the `owner`'s permit nonce is incremented by one, | ||
| /// - a CEP-18 `SetAllowance { owner, spender, allowance: value }` | ||
|
mssteuer marked this conversation as resolved.
|
||
| /// event is emitted. | ||
| /// | ||
| /// The call must revert: | ||
|
mssteuer marked this conversation as resolved.
|
||
| /// - with `PermitExpired` if `deadline != u64::MAX` and the current | ||
| /// block time is strictly greater than `deadline`, | ||
| /// - with `InvalidPublicKey` if `owner` is not the `Address` derived | ||
| /// from `public_key` (see "Signature scheme" below), | ||
| /// - with `InvalidSignature` if `signature` does not verify against | ||
| /// `public_key` over the recomputed digest, | ||
| /// - with `InvalidNonce` if the `nonce` is not exactly equal to | ||
| /// the one expected by the contract. | ||
| /// | ||
| /// The transaction caller (`env().caller()`) is irrelevant — `permit` | ||
| /// is designed to be relayed by any third party. | ||
| fn permit( | ||
| &mut self, | ||
| owner: Key, | ||
| spender: Key, | ||
| value: U256, | ||
| deadline: u64, | ||
| public_key: PublicKey, | ||
| signature: Bytes | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| #### Signature scheme — divergence from EIP-2612 | ||
|
|
||
| EIP-2612 passes the signature as `(v, r, s)` because Ethereum recovers the | ||
| signer's address from the signature itself via ECDSA recovery and then | ||
| asserts that the recovered address equals `owner`. Casper supports | ||
| multiple signature schemes (ed25519 and secp256k1) and does not recover | ||
| the signer from the signature, so this CEP takes an explicit | ||
| `(public_key, signature)` pair instead. | ||
|
|
||
| To preserve the property that a permit signature only authorizes its own | ||
|
davidatwhiletrue marked this conversation as resolved.
|
||
| `owner`'s allowance, implementations MUST enforce: | ||
|
|
||
| 1. `owner == Address::from(public_key)` — i.e. `owner` is the account | ||
| address corresponding to `public_key` AND | ||
| 2. `signature` is a valid signature of `public_key` over the EIP-712 | ||
| digest of the `Permit` typed data (described below). | ||
|
|
||
| Without (2) a third party could sign a digest containing an unrelated `owner` | ||
| field with their own key and have the contract set that `owner`'s allowance. (1) | ||
| reverts with `InvalidPublicKey`, while (2) reverts with `InvalidSignature` if | ||
| not satisfied. | ||
|
|
||
| ### Signed payload (EIP-712) | ||
|
|
||
| The signed payload is the EIP-712 digest of a `Permit` typed struct. | ||
|
|
||
| The Permit type string is: | ||
|
|
||
| ``` | ||
| Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline) | ||
| ``` | ||
|
|
||
| Its typehash is keccak256 of the type string above: | ||
|
|
||
| ``` | ||
| PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9 | ||
| ``` | ||
|
|
||
| `Address` encoding is defined as 33 bytes, where the first byte is a type tag: | ||
| - `0x00` for `AccountHash`, | ||
| - `0x01` for package's `Hash`. | ||
|
|
||
| The encodeData value is the concatenation of the following EIP-712-encoded | ||
| fields, in order: | ||
|
|
||
| - `owner` encoded as an EIP-712 `address`, | ||
| - `spender` encoded as an EIP-712 `address`, | ||
| - `value` encoded as an EIP-712 `uint256`, | ||
| - `nonce` encoded as an EIP-712 `uint256` — equal | ||
| to the current on-chain `permit_nonces[owner]` value at the time the | ||
| signature was produced, | ||
| - `deadline` encoded as an EIP-712 `uint256` (32 bytes, big-endian, | ||
| left-padded). | ||
|
|
||
| The final digest is computed using the standard EIP-712 rule | ||
| (`keccak256("\x19\x01" || domainSeparator || keccak256(typeHash || encodedData))`). | ||
|
|
||
| The `domainSeparator` is defined as: | ||
|
zie1ony marked this conversation as resolved.
|
||
| - `name` — the CEP-18 token's `name`, | ||
| - `version` - the current major version of the signing domain, | ||
| - `chain_name` — the contract's `chain_name` (see Storage below); It should be | ||
| in the [CAIP-2](https://github.com/ChainAgnostic/namespaces/blob/main/casper/caip2.md) | ||
| format, | ||
| - `contract_package_hash` — the contract's own address. | ||
|
|
||
| Using `EIP-712` notation, the domain separator type string is: | ||
|
|
||
| ``` | ||
| EIP712Domain(string name,string version,string chain_name,bytes32 contract_package_hash) | ||
| ``` | ||
|
|
||
| ### Error codes | ||
|
|
||
| The CEP-2612 extension contract should revert with the following error | ||
| codes when the appropriate conditions are met: | ||
|
|
||
| ```rust | ||
| pub enum CEP2612Error { | ||
| /// Either `signature` does not verify against `public_key` over the | ||
| /// recomputed digest. | ||
| InvalidSignature = 36_000, | ||
| /// The current block time is strictly greater than `deadline`, and | ||
| /// `deadline` is not the sentinel `u64::MAX`. | ||
| PermitExpired = 36_001, | ||
| /// The `nonce` is not exactly equal to the one expected by the contract. | ||
| InvalidNonce = 36_002, | ||
| /// `owner` is not the `Address` derived from `public_key`. | ||
| InvalidPublicKey = 36_003, | ||
| } | ||
| ``` | ||
|
|
||
| These codes are disjoint from the CEP-18 error code range | ||
| (`60_001`–`60_003`). | ||
|
|
||
| ### Storage interface | ||
|
|
||
| Querying the permit nonce externally — which a wallet must do before | ||
| producing a signature — requires direct contract storage access. This | ||
| CEP fixes the on-chain layout of the two state items introduced by the | ||
| extension. | ||
|
|
||
| #### Simple values | ||
|
|
||
| The chain name used in the EIP-712 domain separator is stored in the | ||
| contract's named keys: | ||
|
|
||
| - The chain name is stored under the key `chain_name` with the type | ||
| `String`. | ||
|
|
||
| #### Permit nonces | ||
|
mssteuer marked this conversation as resolved.
|
||
|
|
||
| Permit nonces are stored in the dictionary under the named key | ||
| `permit_nonces`. It is a key-value storage where: | ||
|
|
||
| - The key is the account address of the token holder (`owner`). | ||
| - The value is the current nonce for that holder, as a `U256`. Holders | ||
| that have never used `permit` have no stored value; their nonce is | ||
| considered to be `0`. | ||
|
|
||
| The key is created using the same algorithm as CEP-18 balance keys: | ||
|
|
||
| - The account address is converted to bytes using standard `CLType` | ||
| encoding. | ||
| - The bytes are encoded to string using base64 encoding. | ||
|
|
||
| An example permit-nonce key generation implementation: | ||
|
|
||
| ```rust | ||
| use base64::prelude::{Engine, BASE64_STANDARD}; | ||
|
|
||
| fn permit_nonce_key(account: Key) -> String { | ||
| let preimage = account.to_bytes().unwrap(); | ||
| BASE64_STANDARD.encode(preimage) | ||
| } | ||
| ``` | ||
|
|
||
| Each successful `permit` call increments the stored nonce by one. The | ||
| nonce that must appear in the signed `Permit` struct is the value | ||
| present at the moment of signing (typically read off-chain immediately | ||
| before signing). | ||
|
|
||
| ## Existing implementations | ||
|
|
||
| A reference implementation of CEP-2612 is available at: | ||
| - https://github.com/odradev/odra/blob/feature/gasless-op/modules/src/erc2612.rs (TODO: update after acceptance). | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.