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

add ERC20 preapproved conduit #20

Merged
merged 3 commits into from
Nov 29, 2023
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
8 changes: 7 additions & 1 deletion src/lib/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ uint256 constant SOLADY_ERC1155_MASTER_SLOT_SEED = 0x9a31110384e0b0c9;
/// @dev `keccak256(bytes("TransferSingle(address,address,address,uint256,uint256)"))`.
uint256 constant SOLADY_TRANSFER_SINGLE_EVENT_SIGNATURE =
0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62;

/// @dev `keccak256(bytes("TransferBatch(address,address,address,uint256[],uint256[])"))`.
uint256 constant SOLADY_TRANSFER_BATCH_EVENT_SIGNATURE =
0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb;
/// @dev Solady ERC20 allowance slot seed.
uint256 constant SOLADY_ERC20_ALLOWANCE_SLOT_SEED = 0x7f5e9f20;
/// @dev Solady ERC20 balance slot seed.
uint256 constant SOLADY_ERC20_BALANCE_SLOT_SEED = 0x87a211a2;
/// @dev `keccak256(bytes("Transfer(address,address,uint256)"))`.
uint256 constant SOLADY_ERC20_TRANSFER_EVENT_SIGNATURE =
0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef;
12 changes: 12 additions & 0 deletions src/reference/tokens/erc20/ERC20Preapproved_OZ.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {ERC20ConduitPreapproved_OZ, ERC20} from "../../../tokens/erc20/ERC20ConduitPreapproved_OZ.sol";

contract ERC20_OZ is ERC20ConduitPreapproved_OZ {
constructor() ERC20("ERC20_OZ", "ERC20_OZ") {}

function mint(address to, uint256 amount) public {
_mint(to, amount);
}
}
18 changes: 18 additions & 0 deletions src/reference/tokens/erc20/ERC20Preapproved_Solady.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {ERC20ConduitPreapproved_Solady, ERC20} from "../../../tokens/erc20/ERC20ConduitPreapproved_Solady.sol";

contract ERC20_Solady is ERC20ConduitPreapproved_Solady {
function mint(address to, uint256 amount) public {
_mint(to, amount);
}

function name() public pure override returns (string memory) {
return "Test";
}

function symbol() public pure override returns (string memory) {
return "TST";
}
}
49 changes: 49 additions & 0 deletions src/tokens/erc20/ERC20ConduitPreapproved_OZ.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {ERC20} from "openzeppelin-contracts/token/ERC20/ERC20.sol";
import {CONDUIT} from "../../lib/Constants.sol";
import {IPreapprovalForAll} from "../../interfaces/IPreapprovalForAll.sol";

abstract contract ERC20ConduitPreapproved_OZ is ERC20, IPreapprovalForAll {
constructor() {
emit PreapprovalForAll(CONDUIT, true);
}

/**
* @param owner Owner of tokens
* @param spender Account to check allowance of `owner`'s tokens
* @dev If `spender` is `CONDUIT` and allowance is 0, return `type(uint256).max`, since users must explicitly revoke the pre-approved conduit.
* Setting an allowance of 0 for the conduit with `approve` will revoke the pre-approval.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
uint256 allowance = super.allowance(owner, spender);
if (spender == CONDUIT) {
if (allowance == 0) {
return type(uint256).max;
} else if (allowance == type(uint256).max) {
return 0;
}
}
return allowance;
}

/**
* @param owner Owner of tokens
* @param spender Account to approve allowance of `owner`'s tokens
* @param value Amount to approve
* @param emitEvent Whether to emit the Approval event
* @dev `allowance` inverts the value of the approval if `spender` is `CONDUIT`, since users must explicitly revoke the pre-approved conduit.
* E.g. if 0 is passed, it is stored as `type(uint256).max`, and if `type(uint256).max` is passed, it is stored as 0.
*/
function _approve(address owner, address spender, uint256 value, bool emitEvent) internal virtual override {
if (spender == CONDUIT) {
if (value == 0) {
value = type(uint256).max;
} else if (value == type(uint256).max) {
value = 0;
}
}
super._approve(owner, spender, value, emitEvent);
}
}
120 changes: 120 additions & 0 deletions src/tokens/erc20/ERC20ConduitPreapproved_Solady.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {ERC20} from "solady/src/tokens/ERC20.sol";
import {
CONDUIT,
SOLADY_ERC20_ALLOWANCE_SLOT_SEED,
SOLADY_ERC20_BALANCE_SLOT_SEED,
SOLADY_ERC20_TRANSFER_EVENT_SIGNATURE
} from "../../lib/Constants.sol";
import {IPreapprovalForAll} from "../../interfaces/IPreapprovalForAll.sol";

abstract contract ERC20ConduitPreapproved_Solady is ERC20, IPreapprovalForAll {
constructor() {
emit PreapprovalForAll(CONDUIT, true);
}

/**
* @param owner Owner of tokens
* @param spender Account to check allowance of `owner`'s tokens
* @dev If `spender` is `CONDUIT` and allowance is 0, return `type(uint256).max`, since users must explicitly revoke the pre-approved conduit.
* Setting an allowance of 0 for the conduit with `approve` will revoke the pre-approval.
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
uint256 allowance_ = super.allowance(owner, spender);
if (spender == CONDUIT) {
if (allowance_ == 0) {
return type(uint256).max;
} else if (allowance_ == type(uint256).max) {
return 0;
}
}
return allowance_;
}

/**
* @param spender Account to approve allowance of `msg.sender`'s tokens
* @param amount Amount to approve
* @dev `allowance` inverts the value of the approval if `spender` is `CONDUIT`, since users must explicitly revoke the pre-approved conduit.
* E.g. if 0 is passed, it is stored as `type(uint256).max`, and if `type(uint256).max` is passed, it is stored as 0.
*/
function approve(address spender, uint256 amount) public virtual override returns (bool) {
if (spender == CONDUIT) {
if (amount == 0) {
amount = type(uint256).max;
} else if (amount == type(uint256).max) {
amount = 0;
}
}
super._approve(msg.sender, spender, amount);
return true;
}

function transferFrom(address from, address to, uint256 amount) public virtual override 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_, SOLADY_ERC20_ALLOWANCE_SLOT_SEED))
let allowanceSlot := keccak256(0x0c, 0x34)
let allowance_ := sload(allowanceSlot)
// If the caller is the conduit and allowance is 0, set to type(uint256).max. If the allowance is type(uint256).max, set to 0.
let isConduit := eq(caller(), CONDUIT)
if isConduit {
let conduitAllowance_ := allowance_
if eq(allowance_, 0) { conduitAllowance_ := not(0) }
if eq(allowance_, not(0)) { conduitAllowance_ := 0 }
if iszero(eq(allowance_, conduitAllowance_)) { allowance_ := conduitAllowance_ }
}
// 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()`.
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_, SOLADY_ERC20_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, SOLADY_ERC20_TRANSFER_EVENT_SIGNATURE, shr(96, from_), shr(96, mload(0x0c)))
}
_afterTokenTransfer(from, to, amount);
return true;
}

function _spendAllowance(address owner, address spender, uint256 amount) internal virtual override {
if (spender == CONDUIT) {
uint256 allowance_ = super.allowance(owner, spender);
if (allowance_ == type(uint256).max) {
// Max allowance, no need to spend.
return;
} else if (allowance_ == 0) {
revert InsufficientAllowance();
}
}
super._spendAllowance(owner, spender, amount);
}
}
2 changes: 1 addition & 1 deletion test/reference/ExampleNFT.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ contract ExampleNFTTest is Test {
ExampleNFT testExampleNft;

function setUp() public {
testExampleNft = new ExampleNFT('Example', 'EXNFT');
testExampleNft = new ExampleNFT("Example", "EXNFT");
}

function testName() public {
Expand Down
61 changes: 61 additions & 0 deletions test/tokens/ERC20ConduitPreapproved_OZ.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {Test} from "forge-std/Test.sol";
import {ERC20_OZ} from "src/reference/tokens/erc20/ERC20Preapproved_OZ.sol";
import {CONDUIT} from "src/lib/Constants.sol";
import {IPreapprovalForAll} from "src/interfaces/IPreapprovalForAll.sol";

contract ERC20ConduitPreapproved_OZTest is Test, IPreapprovalForAll {
ERC20_OZ test;

function setUp() public {
test = new ERC20_OZ();
}

function testConstructorEvent() public {
vm.expectEmit(true, true, false, false);
emit PreapprovalForAll(CONDUIT, true);
new ERC20_OZ();
}

function testConduitPreapproved(address acct) public {
if (acct == address(0)) {
acct = address(1);
}
assertEq(test.allowance(acct, CONDUIT), type(uint256).max);
vm.prank(acct);
test.approve(CONDUIT, 0);
assertEq(test.allowance(acct, CONDUIT), 0);
vm.prank(acct);
test.approve(CONDUIT, 1 ether);
assertEq(test.allowance(acct, CONDUIT), 1 ether);
}

function testNormalApprovals(address acct, address operator) public {
if (acct == address(0)) {
acct = address(1);
}
if (operator == address(0)) {
operator = address(1);
}
vm.assume(operator != CONDUIT);
vm.assume(acct != operator);
assertEq(test.allowance(acct, operator), 0);
vm.prank(acct);
test.approve(operator, 1 ether);
assertEq(test.allowance(acct, operator), 1 ether);
vm.prank(acct);
test.approve(operator, 0);
assertEq(test.allowance(acct, operator), 0);
}

function testConduitCanTransfer(address acct) public {
if (acct == address(0)) {
acct = address(1);
}
test.mint(address(acct), 1);
vm.prank(CONDUIT);
test.transferFrom(address(acct), address(this), 1);
}
}
61 changes: 61 additions & 0 deletions test/tokens/ERC20ConduitPreapproved_Solady.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import {Test} from "forge-std/Test.sol";
import {ERC20_Solady} from "src/reference/tokens/erc20/ERC20Preapproved_Solady.sol";
import {CONDUIT} from "src/lib/Constants.sol";
import {IPreapprovalForAll} from "src/interfaces/IPreapprovalForAll.sol";

contract ERC20ConduitPreapproved_SoladyTest is Test, IPreapprovalForAll {
ERC20_Solady test;

function setUp() public {
test = new ERC20_Solady();
}

function testConstructorEvent() public {
vm.expectEmit(true, true, false, false);
emit PreapprovalForAll(CONDUIT, true);
new ERC20_Solady();
}

function testConduitPreapproved(address acct) public {
if (acct == address(0)) {
acct = address(1);
}
assertEq(test.allowance(acct, CONDUIT), type(uint256).max);
vm.prank(acct);
test.approve(CONDUIT, 0);
assertEq(test.allowance(acct, CONDUIT), 0);
vm.prank(acct);
test.approve(CONDUIT, 1 ether);
assertEq(test.allowance(acct, CONDUIT), 1 ether);
}

function testNormalApprovals(address acct, address operator) public {
if (acct == address(0)) {
acct = address(1);
}
if (operator == address(0)) {
operator = address(1);
}
vm.assume(operator != CONDUIT);
vm.assume(acct != operator);
assertEq(test.allowance(acct, operator), 0);
vm.prank(acct);
test.approve(operator, 1 ether);
assertEq(test.allowance(acct, operator), 1 ether);
vm.prank(acct);
test.approve(operator, 0);
assertEq(test.allowance(acct, operator), 0);
}

function testConduitCanTransfer(address acct) public {
if (acct == address(0)) {
acct = address(1);
}
test.mint(address(acct), 1);
vm.prank(CONDUIT);
test.transferFrom(address(acct), address(this), 1);
}
}
Loading