-
Notifications
You must be signed in to change notification settings - Fork 0
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 #12 from bitmark-inc/feature/support-operator-filt…
…er-registry Adopt OperatorFilterRegistry to FF exhibition contract
- Loading branch information
Showing
7 changed files
with
778 additions
and
0 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
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); | ||
} | ||
} |
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,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 | ||
); | ||
} | ||
} |
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,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
29
contracts/operator-filter-registry/IOperatorFilterRegistry.sol
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,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); | ||
} |
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,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 | ||
); | ||
}; |
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,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) | ||
}) | ||
}); |
Oops, something went wrong.