Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signature-based minting restriction #7

Draft
wants to merge 14 commits into
base: master
Choose a base branch
from
Draft
91 changes: 91 additions & 0 deletions tokens/contracts/access/MinterAccessControl.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/EnumerableSetUpgradeable.sol";
import "../erc-1271/ERC1271Validator.sol";

contract MinterAccessControl is Initializable, OwnableUpgradeable, ERC1271Validator {
using EnumerableSetUpgradeable for EnumerableSetUpgradeable.AddressSet;

EnumerableSetUpgradeable.AddressSet private _minters;
bool public minterAccessControlEnabled;

event MinterAccessControlEnabled();
event MinterAccessControlDisabled();
event MinterGranted(address indexed account);
event MinterRevoked(address indexed account);

function __MinterAccessControl_init() internal initializer {
__Ownable_init_unchained();
__EIP712_init_unchained("MintControl", "1");
__MinterAccessControl_init_unchained();
}

function __MinterAccessControl_init_unchained() internal initializer {
}

/**
* @dev Enable minter control
* When enabled, only addresses added to `grantMinter` will be allowed to mint
*/
function enableMinterAccessControl() external onlyOwner {
require(!minterAccessControlEnabled, "MinterAccessControl: Already enabled");
minterAccessControlEnabled = true;
emit MinterAccessControlEnabled();
}

/**
* @dev Disable minter control
*/
function disableMinterAccessControl() external onlyOwner {
require(minterAccessControlEnabled, "MinterAccessControl: Already disabled");
minterAccessControlEnabled = false;
emit MinterAccessControlDisabled();
}

/**
* @dev Add `_minter` to the list of allowed minters.
*/
function grantMinter(address _minter) external onlyOwner {
require(!_minters.contains(_minter), 'MinterAccessControl: Already minter');
_minters.add(_minter);
emit MinterGranted(_minter);
}

/**
* @dev Revoke `_minter` from the list of allowed minters.
*/
function revokeMinter(address _minter) external onlyOwner {
require(_minters.contains(_minter), 'MinterAccessControl: Not minter');
_minters.remove(_minter);
emit MinterRevoked(_minter);
}

/**
* @dev Returns `true` if minterControl is not enabled or `account` has been granted to minters.
*/
function isValidMinter(address account) public view returns (bool) {
return !minterAccessControlEnabled || _minters.contains(account);
}

/**
* @dev Returns `true` if minterControl is not enabled or `signature` is valid for `structHash` and signer has been granted to minters role.
* uses EIP-1271 to validate
*/
function isValidMinterSignature(bytes32 structHash, bytes memory signature) public view returns (bool) {
if (!minterAccessControlEnabled)
return true;

for (uint i = 0; i < _minters.length(); i++) {
if (isValid1271(_minters.at(i), structHash, signature))
return true;
}

return false;
}

uint256[50] private __gap;
}
15 changes: 13 additions & 2 deletions tokens/contracts/erc-1155/ERC1155Lazy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import "@rarible/royalties-upgradeable/contracts/RoyaltiesV2Upgradeable.sol";
import "@rarible/lazy-mint/contracts/erc-1155/IERC1155LazyMint.sol";
import "./Mint1155Validator.sol";
import "./ERC1155BaseURI.sol";
import "../access/MinterAccessControl.sol";

abstract contract ERC1155Lazy is IERC1155LazyMint, ERC1155BaseURI, Mint1155Validator, RoyaltiesV2Upgradeable, RoyaltiesV2Impl {
abstract contract ERC1155Lazy is IERC1155LazyMint, ERC1155BaseURI, Mint1155Validator, RoyaltiesV2Upgradeable, RoyaltiesV2Impl, MinterAccessControl {
using SafeMathUpgradeable for uint;

bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7;
Expand Down Expand Up @@ -62,11 +63,21 @@ abstract contract ERC1155Lazy is IERC1155LazyMint, ERC1155BaseURI, Mint1155Valid
require(_amount > 0, "amount incorrect");

if (supply[data.tokenId] == 0) {
bool signedByOperator = data.signatures.length > data.creators.length;

require(minter == data.creators[0].account, "tokenId incorrect");
require(data.supply > 0, "supply incorrect");
require(data.creators.length == data.signatures.length);
require(data.creators.length == data.signatures.length - (signedByOperator ? 1 : 0));

bytes32 hash = LibERC1155LazyMint.hash(data);
if (signedByOperator) {
// first signature after creators must be an operator
require(isValidMinterSignature(hash, data.signatures[data.creators.length]), "ERC1155: invalid operator signature");
}
else {
// minter must be granted minter role
require(isValidMinter(minter), "ERC1155: minter not granted");
}
for (uint i = 0; i < data.creators.length; i++) {
address creator = data.creators[i].account;
if (creator != sender) {
Expand Down
1 change: 1 addition & 0 deletions tokens/contracts/erc-1155/ERC1155Rarible.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ contract ERC1155Rarible is ERC1155Base {
__ERC1155Burnable_init_unchained();
__RoyaltiesV2Upgradeable_init_unchained();
__ERC1155Base_init_unchained(_name, _symbol);
__MinterAccessControl_init_unchained();
_setBaseURI(baseURI);
}

Expand Down
15 changes: 7 additions & 8 deletions tokens/contracts/erc-1271/ERC1271Validator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ abstract contract ERC1271Validator is EIP712Upgradeable {
bytes4 constant internal MAGICVALUE = 0x1626ba7e;

function validate1271(address signer, bytes32 structHash, bytes memory signature) internal view {
require(isValid1271(signer, structHash, signature), SIGNATURE_ERROR);
}

function isValid1271(address signer, bytes32 structHash, bytes memory signature) internal view returns (bool) {
bytes32 hash = _hashTypedDataV4(structHash);
if (signer.isContract()) {
require(
ERC1271(signer).isValidSignature(hash, signature) == MAGICVALUE,
SIGNATURE_ERROR
);
return ERC1271(signer).isValidSignature(hash, signature) == MAGICVALUE;
} else {
require(
hash.recover(signature) == signer,
SIGNATURE_ERROR
);
return hash.recover(signature) == signer;
}
}

uint256[50] private __gap;
}
14 changes: 12 additions & 2 deletions tokens/contracts/erc-721-minimal/ERC721LazyMinimal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import "@rarible/lazy-mint/contracts/erc-721/IERC721LazyMint.sol";
import "../Mint721Validator.sol";
import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol";
import "./ERC721URI.sol";
import "../access/MinterAccessControl.sol";

abstract contract ERC721LazyMinimal is IERC721LazyMint, ERC721UpgradeableMinimal, Mint721Validator, RoyaltiesV2Upgradeable, RoyaltiesV2Impl, ERC721URI {
abstract contract ERC721LazyMinimal is IERC721LazyMint, ERC721UpgradeableMinimal, Mint721Validator, RoyaltiesV2Upgradeable, RoyaltiesV2Impl, ERC721URI, MinterAccessControl {
using SafeMathUpgradeable for uint;

bytes4 private constant _INTERFACE_ID_ERC165 = 0x01ffc9a7;
Expand Down Expand Up @@ -50,12 +51,21 @@ abstract contract ERC721LazyMinimal is IERC721LazyMint, ERC721UpgradeableMinimal
function mintAndTransfer(LibERC721LazyMint.Mint721Data memory data, address to) public override virtual {
address minter = address(data.tokenId >> 96);
address sender = _msgSender();
bool signedByOperator = data.signatures.length > data.creators.length;

require(minter == data.creators[0].account, "tokenId incorrect");
require(data.creators.length == data.signatures.length);
require(data.creators.length == data.signatures.length - (signedByOperator ? 1 : 0));
require(minter == sender || isApprovedForAll(minter, sender), "ERC721: transfer caller is not owner nor approved");

bytes32 hash = LibERC721LazyMint.hash(data);
if (signedByOperator) {
// first signature after creators must be an operator
require(isValidMinterSignature(hash, data.signatures[data.creators.length]), "ERC721: invalid operator signature");
}
else {
// minter must be granted minter role
require(isValidMinter(minter), "ERC721: minter not granted");
}
for (uint i = 0; i < data.creators.length; i++) {
address creator = data.creators[i].account;
if (creator != sender) {
Expand Down
1 change: 1 addition & 0 deletions tokens/contracts/erc-721-minimal/ERC721RaribleMinimal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ contract ERC721RaribleMinimal is ERC721BaseMinimal {
__Ownable_init_unchained();
__ERC721Burnable_init_unchained();
__Mint721Validator_init_unchained();
__MinterAccessControl_init_unchained();
__HasContractURI_init_unchained(contractURI);
__ERC721_init_unchained(_name, _symbol);
emit CreateERC721Rarible(_msgSender(), _name, _symbol);
Expand Down
17 changes: 14 additions & 3 deletions tokens/contracts/erc-721/ERC721Lazy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import "@rarible/royalties/contracts/impl/RoyaltiesV2Impl.sol";
import "@rarible/royalties-upgradeable/contracts/RoyaltiesV2Upgradeable.sol";
import "@rarible/lazy-mint/contracts/erc-721/IERC721LazyMint.sol";
import "../Mint721Validator.sol";
import "../access/MinterAccessControl.sol";

abstract contract ERC721Lazy is IERC721LazyMint, ERC721Upgradeable, Mint721Validator, RoyaltiesV2Upgradeable, RoyaltiesV2Impl {
abstract contract ERC721Lazy is IERC721LazyMint, ERC721Upgradeable, Mint721Validator, RoyaltiesV2Upgradeable, RoyaltiesV2Impl, MinterAccessControl {
using SafeMathUpgradeable for uint;
using EnumerableSetUpgradeable for EnumerableSetUpgradeable.UintSet;
using EnumerableMapUpgradeable for EnumerableMapUpgradeable.UintToAddressMap;
Expand Down Expand Up @@ -50,12 +51,22 @@ abstract contract ERC721Lazy is IERC721LazyMint, ERC721Upgradeable, Mint721Valid
function mintAndTransfer(LibERC721LazyMint.Mint721Data memory data, address to) public override virtual {
address minter = address(data.tokenId >> 96);
address sender = _msgSender();
bool signedByOperator = data.signatures.length > data.creators.length;

require(minter == data.creators[0].account, "tokenId incorrect");
require(data.creators.length == data.signatures.length);
require(data.creators.length == data.signatures.length - (signedByOperator ? 1 : 0));
require(minter == sender || isApprovedForAll(minter, sender), "ERC721: transfer caller is not owner nor approved");

bytes32 hash = LibERC721LazyMint.hash(data);
if (signedByOperator) {
// first signature after creators must be an operator
require(isValidMinterSignature(hash, data.signatures[data.creators.length]), "ERC721: invalid operator signature");
}
else {
// minter must be granted minter role
require(isValidMinter(minter), "ERC721: minter not granted");
}

for (uint i = 0; i < data.creators.length; i++) {
address creator = data.creators[i].account;
if (creator != sender) {
Expand Down
1 change: 1 addition & 0 deletions tokens/contracts/erc-721/ERC721Rarible.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ contract ERC721Rarible is ERC721Base {
__Ownable_init_unchained();
__ERC721Burnable_init_unchained();
__Mint721Validator_init_unchained();
__MinterAccessControl_init_unchained();
__HasContractURI_init_unchained(contractURI);
__ERC721_init_unchained(_name, _symbol);
}
Expand Down
140 changes: 140 additions & 0 deletions tokens/test/erc-1155/ERC1155Rarible.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,146 @@ contract("ERC1155Rarible", accounts => {
);
});

it("mint and transfer with minter access control", async () => {
const minter = accounts[1];
let transferTo = accounts[2];

const tokenId = minter + "b00000000000000000000001";
const tokenURI = "//uri";
let supply = 5;
let mint = 2;

await token.enableMinterAccessControl({from: tokenOwner});
assert.equal(await token.minterAccessControlEnabled(), true);

await expectThrow(
token.mintAndTransfer([tokenId, tokenURI, supply, creators([minter]), [], [zeroWord]], transferTo, mint, {from: minter})
);

await token.grantMinter(minter, {from: tokenOwner});
assert.equal(await token.isValidMinter(minter), true);
assert.equal(await token.isValidMinter(transferTo), false);

await token.mintAndTransfer([tokenId, tokenURI, supply, creators([minter]), [], [zeroWord]], transferTo, mint, {from: minter});
assert.equal(await token.uri(tokenId), "ipfs:/" + tokenURI);
assert.equal(await token.balanceOf(transferTo, tokenId), mint);
assert.equal(await token.balanceOf(minter, tokenId), 0);
});

it("mint and transfer with minter access control and minter signature", async () => {
const minter = accounts[1];
let transferTo = accounts[2];

const tokenId = minter + "b00000000000000000000001";
const tokenURI = "//uri";
let supply = 5;
let mint = 2;

const signature = await getSignature(tokenId, tokenURI, supply, creators([minter]), [], minter);

let whiteListProxy = accounts[5];
await token.setDefaultApproval(whiteListProxy, true, {from: tokenOwner});

await token.enableMinterAccessControl({from: tokenOwner});
assert.equal(await token.minterAccessControlEnabled(), true);

await expectThrow(
token.mintAndTransfer([tokenId, tokenURI, supply, creators([minter]), [], [signature]], transferTo, mint, {from: whiteListProxy})
);

await token.grantMinter(minter, {from: tokenOwner});
assert.equal(await token.isValidMinter(minter), true);
assert.equal(await token.isValidMinter(whiteListProxy), false);

await token.mintAndTransfer([tokenId, tokenURI, supply, creators([minter]), [], [signature]], transferTo, mint, {from: whiteListProxy})
assert.equal(await token.uri(tokenId), "ipfs:/" + tokenURI);
assert.equal(await token.balanceOf(transferTo, tokenId), mint);
assert.equal(await token.balanceOf(minter, tokenId), 0);
});

it("mint and transfer with minter access control and wrong minter signature", async () => {
const minter = accounts[1];
let transferTo = accounts[2];

const tokenId = minter + "b00000000000000000000001";
const tokenURI = "//uri";
let supply = 5;
let mint = 2;

const signature = await getSignature(tokenId, tokenURI, supply, creators([minter]), [], transferTo);

let whiteListProxy = accounts[5];
await token.setDefaultApproval(whiteListProxy, true, {from: tokenOwner});

await token.enableMinterAccessControl({from: tokenOwner});
assert.equal(await token.minterAccessControlEnabled(), true);

await expectThrow(
token.mintAndTransfer([tokenId, tokenURI, supply, creators([minter]), [], [signature]], transferTo, mint, {from: whiteListProxy})
);

await token.grantMinter(minter, {from: tokenOwner});
assert.equal(await token.isValidMinter(minter), true);
assert.equal(await token.isValidMinter(whiteListProxy), false);

await expectThrow(
token.mintAndTransfer([tokenId, tokenURI, supply, creators([minter]), [], [signature]], transferTo, mint, {from: whiteListProxy})
);
});

it("mint and transfer with minter access control and operator signature", async () => {
const minter = accounts[1];
let transferTo = accounts[2];
let operator = accounts[3];

const tokenId = minter + "b00000000000000000000001";
const tokenURI = "//uri";
let supply = 5;
let mint = 2;

const signature = await getSignature(tokenId, tokenURI, supply, creators([minter]), [], minter);
const operatorSignature = await getSignature(tokenId, tokenURI, supply, creators([minter]), [], operator);

await token.enableMinterAccessControl({from: tokenOwner});
assert.equal(await token.minterAccessControlEnabled(), true);

await token.grantMinter(operator, {from: tokenOwner});
assert.equal(await token.isValidMinter(operator), true);
assert.equal(await token.isValidMinter(minter), false);
assert.equal(await token.isValidMinter(transferTo), false);

await token.mintAndTransfer([tokenId, tokenURI, supply, creators([minter]), [], [signature, operatorSignature]], transferTo, mint, {from: minter});
assert.equal(await token.uri(tokenId), "ipfs:/" + tokenURI);
assert.equal(await token.balanceOf(transferTo, tokenId), mint);
assert.equal(await token.balanceOf(minter, tokenId), 0);
});

it("mint and transfer with minter access control and wrong operator signature", async () => {
const minter = accounts[1];
let transferTo = accounts[2];
let operator = accounts[3];

const tokenId = minter + "b00000000000000000000001";
const tokenURI = "//uri";
let supply = 5;
let mint = 2;

const signature = await getSignature(tokenId, tokenURI, supply, creators([minter]), [], minter);
const operatorSignature = await getSignature(tokenId, tokenURI, supply, creators([minter]), [], transferTo);

await token.enableMinterAccessControl({from: tokenOwner});
assert.equal(await token.minterAccessControlEnabled(), true);

await token.grantMinter(operator, {from: tokenOwner});
assert.equal(await token.isValidMinter(operator), true);
assert.equal(await token.isValidMinter(minter), false);
assert.equal(await token.isValidMinter(transferTo), false);

await expectThrow(
token.mintAndTransfer([tokenId, tokenURI, supply, creators([minter]), [], [signature, operatorSignature]], transferTo, mint, {from: minter})
);
});

it("standard transfer from owner", async () => {
let minter = accounts[1];
const tokenId = minter + "b00000000000000000000001";
Expand Down
Loading