Skip to content

Commit 0eb9552

Browse files
Transpile e30b390d
1 parent 52f6007 commit 0eb9552

File tree

10 files changed

+381
-2
lines changed

10 files changed

+381
-2
lines changed

.changeset/serious-carrots-provide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`ERC20TemporaryApproval`: Add an ERC-20 extension that implements temporary approval using transient storage, based on ERC7674 (draft).

contracts/interfaces/README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ are useful to interact with third party contracts that implement them.
4040
- {IERC5313}
4141
- {IERC5805}
4242
- {IERC6372}
43+
- {IERC7674}
4344

4445
== Detailed ABI
4546

@@ -80,3 +81,5 @@ are useful to interact with third party contracts that implement them.
8081
{{IERC5805}}
8182

8283
{{IERC6372}}
84+
85+
{{IERC7674}}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {Address} from "@openzeppelin/contracts/utils/Address.sol";
5+
import {Initializable} from "../proxy/utils/Initializable.sol";
6+
7+
contract BatchCallerUpgradeable is Initializable {
8+
struct Call {
9+
address target;
10+
uint256 value;
11+
bytes data;
12+
}
13+
14+
function __BatchCaller_init() internal onlyInitializing {
15+
}
16+
17+
function __BatchCaller_init_unchained() internal onlyInitializing {
18+
}
19+
function execute(Call[] calldata calls) external returns (bytes[] memory) {
20+
bytes[] memory returndata = new bytes[](calls.length);
21+
for (uint256 i = 0; i < calls.length; ++i) {
22+
returndata[i] = Address.functionCallWithValue(calls[i].target, calls[i].data, calls[i].value);
23+
}
24+
return returndata;
25+
}
26+
}

contracts/mocks/WithInit.sol

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ contract Base64DirtyUpgradeableWithInit is Base64DirtyUpgradeable {
163163
__Base64Dirty_init();
164164
}
165165
}
166+
import "./BatchCallerUpgradeable.sol";
167+
168+
contract BatchCallerUpgradeableWithInit is BatchCallerUpgradeable {
169+
constructor() payable initializer {
170+
__BatchCaller_init();
171+
}
172+
}
166173
import "./CallReceiverMockUpgradeable.sol";
167174

168175
contract CallReceiverMockUpgradeableWithInit is CallReceiverMockUpgradeable {
@@ -714,6 +721,13 @@ contract ERC20ForceApproveMockUpgradeableWithInit is ERC20ForceApproveMockUpgrad
714721
__ERC20ForceApproveMock_init();
715722
}
716723
}
724+
import "./token/ERC20GetterHelperUpgradeable.sol";
725+
726+
contract ERC20GetterHelperUpgradeableWithInit is ERC20GetterHelperUpgradeable {
727+
constructor() payable initializer {
728+
__ERC20GetterHelper_init();
729+
}
730+
}
717731
import "./token/ERC20MockUpgradeable.sol";
718732

719733
contract ERC20MockUpgradeableWithInit is ERC20MockUpgradeable {
@@ -934,6 +948,13 @@ contract ERC20UpgradeableWithInit is ERC20Upgradeable {
934948
__ERC20_init(name_, symbol_);
935949
}
936950
}
951+
import "../token/ERC20/extensions/draft-ERC20TemporaryApprovalUpgradeable.sol";
952+
953+
contract ERC20TemporaryApprovalUpgradeableWithInit is ERC20TemporaryApprovalUpgradeable {
954+
constructor() payable initializer {
955+
__ERC20TemporaryApproval_init();
956+
}
957+
}
937958
import "../token/ERC20/extensions/ERC1363Upgradeable.sol";
938959

939960
contract ERC1363UpgradeableWithInit is ERC1363Upgradeable {
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
6+
import {Initializable} from "../../proxy/utils/Initializable.sol";
7+
8+
contract ERC20GetterHelperUpgradeable is Initializable {
9+
event ERC20TotalSupply(IERC20 token, uint256 totalSupply);
10+
event ERC20BalanceOf(IERC20 token, address account, uint256 balanceOf);
11+
event ERC20Allowance(IERC20 token, address owner, address spender, uint256 allowance);
12+
event ERC20Name(IERC20Metadata token, string name);
13+
event ERC20Symbol(IERC20Metadata token, string symbol);
14+
event ERC20Decimals(IERC20Metadata token, uint8 decimals);
15+
16+
function __ERC20GetterHelper_init() internal onlyInitializing {
17+
}
18+
19+
function __ERC20GetterHelper_init_unchained() internal onlyInitializing {
20+
}
21+
function totalSupply(IERC20 token) external {
22+
emit ERC20TotalSupply(token, token.totalSupply());
23+
}
24+
25+
function balanceOf(IERC20 token, address account) external {
26+
emit ERC20BalanceOf(token, account, token.balanceOf(account));
27+
}
28+
29+
function allowance(IERC20 token, address owner, address spender) external {
30+
emit ERC20Allowance(token, owner, spender, token.allowance(owner, spender));
31+
}
32+
33+
function name(IERC20Metadata token) external {
34+
emit ERC20Name(token, token.name());
35+
}
36+
37+
function symbol(IERC20Metadata token) external {
38+
emit ERC20Symbol(token, token.symbol());
39+
}
40+
41+
function decimals(IERC20Metadata token) external {
42+
emit ERC20Decimals(token, token.decimals());
43+
}
44+
}

contracts/token/ERC20/README.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Additionally there are multiple custom extensions, including:
2222
* {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC-3156).
2323
* {ERC20Votes}: support for voting and vote delegation.
2424
* {ERC20Wrapper}: wrapper to create an ERC-20 backed by another ERC-20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}.
25+
* {ERC20TemporaryApproval}: support for approvals lasting for only one transaction, as defined in ERC-7674.
2526
* {ERC1363}: support for calling the target of a transfer or approval, enabling code execution on the receiver within a single transaction.
2627
* {ERC4626}: tokenized vault that manages shares (represented as ERC-20) that are backed by assets (another ERC-20).
2728
@@ -61,6 +62,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
6162

6263
{{ERC20FlashMint}}
6364

65+
{{ERC20TemporaryApproval}}
66+
6467
{{ERC1363}}
6568

6669
{{ERC4626}}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// SPDX-License-Identifier: MIT
2+
3+
pragma solidity ^0.8.20;
4+
5+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
6+
import {ERC20Upgradeable} from "../ERC20Upgradeable.sol";
7+
import {IERC7674} from "@openzeppelin/contracts/interfaces/draft-IERC7674.sol";
8+
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
9+
import {SlotDerivation} from "@openzeppelin/contracts/utils/SlotDerivation.sol";
10+
import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
11+
import {Initializable} from "../../../proxy/utils/Initializable.sol";
12+
13+
/**
14+
* @dev Extension of {ERC20} that adds support for temporary allowances following ERC-7674.
15+
*
16+
* WARNING: This is a draft contract. The corresponding ERC is still subject to changes.
17+
*/
18+
abstract contract ERC20TemporaryApprovalUpgradeable is Initializable, ERC20Upgradeable, IERC7674 {
19+
using SlotDerivation for bytes32;
20+
using StorageSlot for bytes32;
21+
using StorageSlot for StorageSlot.Uint256SlotType;
22+
23+
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC20_TEMPORARY_APPROVAL_STORAGE")) - 1)) & ~bytes32(uint256(0xff))
24+
bytes32 private constant ERC20_TEMPORARY_APPROVAL_STORAGE =
25+
0xea2d0e77a01400d0111492b1321103eed560d8fe44b9a7c2410407714583c400;
26+
27+
function __ERC20TemporaryApproval_init() internal onlyInitializing {
28+
}
29+
30+
function __ERC20TemporaryApproval_init_unchained() internal onlyInitializing {
31+
}
32+
/**
33+
* @dev {allowance} override that includes the temporary allowance when looking up the current allowance. If
34+
* adding up the persistent and the temporary allowances result in an overflow, type(uint256).max is returned.
35+
*/
36+
function allowance(address owner, address spender) public view virtual override(IERC20, ERC20Upgradeable) returns (uint256) {
37+
(bool success, uint256 amount) = Math.tryAdd(
38+
super.allowance(owner, spender),
39+
_temporaryAllowance(owner, spender)
40+
);
41+
return success ? amount : type(uint256).max;
42+
}
43+
44+
/**
45+
* @dev Internal getter for the current temporary allowance that `spender` has over `owner` tokens.
46+
*/
47+
function _temporaryAllowance(address owner, address spender) internal view virtual returns (uint256) {
48+
return _temporaryAllowanceSlot(owner, spender).tload();
49+
}
50+
51+
/**
52+
* @dev Alternative to {approve} that sets a `value` amount of tokens as the temporary allowance of `spender` over
53+
* the caller's tokens.
54+
*
55+
* Returns a boolean value indicating whether the operation succeeded.
56+
*
57+
* Requirements:
58+
* - `spender` cannot be the zero address.
59+
*
60+
* Does NOT emit an {Approval} event.
61+
*/
62+
function temporaryApprove(address spender, uint256 value) public virtual returns (bool) {
63+
_temporaryApprove(_msgSender(), spender, value);
64+
return true;
65+
}
66+
67+
/**
68+
* @dev Sets `value` as the temporary allowance of `spender` over the `owner` s tokens.
69+
*
70+
* This internal function is equivalent to `temporaryApprove`, and can be used to e.g. set automatic allowances
71+
* for certain subsystems, etc.
72+
*
73+
* Requirements:
74+
* - `owner` cannot be the zero address.
75+
* - `spender` cannot be the zero address.
76+
*
77+
* Does NOT emit an {Approval} event.
78+
*/
79+
function _temporaryApprove(address owner, address spender, uint256 value) internal virtual {
80+
if (owner == address(0)) {
81+
revert ERC20InvalidApprover(address(0));
82+
}
83+
if (spender == address(0)) {
84+
revert ERC20InvalidSpender(address(0));
85+
}
86+
_temporaryAllowanceSlot(owner, spender).tstore(value);
87+
}
88+
89+
/**
90+
* @dev {_spendAllowance} override that consumes the temporary allowance (if any) before eventually falling back
91+
* to consuming the persistent allowance.
92+
* NOTE: This function skips calling `super._spendAllowance` if the temporary allowance
93+
* is enough to cover the spending.
94+
*/
95+
function _spendAllowance(address owner, address spender, uint256 value) internal virtual override {
96+
// load transient allowance
97+
uint256 currentTemporaryAllowance = _temporaryAllowance(owner, spender);
98+
99+
// Check and update (if needed) the temporary allowance + set remaining value
100+
if (currentTemporaryAllowance > 0) {
101+
// All value is covered by the infinite allowance. nothing left to spend, we can return early
102+
if (currentTemporaryAllowance == type(uint256).max) {
103+
return;
104+
}
105+
// check how much of the value is covered by the transient allowance
106+
uint256 spendTemporaryAllowance = Math.min(currentTemporaryAllowance, value);
107+
unchecked {
108+
// decrease transient allowance accordingly
109+
_temporaryApprove(owner, spender, currentTemporaryAllowance - spendTemporaryAllowance);
110+
// update value necessary
111+
value -= spendTemporaryAllowance;
112+
}
113+
}
114+
// reduce any remaining value from the persistent allowance
115+
if (value > 0) {
116+
super._spendAllowance(owner, spender, value);
117+
}
118+
}
119+
120+
function _temporaryAllowanceSlot(
121+
address owner,
122+
address spender
123+
) private pure returns (StorageSlot.Uint256SlotType) {
124+
return ERC20_TEMPORARY_APPROVAL_STORAGE.deriveMapping(owner).deriveMapping(spender).asUint256();
125+
}
126+
}

test/token/ERC20/ERC20.behavior.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,18 @@ function shouldBehaveLikeERC20(initialSupply, opts = {}) {
132132
});
133133

134134
it('reverts when the token owner is the zero address', async function () {
135+
// transferFrom does a spendAllowance before moving the assets
136+
// - default behavior (ERC20) is to always update the approval using `_approve`. This will fail because the
137+
// approver (owner) is address(0). This happens even if the amount transferred is zero, and the approval update
138+
// is not actually necessary.
139+
// - in ERC20TemporaryAllowance, transfer of 0 value will not update allowance (temporary or persistent)
140+
// therefore the spendAllowance does not revert. However, the transfer of asset will revert because the sender
141+
// is address(0)
142+
const errorName = this.token.temporaryApprove ? 'ERC20InvalidSender' : 'ERC20InvalidApprover';
143+
135144
const value = 0n;
136145
await expect(this.token.connect(this.recipient).transferFrom(ethers.ZeroAddress, this.recipient, value))
137-
.to.be.revertedWithCustomError(this.token, 'ERC20InvalidApprover')
146+
.to.be.revertedWithCustomError(this.token, errorName)
138147
.withArgs(ethers.ZeroAddress);
139148
});
140149
});

0 commit comments

Comments
 (0)