Skip to main content

ERC-20 Burn Module

@perfect-abstractions/compose/token/ERC20/Burn/ERC20BurnMod.sol

Helper function for destroying ERC-20 tokens from any account

No Authorization Check

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.

Key Features
  • 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.
Module Usage

Use helper functions from Compose using your own custom facets. See Facets & Modules for more information.

Storage

State Variables

PropertyTypeDescriptionSTORAGE_POSITIONbytes32ERC-20 storage position within the diamond (Value: keccak256("erc20"))

ERC20Storage

This module declares only the two fields it uses. The allowance mapping that other ERC-20 contracts declare is absent.

Definition
/** @custom:storage-location erc8042:erc20 */
struct ERC20Storage {
mapping(address owner => uint256 balance) balanceOf;
uint256 totalSupply;
}
Why the struct is shorter here

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 and the two Bridgeable contracts use the same two-field form. Everything that reads or writes an allowance, including 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 or ERC20ApproveMod.

function getStorage() pure returns (ERC20Storage storage s);

Returns:

PropertyTypeDescriptionsERC20Storage storageThe struct in storage, with balanceOf and totalSupply only.

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:

PropertyTypeDescription_accountaddressThe address whose tokens are destroyed. Cannot be address(0)._valueuint256The number of tokens to destroy. Must not exceed _account's balance.

Reverts:

PropertyTypeDescriptionERC20InvalidSendererror_account is address(0).ERC20InsufficientBalanceerror_account holds fewer than _value tokens.

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:
PropertyTypeDescription_fromaddressThe account the tokens were destroyed from. Always _account._toaddressAlways address(0)._valueuint256The number of tokens destroyed.

Errors

Thrown when _account holds less than _value.

Signature:
error ERC20InsufficientBalance(address _sender, uint256 _balance, uint256 _needed);
Parameters:
PropertyTypeDescription_senderaddressThe account whose tokens would have been destroyed._balanceuint256That account's current balance._neededuint256The amount the call required.

Best Practices

Integration Notes

Import the module under a namespace and call it from your facet:

Calling burn from a guarded 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 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.

Last updated:

Newsletter

Get notified about releases, feature announcements, and technical deep-dives on building smart contracts with Compose.

No spam. Unsubscribe anytime.