Skip to content

Commit

Permalink
Add ERC721APausable as equivalent to ERC721Pausable (chiru-labs#205)
Browse files Browse the repository at this point in the history
* Add ERC721APausable equivalent to ERC721Pausable

* CR: Docs and ContractPaused() error throw

* Unit tests

* Include unit test for burnable composability

* run linter
  • Loading branch information
johnnyshankman authored Mar 28, 2022
1 parent 67ddb34 commit 3f47769
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 8 deletions.
3 changes: 1 addition & 2 deletions contracts/ERC721A.sol
Original file line number Diff line number Diff line change
Expand Up @@ -326,8 +326,7 @@ contract ERC721A is Context, ERC165, IERC721, IERC721Metadata {
* Tokens start existing when they are minted (`_mint`),
*/
function _exists(uint256 tokenId) internal view returns (bool) {
return _startTokenId() <= tokenId && tokenId < _currentIndex &&
!_ownerships[tokenId].burned;
return _startTokenId() <= tokenId && tokenId < _currentIndex && !_ownerships[tokenId].burned;
}

function _safeMint(address to, uint256 quantity) internal {
Expand Down
3 changes: 1 addition & 2 deletions contracts/extensions/ERC721ABurnable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import '../ERC721A.sol';
* @dev ERC721A Token that can be irreversibly burned (destroyed).
*/
abstract contract ERC721ABurnable is ERC721A {

/**
* @dev Burns `tokenId`. See {ERC721A-_burn}.
*
Expand All @@ -21,4 +20,4 @@ abstract contract ERC721ABurnable is ERC721A {
function burn(uint256 tokenId) public virtual {
_burn(tokenId, true);
}
}
}
37 changes: 37 additions & 0 deletions contracts/extensions/ERC721APausable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// SPDX-License-Identifier: MIT
// Creator: Chiru Labs

pragma solidity ^0.8.4;

import '../ERC721A.sol';
import '@openzeppelin/contracts/security/Pausable.sol';

error ContractPaused();

/**
* @dev ERC721A token with pausable token transfers, minting and burning.
*
* Based off of OpenZeppelin's ERC721Pausable extension.
*
* Useful for scenarios such as preventing trades until the end of an evaluation
* period, or having an emergency switch for freezing all token transfers in the
* event of a large bug.
*/
abstract contract ERC721APausable is ERC721A, Pausable {
/**
* @dev See {ERC721A-_beforeTokenTransfers}.
*
* Requirements:
*
* - the contract must not be paused.
*/
function _beforeTokenTransfers(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual override {
super._beforeTokenTransfers(from, to, startTokenId, quantity);
if (paused()) revert ContractPaused();
}
}
4 changes: 2 additions & 2 deletions contracts/mocks/ERC721ABurnableMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ contract ERC721ABurnableMock is ERC721A, ERC721ABurnable {
function safeMint(address to, uint256 quantity) public {
_safeMint(to, quantity);
}

function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownerships[index];
}

function totalMinted() public view returns (uint256) {
return _totalMinted();
}
}
}
2 changes: 1 addition & 1 deletion contracts/mocks/ERC721ABurnableOwnersExplicitMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ contract ERC721ABurnableOwnersExplicitMock is ERC721A, ERC721ABurnable, ERC721AO
function getOwnershipAt(uint256 index) public view returns (TokenOwnership memory) {
return _ownerships[index];
}
}
}
32 changes: 32 additions & 0 deletions contracts/mocks/ERC721APausableMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-License-Identifier: MIT
// Creators: Chiru Labs

pragma solidity ^0.8.4;

import '../extensions/ERC721APausable.sol';
import '../extensions/ERC721ABurnable.sol';

contract ERC721APausableMock is ERC721A, ERC721APausable, ERC721ABurnable {
constructor(string memory name_, string memory symbol_) ERC721A(name_, symbol_) {}

function safeMint(address to, uint256 quantity) public {
_safeMint(to, quantity);
}

function _beforeTokenTransfers(
address from,
address to,
uint256 startTokenId,
uint256 quantity
) internal virtual override(ERC721A, ERC721APausable) {
super._beforeTokenTransfers(from, to, startTokenId, quantity);
}

function pause() external {
_pause();
}

function unpause() external {
_unpause();
}
}
2 changes: 1 addition & 1 deletion contracts/mocks/StartTokenIdHelper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ contract StartTokenIdHelper {
constructor(uint256 startTokenId_) {
startTokenId = startTokenId_;
}
}
}
73 changes: 73 additions & 0 deletions test/extensions/ERC721APausable.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const { deployContract } = require('../helpers.js');
const { expect } = require('chai');

const createTestSuite = ({ contract, constructorArgs }) =>
function () {
context(`${contract}`, function () {
beforeEach(async function () {
this.erc721aPausable = await deployContract(contract, constructorArgs);

this.startTokenId = this.erc721aPausable.startTokenId
? (await this.erc721aPausable.startTokenId()).toNumber()
: 0;
});

beforeEach(async function () {
const [owner, addr1, addr2] = await ethers.getSigners();
this.owner = owner;
this.addr1 = addr1;
this.addr2 = addr2;
this.existingTokenID = 0;
this.numTestTokens = 1;
await this.erc721aPausable['safeMint(address,uint256)'](this.addr1.address, this.numTestTokens);
});

it('cannot burn a valid token id while paused', async function () {
await this.erc721aPausable.connect(this.owner).pause()
const query = this.erc721aPausable
.connect(this.addr1)
.burn(this.existingTokenID);

await expect(query).to.be.revertedWith('ContractPaused');
expect(await this.erc721aPausable.ownerOf(this.existingTokenID)).to.be.equal(this.addr1.address);
});

it('cannot transfer a valid token id while paused', async function () {
await this.erc721aPausable.connect(this.owner).pause()
const query = this.erc721aPausable
.connect(this.addr1)
.transferFrom(this.addr1.address, this.addr2.address, this.existingTokenID);

await expect(query).to.be.revertedWith('ContractPaused');
expect(await this.erc721aPausable.ownerOf(this.existingTokenID)).to.be.equal(this.addr1.address);
});

it('can transfer a valid token id while unpaused', async function () {
await this.erc721aPausable.connect(this.owner).pause()
await this.erc721aPausable.connect(this.owner).unpause()
const query = this.erc721aPausable
.connect(this.addr1)
.transferFrom(this.addr1.address, this.addr2.address, this.existingTokenID);

await expect(query).to.not.be.revertedWith('ContractPaused');
expect(await this.erc721aPausable.ownerOf(this.existingTokenID)).to.be.equal(this.addr2.address);
});

it('can burn a valid token id while unpaused', async function () {
await this.erc721aPausable.connect(this.owner).pause()
await this.erc721aPausable.connect(this.owner).unpause()
const query = this.erc721aPausable
.connect(this.addr1)
.burn(this.existingTokenID);

await expect(query).to.not.be.revertedWith('ContractPaused');
await expect(this.erc721aPausable.ownerOf(this.existingTokenID))
.to.be.revertedWith('OwnerQueryForNonexistentToken');
});
});
};

describe(
'ERC721APausable',
createTestSuite({ contract: 'ERC721APausableMock', constructorArgs: ['Azuki', 'AZUKI'] })
);

0 comments on commit 3f47769

Please sign in to comment.