Skip to content

Commit

Permalink
Merge pull request #12 from bitmark-inc/feature/support-operator-filt…
Browse files Browse the repository at this point in the history
…er-registry

Adopt OperatorFilterRegistry to FF exhibition contract
  • Loading branch information
Jim Yeh authored Nov 29, 2022
2 parents 5098005 + 253b645 commit a5eadfb
Show file tree
Hide file tree
Showing 7 changed files with 778 additions and 0 deletions.
77 changes: 77 additions & 0 deletions contracts/FeralfileArtworkV3_2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/Strings.sol";
import "@openzeppelin/contracts/utils/Base64.sol";

import "./Authorizable.sol";
import "./FeralfileArtworkV3.sol";
import "./UpdateableOperatorFilterer.sol";

contract FeralfileExhibitionV3_2 is
FeralfileExhibitionV3,
UpdateableOperatorFilterer
{
constructor(
string memory name_,
string memory symbol_,
uint256 secondarySaleRoyaltyBPS_,
address royaltyPayoutAddress_,
string memory contractURI_,
string memory tokenBaseURI_,
bool isBurnable_,
bool isBridgeable_
)
FeralfileExhibitionV3(
name_,
symbol_,
secondarySaleRoyaltyBPS_,
royaltyPayoutAddress_,
contractURI_,
tokenBaseURI_,
isBurnable_,
isBridgeable_
)
{}

function setApprovalForAll(address operator, bool approved)
public
override(ERC721, IERC721)
onlyAllowedOperatorApproval(operator)
{
super.setApprovalForAll(operator, approved);
}

function approve(address operator, uint256 tokenId)
public
override(ERC721, IERC721)
onlyAllowedOperatorApproval(operator)
{
super.approve(operator, tokenId);
}

function transferFrom(
address from,
address to,
uint256 tokenId
) public override(ERC721, IERC721) onlyAllowedOperator(from) {
super.transferFrom(from, to, tokenId);
}

function safeTransferFrom(
address from,
address to,
uint256 tokenId
) public override(ERC721, IERC721) onlyAllowedOperator(from) {
super.safeTransferFrom(from, to, tokenId);
}

function safeTransferFrom(
address from,
address to,
uint256 tokenId,
bytes memory data
) public override(ERC721, IERC721) onlyAllowedOperator(from) {
super.safeTransferFrom(from, to, tokenId, data);
}
}
76 changes: 76 additions & 0 deletions contracts/UpdateableOperatorFilterer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {IOperatorFilterRegistry} from "./operator-filter-registry/IOperatorFilterRegistry.sol";

import "./Authorizable.sol";

/**
* @title UpdateableOperatorFilterer
* @notice Abstract contract whose constructor automatically registers and optionally subscribes to or copies another
* registrant's entries in the OperatorFilterRegistry.
* @dev This smart contract is meant to be inherited by token contracts so they can use the following:
* - `onlyAllowedOperator` modifier for `transferFrom` and `safeTransferFrom` methods.
* - `onlyAllowedOperatorApproval` modifier for `approve` and `setApprovalForAll` methods.
*/
abstract contract UpdateableOperatorFilterer is Authorizable {
error OperatorNotAllowed(address operator);

address constant DEFAULT_OPERATOR_FILTER_REGISTRY_ADDRESS =
address(0x000000000000AAeB6D7670E522A718067333cd4E);

address constant DEFAULT_SUBSCRIPTION =
address(0x3cc6CddA760b79bAfa08dF41ECFA224f810dCeB6);

IOperatorFilterRegistry public OperatorFilterRegistry =
IOperatorFilterRegistry(DEFAULT_OPERATOR_FILTER_REGISTRY_ADDRESS);

constructor() {
if (address(OperatorFilterRegistry).code.length > 0) {
OperatorFilterRegistry.registerAndSubscribe(
address(this),
DEFAULT_SUBSCRIPTION
);
}
}

modifier onlyAllowedOperator(address from) virtual {
// Allow spending tokens from addresses with balance
// Note that this still allows listings and marketplaces with escrow to transfer tokens if transferred
// from an EOA.
if (from != msg.sender) {
_checkFilterOperator(msg.sender);
}
_;
}

modifier onlyAllowedOperatorApproval(address operator) virtual {
_checkFilterOperator(operator);
_;
}

function _checkFilterOperator(address operator) internal view virtual {
// Check registry code length to facilitate testing in environments without a deployed registry.
if (address(OperatorFilterRegistry).code.length > 0) {
require(
OperatorFilterRegistry.isOperatorAllowed(
address(this),
operator
),
"operator is not allowed"
);
}
}

/**
* @notice update the operator filter registry
*/
function updateOperatorFilterRegistry(address operatorFilterRegisterAddress)
external
onlyOwner
{
OperatorFilterRegistry = IOperatorFilterRegistry(
operatorFilterRegisterAddress
);
}
}
27 changes: 27 additions & 0 deletions contracts/mocks/MockOperatorFilterRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MockOperatorFilterRegistry {
address private allowedExchange;

constructor(address allowedExchange_) {
allowedExchange = allowedExchange_;
}

function isOperatorAllowed(address registrant, address operator)
external
view
returns (bool)
{
// allow for all regular address
if (operator.code.length == 0) {
return true;
}

if (operator == allowedExchange) {
return true;
}

return false;
}
}
29 changes: 29 additions & 0 deletions contracts/operator-filter-registry/IOperatorFilterRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

interface IOperatorFilterRegistry {
function isOperatorAllowed(address registrant, address operator) external view returns (bool);
function register(address registrant) external;
function registerAndSubscribe(address registrant, address subscription) external;
function registerAndCopyEntries(address registrant, address registrantToCopy) external;
function unregister(address addr) external;
function updateOperator(address registrant, address operator, bool filtered) external;
function updateOperators(address registrant, address[] calldata operators, bool filtered) external;
function updateCodeHash(address registrant, bytes32 codehash, bool filtered) external;
function updateCodeHashes(address registrant, bytes32[] calldata codeHashes, bool filtered) external;
function subscribe(address registrant, address registrantToSubscribe) external;
function unsubscribe(address registrant, bool copyExistingEntries) external;
function subscriptionOf(address addr) external returns (address registrant);
function subscribers(address registrant) external returns (address[] memory);
function subscriberAt(address registrant, uint256 index) external returns (address);
function copyEntriesOf(address registrant, address registrantToCopy) external;
function isOperatorFiltered(address registrant, address operator) external returns (bool);
function isCodeHashOfFiltered(address registrant, address operatorWithCode) external returns (bool);
function isCodeHashFiltered(address registrant, bytes32 codeHash) external returns (bool);
function filteredOperators(address addr) external returns (address[] memory);
function filteredCodeHashes(address addr) external returns (bytes32[] memory);
function filteredOperatorAt(address registrant, uint256 index) external returns (address);
function filteredCodeHashAt(address registrant, uint256 index) external returns (bytes32);
function isRegistered(address addr) external returns (bool);
function codeHashOf(address addr) external returns (bytes32);
}
29 changes: 29 additions & 0 deletions migrations/203_feralfile_exhibition_v3_2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
var FeralfileExhibitionV3_2 = artifacts.require('FeralfileExhibitionV3_2');

const argv = require('minimist')(process.argv.slice(2), {
string: ['exhibition_curator'],
});

module.exports = function (deployer) {
let exhibition_name = argv.exhibition_name || 'Feral File V3_2 Test';
let exhibition_symbol = argv.exhibition_symbol || 'FFDV3_2';
let exhibition_royalty_payout_address =
argv.exhibition_royalty_payout_address ||
'0xa704AA575172F84D917CcaF5EC775Da8227Ea6c3';
let exhibition_secondary_sale_royalty_bps =
argv.exhibition_secondary_sale_royalty_bps || 1000;
let burnable = argv.burnable || true;
let bridgeable = argv.bridgeable || true;

deployer.deploy(
FeralfileExhibitionV3_2,
exhibition_name,
exhibition_symbol,
exhibition_secondary_sale_royalty_bps,
exhibition_royalty_payout_address,
'https://ipfs.bitmark.com/ipfs/QmaQegRqExfFx8zuR6yscxzUwQJUc96LuNNQiAMK9BsUQe',
'https://ipfs.bitmark.com/ipfs/',
burnable,
bridgeable
);
};
139 changes: 139 additions & 0 deletions test/feralfile_exhibition_v3_2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
const FeralfileExhibitionV3_2 = artifacts.require('FeralfileExhibitionV3_2');
const MockOperatorFilterRegistry = artifacts.require('MockOperatorFilterRegistry');

const axios = require('axios');

const IPFS_GATEWAY_PREFIX = 'https://ipfs.bitmark.com/ipfs/';

const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

const ALLOWED_EXCHANGE = '0x2de783974f53AC98b16332f943431df0FD66C9E4';

contract('FeralfileExhibitionV3_2', async (accounts) => {
before(async function () {
this.operatorFilterRegistry = await MockOperatorFilterRegistry.new(ALLOWED_EXCHANGE)
this.testThumbnailCid = "QmV68mphFwMraCE9J6KpQc89Sz8ppvJx5CP6XFruhGQrX8" // AKG test IPFS thumbnail CID
this.testArtworkCid = "QmTn3PfHHvoDHKawTPXutqxAk2k8ynFK9cZfsSwggryjkX" // AKG test IPFS artwork CID

this.exhibition = await FeralfileExhibitionV3_2.new(
'Feral File V3_2 Test 002',
'FFV3',
1000,
'0x8fd310de32848798eB64Bd88f9C5656Eea32415e',
'https://ipfs.bitmark.com/ipfs/QmaptARVxNSP36PQai5oiCPqbrATvpydcJ8SPx6T6Yp1CZ',
IPFS_GATEWAY_PREFIX,
true,
true
);

await this.exhibition.updateOperatorFilterRegistry(this.operatorFilterRegistry.address)
});

it('can register artwork', async function () {
let r = await this.exhibition.createArtworks([
['TestArtwork', 'TestUser', 'TestArtwork', 10, 2, 1],
]);
this.artworkID = r.logs[0].args.artworkID;

let artwork = await this.exhibition.artworks(this.artworkID);
assert.equal(artwork.title, 'TestArtwork');

let artworkEditions = await this.exhibition.totalEditionOfArtwork(
this.artworkID
);
assert.equal(artworkEditions, 0);
});

it('test batch mint 1 artwork', async function () {
let editionIndex0 = 0;
let editionIndex1 = 1;
let editionIndex2 = 2;
let editionIndex3 = 3;

await this.exhibition.batchMint([
[this.artworkID, editionIndex0, accounts[0], accounts[0], 'test0'],
[this.artworkID, editionIndex1, accounts[0], accounts[0], 'test1'],
[this.artworkID, editionIndex2, accounts[0], accounts[0], 'test2'],
[this.artworkID, editionIndex3, accounts[0], accounts[0], 'test3'],
]);

let account0 = accounts[0].toLowerCase()

this.editionID0 = (BigInt(this.artworkID) + BigInt(editionIndex0)).toString();
let ownerOfEdition0 = await this.exhibition.ownerOf(this.editionID0);
assert.equal(ownerOfEdition0.toLowerCase(), account0);

this.editionID1 = (BigInt(this.artworkID) + BigInt(editionIndex1)).toString();
let ownerOfEdition1 = await this.exhibition.ownerOf(this.editionID1);
assert.equal(ownerOfEdition1.toLowerCase(), account0);

this.editionID2 = (BigInt(this.artworkID) + BigInt(editionIndex2)).toString();
let ownerOfEdition2 = await this.exhibition.ownerOf(this.editionID2);
assert.equal(ownerOfEdition2.toLowerCase(), account0);

this.editionID3 = (BigInt(this.artworkID) + BigInt(editionIndex3)).toString();
let ownerOfEdition3 = await this.exhibition.ownerOf(this.editionID3);
assert.equal(ownerOfEdition3.toLowerCase(), account0);
})

it('can not approve an filtered exchange', async function () {
let onError = false;
try {
await this.exhibition.approve(this.operatorFilterRegistry.address, this.editionID0)
} catch (error) {
onError = true
assert.equal(
error.message,
'Returned error: VM Exception while processing transaction: revert operator is not allowed'
);
}
assert.equal(onError, true)
let addr = await this.exhibition.getApproved(this.editionID0)
assert.equal(addr, ZERO_ADDRESS)
})

it('can approve the allowed exchange', async function () {
await this.exhibition.approve(ALLOWED_EXCHANGE, this.editionID0)
let addr = await this.exhibition.getApproved(this.editionID0)
assert.equal(addr, ALLOWED_EXCHANGE)
})


it('can not transfer before approve', async function () {
let onError = false;
try {
await this.exhibition.safeTransferFrom(accounts[0], accounts[1], this.editionID1, {
"from": accounts[1]
})
} catch (error) {
onError = true
assert.equal(
error.message,
'Returned error: VM Exception while processing transaction: revert ERC721: caller is not token owner nor approved'
);
}
assert.equal(onError, true)
})

it('can approve the regular address and transfer', async function () {
let account1 = accounts[1]
await this.exhibition.approve(account1, this.editionID1)
let addr = await this.exhibition.getApproved(this.editionID1)
assert.equal(addr, account1)
await this.exhibition.safeTransferFrom(accounts[0], account1, this.editionID1, {
"from": account1
})

let ownerOfEdition1 = await this.exhibition.ownerOf(this.editionID1);
assert.equal(ownerOfEdition1, account1);
})

it('can approve when unset the registry', async function () {
await this.exhibition.updateOperatorFilterRegistry(ZERO_ADDRESS)
let addressBefore = await this.exhibition.getApproved(this.editionID2)
assert.equal(addressBefore, ZERO_ADDRESS)
await this.exhibition.approve(this.operatorFilterRegistry.address, this.editionID2)
let addressAfter = await this.exhibition.getApproved(this.editionID2)
assert.equal(addressAfter, this.operatorFilterRegistry.address)
})
});
Loading

0 comments on commit a5eadfb

Please sign in to comment.