Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/token/ERC20/Bridgeable/ERC20BridgeableFacet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ contract ERC20BridgeableFacet {
_requireRole("trusted-bridge");

if (_from == address(0)) {
revert ERC20InvalidReceiver(address(0));
revert ERC20InvalidSender(address(0));
}

uint256 accountBalance = erc20Storage.balanceOf[_from];
Expand Down
2 changes: 1 addition & 1 deletion src/token/ERC20/Bridgeable/ERC20BridgeableMod.sol
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ function crosschainBurn(address _from, uint256 _value) {
_requireRole("trusted-bridge");

if (_from == address(0)) {
revert ERC20InvalidReceiver(address(0));
revert ERC20InvalidSender(address(0));
}

uint256 accountBalance = erc20Storage.balanceOf[_from];
Expand Down
2 changes: 1 addition & 1 deletion test/trees/ERC20.tree
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Bridgeable
│ ├── when caller does not have trusted-bridge role
│ │ └── it should revert with AccessControlUnauthorizedAccount
│ ├── when from is zero address
│ │ └── it should revert with ERC20InvalidReceiver
│ │ └── it should revert with ERC20InvalidSender
│ ├── when from balance is less than the amount
│ │ └── it should revert with ERC20InsufficientBalance
│ └── when caller has trusted-bridge role and from has sufficient balance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ contract CrosschainBurn_ERC20BridgeableFacet_Fuzz_Unit_Test is ERC20BridgeableFa
seedTrustedBridge(users.admin);
vm.stopPrank();
vm.prank(users.admin);
vm.expectRevert(abi.encodeWithSelector(ERC20BridgeableFacet.ERC20InvalidReceiver.selector, ADDRESS_ZERO));
vm.expectRevert(abi.encodeWithSelector(ERC20BridgeableFacet.ERC20InvalidSender.selector, ADDRESS_ZERO));
facet.crosschainBurn(ADDRESS_ZERO, 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