-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
373 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
*/ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |