-
Notifications
You must be signed in to change notification settings - Fork 345
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
✨ Permit2 ERC20 #1093
✨ Permit2 ERC20 #1093
Changes from all commits
6d43bc6
9a1aff5
bd229a1
dccbd3d
dd8b77f
c26f476
e6f4cb9
2c46484
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -44,6 +44,9 @@ abstract contract ERC20 { | |
/// @dev The permit has expired. | ||
error PermitExpired(); | ||
|
||
/// @dev The allowance of Permit2 is fixed at infinity. | ||
error Permit2AllowanceIsFixedAtInfinity(); | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* EVENTS */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
@@ -113,6 +116,13 @@ abstract contract ERC20 { | |
bytes32 private constant _PERMIT_TYPEHASH = | ||
0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; | ||
|
||
/// @dev The canonical Permit2 address. | ||
/// For signature-based allowance granting for single transaction ERC20 `transferFrom`. | ||
/// To enable, override `_givePermit2InfiniteAllowance()`. | ||
/// [Github](https://github.com/Uniswap/permit2) | ||
/// [Etherscan](https://etherscan.io/address/0x000000000022D473030F116dDEE9F6B43aC78BA3) | ||
address internal constant _PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* ERC20 METADATA */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
@@ -157,6 +167,9 @@ abstract contract ERC20 { | |
virtual | ||
returns (uint256 result) | ||
{ | ||
if (_givePermit2InfiniteAllowance()) { | ||
if (spender == _PERMIT2) return type(uint256).max; | ||
} | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
mstore(0x20, spender) | ||
|
@@ -170,6 +183,16 @@ abstract contract ERC20 { | |
/// | ||
/// Emits a {Approval} event. | ||
function approve(address spender, uint256 amount) public virtual returns (bool) { | ||
if (_givePermit2InfiniteAllowance()) { | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
// If `spender == _PERMIT2 && amount != type(uint256).max`. | ||
if iszero(or(xor(shr(96, shl(96, spender)), _PERMIT2), iszero(not(amount)))) { | ||
mstore(0x00, 0x3f68539a) // `Permit2AllowanceIsFixedAtInfinity()`. | ||
revert(0x1c, 0x04) | ||
} | ||
} | ||
} | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
// Compute the allowance slot and store the amount. | ||
|
@@ -232,45 +255,91 @@ abstract contract ERC20 { | |
/// Emits a {Transfer} event. | ||
function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { | ||
_beforeTokenTransfer(from, to, amount); | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
let from_ := shl(96, from) | ||
// Compute the allowance slot and load its value. | ||
mstore(0x20, caller()) | ||
mstore(0x0c, or(from_, _ALLOWANCE_SLOT_SEED)) | ||
let allowanceSlot := keccak256(0x0c, 0x34) | ||
let allowance_ := sload(allowanceSlot) | ||
// If the allowance is not the maximum uint256 value. | ||
if add(allowance_, 1) { | ||
// Revert if the amount to be transferred exceeds the allowance. | ||
if gt(amount, allowance_) { | ||
mstore(0x00, 0x13be252b) // `InsufficientAllowance()`. | ||
// Code duplication is for zero-cost abstraction if possible. | ||
if (_givePermit2InfiniteAllowance()) { | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
let from_ := shl(96, from) | ||
if iszero(eq(caller(), _PERMIT2)) { | ||
// Compute the allowance slot and load its value. | ||
mstore(0x20, caller()) | ||
mstore(0x0c, or(from_, _ALLOWANCE_SLOT_SEED)) | ||
let allowanceSlot := keccak256(0x0c, 0x34) | ||
let allowance_ := sload(allowanceSlot) | ||
// If the allowance is not the maximum uint256 value. | ||
if not(allowance_) { | ||
// Revert if the amount to be transferred exceeds the allowance. | ||
if gt(amount, allowance_) { | ||
mstore(0x00, 0x13be252b) // `InsufficientAllowance()`. | ||
revert(0x1c, 0x04) | ||
} | ||
// Subtract and store the updated allowance. | ||
sstore(allowanceSlot, sub(allowance_, amount)) | ||
} | ||
} | ||
// Compute the balance slot and load its value. | ||
mstore(0x0c, or(from_, _BALANCE_SLOT_SEED)) | ||
let fromBalanceSlot := keccak256(0x0c, 0x20) | ||
let fromBalance := sload(fromBalanceSlot) | ||
// Revert if insufficient balance. | ||
if gt(amount, fromBalance) { | ||
mstore(0x00, 0xf4d678b8) // `InsufficientBalance()`. | ||
revert(0x1c, 0x04) | ||
} | ||
// Subtract and store the updated allowance. | ||
sstore(allowanceSlot, sub(allowance_, amount)) | ||
// Subtract and store the updated balance. | ||
sstore(fromBalanceSlot, sub(fromBalance, amount)) | ||
// Compute the balance slot of `to`. | ||
mstore(0x00, to) | ||
let toBalanceSlot := keccak256(0x0c, 0x20) | ||
// Add and store the updated balance of `to`. | ||
// Will not overflow because the sum of all user balances | ||
// cannot exceed the maximum uint256 value. | ||
sstore(toBalanceSlot, add(sload(toBalanceSlot), amount)) | ||
// Emit the {Transfer} event. | ||
mstore(0x20, amount) | ||
log3(0x20, 0x20, _TRANSFER_EVENT_SIGNATURE, shr(96, from_), shr(96, mload(0x0c))) | ||
Comment on lines
+262
to
+300
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This chunk of code is the same for both branches, with the only exception being that for the |
||
} | ||
// Compute the balance slot and load its value. | ||
mstore(0x0c, or(from_, _BALANCE_SLOT_SEED)) | ||
let fromBalanceSlot := keccak256(0x0c, 0x20) | ||
let fromBalance := sload(fromBalanceSlot) | ||
// Revert if insufficient balance. | ||
if gt(amount, fromBalance) { | ||
mstore(0x00, 0xf4d678b8) // `InsufficientBalance()`. | ||
revert(0x1c, 0x04) | ||
} else { | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
let from_ := shl(96, from) | ||
// Compute the allowance slot and load its value. | ||
mstore(0x20, caller()) | ||
mstore(0x0c, or(from_, _ALLOWANCE_SLOT_SEED)) | ||
let allowanceSlot := keccak256(0x0c, 0x34) | ||
let allowance_ := sload(allowanceSlot) | ||
// If the allowance is not the maximum uint256 value. | ||
if not(allowance_) { | ||
// Revert if the amount to be transferred exceeds the allowance. | ||
if gt(amount, allowance_) { | ||
mstore(0x00, 0x13be252b) // `InsufficientAllowance()`. | ||
revert(0x1c, 0x04) | ||
} | ||
// Subtract and store the updated allowance. | ||
sstore(allowanceSlot, sub(allowance_, amount)) | ||
} | ||
// Compute the balance slot and load its value. | ||
mstore(0x0c, or(from_, _BALANCE_SLOT_SEED)) | ||
let fromBalanceSlot := keccak256(0x0c, 0x20) | ||
let fromBalance := sload(fromBalanceSlot) | ||
// Revert if insufficient balance. | ||
if gt(amount, fromBalance) { | ||
mstore(0x00, 0xf4d678b8) // `InsufficientBalance()`. | ||
revert(0x1c, 0x04) | ||
} | ||
// Subtract and store the updated balance. | ||
sstore(fromBalanceSlot, sub(fromBalance, amount)) | ||
// Compute the balance slot of `to`. | ||
mstore(0x00, to) | ||
let toBalanceSlot := keccak256(0x0c, 0x20) | ||
// Add and store the updated balance of `to`. | ||
// Will not overflow because the sum of all user balances | ||
// cannot exceed the maximum uint256 value. | ||
sstore(toBalanceSlot, add(sload(toBalanceSlot), amount)) | ||
// Emit the {Transfer} event. | ||
mstore(0x20, amount) | ||
log3(0x20, 0x20, _TRANSFER_EVENT_SIGNATURE, shr(96, from_), shr(96, mload(0x0c))) | ||
} | ||
// Subtract and store the updated balance. | ||
sstore(fromBalanceSlot, sub(fromBalance, amount)) | ||
// Compute the balance slot of `to`. | ||
mstore(0x00, to) | ||
let toBalanceSlot := keccak256(0x0c, 0x20) | ||
// Add and store the updated balance of `to`. | ||
// Will not overflow because the sum of all user balances | ||
// cannot exceed the maximum uint256 value. | ||
sstore(toBalanceSlot, add(sload(toBalanceSlot), amount)) | ||
// Emit the {Transfer} event. | ||
mstore(0x20, amount) | ||
log3(0x20, 0x20, _TRANSFER_EVENT_SIGNATURE, shr(96, from_), shr(96, mload(0x0c))) | ||
} | ||
_afterTokenTransfer(from, to, amount); | ||
return true; | ||
|
@@ -309,6 +378,16 @@ abstract contract ERC20 { | |
bytes32 r, | ||
bytes32 s | ||
) public virtual { | ||
if (_givePermit2InfiniteAllowance()) { | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
// If `spender == _PERMIT2 && value != type(uint256).max`. | ||
if iszero(or(xor(shr(96, shl(96, spender)), _PERMIT2), iszero(not(value)))) { | ||
mstore(0x00, 0x3f68539a) // `Permit2AllowanceIsFixedAtInfinity()`. | ||
revert(0x1c, 0x04) | ||
} | ||
Comment on lines
+384
to
+388
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This expression's equivalence has been verified with Halmos. |
||
} | ||
} | ||
bytes32 nameHash = _constantNameHash(); | ||
// We simply calculate it on-the-fly to allow for cases where the `name` may change. | ||
if (nameHash == bytes32(0)) nameHash = keccak256(bytes(name())); | ||
|
@@ -494,6 +573,9 @@ abstract contract ERC20 { | |
|
||
/// @dev Updates the allowance of `owner` for `spender` based on spent `amount`. | ||
function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { | ||
if (_givePermit2InfiniteAllowance()) { | ||
if (spender == _PERMIT2) return; // Do nothing, as allowance is infinite. | ||
} | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
// Compute the allowance slot and load its value. | ||
|
@@ -503,7 +585,7 @@ abstract contract ERC20 { | |
let allowanceSlot := keccak256(0x0c, 0x34) | ||
let allowance_ := sload(allowanceSlot) | ||
// If the allowance is not the maximum uint256 value. | ||
if add(allowance_, 1) { | ||
if not(allowance_) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This expression's equivalence has been verified with Halmos. |
||
// Revert if the amount to be transferred exceeds the allowance. | ||
if gt(amount, allowance_) { | ||
mstore(0x00, 0x13be252b) // `InsufficientAllowance()`. | ||
|
@@ -519,6 +601,16 @@ abstract contract ERC20 { | |
/// | ||
/// Emits a {Approval} event. | ||
function _approve(address owner, address spender, uint256 amount) internal virtual { | ||
if (_givePermit2InfiniteAllowance()) { | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
// If `spender == _PERMIT2 && amount != type(uint256).max`. | ||
if iszero(or(xor(shr(96, shl(96, spender)), _PERMIT2), iszero(not(amount)))) { | ||
mstore(0x00, 0x3f68539a) // `Permit2AllowanceIsFixedAtInfinity()`. | ||
revert(0x1c, 0x04) | ||
} | ||
} | ||
} | ||
/// @solidity memory-safe-assembly | ||
assembly { | ||
let owner_ := shl(96, owner) | ||
|
@@ -543,4 +635,17 @@ abstract contract ERC20 { | |
/// @dev Hook that is called after any transfer of tokens. | ||
/// This includes minting and burning. | ||
function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {} | ||
|
||
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/ | ||
/* PERMIT2 */ | ||
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/ | ||
|
||
/// @dev Returns whether to fix the Permit2 contract's allowance at infinity. | ||
/// | ||
/// This value should be kept constant after contract initialization, | ||
/// or else the actual allowance values may not match with the {Approval} events. | ||
/// For best performance, return a compile-time constant for zero-cost abstraction. | ||
function _givePermit2InfiniteAllowance() internal view virtual returns (bool) { | ||
return false; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.4; | ||
|
||
import {MockERC20} from "./MockERC20.sol"; | ||
|
||
/// @dev WARNING! This mock is strictly intended for testing purposes only. | ||
/// Do NOT copy anything here into production code unless you really know what you are doing. | ||
contract MockERC20ForPermit2 is MockERC20 { | ||
constructor(string memory name_, string memory symbol_, uint8 decimals_) | ||
MockERC20(name_, symbol_, decimals_) | ||
{} | ||
|
||
function _givePermit2InfiniteAllowance() internal view virtual override returns (bool) { | ||
return true; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This expression's equivalence has been verified with Halmos.