Skip to content
2 changes: 1 addition & 1 deletion src/token/ERC20/Permit/ERC20PermitMod.sol
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function DOMAIN_SEPARATOR() view returns (bytes32) {

/**
* @notice Validates a permit signature and sets allowance.
* @dev Emits Approval event; must be emitted by the calling facet/contract.
* @dev Emits Approval event;
* @param _owner Token owner.
* @param _spender Token spender.
* @param _value Allowance value.
Expand Down
202 changes: 202 additions & 0 deletions website/docs/library/token/ERC20/Approve/ERC20ApproveFacet.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
---
sidebar_position: 100
title: "ERC-20 Approve Facet"
description: "Set how many of the caller's ERC-20 tokens a spender may move"
sidebar_label: "Facet"
gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Approve/ERC20ApproveFacet.sol"
---

import DocSubtitle from '@site/src/components/docs/DocSubtitle';
import Callout from '@site/src/components/ui/Callout';
import Accordion, { AccordionGroup } from '@site/src/components/ui/Accordion';
import PropertyTable from '@site/src/components/api/PropertyTable';
import ExpandableCode from '@site/src/components/code/ExpandableCode';
import LastUpdated from '@site/src/components/docs/LastUpdated';
import PackageImport from '@site/src/components/docs/PackageImport';

<PackageImport/>

<DocSubtitle>
Grant a spender an allowance over the caller's tokens
</DocSubtitle>

<Callout type="info" title="Key Features">
- `approve` sets the caller's allowance for `_spender` in `ERC20Storage` at `erc8042:erc20`.
- The new value replaces the old one. It is not added to it.
- Always acts for `msg.sender`, so no access control is needed.
</Callout>

## Storage

### State Variables

<PropertyTable
properties={[
{
name: "STORAGE_POSITION",
type: "bytes32",
description: "ERC-20 storage position within the diamond (Value: `keccak256(\"erc20\")`)"
}
]}
showRequired={false}
/>

### ERC20Storage

<ExpandableCode language="solidity" maxLines={15} title="Definition">
{`/** @custom:storage-location erc8042:erc20 */
struct ERC20Storage {
mapping(address owner => uint256 balance) balanceOf;
uint256 totalSupply;
mapping(address owner => mapping(address spender => uint256 allowance)) allowance;
}`}
</ExpandableCode>

## Functions

### approve

Sets how many of the caller's tokens `_spender` may move with `transferFrom`. The stored allowance becomes exactly `_value`, overwriting any previous allowance for that spender.

The caller's balance is not checked. You can approve more tokens than you hold, and the limit is enforced later when the spender actually transfers.

<ExpandableCode language="solidity" maxLines={8}>
{`function approve(address _spender, uint256 _value) external returns (bool);`}
</ExpandableCode>

**Parameters:**

<PropertyTable
properties={[
{
name: "_spender",
type: "address",
description: "The address allowed to spend the caller's tokens. Cannot be `address(0)`."
},
{
name: "_value",
type: "uint256",
description: "The new allowance. `0` revokes it. `type(uint256).max` grants an unlimited allowance that is never decremented."
}
]}
showRequired={false}
/>

**Returns:**

<PropertyTable
properties={[
{
name: "-",
type: "bool",
description: "Always `true`. Failures revert instead of returning `false`."
}
]}
showRequired={false}
/>

**Reverts:**

<PropertyTable
properties={[
{
name: "ERC20InvalidSpender",
type: "error",
description: "`_spender` is `address(0)`."
}
]}
showRequired={false}
/>

## Events

<AccordionGroup>
<Accordion title="Approval" defaultOpen={false}>

<div style={{marginBottom: "1rem"}}>
Emitted on every successful `approve`, including when `_value` is `0` or unchanged.
</div>

<div style={{marginBottom: "1rem"}}>
<strong>Signature:</strong>
<ExpandableCode language="solidity" maxLines={5}>
{`event Approval(address indexed _owner, address indexed _spender, uint256 _value);`}
</ExpandableCode>
</div>

<div style={{marginBottom: "1rem"}}>
<strong>Parameters:</strong>
<PropertyTable
properties={[
{
name: "_owner",
type: "address",
description: "The address granting the allowance. Always `msg.sender`."
},
{
name: "_spender",
type: "address",
description: "The address receiving the allowance."
},
{
name: "_value",
type: "uint256",
description: "The new allowance."
}
]}
showRequired={false}
/>
</div>

</Accordion>
</AccordionGroup>

## Errors

<AccordionGroup>
<Accordion title="ERC20InvalidSpender" defaultOpen={false}>

<div style={{marginBottom: "1rem"}}>
Thrown by `approve` when `_spender` is the zero address.
</div>

<div style={{marginBottom: "1rem"}}>
<strong>Signature:</strong>
<ExpandableCode language="solidity" maxLines={5}>
{`error ERC20InvalidSpender(address _spender);`}
</ExpandableCode>
</div>

<div style={{marginBottom: "1rem"}}>
<strong>Parameters:</strong>
<PropertyTable
properties={[
{
name: "_spender",
type: "address",
description: "The rejected spender. Always `address(0)`."
}
]}
showRequired={false}
/>
</div>

</Accordion>
</AccordionGroup>

## Best Practices

- Add [`ERC20TransferFacet`](/docs/library/token/ERC20/Transfer/ERC20TransferFacet) alongside this facet. An allowance only does something once a spender can call `transferFrom`.
- Add [`ERC20DataFacet`](/docs/library/token/ERC20/Data/ERC20DataFacet) so callers can read the current value with `allowance()`.
- Approve only what a spender needs. An unlimited allowance of `type(uint256).max` stays valid until you explicitly set it back to `0`.
- Revoke an allowance by calling `approve(_spender, 0)`.

## Security Considerations

`approve` only ever writes the allowance of `msg.sender`, so a caller cannot grant spending rights over anyone else's tokens.

**Changing a non-zero allowance can be front-run.** If you lower an allowance from `N` to `M`, the spender can see the pending transaction, spend `N` first, and then spend `M` as well. This facet has no `increaseAllowance` or `decreaseAllowance`. To change a non-zero allowance safely, set it to `0`, confirm that transaction, then set the new value.

`transferFrom` and `burnFrom` reduce allowances without emitting `Approval`. Rebuilding allowances from `Approval` events alone gives stale values, so read `allowance()` for the current number.

<LastUpdated date="2026-09-15T12:00:00.000Z" />
Loading
Loading