Skip to content

Commit

Permalink
ERC721Common contract with:
Browse files Browse the repository at this point in the history
- ERC721Enumerable+Pausable inheritance
- utils/OwnerPausable (new contract) inheritance for pause() toggling only by owner
- Gas-free OpenSea listings thanks to @dievardump's BaseOpenSea
  • Loading branch information
Divergence committed Nov 21, 2021
1 parent 8ecf18c commit acc59a6
Show file tree
Hide file tree
Showing 9 changed files with 388 additions and 2 deletions.
55 changes: 55 additions & 0 deletions contracts/erc721/BaseOpenSea.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: MIT
// Copyright Simon Fremaux (@dievardump)
// https://gist.github.com/dievardump/483eb43bc6ed30b14f01e01842e3339b/
pragma solidity ^0.8.0;

/// @title OpenSea contract helper that defines a few things
/// @author Simon Fremaux (@dievardump)
/// @dev This is a contract used to add OpenSea's support for gas-less trading
/// by checking if operator is owner's proxy
contract BaseOpenSea {
string private _contractURI;
ProxyRegistry private _proxyRegistry;

/// @notice Returns the contract URI function. Used on OpenSea to get details
/// about a contract (owner, royalties etc...)
/// See documentation: https://docs.opensea.io/docs/contract-level-metadata
function contractURI() public view returns (string memory) {
return _contractURI;
}

/// @notice Helper for OpenSea gas-less trading
/// @dev Allows to check if `operator` is owner's OpenSea proxy
/// @param owner the owner we check for
/// @param operator the operator (proxy) we check for
function isOwnersOpenSeaProxy(address owner, address operator)
public
view
returns (bool)
{
ProxyRegistry proxyRegistry = _proxyRegistry;
return
// we have a proxy registry address
address(proxyRegistry) != address(0) &&
// current operator is owner's proxy address
address(proxyRegistry.proxies(owner)) == operator;
}

/// @dev Internal function to set the _contractURI
/// @param contractURI_ the new contract uri
function _setContractURI(string memory contractURI_) internal {
_contractURI = contractURI_;
}

/// @dev Internal function to set the _proxyRegistry
/// @param proxyRegistryAddress the new proxy registry address
function _setOpenSeaRegistry(address proxyRegistryAddress) internal {
_proxyRegistry = ProxyRegistry(proxyRegistryAddress);
}
}

contract OwnableDelegateProxy {}

contract ProxyRegistry {
mapping(address => OwnableDelegateProxy) public proxies;
}
85 changes: 85 additions & 0 deletions contracts/erc721/ERC721Common.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 Divergent Technologies Ltd (github.com/divergencetech)
pragma solidity >=0.8.0 <0.9.0;

import "./BaseOpenSea.sol";
import "../utils/OwnerPausable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Pausable.sol";

/**
@notice An ERC721 contract with common functionality:
- OpenSea gas-free listings
- OpenZeppelin Enumerable and Pausable
- OpenZeppelin Pausable with functions exposed to Owner only
*/
contract ERC721Common is
BaseOpenSea,
ERC721Enumerable,
ERC721Pausable,
OwnerPausable
{
/**
@param openSeaProxyRegistry Set to
0xa5409ec958c83c3f309868babaca7c86dcb077c1 for Mainnet and
0xf57b2c51ded3a29e6891aba85459d600256cf317 for Rinkeby.
*/
constructor(
string memory name,
string memory symbol,
address openSeaProxyRegistry
) ERC721(name, symbol) {
if (openSeaProxyRegistry != address(0)) {
BaseOpenSea._setOpenSeaRegistry(openSeaProxyRegistry);
}
}

/// @notice Requires that the token exists.
modifier tokenExists(uint256 tokenId) {
require(ERC721._exists(tokenId), "ERC721Common: Token doesn't exist");
_;
}

/// @notice Requires that msg.sender owns or is approved for the token.
modifier onlyApprovedOrOwner(uint256 tokenId) {
require(
_isApprovedOrOwner(msg.sender, tokenId),
"ERC721Common: Not approved nor owner"
);
_;
}

/// @notice Overrides _beforeTokenTransfer as required by inheritance.
function _beforeTokenTransfer(
address from,
address to,
uint256 tokenId
) internal virtual override(ERC721Enumerable, ERC721Pausable) {
super._beforeTokenTransfer(from, to, tokenId);
}

/// @notice Overrides supportsInterface as required by inheritance.
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}

/**
@notice Returns true if either standard isApprovedForAll() returns true or
the operator is the OpenSea proxy for the owner.
*/
function isApprovedForAll(address owner, address operator)
public
view
override
returns (bool)
{
return
super.isApprovedForAll(owner, operator) ||
BaseOpenSea.isOwnersOpenSeaProxy(owner, operator);
}
}
19 changes: 19 additions & 0 deletions contracts/utils/OwnerPausable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 Divergent Technologies Ltd (github.com/divergencetech)
pragma solidity >=0.8.0 <0.9.0;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";

/// @notice A Pausable contract that can only be toggled by the Owner.
contract OwnerPausable is Ownable, Pausable {
/// @notice Pauses the contract.
function pause() public onlyOwner {
Pausable._pause();
}

/// @notice Unpauses the contract.
function unpause() public onlyOwner {
Pausable._unpause();
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@divergencetech/ethier",
"version": "0.2.0",
"description": "Golang and Solidity code to make Ethereum work ethier",
"version": "0.2.1",
"description": "Golang and Solidity code to make Ethereum development ethier",
"main": "\"\"",
"scripts": {
"test": "go generate ./... && go test ./...",
Expand Down
28 changes: 28 additions & 0 deletions tests/erc721/TestableERC721Common.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 Divergent Technologies Ltd (github.com/divergencetech)
pragma solidity >=0.8.0 <0.9.0;

import "../../contracts/erc721/ERC721Common.sol";

/**
@notice Exposes a buy() function to allow testing of DutchAuction and, by proxy,
Seller.
@dev Setting the price decrease of the DutchAuction to zero is identical to a
constant Seller. Creating only a single Testable contract is simpler.
*/
contract TestableERC721Common is ERC721Common {
constructor() ERC721Common("Token", "JRR", address(0)) {}

function mint(uint256 tokenId) public {
ERC721._safeMint(msg.sender, tokenId);
}

/// @dev For testing the tokenExists() modifier.
function mustExist(uint256 tokenId) public view tokenExists(tokenId) {}

/// @dev For testing the onlyApprovedOrOwner() modifier.
function mustBeApprovedOrOwner(uint256 tokenId)
public
onlyApprovedOrOwner(tokenId)
{}
}
108 changes: 108 additions & 0 deletions tests/erc721/erc721_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package erc721_test

import (
"math/big"
"testing"

"github.com/divergencetech/ethier/ethtest"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/h-fam/errdiff"
)

// Actors in the tests
const (
owner = iota
tokenOwner
approved
vandal
)

// Token IDs
const (
exists = iota
notExists
)

func deploy(t *testing.T) (*ethtest.SimulatedBackend, *TestableERC721Common) {
t.Helper()

sim := ethtest.NewSimulatedBackendTB(t, 4)
_, _, nft, err := DeployTestableERC721Common(sim.Acc(owner), sim)
if err != nil {
t.Fatalf("DeployTestableERC721Common() error %v", err)
}

if _, err := nft.Mint(sim.Acc(tokenOwner), big.NewInt(exists)); err != nil {
t.Fatalf("Mint(%d) error %v", exists, err)
}
if _, err := nft.Approve(sim.Acc(tokenOwner), sim.Acc(approved).From, big.NewInt(exists)); err != nil {
t.Fatalf("Approve(<approved account>, %d) error %v", exists, err)
}

return sim, nft
}

func TestModifiers(t *testing.T) {
_, nft := deploy(t)

tests := []struct {
name string
tokenID int64
errDiffAgainst interface{}
}{
{
name: "existing token",
tokenID: exists,
errDiffAgainst: nil,
},
{
name: "non-existent token",
tokenID: notExists,
errDiffAgainst: "ERC721Common: Token doesn't exist",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := nft.MustExist(nil, big.NewInt(tt.tokenID))
if diff := errdiff.Check(err, tt.errDiffAgainst); diff != "" {
t.Errorf("MustExist([%s]), modified with tokenExists(); %s", tt.name, diff)
}
})
}
}

func TestOnlyApprovedOrOwner(t *testing.T) {
sim, nft := deploy(t)

tests := []struct {
name string
account *bind.TransactOpts
errDiffAgainst interface{}
}{
{
name: "token owner",
account: sim.Acc(tokenOwner),
errDiffAgainst: nil,
},
{
name: "approved",
account: sim.Acc(approved),
errDiffAgainst: nil,
},
{
name: "vandal",
account: sim.Acc(vandal),
errDiffAgainst: "ERC721Common: Not approved nor owner",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := nft.MustBeApprovedOrOwner(tt.account, big.NewInt(exists))
if diff := errdiff.Check(err, tt.errDiffAgainst); diff != "" {
t.Errorf("MustBeApprovedOrOwner([%s]), modified with onlyApprovedOrOwner(); %s", tt.name, diff)
}
})
}
}
3 changes: 3 additions & 0 deletions tests/erc721/generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package erc721_test

//go:generate sh -c "solc TestableERC721Common.sol --base-path ../../ --include-path ../../node_modules --combined-json abi,bin | abigen --combined-json /dev/stdin --pkg erc721_test --out generated_test.go"
3 changes: 3 additions & 0 deletions tests/utils/generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package utils_test

//go:generate sh -c "solc ../../contracts/utils/OwnerPausable.sol --base-path ../../ --include-path ../../node_modules --combined-json abi,bin | abigen --combined-json /dev/stdin --pkg utils_test --out generated_test.go"
Loading

0 comments on commit acc59a6

Please sign in to comment.