Skip to content

Commit

Permalink
cookin
Browse files Browse the repository at this point in the history
  • Loading branch information
amiecorso committed Nov 1, 2024
1 parent 56c9ff7 commit c111d2c
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 20 deletions.
104 changes: 89 additions & 15 deletions docs/pages/guides/spend-permissions/overview.mdx
Original file line number Diff line number Diff line change
@@ -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
150 changes: 145 additions & 5 deletions docs/pages/guides/spend-permissions/quick-start.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<Callout type="info">
**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).
Expand All @@ -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";
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -90,12 +99,17 @@ export default function Subscribe() {
...

return (
...
<div>
...
</div>
);
}
```
### 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]

Expand Down Expand Up @@ -141,17 +155,143 @@ export default function Subscribe() {
...

return (
...
<div>
...
</div>
);
}
```
### 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

0 comments on commit c111d2c

Please sign in to comment.