Skip to content
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

Merged
merged 8 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 141 additions & 36 deletions src/tokens/ERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand Down Expand Up @@ -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 */
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
Comment on lines +189 to +193
Copy link
Owner Author

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.

}
}
/// @solidity memory-safe-assembly
assembly {
// Compute the allowance slot and store the amount.
Expand Down Expand Up @@ -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
Copy link
Owner Author

Choose a reason for hiding this comment

The 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 _givePermit2InfiniteAllowance() branch, if msg.sender == _PERMIT2, the allowance logic will be skipped.

}
// 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;
Expand Down Expand Up @@ -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
Copy link
Owner Author

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.

}
}
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()));
Expand Down Expand Up @@ -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.
Expand All @@ -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_) {
Copy link
Owner Author

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.

// Revert if the amount to be transferred exceeds the allowance.
if gt(amount, allowance_) {
mstore(0x00, 0x13be252b) // `InsufficientAllowance()`.
Expand All @@ -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)
Expand All @@ -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;
}
}
80 changes: 80 additions & 0 deletions test/ERC20.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,86 @@ import "./utils/SoladyTest.sol";
import "./utils/InvariantTest.sol";

import {ERC20, MockERC20} from "./utils/mocks/MockERC20.sol";
import {MockERC20ForPermit2} from "./utils/mocks/MockERC20ForPermit2.sol";

contract ERC20ForPermit2Test is SoladyTest {
MockERC20ForPermit2 token;

address internal constant _PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3;

function setUp() public {
token = new MockERC20ForPermit2("Token", "TKN", 18);
}

function testApproveToPermit2(address owner, uint256 amount) public {
vm.prank(owner);
if (amount != type(uint256).max) {
vm.expectRevert(ERC20.Permit2AllowanceIsFixedAtInfinity.selector);
}
token.approve(_PERMIT2, amount);
}

function testPermitToPermit2(address owner, uint256 amount) public {
vm.prank(owner);
if (amount != type(uint256).max) {
vm.expectRevert(ERC20.Permit2AllowanceIsFixedAtInfinity.selector);
} else {
vm.expectRevert(ERC20.InvalidPermit.selector);
}
token.permit(owner, _PERMIT2, amount, block.timestamp, 0, bytes32(0), bytes32(0));
}

function testTransferFrom(address owner, uint256 amount) public {
assertEq(token.allowance(owner, _PERMIT2), type(uint256).max);
token.mint(owner, amount);
uint256 amountToTransfer = _bound(_random(), 0, amount);
address notPermit2 = _randomHashedAddress();
address recipient = _randomHashedAddress();
vm.prank(notPermit2);
if (amountToTransfer != 0) {
vm.expectRevert(ERC20.InsufficientAllowance.selector);
}
token.transferFrom(owner, recipient, amountToTransfer);

vm.prank(_PERMIT2);
token.transferFrom(owner, recipient, amountToTransfer);
if (recipient != owner) {
assertEq(token.balanceOf(recipient), amountToTransfer);
assertEq(token.balanceOf(owner), amount - amountToTransfer);
} else {
assertEq(token.balanceOf(owner), amount);
}
assertEq(token.allowance(owner, _PERMIT2), type(uint256).max);
}

function check_IsNotUint256MaxTrickEquivalence(uint256 x) public pure {
bool expected;
bool optimized;
/// @solidity memory-safe-assembly
assembly {
if add(x, 1) { expected := 1 }
if not(x) { optimized := 1 }
}
assert(optimized == expected);
expected = x != type(uint256).max;
assert(optimized == expected);
}

function check_IsPermit2AndValueIsNotInfinityTrickEquivalence(address spender, uint256 amount)
public
pure
{
bool expected = spender == _PERMIT2 && amount != type(uint256).max;
bool optimized;
/// @solidity memory-safe-assembly
assembly {
if iszero(or(xor(shr(96, shl(96, spender)), _PERMIT2), iszero(not(amount)))) {
optimized := 1
}
}
assert(optimized == expected);
}
}

contract ERC20Test is SoladyTest {
MockERC20 token;
Expand Down
16 changes: 16 additions & 0 deletions test/utils/mocks/MockERC20ForPermit2.sol
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;
}
}