Skip to content

Commit

Permalink
Add ERC721CM and ERC721CMBasicRoyalties (#98)
Browse files Browse the repository at this point in the history
* Add ERC721CM and ERC721CMBasicRoyalties

* sync limit break contracts

* Add script to deploy ERC721CMBasicRoyalties

* Add test for ERC721CM

* Add test for ERC721CMBasicRoyalties

* update after LB's deployment

* update package

* update test
  • Loading branch information
channing-magiceden authored Feb 7, 2024
1 parent 14c908a commit 4936133
Show file tree
Hide file tree
Showing 13 changed files with 3,031 additions and 21 deletions.
649 changes: 649 additions & 0 deletions contracts/ERC721CM.sol

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions contracts/ERC721CMBasicRoyalties.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

import "@limitbreak/creator-token-standards/src/programmable-royalties/BasicRoyalties.sol";
import "./ERC721CM.sol";

/**
* @title ERC721CM with BasicRoyalties
*/
contract ERC721CMBasicRoyalties is ERC721CM, BasicRoyalties {
constructor(
string memory collectionName,
string memory collectionSymbol,
string memory tokenURISuffix,
uint256 maxMintableSupply,
uint256 globalWalletLimit,
address cosigner,
uint64 timestampExpirySeconds,
address mintCurrency,
address royaltyReceiver,
uint96 royaltyFeeNumerator)
ERC721CM(
collectionName,
collectionSymbol,
tokenURISuffix,
maxMintableSupply,
globalWalletLimit,
cosigner,
timestampExpirySeconds,
mintCurrency)
BasicRoyalties(royaltyReceiver, royaltyFeeNumerator) {}

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC2981, ERC721ACQueryable, IERC721A) returns (bool) {
return super.supportsInterface(interfaceId);
}
}
265 changes: 265 additions & 0 deletions contracts/creator-token-standards/CreatorTokenBase.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@limitbreak/creator-token-standards/src/access/OwnablePermissions.sol";
import "@limitbreak/creator-token-standards/src/interfaces/ICreatorToken.sol";
import "@limitbreak/creator-token-standards/src/interfaces/ICreatorTokenTransferValidator.sol";
import "@limitbreak/creator-token-standards/src/utils/TransferValidation.sol";
import "@openzeppelin/contracts/interfaces/IERC165.sol";

/**
* @title CreatorTokenBase
* @author Limit Break, Inc.
* @notice CreatorTokenBase is an abstract contract that provides basic functionality for managing token
* transfer policies through an implementation of ICreatorTokenTransferValidator. This contract is intended to be used
* as a base for creator-specific token contracts, enabling customizable transfer restrictions and security policies.
*
* <h4>Features:</h4>
* <ul>Ownable: This contract can have an owner who can set and update the transfer validator.</ul>
* <ul>TransferValidation: Implements the basic token transfer validation interface.</ul>
* <ul>ICreatorToken: Implements the interface for creator tokens, providing view functions for token security policies.</ul>
*
* <h4>Benefits:</h4>
* <ul>Provides a flexible and modular way to implement custom token transfer restrictions and security policies.</ul>
* <ul>Allows creators to enforce policies such as whitelisted operators and permitted contract receivers.</ul>
* <ul>Can be easily integrated into other token contracts as a base contract.</ul>
*
* <h4>Intended Usage:</h4>
* <ul>Use as a base contract for creator token implementations that require advanced transfer restrictions and
* security policies.</ul>
* <ul>Set and update the ICreatorTokenTransferValidator implementation contract to enforce desired policies for the
* creator token.</ul>
*/
abstract contract CreatorTokenBase is OwnablePermissions, TransferValidation, ICreatorToken {

/**
* @dev Thrown when the transfer validator address is the zero address
* @dev or it does not implement the `ICreatorTokenTransferValidator` interface.
*/
error CreatorTokenBase__InvalidTransferValidatorContract();

/// @dev Thrown when attempting to set transfer security settings before a transfer validator is set.
error CreatorTokenBase__SetTransferValidatorFirst();

/// @dev The default transfer validator address for calls to `setToDefaultSecurityPolicy`.
address public constant DEFAULT_TRANSFER_VALIDATOR = address(0x721C00182a990771244d7A71B9FA2ea789A3b433);

/// @dev The default transfer security level for calls to `setToDefaultSecurityPolicy`.
TransferSecurityLevels public constant DEFAULT_TRANSFER_SECURITY_LEVEL = TransferSecurityLevels.One;

/// @dev The default operator whitelist id for calls to `setToDefaultSecurityPolicy`.
uint120 public constant DEFAULT_OPERATOR_WHITELIST_ID = uint120(1);

ICreatorTokenTransferValidator private transferValidator;

/**
* @notice Allows the contract owner to set the transfer validator to the official validator contract
* and set the security policy to the recommended default settings.
* @dev May be overridden to change the default behavior of an individual collection.
*/
function setToDefaultSecurityPolicy() public virtual {
_requireCallerIsContractOwner();
setTransferValidator(DEFAULT_TRANSFER_VALIDATOR);
ICreatorTokenTransferValidator(DEFAULT_TRANSFER_VALIDATOR).setTransferSecurityLevelOfCollection(address(this), DEFAULT_TRANSFER_SECURITY_LEVEL);
ICreatorTokenTransferValidator(DEFAULT_TRANSFER_VALIDATOR).setOperatorWhitelistOfCollection(address(this), DEFAULT_OPERATOR_WHITELIST_ID);
}

/**
* @notice Allows the contract owner to set the transfer validator to a custom validator contract
* and set the security policy to their own custom settings.
*/
function setToCustomValidatorAndSecurityPolicy(
address validator,
TransferSecurityLevels level,
uint120 operatorWhitelistId,
uint120 permittedContractReceiversAllowlistId) public {
_requireCallerIsContractOwner();

setTransferValidator(validator);

ICreatorTokenTransferValidator(validator).
setTransferSecurityLevelOfCollection(address(this), level);

ICreatorTokenTransferValidator(validator).
setOperatorWhitelistOfCollection(address(this), operatorWhitelistId);

ICreatorTokenTransferValidator(validator).
setPermittedContractReceiverAllowlistOfCollection(address(this), permittedContractReceiversAllowlistId);
}

/**
* @notice Allows the contract owner to set the security policy to their own custom settings.
* @dev Reverts if the transfer validator has not been set.
*/
function setToCustomSecurityPolicy(
TransferSecurityLevels level,
uint120 operatorWhitelistId,
uint120 permittedContractReceiversAllowlistId) public {
_requireCallerIsContractOwner();

ICreatorTokenTransferValidator validator = getTransferValidator();
if (address(validator) == address(0)) {
revert CreatorTokenBase__SetTransferValidatorFirst();
}

validator.setTransferSecurityLevelOfCollection(address(this), level);
validator.setOperatorWhitelistOfCollection(address(this), operatorWhitelistId);
validator.setPermittedContractReceiverAllowlistOfCollection(address(this), permittedContractReceiversAllowlistId);
}

/**
* @notice Sets the transfer validator for the token contract.
*
* @dev Throws when provided validator contract is not the zero address and doesn't support
* the ICreatorTokenTransferValidator interface.
* @dev Throws when the caller is not the contract owner.
*
* @dev <h4>Postconditions:</h4>
* 1. The transferValidator address is updated.
* 2. The `TransferValidatorUpdated` event is emitted.
*
* @param transferValidator_ The address of the transfer validator contract.
*/
function setTransferValidator(address transferValidator_) public {
_requireCallerIsContractOwner();

bool isValidTransferValidator = false;

if(transferValidator_.code.length > 0) {
try IERC165(transferValidator_).supportsInterface(type(ICreatorTokenTransferValidator).interfaceId)
returns (bool supportsInterface) {
isValidTransferValidator = supportsInterface;
} catch {}
}

if(transferValidator_ != address(0) && !isValidTransferValidator) {
revert CreatorTokenBase__InvalidTransferValidatorContract();
}

emit TransferValidatorUpdated(address(transferValidator), transferValidator_);

transferValidator = ICreatorTokenTransferValidator(transferValidator_);
}

/**
* @notice Returns the transfer validator contract address for this token contract.
*/
function getTransferValidator() public view override returns (ICreatorTokenTransferValidator) {
return transferValidator;
}

/**
* @notice Returns the security policy for this token contract, which includes:
* Transfer security level, operator whitelist id, permitted contract receiver allowlist id.
*/
function getSecurityPolicy() public view override returns (CollectionSecurityPolicy memory) {
if (address(transferValidator) != address(0)) {
return transferValidator.getCollectionSecurityPolicy(address(this));
}

return CollectionSecurityPolicy({
transferSecurityLevel: TransferSecurityLevels.Recommended,
operatorWhitelistId: 0,
permittedContractReceiversId: 0
});
}

/**
* @notice Returns the list of all whitelisted operators for this token contract.
* @dev This can be an expensive call and should only be used in view-only functions.
*/
function getWhitelistedOperators() public view override returns (address[] memory) {
if (address(transferValidator) != address(0)) {
return transferValidator.getWhitelistedOperators(
transferValidator.getCollectionSecurityPolicy(address(this)).operatorWhitelistId);
}

return new address[](0);
}

/**
* @notice Returns the list of permitted contract receivers for this token contract.
* @dev This can be an expensive call and should only be used in view-only functions.
*/
function getPermittedContractReceivers() public view override returns (address[] memory) {
if (address(transferValidator) != address(0)) {
return transferValidator.getPermittedContractReceivers(
transferValidator.getCollectionSecurityPolicy(address(this)).permittedContractReceiversId);
}

return new address[](0);
}

/**
* @notice Checks if an operator is whitelisted for this token contract.
* @param operator The address of the operator to check.
*/
function isOperatorWhitelisted(address operator) public view override returns (bool) {
if (address(transferValidator) != address(0)) {
return transferValidator.isOperatorWhitelisted(
transferValidator.getCollectionSecurityPolicy(address(this)).operatorWhitelistId, operator);
}

return false;
}

/**
* @notice Checks if a contract receiver is permitted for this token contract.
* @param receiver The address of the receiver to check.
*/
function isContractReceiverPermitted(address receiver) public view override returns (bool) {
if (address(transferValidator) != address(0)) {
return transferValidator.isContractReceiverPermitted(
transferValidator.getCollectionSecurityPolicy(address(this)).permittedContractReceiversId, receiver);
}

return false;
}

/**
* @notice Determines if a transfer is allowed based on the token contract's security policy. Use this function
* to simulate whether or not a transfer made by the specified `caller` from the `from` address to the `to`
* address would be allowed by this token's security policy.
*
* @notice This function only checks the security policy restrictions and does not check whether token ownership
* or approvals are in place.
*
* @param caller The address of the simulated caller.
* @param from The address of the sender.
* @param to The address of the receiver.
* @return True if the transfer is allowed, false otherwise.
*/
function isTransferAllowed(address caller, address from, address to) public view override returns (bool) {
if (address(transferValidator) != address(0)) {
try transferValidator.applyCollectionTransferPolicy(caller, from, to) {
return true;
} catch {
return false;
}
}
return true;
}

/**
* @dev Pre-validates a token transfer, reverting if the transfer is not allowed by this token's security policy.
* Inheriting contracts are responsible for overriding the _beforeTokenTransfer function, or its equivalent
* and calling _validateBeforeTransfer so that checks can be properly applied during token transfers.
*
* @dev Throws when the transfer doesn't comply with the collection's transfer policy, if the transferValidator is
* set to a non-zero address.
*
* @param caller The address of the caller.
* @param from The address of the sender.
* @param to The address of the receiver.
*/
function _preValidateTransfer(
address caller,
address from,
address to,
uint256 /*tokenId*/,
uint256 /*value*/) internal virtual override {
if (address(transferValidator) != address(0)) {
transferValidator.applyCollectionTransferPolicy(caller, from, to);
}
}
}
51 changes: 51 additions & 0 deletions contracts/creator-token-standards/ERC721ACQueryable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "./CreatorTokenBase.sol";
import "erc721a/contracts/extensions/ERC721AQueryable.sol";

/**
* @title ERC721ACQueryable
*/
abstract contract ERC721ACQueryable is ERC721AQueryable, CreatorTokenBase {

constructor(string memory name_, string memory symbol_) CreatorTokenBase() ERC721A(name_, symbol_) {}

function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721A, IERC721A) returns (bool) {
return interfaceId == type(ICreatorToken).interfaceId || super.supportsInterface(interfaceId);
}

/// @dev Ties the erc721a _beforeTokenTransfers hook to more granular transfer validation logic
function _beforeTokenTransfers(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual override {
for (uint256 i = 0; i < quantity;) {
_validateBeforeTransfer(from, to, startTokenId + i);
unchecked {
++i;
}
}
}

/// @dev Ties the erc721a _afterTokenTransfer hook to more granular transfer validation logic
function _afterTokenTransfers(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual override {
for (uint256 i = 0; i < quantity;) {
_validateAfterTransfer(from, to, startTokenId + i);
unchecked {
++i;
}
}
}

function _msgSenderERC721A() internal view virtual override returns (address) {
return _msgSender();
}
}
10 changes: 10 additions & 0 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ const config: HardhatUserConfig = {
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
sepolia: {
url:
process.env.SEPOLIA_URL || 'https://ethereum-sepolia.publicnode.com',
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
mainnet: {
url: process.env.MAINNET_URL || '',
accounts:
Expand Down Expand Up @@ -146,6 +152,10 @@ task('deploy', 'Deploy ERC721M')
'openedition',
'whether or not a open edition mint (unlimited supply, 999,999,999)',
)
.addFlag('useerc721c', 'whether or not to use ERC721C')
.addFlag('useerc2198', 'whether or not to use ERC2198')
.addOptionalParam('erc2198royaltyreceiver', 'erc2198 royalty receiver address')
.addOptionalParam('erc2198royaltyfeenumerator', 'erc2198 royalty fee numerator')
.setAction(deploy);

task('setBaseURI', 'Set the base uri')
Expand Down
Loading

0 comments on commit 4936133

Please sign in to comment.