diff --git a/src/token/ERC20/Permit/ERC20PermitMod.sol b/src/token/ERC20/Permit/ERC20PermitMod.sol index 4a720111..802055e5 100644 --- a/src/token/ERC20/Permit/ERC20PermitMod.sol +++ b/src/token/ERC20/Permit/ERC20PermitMod.sol @@ -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. diff --git a/website/docs/library/token/ERC20/Approve/ERC20ApproveFacet.mdx b/website/docs/library/token/ERC20/Approve/ERC20ApproveFacet.mdx new file mode 100644 index 00000000..3c7013f0 --- /dev/null +++ b/website/docs/library/token/ERC20/Approve/ERC20ApproveFacet.mdx @@ -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'; + + + + +Grant a spender an allowance over the caller's tokens + + + +- `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. + + +## Storage + +### State Variables + + + +### ERC20Storage + + +{`/** @custom:storage-location erc8042:erc20 */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +}`} + + +## 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. + + +{`function approve(address _spender, uint256 _value) external returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +**Reverts:** + + + +## Events + + + + +
+ Emitted on every successful `approve`, including when `_value` is `0` or unchanged. +
+ +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _spender, uint256 _value);`} + +
+ +
+ Parameters: + +
+ +
+
+ +## Errors + + + + +
+ Thrown by `approve` when `_spender` is the zero address. +
+ +
+ Signature: + +{`error ERC20InvalidSpender(address _spender);`} + +
+ +
+ Parameters: + +
+ +
+
+ +## 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. + + diff --git a/website/docs/library/token/ERC20/Approve/ERC20ApproveMod.mdx b/website/docs/library/token/ERC20/Approve/ERC20ApproveMod.mdx new file mode 100644 index 00000000..6a111c33 --- /dev/null +++ b/website/docs/library/token/ERC20/Approve/ERC20ApproveMod.mdx @@ -0,0 +1,234 @@ +--- +sidebar_position: 110 +title: "ERC-20 Approve Module" +description: "ERC-20 allowance helper for your own diamond facets" +sidebar_label: "Module" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Approve/ERC20ApproveMod.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'; + + + + +Helper functions for setting ERC-20 allowances and accessing ERC-20 storage + + + +- `approve` always sets the allowance of `msg.sender`. It cannot approve on behalf of another owner. +- No access control check is performed, and none is needed because the caller only spends their own allowance. +- Emits `Approval` on every successful call. + + + +Use helper functions from Compose using your own custom facets. See Facets & Modules for more information. + + +## Storage + +### State Variables + + + +### ERC20Storage + + +{`/** @custom:storage-location erc8042:erc20 */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +}`} + + +## Functions + +### getStorage + +Returns a pointer to the `ERC20Storage` struct. + + +{`function getStorage() pure returns (ERC20Storage storage s);`} + + +**Returns:** + + + +--- + +### approve + +Sets how many of the caller's tokens `_spender` may move. The stored allowance becomes exactly `_value`, overwriting any previous allowance for that spender. + +Inside a diamond, your facet runs through `delegatecall`, so `msg.sender` here is the account that called the diamond, not the diamond itself. + + +{`function approve(address _spender, uint256 _value) returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +**Reverts:** + + + +## Events + + + +
+ Emitted on every successful `approve`, including when `_value` is `0` or unchanged. +
+ +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _spender, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown by `approve` when `_spender` is the zero address. +
+ +
+ Signature: + +{`error ERC20InvalidSpender(address _spender);`} + +
+ +
+ Parameters: + +
+
+
+ +## Best Practices + +- Use `approve` when your facet sets an allowance on behalf of whoever is calling it, for example a wrapper that approves and then performs a second action in one transaction. +- Keep the same `STORAGE_POSITION` and `ERC20Storage` layout as the other ERC-20 modules. Do not introduce a second allowance mapping. +- Change a non-zero allowance by setting it to `0` first. Compose has no `increaseAllowance` or `decreaseAllowance`, so an `N` to `M` change can be front-run by a spender who uses `N` first. + +## Integration Notes + +Import the module under a namespace and call it from your facet: + + +{`import "src/token/ERC20/Approve/ERC20ApproveMod.sol" as ERC20ApproveMod; + +contract MyFacet { + function approveSpender(address _spender, uint256 _value) external returns (bool) { + return ERC20ApproveMod.approve(_spender, _value); + } +}`} + + +`approve` cannot set an allowance for any owner other than `msg.sender`. If your facet authorizes an owner some other way, such as a signature, write `getStorage().allowance[_owner][_spender]` directly and emit `Approval` yourself. [`ERC20PermitFacet`](/docs/library/token/ERC20/Permit/ERC20PermitFacet) does exactly this. + +`ERC20Storage` lives at `keccak256("erc20")` inside the diamond. That is the same slot [`ERC20DataFacet`](/docs/library/token/ERC20/Data/ERC20DataFacet), [`ERC20TransferFacet`](/docs/library/token/ERC20/Transfer/ERC20TransferFacet), and the other ERC-20 contracts use, so an allowance set here is what `allowance()` returns and what `transferFrom` spends. + + diff --git a/website/docs/library/token/ERC20/Approve/_category_.json b/website/docs/library/token/ERC20/Approve/_category_.json new file mode 100644 index 00000000..ce6d95a1 --- /dev/null +++ b/website/docs/library/token/ERC20/Approve/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Approve", + "position": 3, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/Approve/index" + } +} diff --git a/website/docs/library/token/ERC20/Approve/index.mdx b/website/docs/library/token/ERC20/Approve/index.mdx new file mode 100644 index 00000000..fd5fac6b --- /dev/null +++ b/website/docs/library/token/ERC20/Approve/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-20 Approve" +sidebar_label: "Approve" +description: "Grant a spender an allowance over ERC-20 tokens." +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Set how many of an owner's tokens a spender may move + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/Bridgeable/ERC20BridgeableFacet.mdx b/website/docs/library/token/ERC20/Bridgeable/ERC20BridgeableFacet.mdx new file mode 100644 index 00000000..e69de29b diff --git a/website/docs/library/token/ERC20/Bridgeable/ERC20BridgeableMod.mdx b/website/docs/library/token/ERC20/Bridgeable/ERC20BridgeableMod.mdx new file mode 100644 index 00000000..e69de29b diff --git a/website/docs/library/token/ERC20/Bridgeable/_category_.json b/website/docs/library/token/ERC20/Bridgeable/_category_.json new file mode 100644 index 00000000..fd18450f --- /dev/null +++ b/website/docs/library/token/ERC20/Bridgeable/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Bridgeable", + "position": 8, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/Bridgeable/index" + } +} diff --git a/website/docs/library/token/ERC20/Bridgeable/index.mdx b/website/docs/library/token/ERC20/Bridgeable/index.mdx new file mode 100644 index 00000000..9149ce01 --- /dev/null +++ b/website/docs/library/token/ERC20/Bridgeable/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-20 Bridgeable" +sidebar_label: "Bridgeable" +description: "Crosschain ERC-20 mint and burn for authorized bridges." +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Mint and burn tokens across chains, restricted to authorized bridges + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/Burn/ERC20BurnFacet.mdx b/website/docs/library/token/ERC20/Burn/ERC20BurnFacet.mdx new file mode 100644 index 00000000..6a9ab7b3 --- /dev/null +++ b/website/docs/library/token/ERC20/Burn/ERC20BurnFacet.mdx @@ -0,0 +1,282 @@ +--- +sidebar_position: 100 +title: "ERC-20 Burn Facet" +description: "Destroy ERC-20 tokens from the caller's balance or from an allowance" +sidebar_label: "Facet" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Burn/ERC20BurnFacet.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'; + + + + +Destroy tokens and reduce the total supply + + + +- `burn` destroys the caller's own tokens. `burnFrom` destroys someone else's using an allowance. +- Both reduce `balanceOf` and `totalSupply` in `ERC20Storage` at `erc8042:erc20`. +- Both emit `Transfer` to `address(0)`, which is the ERC-20 convention for a burn. + + +## Storage + +### State Variables + + + +### ERC20Storage + + +{`/** @custom:storage-location erc8042:erc20 */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +}`} + + +## Functions + +### burn + +Destroys `_value` of the caller's own tokens and reduces the total supply by the same amount. + +Burning `0` is allowed and still emits `Transfer`. Neither function returns a value, so check for a revert rather than a boolean. + + +{`function burn(uint256 _value) external;`} + + +**Parameters:** + + + +**Reverts:** + + + +--- +### burnFrom + +Destroys `_value` tokens held by `_account`, spending the allowance that `_account` granted to the caller, and reduces the total supply. + +The allowance is checked **before** the balance, so if both are insufficient the call reverts with `ERC20InsufficientAllowance`. When the allowance is exactly `type(uint256).max` it is left untouched, otherwise it is reduced by `_value`. + + +{`function burnFrom(address _account, uint256 _value) external;`} + + +**Parameters:** + + + +**Reverts:** + + + +## Events + + + + +
+ Emitted by both `burn` and `burnFrom`, always with `address(0)` as the destination. This is how ERC-20 represents a burn, so indexers reading `Transfer` see the supply decrease without a separate event. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 _value);`} + +
+ +
+ Parameters: + +
+ +
+
+ +## Errors + + + + +
+ Thrown by `burn` when the caller, or by `burnFrom` when `_account`, holds less than `_value`. +
+ +
+ Signature: + +{`error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed);`} + +
+ +
+ Parameters: + +
+ +
+ + + +
+ Thrown by `burnFrom` when the caller's allowance over `_account` is below `_value`. +
+ +
+ Signature: + +{`error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed);`} + +
+ +
+ Parameters: + +
+ +
+
+ +## Best Practices + +- `burnFrom` needs an allowance first, from [`ERC20ApproveFacet`](/docs/library/token/ERC20/Approve/ERC20ApproveFacet) or [`ERC20PermitFacet`](/docs/library/token/ERC20/Permit/ERC20PermitFacet). It is the same allowance `transferFrom` spends, so burning consumes budget a spender could otherwise have transferred. +- Add [`ERC20DataFacet`](/docs/library/token/ERC20/Data/ERC20DataFacet) so holders can watch `totalSupply` fall as tokens are destroyed. +- Treat burns as irreversible. Nothing in this facet can recreate destroyed tokens. Supply only rises again through [`ERC20MintMod`](/docs/library/token/ERC20/Mint/ERC20MintMod), which ships without a facet of its own, or through `crosschainMint` on [`ERC20BridgeableFacet`](/docs/library/token/ERC20/Bridgeable/ERC20BridgeableFacet), which is restricted to holders of the `trusted-bridge` role. +- To destroy tokens from an account that has **not** approved the caller, this facet cannot do it. Use [`ERC20BurnMod`](/docs/library/token/ERC20/Burn/ERC20BurnMod) behind your own authorization check. + +## Security Considerations + +**The `totalSupply` subtraction is unchecked and depends on an invariant.** Both functions verify the account's balance, then subtract from the balance and `totalSupply` inside the same `unchecked` block. That is safe only while no balance exceeds `totalSupply`. A custom facet that writes `balanceOf` directly without adjusting `totalSupply` breaks that invariant, and a later burn can then underflow `totalSupply` to a huge number with no revert. + +**`burnFrom` emits no `Approval` when it reduces the allowance.** Only the Approve and Permit contracts emit `Approval` anywhere in Compose, so an indexer tracking allowances from events alone will drift. Read `allowance()` for the current value. + +**`burnFrom` does not reject `address(0)` explicitly.** It has no zero-address guard. A call naming `address(0)` fails the allowance check instead, because that allowance is always zero, unless `_value` is also `0`, in which case the call succeeds as a no-op and emits `Transfer(address(0), address(0), 0)`. + +There are no external calls in either function, so there is no reentrancy surface here. + + diff --git a/website/docs/library/token/ERC20/Burn/ERC20BurnMod.mdx b/website/docs/library/token/ERC20/Burn/ERC20BurnMod.mdx new file mode 100644 index 00000000..5bf5abce --- /dev/null +++ b/website/docs/library/token/ERC20/Burn/ERC20BurnMod.mdx @@ -0,0 +1,265 @@ +--- +sidebar_position: 110 +title: "ERC-20 Burn Module" +description: "ERC-20 burn helper for your own diamond facets" +sidebar_label: "Module" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Burn/ERC20BurnMod.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'; + + + + +Helper function for destroying ERC-20 tokens from any account + + + +`burn` destroys tokens from **any** account you name. It performs no allowance check and no role check. Whatever facet calls it is responsible for proving the caller is allowed to do this. + + + +- `burn(_account, _value)` takes the account as a parameter, unlike the facet's caller-only `burn(_value)`. +- Reduces `balanceOf` and `totalSupply`, then emits `Transfer` to `address(0)`. +- Declares a reduced `ERC20Storage` with no `allowance` field. See below. + + + +Use helper functions from Compose using your own custom facets. See Facets & Modules for more information. + + +## Storage + +### State Variables + + + +### ERC20Storage + +This module declares only the two fields it uses. The `allowance` mapping that other ERC-20 contracts declare is absent. + + +{`/** @custom:storage-location erc8042:erc20 */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; +}`} + + + +This is a layout-compatible prefix of the full struct, not a different slot. `balanceOf` and `totalSupply` occupy the same positions either way, so this module reads and writes exactly the same storage as the rest of ERC-20. It simply does not declare the field it never touches. + +[`ERC20MintMod`](/docs/library/token/ERC20/Mint/ERC20MintMod) and the two Bridgeable contracts use the same two-field form. Everything that reads or writes an allowance, including [`ERC20BurnFacet`](/docs/library/token/ERC20/Burn/ERC20BurnFacet), declares all three fields. + + +## Functions + +### getStorage + +Returns a pointer to the `ERC20Storage` struct. + +Because this module's struct has no `allowance` field, the returned pointer cannot reach allowances. If your facet needs to inspect one, import a module that declares the full struct, such as [`ERC20TransferMod`](/docs/library/token/ERC20/Transfer/ERC20TransferMod) or [`ERC20ApproveMod`](/docs/library/token/ERC20/Approve/ERC20ApproveMod). + + +{`function getStorage() pure returns (ERC20Storage storage s);`} + + +**Returns:** + + + +--- + +### burn + +Destroys `_value` tokens held by `_account` and reduces the total supply by the same amount. + +No allowance is consulted and no permission is checked. Gate this behind your own authorization, exactly as you would with a mint. Burning `0` is allowed and still emits `Transfer`. + + +{`function burn(address _account, uint256 _value);`} + + +**Parameters:** + + + +**Reverts:** + + + +## Events + + + +
+ Emitted on every successful burn, always with `address(0)` as the destination. This is how ERC-20 represents a burn. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when `_account` holds less than `_value`. +
+ +
+ Signature: + +{`error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed);`} + +
+ +
+ Parameters: + +
+
+ + +
+ Thrown when `_account` is the zero address. The facet's `burnFrom` has no equivalent guard, so this module is the stricter of the two. +
+ +
+ Signature: + +{`error ERC20InvalidSender(address _sender);`} + +
+
+
+ +## Best Practices + +- Wrap `burn` in an `external` function that checks permission first, for example an owner or role guard, or proof that the caller holds the tokens. +- If you want the ordinary "holders burn their own tokens" behaviour, use [`ERC20BurnFacet`](/docs/library/token/ERC20/Burn/ERC20BurnFacet) instead of writing a wrapper. It already binds the burn to `msg.sender`. +- If you want allowance-based burning, use the facet's `burnFrom`, or check the allowance yourself through a module that declares it before calling this helper. +- Keep the same `STORAGE_POSITION` and field order as the other ERC-20 contracts. Do not introduce a second balance mapping. + +## Integration Notes + +Import the module under a namespace and call it from your facet: + + +{`import "src/token/ERC20/Burn/ERC20BurnMod.sol" as ERC20BurnMod; +import "src/access/Owner/Data/OwnerDataMod.sol" as OwnerDataMod; + +contract MyFacet { + function burnFromAccount(address _account, uint256 _value) external { + OwnerDataMod.requireOwner(); + ERC20BurnMod.burn(_account, _value); + } +}`} + + +`ERC20Storage` lives at `keccak256("erc20")` inside the diamond, the same slot [`ERC20DataFacet`](/docs/library/token/ERC20/Data/ERC20DataFacet) reads, so a burn here is immediately visible through `balanceOf()` and `totalSupply()`. + +The balance and `totalSupply` subtractions sit inside `unchecked`, after an explicit balance comparison. That is safe only while no balance exceeds `totalSupply`. If your facet writes `balanceOf` directly without adjusting `totalSupply`, a later burn can underflow `totalSupply` silently. Keep the two in step. + + diff --git a/website/docs/library/token/ERC20/Burn/_category_.json b/website/docs/library/token/ERC20/Burn/_category_.json new file mode 100644 index 00000000..7f79bac2 --- /dev/null +++ b/website/docs/library/token/ERC20/Burn/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Burn", + "position": 5, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/Burn/index" + } +} diff --git a/website/docs/library/token/ERC20/Burn/index.mdx b/website/docs/library/token/ERC20/Burn/index.mdx new file mode 100644 index 00000000..9c461136 --- /dev/null +++ b/website/docs/library/token/ERC20/Burn/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-20 Burn" +sidebar_label: "Burn" +description: "Destroy ERC-20 tokens and reduce the total supply." +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Destroy tokens and reduce the total supply + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/Data/ERC20DataFacet.mdx b/website/docs/library/token/ERC20/Data/ERC20DataFacet.mdx new file mode 100644 index 00000000..0ede8cae --- /dev/null +++ b/website/docs/library/token/ERC20/Data/ERC20DataFacet.mdx @@ -0,0 +1,166 @@ +--- +sidebar_position: 100 +title: "ERC-20 Data Facet" +description: "Read-only ERC-20 supply, balance, and allowance queries" +sidebar_label: "Facet" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Data/ERC20DataFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Callout from '@site/src/components/ui/Callout'; +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'; + + + + +Read-only ERC-20 token data access for diamonds + + + +- Exposes the three ERC-20 view functions: `totalSupply()`, `balanceOf()` and `allowance()`. +- Shares `ERC20Storage` at `erc8042:erc20` with every other ERC-20 facet and module. +- Read-only facet. It never writes to storage. + + +## Storage + +### State Variables + + + +### ERC20Storage + + +{`/** storage-location: erc8042:erc20 */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +}`} + + +## Functions + +### totalSupply + +Returns the total number of tokens in circulation. + + +{`function totalSupply() external view returns (uint256);`} + + +**Returns:** + + + +--- +### balanceOf + +Returns the token balance held by an account. + + +{`function balanceOf(address _account) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### allowance + +Returns how many tokens a spender may still move on an owner's behalf. + + +{`function allowance(address _owner, address _spender) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +## Best Practices + +- Add this facet whenever tokens are exposed to wallets, explorers, or other contracts. `totalSupply()`, `balanceOf()` and `allowance()` are what integrators expect from any ERC-20. +- Pair it with `ERC20MetadataFacet` so `name()`, `symbol()` and `decimals()` are available too. That pair is the minimum most integrations assume. +- Do not add a second facet that reads token balances from a different slot. Keep all ERC-20 state at `erc20` so every facet observes the same numbers. +- Reading `allowance()` before calling `transferFrom` does not guarantee the value still holds at execution time. Treat it as informational, not as a lock. + +## Security Considerations + +This facet only reads storage, so it cannot corrupt token state on its own. Two things are still worth knowing: + +- `balanceOf()` reflects whatever the write-side facets recorded. A custom facet that writes to the `erc20` slot with an incompatible struct layout makes these reads report wrong values with no error. +- An allowance of `type(uint256).max` is a deliberate sentinel for "unlimited" in ERC20TransferFacet. Interfaces that render the raw number should special-case it rather than showing the full integer. + + diff --git a/website/docs/library/token/ERC20/Data/_category_.json b/website/docs/library/token/ERC20/Data/_category_.json new file mode 100644 index 00000000..280487d4 --- /dev/null +++ b/website/docs/library/token/ERC20/Data/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Data", + "position": 1, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/Data/index" + } +} diff --git a/website/docs/library/token/ERC20/Data/index.mdx b/website/docs/library/token/ERC20/Data/index.mdx new file mode 100644 index 00000000..de434100 --- /dev/null +++ b/website/docs/library/token/ERC20/Data/index.mdx @@ -0,0 +1,23 @@ +--- +title: "ERC-20 Data" +sidebar_label: "Data" +description: "Read-only ERC-20 supply, balance, and allowance queries." +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Read-only access to ERC-20 supply, balances, and allowances + + + + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/Metadata/ERC20MetadataFacet.mdx b/website/docs/library/token/ERC20/Metadata/ERC20MetadataFacet.mdx new file mode 100644 index 00000000..27babf5f --- /dev/null +++ b/website/docs/library/token/ERC20/Metadata/ERC20MetadataFacet.mdx @@ -0,0 +1,134 @@ +--- +sidebar_position: 100 +title: "ERC-20 Metadata Facet" +description: "Read the ERC-20 token name, symbol, and decimals" +sidebar_label: "Facet" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Metadata/ERC20MetadataFacet.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Callout from '@site/src/components/ui/Callout'; +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'; + + + + +Read-only access to the token name, symbol, and decimals + + + +- Exposes the three ERC-20 metadata view functions: `name()`, `symbol()` and `decimals()`. +- Uses `ERC20MetadataStorage` at `erc8042:erc20.metadata`, a different slot from balances and allowances. +- Read-only facet. Values are written by [`ERC20MetadataMod`](/docs/library/token/ERC20/Metadata/ERC20MetadataMod). + + +## Storage + +### State Variables + + + +### ERC20MetadataStorage + + +{`/** @custom:storage-location erc8042:erc20.metadata */ +struct ERC20MetadataStorage { + string name; + string symbol; + uint8 decimals; +}`} + + +## Functions + +### name + +Returns the descriptive name of the token, for example `"My Token"`. + + +{`function name() external view returns (string memory);`} + + +**Returns:** + + + +--- +### symbol + +Returns the short ticker of the token, for example `"MTK"`. + + +{`function symbol() external view returns (string memory);`} + + +**Returns:** + + + +--- +### decimals + +Returns how many decimal places wallets and interfaces should use when displaying balances. + + +{`function decimals() external view returns (uint8);`} + + +**Returns:** + + + +## Best Practices + +- Add this facet together with [`ERC20DataFacet`](/docs/library/token/ERC20/Data/ERC20DataFacet). Wallets and explorers expect both the balance functions and the metadata functions. +- Set all three values once during deployment with [`ERC20MetadataMod.setMetadata`](/docs/library/token/ERC20/Metadata/ERC20MetadataMod#setmetadata). Reading this facet before that happens returns empty strings and `0`. +- Choose `decimals` before launch and leave it alone. Changing it later reinterprets every balance already held. + +## Security Considerations + +This facet only reads storage, so it cannot corrupt token state on its own. Two consequences of the shared slot are worth knowing: + +- **Renaming the token invalidates outstanding permit signatures.** [`ERC20PermitFacet`](/docs/library/token/ERC20/Permit/ERC20PermitFacet) hashes the stored `name` into its EIP-712 domain, both in `DOMAIN_SEPARATOR()` and again inside `permit()`. If `name` changes, previously signed permits no longer verify and integrators that cached the old domain separator will produce failing signatures. +- **`decimals` is display only.** Nothing in Compose does arithmetic with it. Balances, allowances, and the total supply are raw integers, so treat this value as a hint for user interfaces rather than a unit of account. + + diff --git a/website/docs/library/token/ERC20/Metadata/ERC20MetadataMod.mdx b/website/docs/library/token/ERC20/Metadata/ERC20MetadataMod.mdx new file mode 100644 index 00000000..9f722189 --- /dev/null +++ b/website/docs/library/token/ERC20/Metadata/ERC20MetadataMod.mdx @@ -0,0 +1,145 @@ +--- +sidebar_position: 110 +title: "ERC-20 Metadata Module" +description: "Set the ERC-20 token name, symbol, and decimals during initialization" +sidebar_label: "Module" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Metadata/ERC20MetadataMod.sol" +--- + +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Callout from '@site/src/components/ui/Callout'; +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'; + + + + +Helper functions for writing token metadata and accessing metadata storage + + + +- `setMetadata` writes the name, symbol, and decimals in one call. +- No access control is performed. Use it from trusted initialization code only. +- Emits no event, so metadata changes leave no log for off-chain indexers. + + + +Use helper functions from Compose using your own custom facets. See Facets & Modules for more information. + + +## Storage + +### State Variables + + + +### ERC20MetadataStorage + + +{`/** @custom:storage-location erc8042:erc20.metadata */ +struct ERC20MetadataStorage { + string name; + string symbol; + uint8 decimals; +}`} + + +## Functions + +### getStorage + +Returns a pointer to the `ERC20MetadataStorage` struct. + + +{`function getStorage() pure returns (ERC20MetadataStorage storage s);`} + + +**Returns:** + + + +--- + +### setMetadata + +Writes the token name, symbol, and decimals. + +All three fields are overwritten on every call. There is no way to update one field and leave the others untouched, so pass the current values for the fields you are not changing, or write through [`getStorage`](#getstorage) instead. + +There is **no** `msg.sender` or role check. Use only from trusted initialization code, or wrap it with your own checks in a facet. + + +{`function setMetadata(string memory _name, string memory _symbol, uint8 _decimals);`} + + +**Parameters:** + + + +## Best Practices + +- Call `setMetadata` once, from your diamond constructor or a one-time setup path, before anyone reads [`ERC20MetadataFacet`](/docs/library/token/ERC20/Metadata/ERC20MetadataFacet). +- If you expose metadata updates as an external function, gate them. Anything that reaches this helper can rename the token. +- Settle on `_decimals` before launch. Changing it later reinterprets every balance already held, since balances are stored as raw integers. +- Keep the same `STORAGE_POSITION` and `ERC20MetadataStorage` layout as `ERC20MetadataFacet`. Do not introduce a second metadata slot. + +## Integration Notes + +Import the module under a namespace and call it from your diamond constructor: + + +{`import "src/token/ERC20/Metadata/ERC20MetadataMod.sol" as ERC20MetadataMod; + +contract MyDiamond { + constructor() { + ERC20MetadataMod.setMetadata("My Token", "MTK", 18); + } +}`} + + +`ExampleDiamond` wires the ERC-721 equivalent the same way, calling `ERC721MetadataMod.setMetadata` from its constructor. + +`ERC20MetadataStorage` lives at `keccak256("erc20.metadata")`, which is a separate slot from the `erc20` slot holding balances and allowances. Adding or removing metadata contracts never touches balances. + +[`ERC20PermitFacet`](/docs/library/token/ERC20/Permit/ERC20PermitFacet) reads `name` from this same slot to build its EIP-712 domain. Changing the name after deployment invalidates permit signatures that have already been issued but not yet used. + + diff --git a/website/docs/library/token/ERC20/Metadata/_category_.json b/website/docs/library/token/ERC20/Metadata/_category_.json new file mode 100644 index 00000000..9aa87c7c --- /dev/null +++ b/website/docs/library/token/ERC20/Metadata/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Metadata", + "position": 2, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/Metadata/index" + } +} diff --git a/website/docs/library/token/ERC20/Metadata/index.mdx b/website/docs/library/token/ERC20/Metadata/index.mdx new file mode 100644 index 00000000..a4dece55 --- /dev/null +++ b/website/docs/library/token/ERC20/Metadata/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-20 Metadata" +sidebar_label: "Metadata" +description: "ERC-20 token name, symbol, and decimals." +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Read and initialize the token name, symbol, and decimals + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/Mint/ERC20MintMod.mdx b/website/docs/library/token/ERC20/Mint/ERC20MintMod.mdx new file mode 100644 index 00000000..e7f26dd4 --- /dev/null +++ b/website/docs/library/token/ERC20/Mint/ERC20MintMod.mdx @@ -0,0 +1,230 @@ +--- +sidebar_position: 110 +title: "ERC-20 Mint Module" +description: "Create new ERC-20 tokens from your own diamond facet" +sidebar_label: "Module" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Mint/ERC20MintMod.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'; + + + + +Helper function for creating new ERC-20 tokens + + + +`mint` creates tokens for **any** account you name, with no permission check of any kind. Whatever facet calls it is responsible for deciding who may mint. Compose ships no general-purpose mint facet precisely so that this decision stays yours. + + + +- `mint(_account, _value)` increases both `balanceOf[_account]` and `totalSupply`. +- Emits `Transfer` from `address(0)`, which is the ERC-20 convention for a mint. +- Arithmetic is checked, so an overflowing mint reverts rather than wrapping. +- Declares a reduced `ERC20Storage` with no `allowance` field. See below. + + + +Use helper functions from Compose using your own custom facets. See Facets & Modules for more information. + + +## Storage + +### State Variables + + + +### ERC20Storage + +This module declares only the two fields it uses. The `allowance` mapping that other ERC-20 contracts declare is absent. + + +{`/** @custom:storage-location erc8042:erc20 */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; +}`} + + + +This is a layout-compatible prefix of the full struct, not a different slot. `balanceOf` and `totalSupply` occupy the same positions either way, so this module reads and writes exactly the same storage as the rest of ERC-20. It simply does not declare the field it never touches. + +[`ERC20BurnMod`](/docs/library/token/ERC20/Burn/ERC20BurnMod) and the two Bridgeable contracts use the same two-field form. Everything that reads or writes an allowance declares all three fields. + + +## Functions + +### getStorage + +Returns a pointer to the `ERC20Storage` struct. + +Because this module's struct has no `allowance` field, the returned pointer cannot reach allowances. If your facet needs to inspect one, import a module that declares the full struct, such as [`ERC20TransferMod`](/docs/library/token/ERC20/Transfer/ERC20TransferMod) or [`ERC20ApproveMod`](/docs/library/token/ERC20/Approve/ERC20ApproveMod). + + +{`function getStorage() pure returns (ERC20Storage storage s);`} + + +**Returns:** + + + +--- + +### mint + +Creates `_value` new tokens, credits them to `_account`, and increases the total supply by the same amount. + +No permission is checked. Gate this behind your own authorization. Minting `0` is allowed and still emits `Transfer`. + + +{`function mint(address _account, uint256 _value);`} + + +**Parameters:** + + + +**Reverts:** + + + +## Events + + + +
+ Emitted on every successful mint, always with `address(0)` as the source. This is how ERC-20 represents newly created tokens, so indexers reading `Transfer` see the supply increase without a separate event. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown when `_account` is the zero address. Without this guard the mint would emit `Transfer(address(0), address(0), _value)`, indistinguishable from a burn to any indexer that keys off `_to == address(0)`, and would credit a balance nobody can ever spend. +
+ +
+ Signature: + +{`error ERC20InvalidReceiver(address _receiver);`} + +
+
+
+ +## Best Practices + +- Wrap `mint` in an `external` function that checks permission first, for example an owner guard or a role check. Without one, any caller reaching your facet can inflate the supply. +- Decide on a supply policy before deploying. Nothing here enforces a cap, so if you need one, check it in your facet before calling. +- Mint during initialization if you want a fixed supply. Call `mint` from your diamond constructor and never expose an external mint path at all. +- Keep `balanceOf` and `totalSupply` in step. If you ever write balances directly, adjust the supply to match, or the unchecked subtractions in [`ERC20BurnMod`](/docs/library/token/ERC20/Burn/ERC20BurnMod) can underflow later. +- Keep the same `STORAGE_POSITION` and field order as the other ERC-20 contracts. Do not introduce a second balance mapping. + +## Integration Notes + +Import the module under a namespace and call it from your facet: + + +{`import "src/token/ERC20/Mint/ERC20MintMod.sol" as ERC20MintMod; +import "src/access/Owner/Data/OwnerDataMod.sol" as OwnerDataMod; + +contract MyFacet { + function mintTo(address _account, uint256 _value) external { + OwnerDataMod.requireOwner(); + ERC20MintMod.mint(_account, _value); + } +}`} + + +`ERC20Storage` lives at `keccak256("erc20")` inside the diamond, the same slot [`ERC20DataFacet`](/docs/library/token/ERC20/Data/ERC20DataFacet) reads, so a mint is immediately visible through `balanceOf()` and `totalSupply()`. + +**This module is not the only way supply grows.** `crosschainMint` on [`ERC20BridgeableFacet`](/docs/library/token/ERC20/Bridgeable/ERC20BridgeableFacet) also increases `totalSupply`, gated to holders of the `trusted-bridge` role. The two differ in one important way: this module uses checked arithmetic, so an overflowing mint reverts, while the bridge performs its additions inside an `unchecked` block. + + diff --git a/website/docs/library/token/ERC20/Mint/_category_.json b/website/docs/library/token/ERC20/Mint/_category_.json new file mode 100644 index 00000000..a868bf7b --- /dev/null +++ b/website/docs/library/token/ERC20/Mint/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Mint", + "position": 6, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/Mint/index" + } +} diff --git a/website/docs/library/token/ERC20/Mint/index.mdx b/website/docs/library/token/ERC20/Mint/index.mdx new file mode 100644 index 00000000..c2a3dcc8 --- /dev/null +++ b/website/docs/library/token/ERC20/Mint/index.mdx @@ -0,0 +1,23 @@ +--- +title: "ERC-20 Mint" +sidebar_label: "Mint" +description: "Create new ERC-20 tokens from your own facet." +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Create new tokens from your own facet. Minting ships as a module only, so your facet decides who may mint. + + + + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/Permit/ERC20PermitFacet.mdx b/website/docs/library/token/ERC20/Permit/ERC20PermitFacet.mdx new file mode 100644 index 00000000..8f6928f5 --- /dev/null +++ b/website/docs/library/token/ERC20/Permit/ERC20PermitFacet.mdx @@ -0,0 +1,391 @@ +--- +sidebar_position: 100 +title: "ERC-20 Permit Facet" +description: "Approve an ERC-20 spender with an off-chain signature, following EIP-2612" +sidebar_label: "Facet" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Permit/ERC20PermitFacet.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'; + + + + +Set an allowance from a signed message instead of a transaction, following EIP-2612 + + + +- `permit` sets an allowance from an owner's signature, so the owner never has to send a transaction. +- Anyone may submit a valid signature. The allowance is still recorded for the signer. +- Each success increments the owner's nonce, so a signature works exactly once. + + +## Storage + +This facet touches three separate slots: the allowance it writes, the token name it hashes into the EIP-712 domain, and the nonce it consumes. + +### State Variables + + + +### ERC20MetadataStorage + +Only the `name` field is declared here, because the domain separator needs nothing else. + + +{`/** @custom:storage-location erc8042:erc20.metadata */ +struct ERC20MetadataStorage { + string name; +}`} + + + +This is a layout-compatible prefix of the metadata struct, not a different slot. `name` is the first field either way, so this facet reads exactly the same string that [`ERC20MetadataFacet`](/docs/library/token/ERC20/Metadata/ERC20MetadataFacet) returns from `name()`. The `symbol` and `decimals` fields are simply not declared, because permit never reads them. + + +### ERC20Storage + + +{`/** @custom:storage-location erc8042:erc20 */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +}`} + + +### NoncesStorage + + +{`/** @custom:storage-location erc8042:nonces */ +struct NoncesStorage { + mapping(address owner => uint256) nonces; +}`} + + + +The nonce slot is `keccak256("nonces")`, a bare identifier rather than something like `erc20.nonces`. Any other contract in the same diamond that picks the string `"nonces"` for its own storage will land on this slot and corrupt permit nonces. Nothing else in Compose uses it today. + + +## Functions + +### nonces + +Returns the number of permits `_owner` has already used. The next signature must be signed with this value. + + +{`function nonces(address _owner) external view returns (uint256);`} + + +**Parameters:** + + + +**Returns:** + + + +--- +### DOMAIN_SEPARATOR + +Returns the EIP-712 domain separator that signatures must be built against. + +It is computed on every call rather than cached at deployment, from four inputs: the token `name` read from metadata storage, the hardcoded version string `"1"`, the current `block.chainid`, and the diamond's own address. + + +{`function DOMAIN_SEPARATOR() external view returns (bytes32);`} + + +**Returns:** + + + +--- +### permit + +Verifies an EIP-2612 signature and sets `allowance[_owner][_spender]` to `_value`. + +The call is permissionless. Whoever relays the signature pays the gas, and the allowance is recorded for `_owner` regardless of who submitted it. The owner's nonce increases by one, and only on success. + + +{`function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s +) external;`} + + +**Parameters:** + + + +**Reverts:** + + + +## Events + + + + +
+ Emitted on every successful permit. Identical in shape to the event [`ERC20ApproveFacet`](/docs/library/token/ERC20/Approve/ERC20ApproveFacet) emits, so an allowance set by signature is indistinguishable in the logs from one set by a transaction. +
+ +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _spender, uint256 _value);`} + +
+ +
+ Parameters: + +
+ +
+
+ +## Errors + + + + +
+ Thrown for every signature failure: an expired deadline, a signer that does not match `_owner`, or a recovery that returned `address(0)`. Because one error covers all three, a caller cannot tell an expired permit from a malformed one without checking the deadline separately. +
+ +
+ Signature: + +{`error ERC2612InvalidSignature( + address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s +);`} + +
+ +
+ Parameters: + +
+ +
+ + + +
+ Thrown when `_spender` is the zero address. This check runs before the deadline and signature checks, so it fires even on an otherwise invalid permit. +
+ +
+ Signature: + +{`error ERC20InvalidSpender(address _spender);`} + +
+ +
+ Parameters: + +
+ +
+
+ +## Best Practices + +- Add [`ERC20MetadataFacet`](/docs/library/token/ERC20/Metadata/ERC20MetadataFacet) and call `setMetadata` before anyone signs a permit. With an unset name the domain is built from an empty string, and every signature becomes invalid the moment the name is filled in. +- Read `nonces(owner)` when building a signature, and `DOMAIN_SEPARATOR()` rather than reconstructing the domain by hand. +- Sign permits one at a time per owner. The nonce is a single counter, so two signatures created against the same nonce cannot both be used: whichever lands first invalidates the other. +- Pair this facet with [`ERC20TransferFacet`](/docs/library/token/ERC20/Transfer/ERC20TransferFacet), since a permit is only useful if the spender can then call `transferFrom`. + +## Security Considerations + +**Renaming the token invalidates every outstanding permit.** The domain separator hashes the `name` read from metadata storage, and it is recomputed on each call rather than cached. Calling `setMetadata` with a different name silently changes the domain, so signatures already in flight stop verifying and any integrator caching the old `DOMAIN_SEPARATOR()` starts producing rejected signatures. + +**The nonce is consumed only on success.** It is incremented after the signature check passes, so failed submissions do not burn nonces and cannot be used to grief an owner's pending signatures. + +**Anyone can submit someone else's permit.** That is the design, but it means a contract that bundles `permit` and a follow-up action in one transaction can be broken by a third party submitting the permit first: the bundled call then reverts on the now-stale nonce. Tolerate an already-consumed permit if you bundle. + +**Signature malleability is not rejected, but replay is prevented.** `ecrecover` is called directly, with no low-`s` bound and no check that `_v` is 27 or 28, so an altered but still valid encoding of a signature recovers the same signer. This does not enable replay, because the nonce has already moved on after the first use. The one failure mode `ecrecover` does have is covered: a recovery that returns `address(0)` is rejected explicitly, so a malformed signature cannot be passed off as a permit from the zero address. + +**The deadline is inclusive.** The check rejects only when `block.timestamp` is strictly greater than `_deadline`, so a permit remains usable during the block whose timestamp equals its deadline. + + diff --git a/website/docs/library/token/ERC20/Permit/ERC20PermitMod.mdx b/website/docs/library/token/ERC20/Permit/ERC20PermitMod.mdx new file mode 100644 index 00000000..256e8ef9 --- /dev/null +++ b/website/docs/library/token/ERC20/Permit/ERC20PermitMod.mdx @@ -0,0 +1,426 @@ +--- +sidebar_position: 110 +title: "ERC-20 Permit Module" +description: "EIP-2612 signature approval helpers for your own diamond facets" +sidebar_label: "Module" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Permit/ERC20PermitMod.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'; + + + + +Helper functions for verifying EIP-2612 permits and accessing permit storage + + + +- `permit` performs the full EIP-2612 flow: deadline check, signature recovery, allowance write, nonce bump. +- It emits `Approval` itself, so your facet must not emit it again. +- Exposes three storage getters, one per slot it touches. + + + +Use helper functions from Compose using your own custom facets. See Facets & Modules for more information. + + +## Storage + +This module touches three separate slots: the allowance it writes, the token name it hashes into the EIP-712 domain, and the nonce it consumes. + +### State Variables + + + +### ERC20MetadataStorage + +Only the `name` field is declared here, because the domain separator needs nothing else. + + +{`/** @custom:storage-location erc8042:erc20.metadata */ +struct ERC20MetadataStorage { + string name; +}`} + + + +This is a layout-compatible prefix of the metadata struct, not a different slot. `name` is the first field either way, so this module reads exactly the same string [`ERC20MetadataMod`](/docs/library/token/ERC20/Metadata/ERC20MetadataMod) writes. The `symbol` and `decimals` fields are simply not declared, because permit never reads them. + + +### ERC20Storage + + +{`/** @custom:storage-location erc8042:erc20 */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +}`} + + +### NoncesStorage + + +{`/** @custom:storage-location erc8042:nonces */ +struct NoncesStorage { + mapping(address owner => uint256) nonces; +}`} + + + +The nonce slot is `keccak256("nonces")`, a bare identifier rather than something like `erc20.nonces`. Any other contract in the same diamond that picks the string `"nonces"` for its own storage will land on this slot and corrupt permit nonces. Nothing else in Compose uses it today. + + +## Functions + +### getERC20MetadataStorage + +Returns a pointer to the one-field `ERC20MetadataStorage` struct, used to read the token name for the domain separator. + + +{`function getERC20MetadataStorage() pure returns (ERC20MetadataStorage storage s);`} + + +**Returns:** + + + +--- + +### getERC20Storage + +Returns a pointer to the `ERC20Storage` struct, where allowances live. + + +{`function getERC20Storage() pure returns (ERC20Storage storage s);`} + + +**Returns:** + + + +--- + +### getPermitStorage + +Returns a pointer to the `NoncesStorage` struct. + +Read an owner's nonce with `getPermitStorage().nonces[_owner]`. This module has no `nonces()` view of its own, unlike [`ERC20PermitFacet`](/docs/library/token/ERC20/Permit/ERC20PermitFacet). + + +{`function getPermitStorage() pure returns (NoncesStorage storage s);`} + + +**Returns:** + + + +--- + +### DOMAIN_SEPARATOR + +Returns the EIP-712 domain separator that signatures must be built against. + +It is computed on every call rather than cached, from the token `name` in metadata storage, the hardcoded version string `"1"`, the current `block.chainid`, and the address of the contract executing the code. + + +{`function DOMAIN_SEPARATOR() view returns (bytes32);`} + + +**Returns:** + + + + +A file-level free function has no `this`, so the module cannot write `address(this)` the way the facet does. It reads the executing address with a one-line `assembly` block instead. Under `delegatecall` both forms resolve to the same value, the diamond's address, so the module and the facet produce identical domain separators. + + +--- + +### permit + +Verifies an EIP-2612 signature and sets `allowance[_owner][_spender]` to `_value`, then increments the owner's nonce and emits `Approval`. + +The whole flow lives here. Your facet supplies the external entrypoint and nothing else, and it must not emit `Approval` a second time. + + +{`function permit(address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s);`} + + +**Parameters:** + + + +**Reverts:** + + + +## Events + + + +
+ Emitted by this module on every successful permit. Your facet should not emit it again, or the allowance change will appear twice in the logs. +
+ +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _spender, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+
+ +## Errors + + + +
+ Thrown for every signature failure: an expired deadline, a signer that does not match `_owner`, or a recovery that returned `address(0)`. One error covers all three, so a caller cannot tell them apart without checking the deadline separately. +
+ +
+ Signature: + +{`error ERC2612InvalidSignature( + address _owner, address _spender, uint256 _value, uint256 _deadline, uint8 _v, bytes32 _r, bytes32 _s +);`} + +
+ +
+ Parameters: + +
+
+ + +
+ Thrown when `_spender` is the zero address. This check runs before the deadline and signature checks. +
+ +
+ Signature: + +{`error ERC20InvalidSpender(address _spender);`} + +
+
+
+ +## Best Practices + +- Set the token name with [`ERC20MetadataMod.setMetadata`](/docs/library/token/ERC20/Metadata/ERC20MetadataMod#setmetadata) during deployment, before anyone signs a permit. Changing the name later invalidates every signature already in circulation. +- Expose `DOMAIN_SEPARATOR` and a nonce getter from your facet as well. Wallets and signing libraries expect to read both on-chain, and EIP-2612 requires a `nonces(address)` view that this module does not provide. +- Do not emit `Approval` from your wrapper. This module already emits it. +- If you only need the standard EIP-2612 surface, use [`ERC20PermitFacet`](/docs/library/token/ERC20/Permit/ERC20PermitFacet) instead. It exports `nonces`, `DOMAIN_SEPARATOR` and `permit` ready to install. + +## Integration Notes + +Import the module under a namespace and call it from your facet: + + +{`import "src/token/ERC20/Permit/ERC20PermitMod.sol" as ERC20PermitMod; + +contract MyFacet { + function permit( + address _owner, + address _spender, + uint256 _value, + uint256 _deadline, + uint8 _v, + bytes32 _r, + bytes32 _s + ) external { + ERC20PermitMod.permit(_owner, _spender, _value, _deadline, _v, _r, _s); + } + + function nonces(address _owner) external view returns (uint256) { + return ERC20PermitMod.getPermitStorage().nonces[_owner]; + } +}`} + + +The allowance is written to `ERC20Storage` at `keccak256("erc20")`, the same slot [`ERC20DataFacet`](/docs/library/token/ERC20/Data/ERC20DataFacet) reads and [`ERC20TransferFacet`](/docs/library/token/ERC20/Transfer/ERC20TransferFacet) spends, so a permit and a plain `approve` are interchangeable from every other contract's point of view. + +This module writes the allowance directly rather than calling [`ERC20ApproveMod`](/docs/library/token/ERC20/Approve/ERC20ApproveMod). It has to: that helper always acts for `msg.sender`, and permit must act for the signer instead. + + diff --git a/website/docs/library/token/ERC20/Permit/_category_.json b/website/docs/library/token/ERC20/Permit/_category_.json new file mode 100644 index 00000000..a480187b --- /dev/null +++ b/website/docs/library/token/ERC20/Permit/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Permit", + "position": 7, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/Permit/index" + } +} diff --git a/website/docs/library/token/ERC20/Permit/index.mdx b/website/docs/library/token/ERC20/Permit/index.mdx new file mode 100644 index 00000000..741b27b2 --- /dev/null +++ b/website/docs/library/token/ERC20/Permit/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-20 Permit" +sidebar_label: "Permit" +description: "ERC-20 approvals signed off-chain with EIP-2612." +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Approve a spender with an EIP-2612 signature instead of a transaction + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/Transfer/ERC20TransferFacet.mdx b/website/docs/library/token/ERC20/Transfer/ERC20TransferFacet.mdx new file mode 100644 index 00000000..94b4a153 --- /dev/null +++ b/website/docs/library/token/ERC20/Transfer/ERC20TransferFacet.mdx @@ -0,0 +1,404 @@ +--- +sidebar_position: 100 +title: "ERC-20 Transfer Facet" +description: "Move ERC-20 tokens directly or by spending an allowance" +sidebar_label: "Facet" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Transfer/ERC20TransferFacet.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'; + + + + +Move tokens between accounts, directly or by spending an allowance + + + +- `transfer` moves the caller's own tokens. `transferFrom` moves someone else's using an allowance. +- Both write balances in `ERC20Storage` at `erc8042:erc20`. +- An allowance of `type(uint256).max` is treated as unlimited and is never decremented. + + +## Storage + +### State Variables + + + +### ERC20Storage + + +{`/** @custom:storage-location erc8042:erc20 */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +}`} + + +## Functions + +### transfer + +Moves `_value` tokens from the caller to `_to`. + +Transferring `0` is allowed and still emits `Transfer`. Sending to `address(0)` is rejected, so use [`ERC20BurnFacet`](/docs/library/token/ERC20/Burn/ERC20BurnFacet) to destroy tokens. + + +{`function transfer(address _to, uint256 _value) external returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +**Reverts:** + + + +--- +### transferFrom + +Moves `_value` tokens from `_from` to `_to`, spending the allowance that `_from` granted to the caller. + +The allowance is checked **before** the balance. If both are insufficient, the call reverts with `ERC20InsufficientAllowance`. When the allowance is exactly `type(uint256).max` it is left untouched, otherwise it is reduced by `_value`. + + +{`function transferFrom(address _from, address _to, uint256 _value) external returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +**Reverts:** + + + +## Events + + + + +
+ Emitted by both `transfer` and `transferFrom`, including when `_value` is `0`. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 _value);`} + +
+ +
+ Parameters: + +
+ +
+
+ +## Errors + + + + +
+ Thrown when the account sending tokens holds less than `_value`. +
+ +
+ Signature: + +{`error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed);`} + +
+ +
+ Parameters: + +
+ +
+ + + +
+ Thrown by `transferFrom` when `_from` is the zero address. +
+ +
+ Signature: + +{`error ERC20InvalidSender(address _sender);`} + +
+ +
+ Parameters: + +
+ +
+ + + +
+ Thrown by `transfer` and `transferFrom` when `_to` is the zero address. +
+ +
+ Signature: + +{`error ERC20InvalidReceiver(address _receiver);`} + +
+ +
+ Parameters: + +
+ +
+ + + +
+ Thrown by `transferFrom` when the caller's allowance over `_from` is below `_value`. +
+ +
+ Signature: + +{`error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed);`} + +
+ +
+ Parameters: + +
+ +
+ + + +
+ Declared here for shared use across the ERC-20 contracts. This facet does not revert with it. [`ERC20ApproveFacet`](/docs/library/token/ERC20/Approve/ERC20ApproveFacet) throws it when approving the zero address. +
+ +
+ Signature: + +{`error ERC20InvalidSpender(address _spender);`} + +
+ +
+
+ +## Best Practices + +- Add [`ERC20DataFacet`](/docs/library/token/ERC20/Data/ERC20DataFacet) alongside this facet so holders can read balances and allowances. +- `transferFrom` needs an allowance first, from [`ERC20ApproveFacet`](/docs/library/token/ERC20/Approve/ERC20ApproveFacet) or [`ERC20PermitFacet`](/docs/library/token/ERC20/Permit/ERC20PermitFacet). +- Check the return value or rely on the revert. This facet never returns `false`, but tokens from other implementations might. +- Do not treat a successful `transfer` as proof the recipient noticed it. There is no callback. + +## Security Considerations + +**Balance arithmetic is safe.** Every subtraction sits inside `unchecked`, but only after an explicit comparison has proved the balance or allowance is large enough, so it cannot underflow. + +**Spending an allowance emits no `Approval` event.** `transferFrom` lowers the stored allowance and emits only `Transfer`. Across all of Compose, `Approval` is emitted solely by the Approve and Permit contracts, so an indexer that tracks allowances from events alone will drift. Read `allowance()` for the current value. + +**There are no external calls and no receiver hooks.** Neither function calls into `_to`, so there is no reentrancy surface here, and no ERC-721 style check that the recipient can handle tokens. Tokens sent to a contract that cannot move them are stuck permanently. + + diff --git a/website/docs/library/token/ERC20/Transfer/ERC20TransferMod.mdx b/website/docs/library/token/ERC20/Transfer/ERC20TransferMod.mdx new file mode 100644 index 00000000..78de4c5c --- /dev/null +++ b/website/docs/library/token/ERC20/Transfer/ERC20TransferMod.mdx @@ -0,0 +1,443 @@ +--- +sidebar_position: 110 +title: "ERC-20 Transfer Module" +description: "ERC-20 transfer helpers for your own diamond facets" +sidebar_label: "Module" +gitSource: "https://github.com/Perfect-Abstractions/Compose/tree/main/src/token/ERC20/Transfer/ERC20TransferMod.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'; + + + + +Helper functions for moving ERC-20 tokens and accessing ERC-20 storage + + + +- `transfer` moves `msg.sender`'s own tokens. It cannot move tokens for any other account. +- `transferFrom` spends the allowance that `_from` granted to `msg.sender`. +- Neither helper performs a role check, and none is needed because both are bound to `msg.sender`. + + + +Use helper functions from Compose using your own custom facets. See Facets & Modules for more information. + + +## Storage + +### State Variables + + + +### ERC20Storage + + +{`/** @custom:storage-location erc8042:erc20 */ +struct ERC20Storage { + mapping(address owner => uint256 balance) balanceOf; + uint256 totalSupply; + mapping(address owner => mapping(address spender => uint256 allowance)) allowance; +}`} + + +## Functions + +### getStorage + +Returns a pointer to the `ERC20Storage` struct. + + +{`function getStorage() pure returns (ERC20Storage storage s);`} + + +**Returns:** + + + +--- + +### transfer + +Moves `_value` tokens from the caller to `_to`. + +Inside a diamond, your facet runs through `delegatecall`, so `msg.sender` here is the account that called the diamond, not the diamond itself. Transferring `0` is allowed and still emits `Transfer`. + + +{`function transfer(address _to, uint256 _value) returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +**Reverts:** + + + +--- + +### transferFrom + +Moves `_value` tokens from `_from` to `_to`, spending the allowance that `_from` granted to `msg.sender`. + +The allowance is checked **before** the balance. If both are insufficient, the call reverts with `ERC20InsufficientAllowance`. When the allowance is exactly `type(uint256).max` it is left untouched, otherwise it is reduced by `_value`. + + +{`function transferFrom(address _from, address _to, uint256 _value) returns (bool);`} + + +**Parameters:** + + + +**Returns:** + + + +**Reverts:** + + + +## Events + + + +
+ Emitted by both `transfer` and `transferFrom`, including when `_value` is `0`. +
+ +
+ Signature: + +{`event Transfer(address indexed _from, address indexed _to, uint256 _value);`} + +
+ +
+ Parameters: + +
+
+ + +
+ Declared here for shared use across the ERC-20 contracts. This module never emits it, not even when `transferFrom` lowers an allowance. [`ERC20ApproveMod`](/docs/library/token/ERC20/Approve/ERC20ApproveMod) emits it when an allowance is set. +
+ +
+ Signature: + +{`event Approval(address indexed _owner, address indexed _spender, uint256 _value);`} + +
+
+
+ +## Errors + + + +
+ Thrown when the account sending tokens holds less than `_value`. +
+ +
+ Signature: + +{`error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed);`} + +
+ +
+ Parameters: + +
+
+ + +
+ Thrown by `transferFrom` when `_from` is the zero address. +
+ +
+ Signature: + +{`error ERC20InvalidSender(address _sender);`} + +
+ +
+ Parameters: + +
+
+ + +
+ Thrown by `transfer` and `transferFrom` when `_to` is the zero address. +
+ +
+ Signature: + +{`error ERC20InvalidReceiver(address _receiver);`} + +
+ +
+ Parameters: + +
+
+ + +
+ Thrown by `transferFrom` when `msg.sender`'s allowance over `_from` is below `_value`. +
+ +
+ Signature: + +{`error ERC20InsufficientAllowance(address _spender, uint256 _allowance, uint256 _needed);`} + +
+ +
+ Parameters: + +
+
+ + +
+ Declared here for shared use across the ERC-20 contracts. This module does not revert with it. [`ERC20ApproveMod`](/docs/library/token/ERC20/Approve/ERC20ApproveMod) throws it when approving the zero address. +
+ +
+ Signature: + +{`error ERC20InvalidSpender(address _spender);`} + +
+
+
+ +## Best Practices + +- Use `transfer` when your facet moves tokens on behalf of whoever called it, for example a wrapper that transfers and then performs a second action in one transaction. +- To move tokens for an account that is **not** `msg.sender`, these helpers will not do it. Write `getStorage().balanceOf` directly and emit `Transfer` yourself, after your own authorization check. +- Keep the same `STORAGE_POSITION` and `ERC20Storage` layout as the other ERC-20 modules. Do not introduce a second balance mapping. +- Do not assume the recipient reacted to the transfer. Neither helper calls into `_to`. + +## Integration Notes + +Import the module under a namespace and call it from your facet: + + +{`import "src/token/ERC20/Transfer/ERC20TransferMod.sol" as ERC20TransferMod; + +contract MyFacet { + function transferTokens(address _to, uint256 _value) external returns (bool) { + return ERC20TransferMod.transfer(_to, _value); + } +}`} + + +`ERC20Storage` lives at `keccak256("erc20")` inside the diamond. That is the same slot [`ERC20DataFacet`](/docs/library/token/ERC20/Data/ERC20DataFacet), [`ERC20TransferFacet`](/docs/library/token/ERC20/Transfer/ERC20TransferFacet), and the other ERC-20 contracts use, so balances written here are what `balanceOf()` reports. + +Every balance subtraction sits inside `unchecked`, but only after an explicit comparison has proved the balance or allowance is large enough, so it cannot underflow. Keep that ordering if you adapt the code. + + diff --git a/website/docs/library/token/ERC20/Transfer/_category_.json b/website/docs/library/token/ERC20/Transfer/_category_.json new file mode 100644 index 00000000..1dc05e99 --- /dev/null +++ b/website/docs/library/token/ERC20/Transfer/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Transfer", + "position": 4, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/Transfer/index" + } +} diff --git a/website/docs/library/token/ERC20/Transfer/index.mdx b/website/docs/library/token/ERC20/Transfer/index.mdx new file mode 100644 index 00000000..14acf649 --- /dev/null +++ b/website/docs/library/token/ERC20/Transfer/index.mdx @@ -0,0 +1,30 @@ +--- +title: "ERC-20 Transfer" +sidebar_label: "Transfer" +description: "Move ERC-20 tokens directly or through an allowance." +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + + + Move tokens between accounts, directly or by spending an allowance + + + + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/ERC20/_category_.json b/website/docs/library/token/ERC20/_category_.json new file mode 100644 index 00000000..0e078cb1 --- /dev/null +++ b/website/docs/library/token/ERC20/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "ERC-20", + "position": 1, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/ERC20/index" + } +} diff --git a/website/docs/library/token/ERC20/index.mdx b/website/docs/library/token/ERC20/index.mdx new file mode 100644 index 00000000..1f71bc25 --- /dev/null +++ b/website/docs/library/token/ERC20/index.mdx @@ -0,0 +1,74 @@ +--- +title: "ERC-20 — Fungible Token Standard" +sidebar_label: "ERC-20" +description: "ERC-20 fungible token implementation, split into composable facets and modules." +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + +# ERC-20: Fungible Tokens + + + Fungible token functionality, split into granular facets you add only as needed + + + + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + } + size="medium" + /> + diff --git a/website/docs/library/token/_category_.json b/website/docs/library/token/_category_.json new file mode 100644 index 00000000..79ed7f59 --- /dev/null +++ b/website/docs/library/token/_category_.json @@ -0,0 +1,10 @@ +{ + "label": "Tokens", + "position": 3, + "collapsible": true, + "collapsed": true, + "link": { + "type": "doc", + "id": "library/token/index" + } +} diff --git a/website/docs/library/token/index.mdx b/website/docs/library/token/index.mdx new file mode 100644 index 00000000..1a7db416 --- /dev/null +++ b/website/docs/library/token/index.mdx @@ -0,0 +1,25 @@ +--- +title: "Tokens — Token Standards for Diamonds" +sidebar_label: "Tokens" +description: "Token standard implementations for Compose diamonds, composed as granular facets and modules." +--- + +import DocCard, { DocCardGrid } from '@site/src/components/docs/DocCard'; +import DocSubtitle from '@site/src/components/docs/DocSubtitle'; +import Icon from '@site/src/components/ui/Icon'; + +# Token Standards + + + Add fungible, non-fungible, and multi-token functionality to your diamond + + + + } + size="medium" + /> +