Skip to content

Commit

Permalink
Merge pull request #4 from coinbase/conner/update-docs
Browse files Browse the repository at this point in the history
Update docs
  • Loading branch information
ilikesymmetry authored Oct 17, 2024
2 parents 4c1ca3b + efe6e25 commit 330fe5d
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 0 deletions.
107 changes: 107 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,110 @@
# Spend Permissions

> :information_source: These contracts are unaudited. Please use at your own risk.
**Spend Permissions enable apps to spend native and ERC-20 tokens on behalf of users.**

## Design Overview

### 1. Periphery addition to Coinbase Smart Wallet V1

While implementing this feature as a new V2 wallet implementation was tempting, we decided to leverage the modular owner system from [Smart Wallet V1](https://github.com/coinbase/smart-wallet) and avoid a hard upgrade.

### 2. Only Native and ERC-20 token support

Spend Permissions only supports spending Native (e.g. ETH) and ERC-20 (e.g. USDC) tokens on a recurring period. This enables use cases like subscriptions out of the box (e.g 10 USDC per month) and also can support apps that want to avoid asking users for spend permissions every session.

This approach does **not** enable apps to make arbitrary external calls from user accounts, improving security by having a tighter and fully-known scope of account control.

### 3. Spender-originated calls

Spend Permissions allow users to delegate token spending to a `spender` address, presumably controlled by the app. When an app wants to spend user tokens, it calls into `SpendPermissionManager` from this `spender` address. `SpendPermissionManager` will then validate the spend is within the approved permission's allowance and calls into the user's account to transfer tokens.

This approach does **not** use the ERC-4337 EntryPoint to prompt external calls from user accounts, improving security by avoiding the possibility of ERC-4337 Paymasters spending users' tokens on gas fees.

## End-to-end Journey

### 1. App requests and user signs permissions (offchain)

Apps request spend permissions for users to sign by sending an `eth_signTypedData` RPC request containing the permission details.

Read more details [here](./docs/diagrams/signSpendPermission.md).

```mermaid
sequenceDiagram
autonumber
participant A as App
participant WF as Wallet Frontend
participant U as User
participant WB as Wallet Backend
A->>WF: eth_signTypedData
WF->>U: approve permission
U-->>WF: signature
WF->>WB: get account status
WB-->>WF: deploy status, initCode, current + pending owners
alt account not deployed && manager in initCode
Note right of WF: wrap signature in ERC-6492
else manager not in initCode && manager not owner
WF->>U: add manager
U-->>WF: signature
end
WF-->>A: signature
```

### 2. App approves and spends (onchain)

Spenders (apps) spend tokens by calling `SpendPermissionManager.spend` with their spend permission values, a recipient, and an amount of tokens to spend.

Spenders may want to batch this call with an additionally prepended call to [approve their permission via user signature](./approveWithSignature.md) or the convenience function `SpendPermissionManager.spendWithSignature`.

Read more details [here](./docs/diagrams/spend.md).

```mermaid
sequenceDiagram
autonumber
participant S as Spender
participant PM as Permission Manager
participant A as Account
participant ERC20
opt
S->>PM: approveWithSignature
Note over PM: validate signature and store approval
end
S->>PM: spend
Note over PM: validate permission approved <br> and spend value within allowance
PM->>A: execute
Note over PM,A: transfer tokens
alt token is ERC-7528 address
A->>S: call{value}()
Note over A,S: transfer native token to spender
else else is ERC-20 contract
A->>ERC20: transfer(spender, value)
Note over A,ERC20: transfer ERC-20 to spender
end
```

### 3. User revokes permission (onchain)

Users can revoke permissions at any time by calling `SpendPermissionManager.revoke`, which can also be batched via `CoinbaseSmartWallet.executeBatch`.

Read more details [here](./docs/diagrams/revoke.md).

```mermaid
sequenceDiagram
autonumber
participant E as Entrypoint
participant A as Account
participant PM as Permission Manager
Note over E: Validation phase
E->>A: validateUserOp
A-->>E: validation data
Note over E: Execution phase
E->>A: executeBatch
loop
A->>PM: revoke
Note over A,PM: SpendPermission data
end
```
68 changes: 68 additions & 0 deletions docs/SpendPermissionAccounting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Spend Permission Accounting

## Onchain Accounting

Tracking asset expenditure accurately is important for enforcing user-approved limits. Given the fragility and complications with doing this accounting offchain and the severity of inaccuracies, we designed for fully onchain accounting. Onchain accounting enables us to have higher confidence in its reliability and keep our system trust-minimized.
The accounting for each spend permission is stored in a single slot, via a mapping keyed by the hash of its struct values.
A spend permission contains 3 entity values:

1. `account`: user whose tokens will be spent
1. `spender`: app who is able to spend tokens
1. `token`: ERC-20 contract or ERC-7528 native token

A spend permission contains 4 accounting values:

1. `start`: time this permission is valid starting at
1. `end`: time this permission is valid until
1. `period`: duration of a recurring interval that resets the `spender`'s allowance
1. `allowance`: amount of tokens spendable per period

### Recurring Accounting

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 recurring logic automatically increments 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 design allows users and apps to have reduced friction in approving asset use, while still giving the user control to manage risk and keep asset allowance small upfront. This design is also intuitive for users and can easily support recurring models like subscriptions, automated trading strategies, and payroll.

The start time and period period set a deterministic schedule infinitely into the future for when allowances reset to zero for the next period. The end time enforces when the permission can no longer be used and does not have to correlate with a clean period boundary.
Consider this example configuration:

```
start = 0
end = 1000
period = 100
allowance = 100
```

This configuration would produce the following period-size ranges each with their own allowance:

```
[0, 99], [100, 199], [200, 299], ...
```

When a new spend is attempted, the contract first determines what the current period range is. If the current time falls within the period of last stored use, we simply check if this new usage will exceed the allowance.

```
new spend=25 @ t=0
period = [0, 99]
allowance = 0 + 25 = 25,
overspend = 25 > 100 = false
new spend=25 @ t=50
period = [0, 99]
allowance = 25 + 25 = 50
overspend = 50 > 100 = false
```

If the current time exceeds the period of last stored use, that means we are in a new period and should reset the allowance to zero and then add our new attempted spend.

```
new spend=25 @ t=0
period = [0, 99]
allowance = 0 + 25 = 25,
overspend = 25 > 100 = false
new spend=25 @ t=150
period = [100, 199]
allowance = 0 + 25 = 25
overspend = 25 > 100 = false
```
23 changes: 23 additions & 0 deletions docs/diagrams/approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Approve Permissions

While the default experience is for apps to request the user [sign spend permissions](signSpendPermission.md) and [approve with signatures](./approveWithSignature.md), it can also be valuable to approve permissions via direct calls to `SpendPermissionManager.approve`. For example, paying now to start a subscription and approving a permission to pay the same amount every month.

```mermaid
sequenceDiagram
autonumber
participant E as Entrypoint
participant A as Account
participant PM as Permission Manager
participant EC as External Contract
Note over E: Validation phase
E->>A: validateUserOp
A-->>E: validation data
Note over E: Execution phase
E->>A: executeBatch
opt
A->>EC: call
end
A->>PM: approve
Note over A,PM: SpendPermission data
```
24 changes: 24 additions & 0 deletions docs/diagrams/approveWithSignature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Approve Permission With Signature

Apps approve their permission by calling `SpendPermissionManager.approveWithSignature` using the signature returned from the wallet when [signing spend permissions](signSpendPermission.md).

If the signature is [ERC-6492](https://eips.ethereum.org/EIPS/eip-6492) formatted, `SpendPermissionManager` will automatically detect this and deploy the account on behalf of the app. Afterwards, it will call `isValidSignature` to verify the account signed the permission.

```mermaid
sequenceDiagram
autonumber
participant S as Spender
participant PM as Permission Manager
participant A as Account
participant F as Factory
S->>PM: approveWithSignature
Note over PM: validate signature
opt if 6492 initCode
PM->>F: createAccount
F->>A: create2
end
PM->>A: isValidSignature
A-->>PM: EIP-1271 magic value
Note over PM: revert or store approval
```
21 changes: 21 additions & 0 deletions docs/diagrams/revoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Revoke Permissions

Users can revoke permissions at any time by calling `SpendPermissionManager.revoke`, which can also be batched via `CoinbaseSmartWallet.executeBatch`.

```mermaid
sequenceDiagram
autonumber
participant E as Entrypoint
participant A as Account
participant PM as Permission Manager
Note over E: Validation phase
E->>A: validateUserOp
A-->>E: validation data
Note over E: Execution phase
E->>A: executeBatch
loop
A->>PM: revoke
Note over A,PM: SpendPermission data
end
```
31 changes: 31 additions & 0 deletions docs/diagrams/signSpendPermission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Sign Spend Permission

Apps request spend permissions from users by sending an `eth_signTypedData` request containing the permission details.

Users are guided to sign the permission hash and add the `SpendPermissionManager` contract as an owner if it is not already. Signing to approve enables users to not spend gas on this action, offsetting this cost to the app.

If a users account is not yet deployed, but has the `SpendPermissionManager` as an initial owner in its `initCode`, the returned signature is formatted according to [ERC-6492](https://eips.ethereum.org/EIPS/eip-6492) with the `CoinbaseSmartWalletFactory` address and this `initCode`.

This entire process takes place offchain and requires no transactions or network fees.

```mermaid
sequenceDiagram
autonumber
participant A as App
participant WF as Wallet Frontend
participant U as User
participant WB as Wallet Backend
A->>WF: eth_signTypedData
WF->>U: approve permission
U-->>WF: signature
WF->>WB: get account status
WB-->>WF: deploy status, initCode, current + pending owners
alt account not deployed && manager in initCode
Note right of WF: wrap signature in ERC-6492
else manager not in initCode && manager not owner
WF->>U: add manager
U-->>WF: signature
end
WF-->>A: signature
```
32 changes: 32 additions & 0 deletions docs/diagrams/spend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Spend Tokens

Spenders (apps) spend tokens by calling `SpendPermissionManager.spend` with their spend permission values, a recipient, and an amount of tokens to spend.

Spenders may want to batch this call with an additionally prepended call to [approve their permission via user signature](./approveWithSignature.md) or the convenience function `SpendPermissionManager.spendWithSignature`. After a permission is approved, it is cheaper to avoid re-validating the signature and approval by just calling `SpendPermissionManager.spend` for repeated use.

When executing a spend, `SpendPermissionManager` calls `CoinbaseSmartWallet.execute` with either a call to transfer native or ERC-20 tokens to the recipient. Only these two kinds of calls are allowed by the `SpendPermissionManager` on a `CoinbaseSmartWallet` for reduced risk.

```mermaid
sequenceDiagram
autonumber
participant S as Spender
participant PM as Permission Manager
participant A as Account
participant ERC20
opt
S->>PM: approveWithSignature
Note over PM: validate signature and store approval
end
S->>PM: spend
Note over PM: validate permission approved <br> and spend value within allowance
PM->>A: execute
Note over PM,A: transfer tokens
alt token is ERC-7528 address
A->>S: call{value}()
Note over A,S: transfer native token to spender
else else is ERC-20 contract
A->>ERC20: transfer(spender, value)
Note over A,ERC20: transfer ERC-20 to spender
end
```

0 comments on commit 330fe5d

Please sign in to comment.