From bde331ac79f858f764e32cc0fe8c65d4b30468bf Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:35:24 +0200 Subject: [PATCH 01/12] progress --- contracts/interfaces/README.adoc | 2 + contracts/interfaces/draft-IERC7246.sol | 65 +++++++++++++ contracts/token/ERC20/ERC20.sol | 4 +- contracts/token/ERC20/README.adoc | 3 + .../token/ERC20/extensions/draft-ERC7246.sol | 93 +++++++++++++++++++ docs/templates/contract.hbs | 4 + docs/templates/properties.js | 19 +++- package-lock.json | 28 ++---- package.json | 2 +- 9 files changed, 196 insertions(+), 24 deletions(-) create mode 100644 contracts/interfaces/draft-IERC7246.sol create mode 100644 contracts/token/ERC20/extensions/draft-ERC7246.sol diff --git a/contracts/interfaces/README.adoc b/contracts/interfaces/README.adoc index 3dda81e3e82..c66bde3e3b8 100644 --- a/contracts/interfaces/README.adoc +++ b/contracts/interfaces/README.adoc @@ -136,3 +136,5 @@ are useful to interact with third party contracts that implement them. {{IERC7802}} {{IERC7913SignatureVerifier}} + +{{IERC7246}} diff --git a/contracts/interfaces/draft-IERC7246.sol b/contracts/interfaces/draft-IERC7246.sol new file mode 100644 index 00000000000..b7b5be685ea --- /dev/null +++ b/contracts/interfaces/draft-IERC7246.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +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 {Encumber-address-address-uint256} 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 {Encumber-address-address-uint256} event. + */ + function encumberFrom(address owner, address spender, uint256 amount) external; + + /** + * @dev Reduces amount of tokens encumbered from `owner` to caller by `amount` + * + * Emits a {Release-address-address-uint256} event. + */ + function release(address owner, uint256 amount) external; +} diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index 4d9d6b6d1c1..bbc269fecd6 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -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. */ @@ -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 {Transfer-address-address-uint256} event. */ function _update(address from, address to, uint256 value) internal virtual { if (from == address(0)) { diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index 8f8b7a438d9..8ba7aaa4867 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -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: @@ -74,6 +75,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC4626}} +{{ERC7246}} + == Utilities {{SafeERC20}} diff --git a/contracts/token/ERC20/extensions/draft-ERC7246.sol b/contracts/token/ERC20/extensions/draft-ERC7246.sol new file mode 100644 index 00000000000..5ad80e4780c --- /dev/null +++ b/contracts/token/ERC20/extensions/draft-ERC7246.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC20} from "../ERC20.sol"; +import {IERC7246} from "../../../interfaces/draft-IERC7246.sol"; +import {Math} from "../../../utils/math/Math.sol"; + +abstract contract ERC7246 is ERC20, IERC7246 { + error ERC7246InsufficientAvailableBalance(uint256 available, uint256 required); + error ERC7246InsufficientEncumbrance(uint256 encumbered, uint256 required); + + mapping(address owner => mapping(address spender => uint256)) private _encumbrances; + mapping(address owner => uint256) private _encumberedBalances; + + /// @inheritdoc IERC7246 + function encumberedBalanceOf(address owner) public view returns (uint256) { + return _encumberedBalances[owner]; + } + + /// @inheritdoc IERC7246 + function availableBalanceOf(address owner) public view returns (uint256) { + return balanceOf(owner) - encumberedBalanceOf(owner); + } + + /// @inheritdoc IERC7246 + function encumbrances(address owner, address spender) public view returns (uint256) { + return _encumbrances[owner][spender]; + } + + /// @inheritdoc IERC7246 + function encumber(address spender, uint256 amount) public { + _encumber(msg.sender, spender, amount); + } + + /// @inheritdoc IERC7246 + function encumberFrom(address owner, address spender, uint256 amount) public { + _spendAllowance(owner, msg.sender, amount); + _encumber(owner, spender, amount); + } + + /// @inheritdoc IERC7246 + function release(address owner, uint256 amount) public { + uint256 encumbered = encumbrances(owner, msg.sender); + require(encumbered >= amount, ERC7246InsufficientEncumbrance(encumbered, amount)); + + unchecked { + _encumbrances[owner][msg.sender] -= amount; + _encumberedBalances[owner] -= amount; + } + + emit Release(owner, msg.sender, amount); + } + + function _encumber(address owner, address spender, uint256 amount) internal virtual { + 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); + } + + /// @inheritdoc ERC20 + 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); + unchecked { + _encumbrances[owner][spender] -= encumberedToUse; + _encumberedBalances[owner] -= encumberedToUse; + remainingAllowance -= encumberedToUse; + } + } + + super._spendAllowance(owner, spender, remainingAllowance); + } + + /// @inheritdoc ERC20 + function _update(address from, address to, uint256 amount) internal virtual override { + uint256 availableBalance = availableBalanceOf(from); + require(availableBalance >= amount, ERC7246InsufficientAvailableBalance(availableBalance, amount)); + super._update(from, to, amount); + } +} diff --git a/docs/templates/contract.hbs b/docs/templates/contract.hbs index 458b511b050..fb772219858 100644 --- a/docs/templates/contract.hbs +++ b/docs/templates/contract.hbs @@ -6,6 +6,10 @@ :{{fullname}}: pass:normal[xref:#{{anchor}}[`++{{name}}++`]] {{/each}} +{{#each events}} +:{{fullname}}: pass:normal[xref:#{{anchor}}[`++{{name}}++`]] +{{/each}} + [.contract] [[{{anchor}}]] === `++{{name}}++` link:https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v{{oz-version}}/{{__item_context.file.absolutePath}}[{github-icon},role=heading-link] diff --git a/docs/templates/properties.js b/docs/templates/properties.js index 5a6d18eced9..f54d01cfb97 100644 --- a/docs/templates/properties.js +++ b/docs/templates/properties.js @@ -20,16 +20,31 @@ module.exports.anchor = function anchor({ item, contract }) { module.exports.fullname = function fullname({ item }) { let res = ''; res += item.name; + + if(item.nodeType == "EventDefinition") { + console.log("EVENT DEF"); + } if ('parameters' in item) { const signature = item.parameters.parameters.map(v => v.typeName.typeDescriptions.typeString).join(','); res += slug('(' + signature + ')'); + } + if(item.nodeType == "EventDefinition") { + console.log("EVENT DEF2"); } if (isNodeType('VariableDeclaration', item)) { res += '-' + slug(item.typeName.typeDescriptions.typeString); + } + if(item.nodeType == "EventDefinition") { + console.log("EVENT DEF3"); + console.log(res); } if (res.charAt(res.length - 1) === '-') { return res.slice(0, -1); } + if(item.nodeType == "EventDefinition") { + console.log("EVENT DEF4"); + console.log(res); + } return res; }; @@ -65,8 +80,8 @@ module.exports['has-internal-variables'] = function ({ item }) { module.exports.functions = function ({ item }) { return [ - ...findAll('FunctionDefinition', item).filter(f => f.visibility !== 'private'), - ...findAll('VariableDeclaration', item).filter(f => f.visibility === 'public'), + ...[...findAll('FunctionDefinition', item)].filter(f => f.visibility !== 'private'), + ...[...findAll('VariableDeclaration', item)].filter(f => f.visibility === 'public'), ]; }; diff --git a/package-lock.json b/package-lock.json index 73099e6e7a1..95384095446 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", "@nomicfoundation/hardhat-ethers": "^3.0.9", "@nomicfoundation/hardhat-network-helpers": "^1.0.13", - "@openzeppelin/docs-utils": "^0.1.5", + "@openzeppelin/docs-utils": "^0.1.6", "@openzeppelin/merkle-tree": "^1.0.7", "@openzeppelin/upgrade-safe-transpiler": "^0.4.1", "@openzeppelin/upgrades-core": "^1.20.6", @@ -1412,14 +1412,14 @@ } }, "node_modules/@frangio/servbot": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@frangio/servbot/-/servbot-0.2.5.tgz", - "integrity": "sha512-ogja4iAPZ1VwM5MU3C1ZhB88358F0PGbmSTGOkIZwOyLaDoMHIqOVCnavHjR7DV5h+oAI4Z4KDqlam3myQUrmg==", + "version": "0.3.0-1", + "resolved": "https://registry.npmjs.org/@frangio/servbot/-/servbot-0.3.0-1.tgz", + "integrity": "sha512-eKXRqt8Zh3aqtVYoyayuLiktcW6vnYCuwlcqg91cv3HSdM5foTmIECJEJiwI+GBmSLY37mzczpt/ZfvYqzrQWQ==", "dev": true, "license": "MIT", "engines": { "node": ">=12.x", - "pnpm": "7.5.1" + "pnpm": "10.x" } }, "node_modules/@humanfs/core": { @@ -1941,7 +1941,6 @@ "integrity": "sha512-xBJdRUiCwKpr0OYrOzPwAyNGtsVzoBx32HFPJVv6S+sFA9TmBIBDaqNlFPmBH58ZjgNnGhEr/4oBZvGr4q4TjQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.1.1", "lodash.isequal": "^4.5.0" @@ -2071,13 +2070,13 @@ } }, "node_modules/@openzeppelin/docs-utils": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@openzeppelin/docs-utils/-/docs-utils-0.1.5.tgz", - "integrity": "sha512-GfqXArKmdq8rv+hsP+g8uS1VEkvMIzWs31dCONffzmqFwJ+MOsaNQNZNXQnLRgUkzk8i5mTNDjJuxDy+aBZImQ==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@openzeppelin/docs-utils/-/docs-utils-0.1.6.tgz", + "integrity": "sha512-cVLtDPrCdVgnLV9QRK9D1jrTB8ezQ8tCLTM4g6PHe9TIK3DbO6lSizLF98DhncK2bk6uodOLRT3LO1WtNzei1Q==", "dev": true, "license": "MIT", "dependencies": { - "@frangio/servbot": "^0.2.5", + "@frangio/servbot": "^0.3.0-1", "chalk": "^3.0.0", "chokidar": "^3.5.3", "env-paths": "^2.2.0", @@ -2611,7 +2610,6 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2679,7 +2677,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3283,7 +3280,6 @@ "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -4324,7 +4320,6 @@ "integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4742,7 +4737,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@adraffy/ens-normalize": "1.10.1", "@noble/curves": "1.2.0", @@ -5525,7 +5519,6 @@ "integrity": "sha512-hwEUBvMJzl3Iuru5bfMOEDeF2d7cbMNNF46rkwdo8AeW2GDT4VxFLyYWTi6PTLrZiftHPDiKDlAdAiGvsR9FYA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@ethereumjs/util": "^9.1.0", "@ethersproject/abi": "^5.1.2", @@ -8453,7 +8446,6 @@ "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10390,7 +10382,6 @@ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10969,7 +10960,6 @@ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 6f2d411dd5b..41095bfa529 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", "@nomicfoundation/hardhat-ethers": "^3.0.9", "@nomicfoundation/hardhat-network-helpers": "^1.0.13", - "@openzeppelin/docs-utils": "^0.1.5", + "@openzeppelin/docs-utils": "^0.1.6", "@openzeppelin/merkle-tree": "^1.0.7", "@openzeppelin/upgrade-safe-transpiler": "^0.4.1", "@openzeppelin/upgrades-core": "^1.20.6", From 41892dc8d38a40f5af6ba111be4b8a1e6fe852c9 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Sat, 20 Dec 2025 12:34:07 +0200 Subject: [PATCH 02/12] up --- .../token/ERC20/extensions/draft-ERC7246.sol | 49 ++++++++++----- docs/templates/properties.js | 19 +----- .../ERC20/extensions/draft-ERC7246.test.js | 60 +++++++++++++++++++ 3 files changed, 97 insertions(+), 31 deletions(-) create mode 100644 test/token/ERC20/extensions/draft-ERC7246.test.js diff --git a/contracts/token/ERC20/extensions/draft-ERC7246.sol b/contracts/token/ERC20/extensions/draft-ERC7246.sol index 5ad80e4780c..b774dcddbb2 100644 --- a/contracts/token/ERC20/extensions/draft-ERC7246.sol +++ b/contracts/token/ERC20/extensions/draft-ERC7246.sol @@ -9,6 +9,7 @@ import {Math} from "../../../utils/math/Math.sol"; abstract contract ERC7246 is ERC20, IERC7246 { error ERC7246InsufficientAvailableBalance(uint256 available, uint256 required); error ERC7246InsufficientEncumbrance(uint256 encumbered, uint256 required); + error ERC7246SelfEncumbrance(); mapping(address owner => mapping(address spender => uint256)) private _encumbrances; mapping(address owner => uint256) private _encumberedBalances; @@ -41,18 +42,16 @@ abstract contract ERC7246 is ERC20, IERC7246 { /// @inheritdoc IERC7246 function release(address owner, uint256 amount) public { - uint256 encumbered = encumbrances(owner, msg.sender); - require(encumbered >= amount, ERC7246InsufficientEncumbrance(encumbered, amount)); - - unchecked { - _encumbrances[owner][msg.sender] -= amount; - _encumberedBalances[owner] -= amount; - } - - emit Release(owner, msg.sender, amount); + _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 the 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)); @@ -67,16 +66,36 @@ abstract contract ERC7246 is ERC20, IERC7246 { emit Encumber(owner, spender, amount); } - /// @inheritdoc ERC20 + /** + * @dev Release `amount` of encumbered tokens from `owner` to `spender`. + * + * - Will revert if there are insufficient encumbered tokens. + * - Emits the {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 spent 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 { - _encumbrances[owner][spender] -= encumberedToUse; - _encumberedBalances[owner] -= encumberedToUse; remainingAllowance -= encumberedToUse; } } @@ -86,8 +105,10 @@ abstract contract ERC7246 is ERC20, IERC7246 { /// @inheritdoc ERC20 function _update(address from, address to, uint256 amount) internal virtual override { - uint256 availableBalance = availableBalanceOf(from); - require(availableBalance >= amount, ERC7246InsufficientAvailableBalance(availableBalance, amount)); + if (from != address(0)) { + uint256 availableBalance = availableBalanceOf(from); + require(availableBalance >= amount, ERC7246InsufficientAvailableBalance(availableBalance, amount)); + } super._update(from, to, amount); } } diff --git a/docs/templates/properties.js b/docs/templates/properties.js index f54d01cfb97..5a6d18eced9 100644 --- a/docs/templates/properties.js +++ b/docs/templates/properties.js @@ -20,31 +20,16 @@ module.exports.anchor = function anchor({ item, contract }) { module.exports.fullname = function fullname({ item }) { let res = ''; res += item.name; - - if(item.nodeType == "EventDefinition") { - console.log("EVENT DEF"); - } if ('parameters' in item) { const signature = item.parameters.parameters.map(v => v.typeName.typeDescriptions.typeString).join(','); res += slug('(' + signature + ')'); - } - if(item.nodeType == "EventDefinition") { - console.log("EVENT DEF2"); } if (isNodeType('VariableDeclaration', item)) { res += '-' + slug(item.typeName.typeDescriptions.typeString); - } - if(item.nodeType == "EventDefinition") { - console.log("EVENT DEF3"); - console.log(res); } if (res.charAt(res.length - 1) === '-') { return res.slice(0, -1); } - if(item.nodeType == "EventDefinition") { - console.log("EVENT DEF4"); - console.log(res); - } return res; }; @@ -80,8 +65,8 @@ module.exports['has-internal-variables'] = function ({ item }) { module.exports.functions = function ({ item }) { return [ - ...[...findAll('FunctionDefinition', item)].filter(f => f.visibility !== 'private'), - ...[...findAll('VariableDeclaration', item)].filter(f => f.visibility === 'public'), + ...findAll('FunctionDefinition', item).filter(f => f.visibility !== 'private'), + ...findAll('VariableDeclaration', item).filter(f => f.visibility === 'public'), ]; }; diff --git a/test/token/ERC20/extensions/draft-ERC7246.test.js b/test/token/ERC20/extensions/draft-ERC7246.test.js new file mode 100644 index 00000000000..d7c988ef050 --- /dev/null +++ b/test/token/ERC20/extensions/draft-ERC7246.test.js @@ -0,0 +1,60 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { + shouldBehaveLikeERC20, + shouldBehaveLikeERC20Transfer, + shouldBehaveLikeERC20Approve, +} = require('../ERC20.behavior.js'); + +const name = 'My Token'; +const symbol = 'MTKN'; +const value = 1000n; + +async function fixture() { + // this.accounts is used by shouldBehaveLikeERC20 + const accounts = await ethers.getSigners(); + const [holder, other] = accounts; + + const token = await ethers.deployContract('$ERC7246', [name, symbol]); + + await token.$_mint(holder, value); + + return { + accounts, + holder, + other, + token, + }; +} + +describe.only('ERC7246', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe.only('encumber', function () { + beforeEach(async function () { + await this.token.connect(this.holder).encumber(this.other, 400n); + }); + + it('should reduce available balance', async function () { + await expect(this.token.availableBalanceOf(this.holder)).to.eventually.equal(value - 400n); + }); + + it('should restrict transfer to available balance', async function () { + await expect(this.token.connect(this.holder).transfer(this.other, value)) + .to.be.revertedWithCustomError(this.token, 'ERC7246InsufficientAvailableBalance') + .withArgs(value - 400n, value); + }); + + it('should allow transfer within available balance', async function () { + await expect(this.token.connect(this.holder).transfer(this.other, value - 400n)) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder.address, this.other.address, value - 400n); + }); + }); + + shouldBehaveLikeERC20(value); +}); From 899073da99995c7218cf2685c1d6f31d65587bda Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:55:50 +0200 Subject: [PATCH 03/12] fix references --- contracts/interfaces/README.adoc | 5 +++-- contracts/interfaces/draft-IERC7246.sol | 6 +++--- contracts/token/ERC20/ERC20.sol | 6 +++--- contracts/token/ERC20/IERC20.sol | 6 +++--- contracts/token/ERC20/extensions/draft-ERC7246.sol | 7 ++++++- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/contracts/interfaces/README.adoc b/contracts/interfaces/README.adoc index c66bde3e3b8..e0da397317e 100644 --- a/contracts/interfaces/README.adoc +++ b/contracts/interfaces/README.adoc @@ -46,6 +46,7 @@ are useful to interact with third party contracts that implement them. - {IERC6909ContentURI} - {IERC6909Metadata} - {IERC6909TokenSupply} +- {IERC7246} - {IERC7579Module} - {IERC7579Validator} - {IERC7579Hook} @@ -113,6 +114,8 @@ are useful to interact with third party contracts that implement them. {{IERC6909TokenSupply}} +{{IERC7246}} + {{IERC7579Module}} {{IERC7579Validator}} @@ -136,5 +139,3 @@ are useful to interact with third party contracts that implement them. {{IERC7802}} {{IERC7913SignatureVerifier}} - -{{IERC7246}} diff --git a/contracts/interfaces/draft-IERC7246.sol b/contracts/interfaces/draft-IERC7246.sol index b7b5be685ea..d342c7b64d9 100644 --- a/contracts/interfaces/draft-IERC7246.sol +++ b/contracts/interfaces/draft-IERC7246.sol @@ -39,7 +39,7 @@ interface IERC7246 is IERC20 { * * - MUST revert if caller does not have `amount` tokens available * (e.g. if `balanceOf(caller) - encumbrances(caller) < amount`). - * - Emits an {Encumber-address-address-uint256} event. + * - Emits an {IERC7246-Encumber} event. */ function encumber(address spender, uint256 amount) external; @@ -52,14 +52,14 @@ interface IERC7246 is IERC20 { * MUST revert if `owner` does not have `amount` tokens available * (e.g. if `balanceOf(owner) - encumbrances(owner) < amount`). * - * Emits an {Encumber-address-address-uint256} event. + * 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 {Release-address-address-uint256} event. + * Emits a {IERC7246-Release} event. */ function release(address owner, uint256 amount) external; } diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index bbc269fecd6..dc6929cfe0b 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -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-address-address-uint256} event. + * Emits a {IERC20-Transfer} event. */ function _update(address from, address to, uint256 value) internal virtual { if (from == address(0)) { @@ -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. */ @@ -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: * diff --git a/contracts/token/ERC20/IERC20.sol b/contracts/token/ERC20/IERC20.sol index b493743a10c..1fd6e589355 100644 --- a/contracts/token/ERC20/IERC20.sol +++ b/contracts/token/ERC20/IERC20.sol @@ -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); @@ -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); @@ -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); } diff --git a/contracts/token/ERC20/extensions/draft-ERC7246.sol b/contracts/token/ERC20/extensions/draft-ERC7246.sol index b774dcddbb2..04e117c8ab4 100644 --- a/contracts/token/ERC20/extensions/draft-ERC7246.sol +++ b/contracts/token/ERC20/extensions/draft-ERC7246.sol @@ -7,8 +7,13 @@ import {IERC7246} from "../../../interfaces/draft-IERC7246.sol"; import {Math} from "../../../utils/math/Math.sol"; 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; @@ -70,7 +75,7 @@ abstract contract ERC7246 is ERC20, IERC7246 { * @dev Release `amount` of encumbered tokens from `owner` to `spender`. * * - Will revert if there are insufficient encumbered tokens. - * - Emits the {Release} event. + * - Emits the {ERC7246-Release} event. */ function _releaseEncumbrance(address owner, address spender, uint256 amount) internal virtual { uint256 encumbered = encumbrances(owner, spender); From 45f8600b3675e9b8eb8fdcacad4af880448d1672 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 22 Dec 2025 19:02:58 +0200 Subject: [PATCH 04/12] virtual --- contracts/token/ERC20/extensions/draft-ERC7246.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/token/ERC20/extensions/draft-ERC7246.sol b/contracts/token/ERC20/extensions/draft-ERC7246.sol index 04e117c8ab4..b7e12e1963b 100644 --- a/contracts/token/ERC20/extensions/draft-ERC7246.sol +++ b/contracts/token/ERC20/extensions/draft-ERC7246.sol @@ -20,33 +20,33 @@ abstract contract ERC7246 is ERC20, IERC7246 { mapping(address owner => uint256) private _encumberedBalances; /// @inheritdoc IERC7246 - function encumberedBalanceOf(address owner) public view returns (uint256) { + function encumberedBalanceOf(address owner) public view virtual returns (uint256) { return _encumberedBalances[owner]; } /// @inheritdoc IERC7246 - function availableBalanceOf(address owner) public view returns (uint256) { + function availableBalanceOf(address owner) public view virtual returns (uint256) { return balanceOf(owner) - encumberedBalanceOf(owner); } /// @inheritdoc IERC7246 - function encumbrances(address owner, address spender) public view returns (uint256) { + function encumbrances(address owner, address spender) public view virtual returns (uint256) { return _encumbrances[owner][spender]; } /// @inheritdoc IERC7246 - function encumber(address spender, uint256 amount) public { + function encumber(address spender, uint256 amount) public virtual { _encumber(msg.sender, spender, amount); } /// @inheritdoc IERC7246 - function encumberFrom(address owner, address spender, uint256 amount) public { + 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 { + function release(address owner, uint256 amount) public virtual { _releaseEncumbrance(owner, msg.sender, amount); } @@ -73,7 +73,7 @@ abstract contract ERC7246 is ERC20, IERC7246 { /** * @dev Release `amount` of encumbered tokens from `owner` to `spender`. - * + * * - Will revert if there are insufficient encumbered tokens. * - Emits the {ERC7246-Release} event. */ From fe7ea4319dd9b7bf342b614cff77de3bfc61c625 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Tue, 30 Dec 2025 20:30:55 +0200 Subject: [PATCH 05/12] add tests and docs --- contracts/interfaces/draft-IERC7246.sol | 12 +- .../token/ERC20/extensions/draft-ERC7246.sol | 17 +- .../ERC20/extensions/draft-ERC7246.test.js | 152 ++++++++++++++++-- 3 files changed, 161 insertions(+), 20 deletions(-) diff --git a/contracts/interfaces/draft-IERC7246.sol b/contracts/interfaces/draft-IERC7246.sol index d342c7b64d9..7ea6a9b80b0 100644 --- a/contracts/interfaces/draft-IERC7246.sol +++ b/contracts/interfaces/draft-IERC7246.sol @@ -47,19 +47,17 @@ interface IERC7246 is IERC20 { * @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. + * - 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. + * - Emits a {IERC7246-Release} event. */ function release(address owner, uint256 amount) external; } diff --git a/contracts/token/ERC20/extensions/draft-ERC7246.sol b/contracts/token/ERC20/extensions/draft-ERC7246.sol index b7e12e1963b..ffe67ab9a79 100644 --- a/contracts/token/ERC20/extensions/draft-ERC7246.sol +++ b/contracts/token/ERC20/extensions/draft-ERC7246.sol @@ -6,6 +6,13 @@ 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); @@ -52,7 +59,7 @@ abstract contract ERC7246 is ERC20, IERC7246 { /** * @dev Encumber `amount` of tokens from `owner` to `spender`. Encumbering tokens grants an exclusive right - * to transfer the tokens without removing them from the owner's balance. Release the tokens by calling + * 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 { @@ -75,7 +82,7 @@ abstract contract ERC7246 is ERC20, IERC7246 { * @dev Release `amount` of encumbered tokens from `owner` to `spender`. * * - Will revert if there are insufficient encumbered tokens. - * - Emits the {ERC7246-Release} event. + * - Emits the {IERC7246-Release} event. */ function _releaseEncumbrance(address owner, address spender, uint256 amount) internal virtual { uint256 encumbered = encumbrances(owner, spender); @@ -90,7 +97,7 @@ abstract contract ERC7246 is ERC20, IERC7246 { } /** - * @dev See {ERC20-_spendAllowance}. Encumbrances are spent first, then the remaining 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 { @@ -108,8 +115,10 @@ abstract contract ERC7246 is ERC20, IERC7246 { super._spendAllowance(owner, spender, remainingAllowance); } - /// @inheritdoc ERC20 + /// @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)); diff --git a/test/token/ERC20/extensions/draft-ERC7246.test.js b/test/token/ERC20/extensions/draft-ERC7246.test.js index d7c988ef050..856f0adbddb 100644 --- a/test/token/ERC20/extensions/draft-ERC7246.test.js +++ b/test/token/ERC20/extensions/draft-ERC7246.test.js @@ -15,10 +15,9 @@ const value = 1000n; async function fixture() { // this.accounts is used by shouldBehaveLikeERC20 const accounts = await ethers.getSigners(); - const [holder, other] = accounts; + const [holder, recipient, other] = accounts; const token = await ethers.deployContract('$ERC7246', [name, symbol]); - await token.$_mint(holder, value); return { @@ -26,6 +25,7 @@ async function fixture() { holder, other, token, + recipient, }; } @@ -34,19 +34,55 @@ describe.only('ERC7246', function () { Object.assign(this, await loadFixture(fixture)); }); - describe.only('encumber', function () { + describe('encumber', function () { beforeEach(async function () { - await this.token.connect(this.holder).encumber(this.other, 400n); + this.encumberAmount = 400n; + await this.token.connect(this.holder).encumber(this.other, this.encumberAmount); + }); + + it('should reduce `availableBalanceOf`', async function () { + await expect(this.token.availableBalanceOf(this.holder)).to.eventually.equal(value - this.encumberAmount); + }); + + it('should not reduce `balanceOf`', async function () { + await expect(this.token.balanceOf(this.holder)).to.eventually.equal(value); + }); + + it('should update `encumbrances`', async function () { + await expect(this.token.encumbrances(this.holder, this.other)).to.eventually.eq(this.encumberAmount); }); - it('should reduce available balance', async function () { - await expect(this.token.availableBalanceOf(this.holder)).to.eventually.equal(value - 400n); + it('should update `encumberedBalanceOf`', async function () { + await expect(this.token.encumberedBalanceOf(this.holder)).to.eventually.eq(this.encumberAmount); }); - it('should restrict transfer to available balance', async function () { + it('should revert if self encumbrance', async function () { + await expect(this.token.connect(this.holder).encumber(this.holder, 1n)).to.be.revertedWithCustomError( + this.token, + 'ERC7246SelfEncumbrance', + ); + }); + + it('should revert if encumbrance is over `availableBalanceOf`', async function () { + const availableBalanceOf = value - this.encumberAmount; + await expect(this.token.connect(this.holder).encumber(this.other, availableBalanceOf + 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC7246InsufficientAvailableBalance') + .withArgs(availableBalanceOf, availableBalanceOf + 1n); + }); + + it('should restrict `transfer` to available balance', async function () { await expect(this.token.connect(this.holder).transfer(this.other, value)) .to.be.revertedWithCustomError(this.token, 'ERC7246InsufficientAvailableBalance') - .withArgs(value - 400n, value); + .withArgs(value - this.encumberAmount, value); + }); + + it('should restrict `transferFrom` to available balance', async function () { + await this.token.connect(this.holder).approve(this.recipient, value); + await this.token.connect(this.recipient).transferFrom(this.holder, this.recipient, value - this.encumberAmount); + + await expect(this.token.connect(this.recipient).transferFrom(this.holder, this.recipient, 1)) + .to.be.revertedWithCustomError(this.token, 'ERC7246InsufficientAvailableBalance') + .withArgs(0, 1); }); it('should allow transfer within available balance', async function () { @@ -56,5 +92,103 @@ describe.only('ERC7246', function () { }); }); - shouldBehaveLikeERC20(value); + describe('encumberFrom', function () { + beforeEach(async function () { + await this.token.connect(this.holder).approve(this.other, 100n); + this.encumberTx = this.token.connect(this.other).encumberFrom(this.holder, this.other, 100n); + }); + + it('should increase `encumbrances`', async function () { + await this.encumberTx; + await expect(this.token.encumbrances(this.holder, this.other)).to.eventually.eq(100n); + }); + + it('should emit event', async function () { + await expect(this.encumberTx).to.emit(this.token, 'Encumber').withArgs(this.holder, this.other, 100n); + }); + + it('should consume approval', async function () { + await this.encumberTx; + await expect(this.token.allowance(this.holder, this.other)).to.eventually.eq(0); + }); + + // Encumbrances can be forwarded--meaning an account with an encumbrance can give it to another account + it('can forward encumbrance', async function () { + await this.encumberTx; + await expect(this.token.connect(this.other).encumberFrom(this.holder, this.recipient, 100n)) + .to.emit(this.token, 'Release') + .withArgs(this.holder, this.other, 100n) + .to.emit(this.token, 'Encumber') + .withArgs(this.holder, this.recipient, 100n); + + await expect(this.token.encumbrances(this.holder, this.other)).to.eventually.eq(0); + await expect(this.token.encumbrances(this.holder, this.recipient)).to.eventually.eq(100n); + }); + }); + + describe('release', function () { + beforeEach(async function () { + this.encumberAmount = 400n; + this.releaseAmount = 100n; + + await this.token.connect(this.holder).encumber(this.other, this.encumberAmount); + this.releaseTx = this.token.connect(this.other).release(this.holder, this.releaseAmount); + }); + + it('should emit event', async function () { + await expect(this.releaseTx).to.emit(this.token, 'Release').withArgs(this.holder, this.other, this.releaseAmount); + }); + + it('should reduce `encumberedBalanceOf`', async function () { + await this.releaseTx; + await expect(this.token.encumberedBalanceOf(this.holder)).to.eventually.eq( + this.encumberAmount - this.releaseAmount, + ); + }); + + it('should reduce `encumbrances`', async function () { + await this.releaseTx; + await expect(this.token.encumbrances(this.holder, this.other)).to.eventually.eq( + this.encumberAmount - this.releaseAmount, + ); + }); + + it('should revert if above respective `encumbrance`', async function () { + await this.releaseTx; + const remainingEncumbrance = this.encumberAmount - this.releaseAmount; + await expect(this.token.connect(this.other).release(this.holder, remainingEncumbrance + 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC7246InsufficientEncumbrance') + .withArgs(remainingEncumbrance, remainingEncumbrance + 1n); + }); + }); + + // This is the main expected flow for consuming encumbrances. + describe('transferFrom', function () { + beforeEach(async function () { + this.encumberAmount = 400n; + await this.token.connect(this.holder).encumber(this.other, this.encumberAmount); + }); + + it('should emit release event', async function () { + await expect(this.token.connect(this.other).transferFrom(this.holder, this.recipient, this.encumberAmount)) + .to.emit(this.token, 'Release') + .withArgs(this.holder, this.other, this.encumberAmount); + }); + + it('should decrease encumbrances', async function () { + await this.token.connect(this.other).transferFrom(this.holder, this.recipient, this.encumberAmount - 100n); + await expect(this.token.encumbrances(this.holder, this.other)).to.eventually.eq(100n); + }); + + it('should consume encumbrance first, then allowance', async function () { + await this.token.connect(this.holder).approve(this.other, 200n); + await expect(this.token.connect(this.other).transferFrom(this.holder, this.recipient, this.encumberAmount + 150n)) + .to.emit(this.token, 'Release') + .withArgs(this.holder, this.other, this.encumberAmount); + await expect(this.token.encumbrances(this.holder, this.other)).to.eventually.eq(0n); + await expect(this.token.allowance(this.holder, this.other)).to.eventually.eq(50n); + }); + }); + + // shouldBehaveLikeERC20(value); }); From f0d4989a30db55f096c82182ed744d2391030453 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Wed, 31 Dec 2025 21:04:29 +0200 Subject: [PATCH 06/12] correct pragmas --- contracts/interfaces/draft-IERC7246.sol | 2 +- contracts/token/ERC20/extensions/draft-ERC7246.sol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/interfaces/draft-IERC7246.sol b/contracts/interfaces/draft-IERC7246.sol index 7ea6a9b80b0..74e651bc574 100644 --- a/contracts/interfaces/draft-IERC7246.sol +++ b/contracts/interfaces/draft-IERC7246.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity >=0.6.2; import {IERC20} from "./IERC20.sol"; diff --git a/contracts/token/ERC20/extensions/draft-ERC7246.sol b/contracts/token/ERC20/extensions/draft-ERC7246.sol index ffe67ab9a79..53975a203e7 100644 --- a/contracts/token/ERC20/extensions/draft-ERC7246.sol +++ b/contracts/token/ERC20/extensions/draft-ERC7246.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.26; import {ERC20} from "../ERC20.sol"; import {IERC7246} from "../../../interfaces/draft-IERC7246.sol"; From bfc90905d9073eea27efd60035063863456544db Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Wed, 31 Dec 2025 21:35:43 +0200 Subject: [PATCH 07/12] revert template change --- docs/templates/contract.hbs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/templates/contract.hbs b/docs/templates/contract.hbs index fb772219858..458b511b050 100644 --- a/docs/templates/contract.hbs +++ b/docs/templates/contract.hbs @@ -6,10 +6,6 @@ :{{fullname}}: pass:normal[xref:#{{anchor}}[`++{{name}}++`]] {{/each}} -{{#each events}} -:{{fullname}}: pass:normal[xref:#{{anchor}}[`++{{name}}++`]] -{{/each}} - [.contract] [[{{anchor}}]] === `++{{name}}++` link:https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v{{oz-version}}/{{__item_context.file.absolutePath}}[{github-icon},role=heading-link] From 18a7144701207b1a87f0728c48bf3fadbba2adf8 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Wed, 31 Dec 2025 21:40:51 +0200 Subject: [PATCH 08/12] Update test/token/ERC20/extensions/draft-ERC7246.test.js --- test/token/ERC20/extensions/draft-ERC7246.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/token/ERC20/extensions/draft-ERC7246.test.js b/test/token/ERC20/extensions/draft-ERC7246.test.js index 856f0adbddb..c9cf3214c12 100644 --- a/test/token/ERC20/extensions/draft-ERC7246.test.js +++ b/test/token/ERC20/extensions/draft-ERC7246.test.js @@ -29,7 +29,7 @@ async function fixture() { }; } -describe.only('ERC7246', function () { +describe('ERC7246', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); }); From 2c2a409a99a532c2393d00c30ba6723d460b0e5f Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Wed, 31 Dec 2025 21:45:53 +0200 Subject: [PATCH 09/12] fix docs --- contracts/interfaces/draft-IERC7246.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/interfaces/draft-IERC7246.sol b/contracts/interfaces/draft-IERC7246.sol index 74e651bc574..532704e8960 100644 --- a/contracts/interfaces/draft-IERC7246.sol +++ b/contracts/interfaces/draft-IERC7246.sol @@ -38,7 +38,7 @@ interface IERC7246 is IERC20 { * 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`). + * (e.g. if `balanceOf(caller) - encumberedBalanceOf(caller) < amount`). * - Emits an {IERC7246-Encumber} event. */ function encumber(address spender, uint256 amount) external; @@ -49,7 +49,7 @@ interface IERC7246 is IERC20 { * * - 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`). + * (e.g. if `balanceOf(owner) - encumberedBalanceOf(owner) < amount`). * - Emits an {IERC7246-Encumber} event. */ function encumberFrom(address owner, address spender, uint256 amount) external; From aa7a94a9e48a700228bb55d2d524905208c1b308 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:46:16 +0100 Subject: [PATCH 10/12] change update flow to behave like ERC20 --- contracts/token/ERC20/extensions/draft-ERC7246.sol | 12 +++++++----- test/token/ERC20/extensions/draft-ERC7246.test.js | 8 ++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/contracts/token/ERC20/extensions/draft-ERC7246.sol b/contracts/token/ERC20/extensions/draft-ERC7246.sol index 53975a203e7..af24d273890 100644 --- a/contracts/token/ERC20/extensions/draft-ERC7246.sol +++ b/contracts/token/ERC20/extensions/draft-ERC7246.sol @@ -117,12 +117,14 @@ abstract contract ERC7246 is ERC20, IERC7246 { /// @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) + super._update(from, to, amount); if (from != address(0)) { - uint256 availableBalance = availableBalanceOf(from); - require(availableBalance >= amount, ERC7246InsufficientAvailableBalance(availableBalance, amount)); + uint256 balanceOfFrom = balanceOf(from); + uint256 encumberedBalanceOfFrom = encumberedBalanceOf(from); + require( + balanceOfFrom >= encumberedBalanceOfFrom, + ERC7246InsufficientAvailableBalance(balanceOfFrom + amount - encumberedBalanceOfFrom, amount) + ); } - super._update(from, to, amount); } } diff --git a/test/token/ERC20/extensions/draft-ERC7246.test.js b/test/token/ERC20/extensions/draft-ERC7246.test.js index c9cf3214c12..f239e1d9269 100644 --- a/test/token/ERC20/extensions/draft-ERC7246.test.js +++ b/test/token/ERC20/extensions/draft-ERC7246.test.js @@ -2,11 +2,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); -const { - shouldBehaveLikeERC20, - shouldBehaveLikeERC20Transfer, - shouldBehaveLikeERC20Approve, -} = require('../ERC20.behavior.js'); +const { shouldBehaveLikeERC20 } = require('../ERC20.behavior.js'); const name = 'My Token'; const symbol = 'MTKN'; @@ -190,5 +186,5 @@ describe('ERC7246', function () { }); }); - // shouldBehaveLikeERC20(value); + shouldBehaveLikeERC20(value); }); From 9e8c90e95006f26a0507f52ea69680fdd5d2a0c5 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Wed, 7 Jan 2026 09:57:15 -0600 Subject: [PATCH 11/12] Apply suggestions from code review --- contracts/token/ERC20/IERC20.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/token/ERC20/IERC20.sol b/contracts/token/ERC20/IERC20.sol index 1fd6e589355..b493743a10c 100644 --- a/contracts/token/ERC20/IERC20.sol +++ b/contracts/token/ERC20/IERC20.sol @@ -36,7 +36,7 @@ interface IERC20 { * * Returns a boolean value indicating whether the operation succeeded. * - * Emits a {IERC20-Transfer} event. + * Emits a {Transfer} event. */ function transfer(address to, uint256 value) external returns (bool); @@ -62,7 +62,7 @@ interface IERC20 { * desired value afterwards: * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 * - * Emits an {IERC20-Approval} event. + * Emits an {Approval} event. */ function approve(address spender, uint256 value) external returns (bool); @@ -73,7 +73,7 @@ interface IERC20 { * * Returns a boolean value indicating whether the operation succeeded. * - * Emits a {IERC20-Transfer} event. + * Emits a {Transfer} event. */ function transferFrom(address from, address to, uint256 value) external returns (bool); } From a06c4c6b5b62d385dcb087cdced195a7e955d94b Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:00:53 -0600 Subject: [PATCH 12/12] Apply suggestions from code review --- contracts/token/ERC20/ERC20.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index dc6929cfe0b..4d9d6b6d1c1 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -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-address-address-uint256} event. + * Emits a {Transfer} event. * * NOTE: This function is not virtual, {_update} should be overridden instead. */ @@ -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 {IERC20-Transfer} event. + * Emits a {Transfer} event. */ function _update(address from, address to, uint256 value) internal virtual { if (from == address(0)) { @@ -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 {IERC20-Transfer} event with `from` set to the zero address. + * Emits a {Transfer} event with `from` set to the zero address. * * NOTE: This function is not virtual, {_update} should be overridden instead. */ @@ -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 {IERC20-Approval} event. + * Emits an {Approval} event. * * Requirements: *