Skip to content

Commit

Permalink
Merge pull request #20 from ProjectOpenSea/ryan/erc20-conduit-preappr…
Browse files Browse the repository at this point in the history
…oved

add ERC20 preapproved conduit
  • Loading branch information
0age authored Nov 29, 2023
2 parents e30a3a9 + 391bc98 commit 38aad7a
Show file tree
Hide file tree
Showing 8 changed files with 329 additions and 2 deletions.
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);
}
}

0 comments on commit 38aad7a

Please sign in to comment.