diff --git a/docs/pages/guides/spend-permissions/overview.mdx b/docs/pages/guides/spend-permissions/overview.mdx index 2d463d0..825ec35 100644 --- a/docs/pages/guides/spend-permissions/overview.mdx +++ b/docs/pages/guides/spend-permissions/overview.mdx @@ -1,25 +1,99 @@ # Spend Permissions -Spend Permissions enable apps/third-party spenders to spend native tokens and ERC-20 tokens on behalf of users in a recurring way. -This unlocks use cases that would otherwise be prohibited by the burden of requiring real-time signatures from users, such as frequent in-game spending, -subscription payments, auto-renewals, asynchronous spends such as automated or event-based trading, and more. Designed to integrate with Coinbase Smart Wallet V1, -a smart wallet adds the single `SpendPermissionManager` contract as an owner of their account, and can then use this gatekeeper to authorize third-party spenders -to spend from their account within their specified parameters. In addition to the spender, spend permissions specify the start and end times that bound a -permission’s validity, as well as an allowance and a recurring period duration, allowing the spender to spend up to this allowance per-period. Users can revoke -existing permissions at any time. While authorized spending of ERC-20s has long been supported by allowance and permit mechanisms in token standards like ERC-20, -the abstract flexibility provided by smart contract wallets enables unlimited creativity around the programmability of these wallets and other logic they may integrate with. -Spend permissions are an early example of functionality that can be achieved within this new paradigm. +Spend Permissions enable apps/third-party spenders to spend native tokens and ERC-20 tokens on behalf of users in a +recurring way. This unlocks use cases that would otherwise be prohibited by the burden of requiring real-time signatures +from users, such as +- frequent in-game spending +- subscription payments +- auto-renewals +- automated or event-based trading +- micropayment streaming -## What is a spendpermission / what is it parameterized by? +Designed to integrate with Coinbase Smart Wallet V1, a smart wallet adds the +single `SpendPermissionManager` contract as an owner of their account, and can then use this gatekeeper to authorize +third-party spenders to spend from their account within their specified parameters. In addition to the spender, +spend permissions specify the start and end times that bound a permission’s validity, as well as an allowance and a +recurring period duration, allowing the spender to spend up to this allowance per-period. Users can revoke existing +permissions at any time. -## How to approve? + batching +While authorized spending of ERC-20s has long been supported by allowance and permit mechanisms +in token standards like ERC-20, the arbitrary programmability of smart contract wallets unlocks a vast design space for implementing +controls like spend permissions in a per-wallet way. Spend permissions are an early example of functionality that can be achieved +within this new paradigm. -## How to revoke? +## The `SpendPermission` details +A spend permission is defined by the following parameters + +```solidity +/// @notice A spend permission for an external entity to be able to spend an account's tokens. +struct SpendPermission { + /// @dev Smart account this spend permission is valid for. + address account; + /// @dev Entity that can spend `account`'s tokens. + address spender; + /// @dev Token address (ERC-7528 native token address or ERC-20 contract). + address token; + /// @dev Maximum allowed value to spend within each `period`. + uint160 allowance; + /// @dev Time duration for resetting used `allowance` on a recurring basis (seconds). + uint48 period; + /// @dev Timestamp this spend permission is valid after (unix seconds). + uint48 start; + /// @dev Timestamp this spend permission is valid until (unix seconds). + uint48 end; + /// @dev An arbitrary salt to differentiate unique spend permissions with otherwise identical data. + uint256 salt; + /// @dev Arbitrary data to include in the signature. + bytes extraData; +} +``` + + +## Approving + +Spend permissions can be approved in two ways. The user can call `approve` directly with the details +of a spend permission to establish an approval for a spender. Alternatively, the user can sign the hash +of a spend permission and anyone can submit that signature and the details of the spend permission to +`approveWithSignature` (or `approveBatchWithSignature` in the case of a batch signature) to authorize +the approval. + + +## Revoking + +Users can revoke permissions at any time by calling `SpendPermissionManager.revoke`, which can also be batched via +`CoinbaseSmartWallet.executeBatch`. Once a spend permission has been revoked, it can never be re-approved. + +If the user wants to re-authorize the spend permission, the spender will need to generate a new spend permission +that has a unique hash from the original spend permission. If the details of the new spend permission are identical +to the revoked permission, the `salt` field of the permission should be used to generate a unique has. ## Cycle accounting ---> Known accounting issue with the non-reverting ERC20s +Spend permissions allow an app to request to spend user assets on a recurring basis (e.g. 10 USDC / month). +As apps spend user assets, the `SpendPermissionManager` contract tracks cumulative spend and enforces the allowance for the +current period. Once enough time passes to enter the next period, the allowance usage is reset to zero and +the app can keep spending up to the same allowance. + +This behavior is parameterized by the following properties of a spend permission: + +`start`: time this permission is valid starting at (unix time, seconds) +`end`: time this permission is valid until (unix time, seconds) +`period`: duration of a recurring interval that resets the spender's allowance (seconds) +`allowance`: amount of tokens spendable per period + +Note that spend permissions can be used for one-time allowances, either time-bounded or indefinite, by setting +a period durantion that spans the entire range between start and end. + +A comprehensive example of spend permission accounting can be found [here](https://github.com/coinbase/spend-permissions/blob/main/docs/SpendPermissionAccounting.md). + +## Additional documentation +Contract sourcecode, diagrams, and additional documentation can be found in the open-source [contracts repository](https://github.com/coinbase/spend-permissions). + +## Known issues +ERC20s w non-reverting transfers will lead to inconsistent spend state +Duplicate spend permissions can be approved (and regardless of revocation status) +Unlimited batch size -## ERC6492 and general notes on SCW signature validation (link to other sig validation docs?) +## Misc +ERC6492 Signatures are supported -Maybe leave this out entirely unless we find we're getting a lot of questions about it diff --git a/docs/pages/guides/spend-permissions/quick-start.mdx b/docs/pages/guides/spend-permissions/quick-start.mdx index 265503c..554de74 100644 --- a/docs/pages/guides/spend-permissions/quick-start.mdx +++ b/docs/pages/guides/spend-permissions/quick-start.mdx @@ -4,6 +4,9 @@ import { Callout } from "vocs/components"; This guide will walk you through a simple example of building an app that leverages Spend Permissions, using [Viem](https://viem.sh/) and [Wagmi](https://wagmi.sh/). +TODO: directly post deployment addresses here w/ links to basescan? +For the contract deployment addresses see the contracts [README](https://github.com/coinbase/spend-permissions/blob/main/README.md). + **Tips**: If you need a template to start with, you can check out [Onchain App Template](https://github.com/coinbase/onchain-app-template/tree/main). @@ -13,6 +16,9 @@ This guide will walk you through a simple example of building an app that levera ### Create a spender client +This is an example of a spender that is a Coinbase Smart Wallet that is sponsored by a paymaster. +EOAs or other smart contracts can also be spenders. + ```ts [spender. ts] import { createPublicClient, Hex, http } from "viem"; import { baseSepolia } from "viem/chains"; @@ -55,6 +61,9 @@ export async function getSpenderBundlerClient() { ### Construct a `SpendPermission` object to obtain signed approval from the user +A `SpendPermission` is the struct that defines the parameters of the permission. +See the solidity struct [here](https://github.com/coinbase/spend-permissions/blob/07067168108b83d9662435b056a2a580c08be214/src/SpendPermissionManager.sol#L20). + ```ts [Subscribe.tsx] export default function Subscribe() { @@ -90,12 +99,17 @@ export default function Subscribe() { ... return ( - ... +
+ ... +
); } ``` -### Obtain signed approval from the user +### Obtain a signature over the spend permission from the user + +The `SpendPermissionManager.sol` uses [ERC-712](https://eips.ethereum.org/EIPS/eip-712) signatures. +The following will prompt the user to sign this spend permission data with their smart wallet. ```ts [Subscribe.tsx] @@ -141,17 +155,143 @@ export default function Subscribe() { ... return ( - ... +
+ ... +
); } ``` ### Submit the signature to `SpendPermissionManager.approveWithSignature` -### Spend user funds +Spend permissions can be approved in two ways. The user can call `approve` directly with the details +of a spend permission to establish an approval for a spender. Alternatively, the user can sign the hash +of a spend permission and anyone can submit that signature and the details of the spend permission to +`approveWithSignature` (or `approveBatchWithSignature` in the case of a batch signature) to authorize +the approval. -::: +This example shows our spender app submitting the user's signature to approve the permission. +```ts [Subscribe.tsx] +// We send the permission details and the user signature to our +// backend route +async function handleCollectSubscription() { + setIsDisabled(true); + let data; + try { + const replacer = (key: string, value: any) => { + if (typeof value === "bigint") { + return value.toString(); + } + return value; + }; + const response = await fetch("/collect", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify( + { + spendPermission, + signature, + dummyData: Math.ceil(Math.random() * 100), + }, + replacer + ), + }); + if (!response.ok) { + throw new Error("Network response was not ok"); + } + data = await response.json(); + } catch (e) { + // Handle error + } + setIsDisabled(false); + return data; + } ``` +We use our bundler client and the `SpendPermissionManager.sol` contract address and abi to call `approveWithSignature` +```ts [route.tsx] +import { NextRequest, NextResponse } from "next/server"; +import { getSpenderBundlerClient } from "../../lib/spender"; +import { + spendPermissionManagerAbi, + spendPermissionManagerAddress, +} from "../../lib/abi/SpendPermissionManager"; + +export async function POST(request: NextRequest) { + const spenderBundlerClient = await getSpenderBundlerClient(); + try { + const body = await request.json(); + const { spendPermission, signature } = body; + + const userOpHash = await spenderBundlerClient.sendUserOperation({ + calls: [ + { + abi: spendPermissionManagerAbi, + functionName: "approveWithSignature", + to: spendPermissionManagerAddress, + args: [spendPermission, signature], + }, + ], + }); + + const userOpReceipt = + await spenderBundlerClient.waitForUserOperationReceipt({ + hash: userOpHash, + }); + + if (userOpReceipt.success) { + console.log("Spend Permission approved"); + } + + ... + } + ... +} ``` + + +### Spend user funds + +If the permission was successfully approved, we're ready to spend. + +```ts [route.tsx] +... + +export async function POST(request: NextRequest) { + ... + + const spendUserOpHash = await spenderBundlerClient.sendUserOperation({ // [!code focus] + calls: [ // [!code focus] + { // [!code focus] + abi: spendPermissionManagerAbi, // [!code focus] + functionName: "spend", // [!code focus] + to: spendPermissionManagerAddress, // [!code focus] + args: [spendPermission, "1"], // spend 1 wei // [!code focus] + }, // [!code focus] + ], // [!code focus] + }); // [!code focus] + + const spendUserOpReceipt = + await spenderBundlerClient.waitForUserOperationReceipt({ + hash: spendUserOpHash, + }); + + return NextResponse.json({ + status: spendUserOpReceipt.success ? "success" : "failure", + transactionHash: spendUserOpReceipt.receipt.transactionHash, + transactionUrl: `https://sepolia.basescan.org/tx/${spendUserOpReceipt.receipt.transactionHash}`, + }); + } catch (error) { + // Handle error + } +} + +``` +::: + +That's it! We've successfully created and used permission to spend our user's funds. + +TODO: link to more comprehensive documentation \ No newline at end of file