-
Notifications
You must be signed in to change notification settings - Fork 12.3k
Add ERC7674 (draft)
#5071
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add ERC7674 (draft)
#5071
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
bbf64d3
Add `ERC20TemporaryApproval`
Amxx 4f94434
Update .changeset/serious-carrots-provide.md
Amxx 119f718
testing
Amxx 76f1745
update
Amxx 1e6911e
coverage
Amxx 28402a6
doc
Amxx 43ee727
Update draft-IERC7674.sol
Amxx e729394
Update test/token/ERC20/extensions/ERC20TemporaryApproval.test.js
Amxx 2bbd0b2
rename events in helper
Amxx 580c0ec
update
Amxx f48694c
update
Amxx 8119a6e
rename ERC20TemporaryApproval -> ERC7674
Amxx b8fa057
rename
Amxx 5bb7662
if value greater than zero check in ERC20TemporaryApproval
Amxx fc031f1
fix tests
Amxx 112c22b
spelling
Amxx 6f3df41
Update test/token/ERC20/ERC20.behavior.js
Amxx 352f56c
Apply suggestions from code review
ernestognw a18f963
Update contracts/token/ERC20/extensions/draft-ERC20TemporaryApproval.sol
Amxx 17f3410
Apply suggestions from code review
Amxx 461d527
Update draft-IERC7674.sol
Amxx 12a79e7
Update draft-IERC7674.sol
Amxx 887121c
fix override
Amxx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'openzeppelin-solidity': minor | ||
| --- | ||
|
|
||
| `ERC20TemporaryApproval`: Add an ERC-20 extension that implements temporary approval using transient storage, based on ERC7674 (draft). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {IERC20} from "./IERC20.sol"; | ||
|
|
||
| /** | ||
| * @dev Temporary Approval Extension for ERC-20 (https://github.com/ethereum/ERCs/pull/358[ERC-7674]) | ||
| */ | ||
| interface IERC7674 is IERC20 { | ||
| /** | ||
| * @dev Set the temporary allowance, allowing `spender` to withdraw (within the same transaction) assets | ||
| * held by the caller. | ||
| */ | ||
| function temporaryApprove(address spender, uint256 value) external returns (bool success); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {Address} from "../utils/Address.sol"; | ||
|
|
||
| contract BatchCaller { | ||
| struct Call { | ||
| address target; | ||
| uint256 value; | ||
| bytes data; | ||
| } | ||
|
|
||
| function execute(Call[] calldata calls) external returns (bytes[] memory) { | ||
| bytes[] memory returndata = new bytes[](calls.length); | ||
| for (uint256 i = 0; i < calls.length; ++i) { | ||
| returndata[i] = Address.functionCallWithValue(calls[i].target, calls[i].data, calls[i].value); | ||
| } | ||
| return returndata; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // SPDX-License-Identifier: MIT | ||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {IERC20} from "../../token/ERC20/IERC20.sol"; | ||
| import {IERC20Metadata} from "../../token/ERC20/extensions/IERC20Metadata.sol"; | ||
|
|
||
| contract ERC20GetterHelper { | ||
| event ERC20TotalSupply(IERC20 token, uint256 totalSupply); | ||
| event ERC20BalanceOf(IERC20 token, address account, uint256 balanceOf); | ||
| event ERC20Allowance(IERC20 token, address owner, address spender, uint256 allowance); | ||
| event ERC20Name(IERC20Metadata token, string name); | ||
| event ERC20Symbol(IERC20Metadata token, string symbol); | ||
| event ERC20Decimals(IERC20Metadata token, uint8 decimals); | ||
|
|
||
| function totalSupply(IERC20 token) external { | ||
| emit ERC20TotalSupply(token, token.totalSupply()); | ||
| } | ||
|
|
||
| function balanceOf(IERC20 token, address account) external { | ||
| emit ERC20BalanceOf(token, account, token.balanceOf(account)); | ||
| } | ||
|
|
||
| function allowance(IERC20 token, address owner, address spender) external { | ||
| emit ERC20Allowance(token, owner, spender, token.allowance(owner, spender)); | ||
| } | ||
|
|
||
| function name(IERC20Metadata token) external { | ||
| emit ERC20Name(token, token.name()); | ||
| } | ||
|
|
||
| function symbol(IERC20Metadata token) external { | ||
| emit ERC20Symbol(token, token.symbol()); | ||
| } | ||
|
|
||
| function decimals(IERC20Metadata token) external { | ||
| emit ERC20Decimals(token, token.decimals()); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
contracts/token/ERC20/extensions/draft-ERC20TemporaryApproval.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| pragma solidity ^0.8.20; | ||
|
|
||
| import {IERC20, ERC20} from "../ERC20.sol"; | ||
| import {IERC7674} from "../../../interfaces/draft-IERC7674.sol"; | ||
| import {Math} from "../../../utils/math/Math.sol"; | ||
| import {SlotDerivation} from "../../../utils/SlotDerivation.sol"; | ||
| import {StorageSlot} from "../../../utils/StorageSlot.sol"; | ||
|
|
||
| /** | ||
| * @dev Extension of {ERC20} that adds support for temporary allowances following ERC-7674. | ||
| * | ||
| * WARNING: This is a draft contract. The corresponding ERC is still subject to changes. | ||
| */ | ||
| abstract contract ERC20TemporaryApproval is ERC20, IERC7674 { | ||
| using SlotDerivation for bytes32; | ||
| using StorageSlot for bytes32; | ||
| using StorageSlot for StorageSlot.Uint256SlotType; | ||
|
|
||
| // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC20_TEMPORARY_APPROVAL_STORAGE")) - 1)) & ~bytes32(uint256(0xff)) | ||
| bytes32 private constant ERC20_TEMPORARY_APPROVAL_STORAGE = | ||
| 0xea2d0e77a01400d0111492b1321103eed560d8fe44b9a7c2410407714583c400; | ||
|
|
||
| /** | ||
| * @dev {allowance} override that includes the temporary allowance when looking up the current allowance. If | ||
| * adding up the persistent and the temporary allowances result in an overflow, type(uint256).max is returned. | ||
| */ | ||
| function allowance(address owner, address spender) public view virtual override(IERC20, ERC20) returns (uint256) { | ||
| (bool success, uint256 amount) = Math.tryAdd( | ||
| super.allowance(owner, spender), | ||
| _temporaryAllowance(owner, spender) | ||
| ); | ||
| return success ? amount : type(uint256).max; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Internal getter for the current temporary allowance that `spender` has over `owner` tokens. | ||
| */ | ||
| function _temporaryAllowance(address owner, address spender) internal view virtual returns (uint256) { | ||
| return _temporaryAllowanceSlot(owner, spender).tload(); | ||
| } | ||
|
|
||
| /** | ||
| * @dev Alternative to {approve} that sets a `value` amount of tokens as the temporary allowance of `spender` over | ||
| * the caller's tokens. | ||
| * | ||
| * Returns a boolean value indicating whether the operation succeeded. | ||
| * | ||
| * Requirements: | ||
| * - `spender` cannot be the zero address. | ||
| * | ||
| * Does NOT emit an {Approval} event. | ||
| */ | ||
| function temporaryApprove(address spender, uint256 value) public virtual returns (bool) { | ||
| _temporaryApprove(_msgSender(), spender, value); | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Sets `value` as the temporary allowance of `spender` over the `owner` s tokens. | ||
| * | ||
| * This internal function is equivalent to `temporaryApprove`, and can be used to e.g. set automatic allowances | ||
| * for certain subsystems, etc. | ||
| * | ||
| * Requirements: | ||
| * - `owner` cannot be the zero address. | ||
| * - `spender` cannot be the zero address. | ||
| * | ||
| * Does NOT emit an {Approval} event. | ||
| */ | ||
| function _temporaryApprove(address owner, address spender, uint256 value) internal virtual { | ||
| if (owner == address(0)) { | ||
| revert ERC20InvalidApprover(address(0)); | ||
| } | ||
| if (spender == address(0)) { | ||
| revert ERC20InvalidSpender(address(0)); | ||
| } | ||
cairoeth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| _temporaryAllowanceSlot(owner, spender).tstore(value); | ||
| } | ||
|
|
||
| /** | ||
| * @dev {_spendAllowance} override that consumes the temporary allowance (if any) before eventually falling back | ||
| * to consuming the persistent allowance. | ||
| * NOTE: This function skips calling `super._spendAllowance` if the temporary allowance | ||
| * is enough to cover the spending. | ||
| */ | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| function _spendAllowance(address owner, address spender, uint256 value) internal virtual override { | ||
| // load transient allowance | ||
| uint256 currentTemporaryAllowance = _temporaryAllowance(owner, spender); | ||
|
|
||
| // Check and update (if needed) the temporary allowance + set remaining value | ||
| if (currentTemporaryAllowance > 0) { | ||
| // All value is covered by the infinite allowance. nothing left to spend, we can return early | ||
| if (currentTemporaryAllowance == type(uint256).max) { | ||
| return; | ||
| } | ||
| // check how much of the value is covered by the transient allowance | ||
| uint256 spendTemporaryAllowance = Math.min(currentTemporaryAllowance, value); | ||
| unchecked { | ||
| // decrease transient allowance accordingly | ||
| _temporaryApprove(owner, spender, currentTemporaryAllowance - spendTemporaryAllowance); | ||
| // update value necessary | ||
| value -= spendTemporaryAllowance; | ||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| // reduce any remaining value from the persistent allowance | ||
| if (value > 0) { | ||
| super._spendAllowance(owner, spender, value); | ||
| } | ||
ernestognw marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| function _temporaryAllowanceSlot( | ||
| address owner, | ||
| address spender | ||
| ) private pure returns (StorageSlot.Uint256SlotType) { | ||
| return ERC20_TEMPORARY_APPROVAL_STORAGE.deriveMapping(owner).deriveMapping(spender).asUint256(); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
test/token/ERC20/extensions/draft-ERC20TemporaryApproval.test.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| const { ethers } = require('hardhat'); | ||
| const { expect } = require('chai'); | ||
| const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); | ||
| const { max, min } = require('../../../helpers/math.js'); | ||
|
|
||
| const { shouldBehaveLikeERC20 } = require('../ERC20.behavior.js'); | ||
|
|
||
| const name = 'My Token'; | ||
| const symbol = 'MTKN'; | ||
| const initialSupply = 100n; | ||
|
|
||
| async function fixture() { | ||
| // this.accounts is used by shouldBehaveLikeERC20 | ||
| const accounts = await ethers.getSigners(); | ||
| const [holder, recipient, other] = accounts; | ||
|
|
||
| const token = await ethers.deployContract('$ERC20TemporaryApproval', [name, symbol]); | ||
| await token.$_mint(holder, initialSupply); | ||
|
|
||
| const spender = await ethers.deployContract('$Address'); | ||
| const batch = await ethers.deployContract('BatchCaller'); | ||
| const getter = await ethers.deployContract('ERC20GetterHelper'); | ||
|
|
||
| return { accounts, holder, recipient, other, token, spender, batch, getter }; | ||
| } | ||
|
|
||
| describe('ERC20TemporaryApproval', function () { | ||
| beforeEach(async function () { | ||
| Object.assign(this, await loadFixture(fixture)); | ||
| }); | ||
|
|
||
| shouldBehaveLikeERC20(initialSupply); | ||
|
|
||
| describe('setting and spending temporary allowance', function () { | ||
| beforeEach(async function () { | ||
| await this.token.connect(this.holder).transfer(this.batch, initialSupply); | ||
| }); | ||
|
|
||
| for (let { | ||
| description, | ||
| persistentAllowance, | ||
| temporaryAllowance, | ||
| amount, | ||
| temporaryExpected, | ||
| persistentExpected, | ||
| } of [ | ||
| { description: 'can set temporary allowance', temporaryAllowance: 42n }, | ||
| { | ||
| description: 'can set temporary allowance on top of persistent allowance', | ||
| temporaryAllowance: 42n, | ||
| persistentAllowance: 17n, | ||
| }, | ||
| { description: 'support allowance overflow', temporaryAllowance: ethers.MaxUint256, persistentAllowance: 17n }, | ||
| { description: 'consuming temporary allowance alone', temporaryAllowance: 42n, amount: 2n }, | ||
| { | ||
| description: 'fallback to persistent allowance if temporary allowance is not sufficient', | ||
| temporaryAllowance: 42n, | ||
| persistentAllowance: 17n, | ||
| amount: 50n, | ||
| }, | ||
| { | ||
| description: 'do not reduce infinite temporary allowance #1', | ||
| temporaryAllowance: ethers.MaxUint256, | ||
| amount: 50n, | ||
| temporaryExpected: ethers.MaxUint256, | ||
| }, | ||
| { | ||
| description: 'do not reduce infinite temporary allowance #2', | ||
| temporaryAllowance: 17n, | ||
| persistentAllowance: ethers.MaxUint256, | ||
| amount: 50n, | ||
| temporaryExpected: ethers.MaxUint256, | ||
| persistentExpected: ethers.MaxUint256, | ||
| }, | ||
| ]) { | ||
| persistentAllowance ??= 0n; | ||
| temporaryAllowance ??= 0n; | ||
| amount ??= 0n; | ||
| temporaryExpected ??= min(persistentAllowance + temporaryAllowance - amount, ethers.MaxUint256); | ||
| persistentExpected ??= persistentAllowance - max(amount - temporaryAllowance, 0n); | ||
|
|
||
| it(description, async function () { | ||
| await expect( | ||
| this.batch.execute( | ||
| [ | ||
| persistentAllowance && { | ||
| target: this.token, | ||
| value: 0n, | ||
| data: this.token.interface.encodeFunctionData('approve', [this.spender.target, persistentAllowance]), | ||
| }, | ||
| temporaryAllowance && { | ||
| target: this.token, | ||
| value: 0n, | ||
| data: this.token.interface.encodeFunctionData('temporaryApprove', [ | ||
| this.spender.target, | ||
| temporaryAllowance, | ||
| ]), | ||
| }, | ||
| amount && { | ||
| target: this.spender, | ||
| value: 0n, | ||
| data: this.spender.interface.encodeFunctionData('$functionCall', [ | ||
| this.token.target, | ||
| this.token.interface.encodeFunctionData('transferFrom', [ | ||
| this.batch.target, | ||
| this.recipient.address, | ||
| amount, | ||
| ]), | ||
| ]), | ||
| }, | ||
| { | ||
| target: this.getter, | ||
| value: 0n, | ||
| data: this.getter.interface.encodeFunctionData('allowance', [ | ||
| this.token.target, | ||
| this.batch.target, | ||
| this.spender.target, | ||
| ]), | ||
| }, | ||
| ].filter(Boolean), | ||
| ), | ||
| ) | ||
| .to.emit(this.getter, 'ERC20Allowance') | ||
| .withArgs(this.token, this.batch, this.spender, temporaryExpected); | ||
|
|
||
| expect(await this.token.allowance(this.batch, this.spender)).to.equal(persistentExpected); | ||
| }); | ||
| } | ||
|
|
||
| it('reverts when the recipient is the zero address', async function () { | ||
| await expect(this.token.connect(this.holder).temporaryApprove(ethers.ZeroAddress, 1n)) | ||
| .to.be.revertedWithCustomError(this.token, 'ERC20InvalidSpender') | ||
| .withArgs(ethers.ZeroAddress); | ||
| }); | ||
|
|
||
| it('reverts when the token owner is the zero address', async function () { | ||
| await expect(this.token.$_temporaryApprove(ethers.ZeroAddress, this.recipient, 1n)) | ||
| .to.be.revertedWithCustomError(this.token, 'ERC20InvalidApprover') | ||
| .withArgs(ethers.ZeroAddress); | ||
| }); | ||
| }); | ||
| }); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.