Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
3 changes: 3 additions & 0 deletions contracts/interfaces/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ are useful to interact with third party contracts that implement them.
- {IERC6909ContentURI}
- {IERC6909Metadata}
- {IERC6909TokenSupply}
- {IERC7246}
- {IERC7579Module}
- {IERC7579Validator}
- {IERC7579Hook}
Expand Down Expand Up @@ -113,6 +114,8 @@ are useful to interact with third party contracts that implement them.

{{IERC6909TokenSupply}}

{{IERC7246}}

{{IERC7579Module}}

{{IERC7579Validator}}
Expand Down
63 changes: 63 additions & 0 deletions contracts/interfaces/draft-IERC7246.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.2;

import {IERC20} from "./IERC20.sol";

interface IERC7246 is IERC20 {
/// @dev Emitted when `amount` tokens are encumbered from `owner` to `spender`.
event Encumber(address indexed owner, address indexed spender, uint256 amount);

/// @dev Emitted when the encumbrance of a `spender` to an `owner` is reduced by `amount`.
event Release(address indexed owner, address indexed spender, uint256 amount);

/**
* @dev Returns the total amount of tokens owned by `owner` that are currently encumbered.
*
* - MUST never exceed `balanceOf(owner)`
* - Any function which would reduce `balanceOf(owner)` below `encumberedBalanceOf(owner)` MUST revert
*/
function encumberedBalanceOf(address owner) external view returns (uint256);

/**
* @dev Convenience function for reading the unencumbered balance of an address.
* Trivially implemented as `balanceOf(owner) - encumberedBalanceOf(owner)`
*/
function availableBalanceOf(address owner) external view returns (uint256);

/**
* @dev Returns the number of tokens that `owner` has encumbered to `spender`.
*
* - This value increases when {encumber} or {encumberFrom} are called by the `owner` or by another permitted account.
* - This value decreases when {release} or {transferFrom} are called by `spender`.
*/
function encumbrances(address owner, address spender) external view returns (uint256);

/**
* @dev Increases the amount of tokens that the caller has encumbered to `spender` by `amount`.
* Grants `spender` a guaranteed right to transfer `amount` from the caller's by using `transferFrom`.
*
* - MUST revert if caller does not have `amount` tokens available
* (e.g. if `balanceOf(caller) - encumbrances(caller) < amount`).
* - Emits an {IERC7246-Encumber} event.
*/
function encumber(address spender, uint256 amount) external;

/**
* @dev Increases the amount of tokens that `owner` has encumbered to `spender` by `amount`.
* Grants `spender` a guaranteed right to transfer `amount` from `owner` using transferFrom.
*
* - The function SHOULD revert unless the owner account has deliberately authorized the sender of the message via some mechanism.
* - MUST revert if `owner` does not have `amount` tokens available
* (e.g. if `balanceOf(owner) - encumbrances(owner) < amount`).
* - Emits an {IERC7246-Encumber} event.
*/
function encumberFrom(address owner, address spender, uint256 amount) external;

/**
* @dev Reduces amount of tokens encumbered from `owner` to caller by `amount`
*
* - Emits a {IERC7246-Release} event.
*/
function release(address owner, uint256 amount) external;
}
8 changes: 4 additions & 4 deletions contracts/token/ERC20/ERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
* This internal function is equivalent to {transfer}, and can be used to
* e.g. implement automatic token fees, slashing mechanisms, etc.
*
* Emits a {Transfer} event.
* Emits a {Transfer-address-address-uint256} event.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
Expand All @@ -171,7 +171,7 @@ abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
* (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding
* this function.
*
* Emits a {Transfer} event.
* Emits a {IERC20-Transfer} event.
*/
function _update(address from, address to, uint256 value) internal virtual {
if (from == address(0)) {
Expand Down Expand Up @@ -207,7 +207,7 @@ abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
* @dev Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0).
* Relies on the `_update` mechanism
*
* Emits a {Transfer} event with `from` set to the zero address.
* Emits a {IERC20-Transfer} event with `from` set to the zero address.
*
* NOTE: This function is not virtual, {_update} should be overridden instead.
*/
Expand Down Expand Up @@ -239,7 +239,7 @@ abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors {
* This internal function is equivalent to `approve`, and can be used to
* e.g. set automatic allowances for certain subsystems, etc.
*
* Emits an {Approval} event.
* Emits an {IERC20-Approval} event.
*
* Requirements:
*
Expand Down
6 changes: 3 additions & 3 deletions contracts/token/ERC20/IERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ interface IERC20 {
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
* Emits a {IERC20-Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);

Expand All @@ -62,7 +62,7 @@ interface IERC20 {
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
* Emits an {IERC20-Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);

Expand All @@ -73,7 +73,7 @@ interface IERC20 {
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
* Emits a {IERC20-Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
3 changes: 3 additions & 0 deletions contracts/token/ERC20/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Additionally there are multiple custom extensions, including:
* {ERC20TemporaryApproval}: support for approvals lasting for only one transaction, as defined in ERC-7674.
* {ERC1363}: support for calling the target of a transfer or approval, enabling code execution on the receiver within a single transaction.
* {ERC4626}: tokenized vault that manages shares (represented as ERC-20) that are backed by assets (another ERC-20).
* {ERC7246}

Finally, there are some utilities to interact with ERC-20 contracts in various ways:

Expand Down Expand Up @@ -74,6 +75,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel

{{ERC4626}}

{{ERC7246}}

== Utilities

{{SafeERC20}}
Expand Down
128 changes: 128 additions & 0 deletions contracts/token/ERC20/extensions/draft-ERC7246.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.26;

import {ERC20} from "../ERC20.sol";
import {IERC7246} from "../../../interfaces/draft-IERC7246.sol";
import {Math} from "../../../utils/math/Math.sol";

/**
* @title ERC7246
* @dev An extension of {ERC20} that adds support for encumbrances of token balances. Encumbrances are a
* stronger version of allowances: they grant the `spender` an exclusive right to transfer tokens from the
* `owner`'s balance without reducing the `owner`'s balance until the tokens are transferred or the
* encumbrance is released.
*/
abstract contract ERC7246 is ERC20, IERC7246 {
/// @dev Thrown when the result of an {_update} or {_encumber} call would result in negative {availableBalanceOf}.
error ERC7246InsufficientAvailableBalance(uint256 available, uint256 required);

/// @dev Thrown when an account tries to release more encumbered tokens than it has.
error ERC7246InsufficientEncumbrance(uint256 encumbered, uint256 required);

/// @dev Thrown when an account tries to encumber tokens to itself.
error ERC7246SelfEncumbrance();

mapping(address owner => mapping(address spender => uint256)) private _encumbrances;
mapping(address owner => uint256) private _encumberedBalances;

/// @inheritdoc IERC7246
function encumberedBalanceOf(address owner) public view virtual returns (uint256) {
return _encumberedBalances[owner];
}

/// @inheritdoc IERC7246
function availableBalanceOf(address owner) public view virtual returns (uint256) {
return balanceOf(owner) - encumberedBalanceOf(owner);
}

/// @inheritdoc IERC7246
function encumbrances(address owner, address spender) public view virtual returns (uint256) {
return _encumbrances[owner][spender];
}

/// @inheritdoc IERC7246
function encumber(address spender, uint256 amount) public virtual {
_encumber(msg.sender, spender, amount);
}

/// @inheritdoc IERC7246
function encumberFrom(address owner, address spender, uint256 amount) public virtual {
_spendAllowance(owner, msg.sender, amount);
_encumber(owner, spender, amount);
}

/// @inheritdoc IERC7246
function release(address owner, uint256 amount) public virtual {
_releaseEncumbrance(owner, msg.sender, amount);
}

/**
* @dev Encumber `amount` of tokens from `owner` to `spender`. Encumbering tokens grants an exclusive right
* to transfer the tokens without removing them from `owner`'s balance. Release the tokens by calling
* {release} or transfer them by calling {transferFrom}.
*/
function _encumber(address owner, address spender, uint256 amount) internal virtual {
require(owner != spender, ERC7246SelfEncumbrance());
uint256 availableBalance = availableBalanceOf(owner);
require(availableBalance >= amount, ERC7246InsufficientAvailableBalance(availableBalance, amount));

// Given that the `availableBalanceOf` is `balanceOf(owner) - encumberedBalanceOf(owner)`,
// we know that the new `_encumberedBalances[owner] <= balanceOf(owner)` and thus no overflow is possible.
// `_encumberedBalances[owner] >= _encumbrances[owner][spender]`, so no overflow is possible there either.
unchecked {
_encumbrances[owner][spender] += amount;
_encumberedBalances[owner] += amount;
}

emit Encumber(owner, spender, amount);
}

/**
* @dev Release `amount` of encumbered tokens from `owner` to `spender`.
*
* - Will revert if there are insufficient encumbered tokens.
* - Emits the {IERC7246-Release} event.
*/
function _releaseEncumbrance(address owner, address spender, uint256 amount) internal virtual {
uint256 encumbered = encumbrances(owner, spender);
require(encumbered >= amount, ERC7246InsufficientEncumbrance(encumbered, amount));

unchecked {
_encumbrances[owner][spender] -= amount;
_encumberedBalances[owner] -= amount;
}

emit Release(owner, spender, amount);
}

/**
* @dev See {ERC20-_spendAllowance}. Encumbrances are consumed first, then the remaining amount
* is passed to `super._spendAllowance`.
*/
function _spendAllowance(address owner, address spender, uint256 amount) internal virtual override {
uint256 amountEncumbered = encumbrances(owner, spender);
uint256 remainingAllowance = amount;

if (amountEncumbered != 0) {
uint256 encumberedToUse = Math.min(amount, amountEncumbered);
_releaseEncumbrance(owner, spender, encumberedToUse);
unchecked {
remainingAllowance -= encumberedToUse;
}
}

super._spendAllowance(owner, spender, remainingAllowance);
}

/// @dev See {ERC20-_update}. Ensures that `from` has sufficient {availableBalanceOf} to cover the `amount` being transferred.
function _update(address from, address to, uint256 amount) internal virtual override {
// TODO: Open question: should we keep the same revert message for normal insufficient balance? If so call super first.
// Would require some changes in the calculations to work properly (update changes balance)
if (from != address(0)) {
uint256 availableBalance = availableBalanceOf(from);
require(availableBalance >= amount, ERC7246InsufficientAvailableBalance(availableBalance, amount));
}
super._update(from, to, amount);
}
}
Loading
Loading