-
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.
Merge pull request #20 from ProjectOpenSea/ryan/erc20-conduit-preappr…
…oved add ERC20 preapproved conduit
- Loading branch information
Showing
8 changed files
with
329 additions
and
2 deletions.
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,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); | ||
} | ||
} |
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,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); | ||
} | ||
} |