Skip to content

Commit

Permalink
Add ERC721OperatorEnforced
Browse files Browse the repository at this point in the history
  • Loading branch information
ScreamingHawk committed Dec 4, 2024
1 parent 3f66a7d commit 67e750f
Show file tree
Hide file tree
Showing 14 changed files with 687 additions and 2 deletions.
2 changes: 2 additions & 0 deletions scripts/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const DEPLOYABLE_CONTRACT_NAMES = [
'ERC721ItemsFactory',
'ERC721SaleFactory',
'ERC721SoulboundFactory',
'ERC721OperatorEnforcedFactory',
'ERC1155ItemsFactory',
'ERC1155SaleFactory',
'ERC1155SoulboundFactory',
Expand All @@ -17,6 +18,7 @@ export const PROXIED_TOKEN_CONTRACT_NAMES = [
'ERC721Items',
'ERC721Sale',
'ERC721Soulbound',
'ERC721OperatorEnforced',
'ERC1155Items',
'ERC1155Sale',
'ERC1155Soulbound',
Expand Down
6 changes: 6 additions & 0 deletions src/tokens/ERC721/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ This folder contains contracts that are pre-configured for specific use cases.

The `ERC721Items` contract is a preset that configures the `ERC721BaseToken` contract to allow minting of tokens. It adds a `MINTER_ROLE` and a `mint(address to, uint256 amount)` function that can only be called by accounts with the `MINTER_ROLE`.

### Operator Enforced

The `ERC721OperatorEnforced` contract is a preset that configures the `ERC721BaseToken` contract to allow for operator enforced transfers and approvals. It adds an `operatorAllowlist` parameter to the constructor that must point to an `OperatorAllowlist` contract.

For more information on Operator Allowlist Enforcement, please refer to the [Immutable Operator Allowlist Specification](https://docs.immutable.com/products/zkevm/minting/royalties/allowlist-spec) documentation.

### Sale

The `ERC721Sale` contract is a preset that configures the `ERC721BaseToken` contract to allow for the sale of tokens. It adds a `mint(address to, uint256 amount, bytes32[] memory proof)` function allows for the minting of tokens under various conditions.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.19;

import {ERC721Items} from "@0xsequence/contracts-library/tokens/ERC721/presets/items/ERC721Items.sol";
import {OperatorAllowlistEnforced} from
"@0xsequence/contracts-library/tokens/common/immutable/OperatorAllowlistEnforced.sol";
import {ERC721A, IERC721A} from "erc721a/contracts/ERC721A.sol";

error InvalidInitialization();

/**
* An implementation of ERC-721 that enforces an operator allowlist.
* See {OperatorAllowlistEnforced} for more details.
*/
contract ERC721OperatorEnforced is ERC721Items, OperatorAllowlistEnforced {
constructor() ERC721Items() {}

/**
* Initialize contract.
* @param owner The owner of the contract
* @param tokenName Name of the token
* @param tokenSymbol Symbol of the token
* @param tokenBaseURI Base URI of the token
* @param tokenContractURI Contract URI of the token
* @param royaltyReceiver Address of who should be sent the royalty payment
* @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500)
* @param operatorAllowlist Address of the operator allowlist
* @dev This should be called immediately after deployment.
*/
function initialize(
address owner,
string memory tokenName,
string memory tokenSymbol,
string memory tokenBaseURI,
string memory tokenContractURI,
address royaltyReceiver,
uint96 royaltyFeeNumerator,
address operatorAllowlist
) public virtual {
_setOperatorAllowlistRegistry(operatorAllowlist);
ERC721Items.initialize(
owner, tokenName, tokenSymbol, tokenBaseURI, tokenContractURI, royaltyReceiver, royaltyFeeNumerator
);
}

//
// Operator Allowlist
//

/**
* Set the operator allowlist registry.
* @param operatorAllowlist Address of the operator allowlist
*/
function setOperatorAllowlistRegistry(address operatorAllowlist) external onlyRole(DEFAULT_ADMIN_ROLE) {
_setOperatorAllowlistRegistry(operatorAllowlist);
}

/// @inheritdoc ERC721A
function _approve(address to, uint256 tokenId, bool approvalCheck) internal override validateApproval(to) {
super._approve(to, tokenId, approvalCheck);
}

/// @inheritdoc ERC721A
function setApprovalForAll(address operator, bool approved)
public
override(ERC721A, IERC721A)
validateApproval(operator)
{
super.setApprovalForAll(operator, approved);
}

/// @inheritdoc ERC721A
function _beforeTokenTransfers(address from, address to, uint256 startTokenId, uint256 quantity)
internal
override
{
if (from != address(0)) {
// Ignore validation on minting
_validateTransfer(from, to);
}
super._beforeTokenTransfers(from, to, startTokenId, quantity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.19;

import {
IERC721OperatorEnforcedFactory,
IERC721OperatorEnforcedFactoryFunctions
} from "@0xsequence/contracts-library/tokens/ERC721/presets/operator-enforced/IERC721OperatorEnforcedFactory.sol";
import {ERC721OperatorEnforced} from
"@0xsequence/contracts-library/tokens/ERC721/presets/operator-enforced/ERC721OperatorEnforced.sol";
import {SequenceProxyFactory} from "@0xsequence/contracts-library/proxies/SequenceProxyFactory.sol";

/**
* Deployer of ERC-721 Operator Enforced proxies.
*/
contract ERC721OperatorEnforcedFactory is IERC721OperatorEnforcedFactory, SequenceProxyFactory {
/**
* Creates an ERC-721 Operator Enforced Factory.
* @param factoryOwner The owner of the ERC-721 Operator Enforced Factory
*/
constructor(address factoryOwner) {
ERC721OperatorEnforced impl = new ERC721OperatorEnforced();
SequenceProxyFactory._initialize(address(impl), factoryOwner);
}

/// @inheritdoc IERC721OperatorEnforcedFactoryFunctions
function deploy(
address proxyOwner,
address tokenOwner,
string memory name,
string memory symbol,
string memory baseURI,
string memory contractURI,
address royaltyReceiver,
uint96 royaltyFeeNumerator,
address operatorAllowlist
) external returns (address proxyAddr) {
bytes32 salt = keccak256(
abi.encode(
tokenOwner, name, symbol, baseURI, contractURI, royaltyReceiver, royaltyFeeNumerator, operatorAllowlist
)
);
proxyAddr = _createProxy(salt, proxyOwner, "");
ERC721OperatorEnforced(proxyAddr).initialize(
tokenOwner, name, symbol, baseURI, contractURI, royaltyReceiver, royaltyFeeNumerator, operatorAllowlist
);
emit ERC721OperatorEnforcedDeployed(proxyAddr);
return proxyAddr;
}

/// @inheritdoc IERC721OperatorEnforcedFactoryFunctions
function determineAddress(
address proxyOwner,
address tokenOwner,
string memory name,
string memory symbol,
string memory baseURI,
string memory contractURI,
address royaltyReceiver,
uint96 royaltyFeeNumerator,
address operatorAllowlist
) external view returns (address proxyAddr) {
bytes32 salt = keccak256(
abi.encode(
tokenOwner, name, symbol, baseURI, contractURI, royaltyReceiver, royaltyFeeNumerator, operatorAllowlist
)
);
return _computeProxyAddress(salt, proxyOwner, "");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.19;

interface IERC721OperatorEnforcedFactoryFunctions {
/**
* Creates an ERC-721 Operator Enforced proxy.
* @param proxyOwner The owner of the ERC-721 Operator Enforced proxy
* @param tokenOwner The owner of the ERC-721 Operator Enforced implementation
* @param name The name of the ERC-721 Operator Enforced proxy
* @param symbol The symbol of the ERC-721 Operator Enforced proxy
* @param baseURI The base URI of the ERC-721 Operator Enforced proxy
* @param contractURI The contract URI of the ERC-721 Operator Enforced proxy
* @param royaltyReceiver Address of who should be sent the royalty payment
* @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500)
* @param operatorAllowlist Address of the operator allowlist
* @return proxyAddr The address of the ERC-721 Operator Enforced Proxy
*/
function deploy(
address proxyOwner,
address tokenOwner,
string memory name,
string memory symbol,
string memory baseURI,
string memory contractURI,
address royaltyReceiver,
uint96 royaltyFeeNumerator,
address operatorAllowlist
) external returns (address proxyAddr);

/**
* Computes the address of a proxy instance.
* @param proxyOwner The owner of the ERC-721 Operator Enforced proxy
* @param tokenOwner The owner of the ERC-721 Operator Enforced implementation
* @param name The name of the ERC-721 Operator Enforced proxy
* @param symbol The symbol of the ERC-721 Operator Enforced proxy
* @param baseURI The base URI of the ERC-721 Operator Enforced proxy
* @param contractURI The contract URI of the ERC-721 Operator Enforced proxy
* @param royaltyReceiver Address of who should be sent the royalty payment
* @param royaltyFeeNumerator The royalty fee numerator in basis points (e.g. 15% would be 1500)
* @param operatorAllowlist Address of the operator allowlist
* @return proxyAddr The address of the ERC-721 Operator Enforced Proxy
*/
function determineAddress(
address proxyOwner,
address tokenOwner,
string memory name,
string memory symbol,
string memory baseURI,
string memory contractURI,
address royaltyReceiver,
uint96 royaltyFeeNumerator,
address operatorAllowlist
) external returns (address proxyAddr);
}

interface IERC721OperatorEnforcedFactorySignals {
/**
* Event emitted when a new ERC-721 Operator Enforced proxy contract is deployed.
* @param proxyAddr The address of the deployed proxy.
*/
event ERC721OperatorEnforcedDeployed(address proxyAddr);
}

interface IERC721OperatorEnforcedFactory is
IERC721OperatorEnforcedFactoryFunctions,
IERC721OperatorEnforcedFactorySignals
{}
14 changes: 14 additions & 0 deletions src/tokens/common/immutable/IOperatorAllowlist.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright Immutable Pty Ltd 2018 - 2023
// SPDX-License-Identifier: Apache 2.0
pragma solidity 0.8.19;

/**
* @notice Required interface of an OperatorAllowlist compliant contract
*/
interface IOperatorAllowlist {
/**
* @notice Returns true if an address is Allowlisted false otherwise
* @param target the address to be checked against the Allowlist
*/
function isAllowlisted(address target) external view returns (bool);
}
122 changes: 122 additions & 0 deletions src/tokens/common/immutable/OperatorAllowlistEnforced.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright Immutable Pty Ltd 2018 - 2023
// SPDX-License-Identifier: Apache 2.0
// slither-disable-start calls-loop
pragma solidity 0.8.19;

// Allowlist Registry
import {IOperatorAllowlist} from "./IOperatorAllowlist.sol";

// Interface
import {IERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";

// Errors
import {OperatorAllowlistEnforcementErrors} from "./OperatorAllowlistEnforcementErrors.sol";

/*
OperatorAllowlistEnforced is an abstract contract that token contracts can inherit in order to set the
address of the OperatorAllowlist registry that it will interface with, so that the token contract may
enable the restriction of approvals and transfers to allowlisted users.
OperatorAllowlistEnforced is not designed to be upgradeable or extended.
*/

abstract contract OperatorAllowlistEnforced is OperatorAllowlistEnforcementErrors {
/// ===== State Variables =====

/// @notice Interface that implements the `IOperatorAllowlist` interface
IOperatorAllowlist public operatorAllowlist;

/// ===== Events =====

/// @notice Emitted whenever the transfer Allowlist registry is updated
event OperatorAllowlistRegistryUpdated(address oldRegistry, address newRegistry);

/// ===== Modifiers =====

/**
* @notice Internal function to validate an approval, according to whether the target is an EOA or Allowlisted
* @param targetApproval the address of the approval target to be validated
*/
modifier validateApproval(address targetApproval) {
_validateApproval(targetApproval);
_;
}

/**
* @notice Internal function to validate an approval, according to whether the target is an EOA or Allowlisted
* @param targetApproval the address of the approval target to be validated
*/
function _validateApproval(address targetApproval) internal view {
// Check for:
// 1. approver is an EOA. Contract constructor is handled as transfers 'from' are blocked
// 2. approver is address or bytecode is allowlisted
if (msg.sender.code.length != 0 && !operatorAllowlist.isAllowlisted(msg.sender)) {
revert ApproverNotInAllowlist(msg.sender);
}

// Check for:
// 1. approval target is an EOA
// 2. approval target address is Allowlisted or target address bytecode is Allowlisted
if (targetApproval.code.length != 0 && !operatorAllowlist.isAllowlisted(targetApproval)) {
revert ApproveTargetNotInAllowlist(targetApproval);
}
}

/**
* @notice Internal function to validate a transfer, according to whether the calling address,
* from address and to address is an EOA or Allowlisted
* @param from the address of the from target to be validated
* @param to the address of the to target to be validated
*/
modifier validateTransfer(address from, address to) {
_validateTransfer(from, to);
_;
}

/**
* @notice Internal function to validate a transfer, according to whether the calling address,
* from address and to address is an EOA or Allowlisted
* @param from the address of the from target to be validated
* @param to the address of the to target to be validated
*/
function _validateTransfer(address from, address to) internal view {
// Check for:
// 1. caller is an EOA
// 2. caller is Allowlisted or is the calling address bytecode is Allowlisted
if (
msg.sender != tx.origin // solhint-disable-line avoid-tx-origin
&& !operatorAllowlist.isAllowlisted(msg.sender)
) {
revert CallerNotInAllowlist(msg.sender);
}

// Check for:
// 1. from is an EOA
// 2. from is Allowlisted or from address bytecode is Allowlisted
if (from.code.length != 0 && !operatorAllowlist.isAllowlisted(from)) {
revert TransferFromNotInAllowlist(from);
}

// Check for:
// 1. to is an EOA
// 2. to is Allowlisted or to address bytecode is Allowlisted
if (to.code.length != 0 && !operatorAllowlist.isAllowlisted(to)) {
revert TransferToNotInAllowlist(to);
}
}

/// ===== External functions =====

/**
* @notice Internal function to set the operator allowlist the calling contract will interface with
* @param _operatorAllowlist the address of the Allowlist registry
*/
function _setOperatorAllowlistRegistry(address _operatorAllowlist) internal {
if (!IERC165(_operatorAllowlist).supportsInterface(type(IOperatorAllowlist).interfaceId)) {
revert AllowlistDoesNotImplementIOperatorAllowlist();
}

emit OperatorAllowlistRegistryUpdated(address(operatorAllowlist), _operatorAllowlist);
operatorAllowlist = IOperatorAllowlist(_operatorAllowlist);
}
}
// slither-disable-end calls-loop
Loading

0 comments on commit 67e750f

Please sign in to comment.