From 12685b133826dc5978fc930ed38ea9e74f23c04b Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 28 Nov 2023 16:17:51 -0800 Subject: [PATCH] add ERC20 preapproved conduit --- src/lib/Constants.sol | 8 +- .../tokens/erc20/ERC20Preapproved_OZ.sol | 12 ++ .../tokens/erc20/ERC20Preapproved_Solady.sol | 18 ++ .../erc20/ERC20ConduitPreapproved_OZ.sol | 49 ++++++ .../erc20/ERC20ConduitPreapproved_Solady.sol | 165 ++++++++++++++++++ test/tokens/ERC20ConduitPreapproved_OZ.t.sol | 61 +++++++ .../ERC20ConduitPreapproved_Solady.t.sol | 61 +++++++ 7 files changed, 373 insertions(+), 1 deletion(-) create mode 100644 src/reference/tokens/erc20/ERC20Preapproved_OZ.sol create mode 100644 src/reference/tokens/erc20/ERC20Preapproved_Solady.sol create mode 100644 src/tokens/erc20/ERC20ConduitPreapproved_OZ.sol create mode 100644 src/tokens/erc20/ERC20ConduitPreapproved_Solady.sol create mode 100644 test/tokens/ERC20ConduitPreapproved_OZ.t.sol create mode 100644 test/tokens/ERC20ConduitPreapproved_Solady.t.sol diff --git a/src/lib/Constants.sol b/src/lib/Constants.sol index b9140c8..53b6e63 100644 --- a/src/lib/Constants.sol +++ b/src/lib/Constants.sol @@ -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; diff --git a/src/reference/tokens/erc20/ERC20Preapproved_OZ.sol b/src/reference/tokens/erc20/ERC20Preapproved_OZ.sol new file mode 100644 index 0000000..f442261 --- /dev/null +++ b/src/reference/tokens/erc20/ERC20Preapproved_OZ.sol @@ -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); + } +} diff --git a/src/reference/tokens/erc20/ERC20Preapproved_Solady.sol b/src/reference/tokens/erc20/ERC20Preapproved_Solady.sol new file mode 100644 index 0000000..4f66373 --- /dev/null +++ b/src/reference/tokens/erc20/ERC20Preapproved_Solady.sol @@ -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"; + } +} diff --git a/src/tokens/erc20/ERC20ConduitPreapproved_OZ.sol b/src/tokens/erc20/ERC20ConduitPreapproved_OZ.sol new file mode 100644 index 0000000..9eb0a6d --- /dev/null +++ b/src/tokens/erc20/ERC20ConduitPreapproved_OZ.sol @@ -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); + } +} diff --git a/src/tokens/erc20/ERC20ConduitPreapproved_Solady.sol b/src/tokens/erc20/ERC20ConduitPreapproved_Solady.sol new file mode 100644 index 0000000..1808c60 --- /dev/null +++ b/src/tokens/erc20/ERC20ConduitPreapproved_Solady.sol @@ -0,0 +1,165 @@ +// 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 newAllowance_ := allowance_ + if eq(allowance_, 0) { newAllowance_ := not(0) } + if eq(allowance_, not(0)) { newAllowance_ := 0 } + allowance_ := newAllowance_ + } + // 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); + } + + /* + function transferFrom(address from, address to, uint256 id) public payable virtual override { + _transfer(_by(from), from, to, id); + } + + function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + bool approved = super.isApprovedForAll(owner, operator); + return (operator == CONDUIT) ? !approved : approved; + } + + function setApprovalForAll(address operator, bool isApproved) public virtual override { + /// @solidity memory-safe-assembly + assembly { + // Convert to 0 or 1. + isApproved := iszero(iszero(isApproved)) + let isConduit := eq(operator, CONDUIT) + // if isConduit, flip isApproved, otherwise leave as is + let storedValue := + or( + // isConduit && !isApproved + and(isConduit, iszero(isApproved)), + // !isConduit && isApproved + and(iszero(isConduit), isApproved) + ) + // Update the `isApproved` for (`msg.sender`, `operator`). + mstore(0x1c, operator) + mstore(0x08, SOLADY_ERC20_MASTER_SLOT_SEED_MASKED) + mstore(0x00, caller()) + sstore(keccak256(0x0c, 0x30), storedValue) + // Emit the {ApprovalForAll} event. + mstore(0x00, isApproved) + log3(0x00, 0x20, _APPROVAL_FOR_ALL_EVENT_SIGNATURE, caller(), shr(96, shl(96, operator))) + } + } + + function _by(address from) internal view virtual returns (address result) { + if (msg.sender == CONDUIT) { + if (isApprovedForAll(from, CONDUIT)) { + return address(0); + } + } + return msg.sender; + } + */ +} diff --git a/test/tokens/ERC20ConduitPreapproved_OZ.t.sol b/test/tokens/ERC20ConduitPreapproved_OZ.t.sol new file mode 100644 index 0000000..b100dcb --- /dev/null +++ b/test/tokens/ERC20ConduitPreapproved_OZ.t.sol @@ -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); + } +} diff --git a/test/tokens/ERC20ConduitPreapproved_Solady.t.sol b/test/tokens/ERC20ConduitPreapproved_Solady.t.sol new file mode 100644 index 0000000..610459f --- /dev/null +++ b/test/tokens/ERC20ConduitPreapproved_Solady.t.sol @@ -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); + } +}