diff --git a/.changeset/full-chicken-beg.md b/.changeset/full-chicken-beg.md new file mode 100644 index 00000000000..929514adba6 --- /dev/null +++ b/.changeset/full-chicken-beg.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +Implements ERC6909Pausable diff --git a/contracts/token/ERC6909/extensions/ERC6909Pausable.sol b/contracts/token/ERC6909/extensions/ERC6909Pausable.sol new file mode 100644 index 00000000000..c0008b80898 --- /dev/null +++ b/contracts/token/ERC6909/extensions/ERC6909Pausable.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.5.0) (token/ERC6909/extensions/ERC6909Pausable.sol) + +pragma solidity ^0.8.24; + +import {ERC6909} from "../ERC6909.sol"; +import {Pausable} from "../../../utils/Pausable.sol"; + +/** + * @dev ERC-6909 token with pausable token transfers, minting and burning. + * + * 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. + * + * IMPORTANT: This contract does not include public pause and unpause functions. In + * addition to inheriting this contract, you must define both functions, invoking the + * {Pausable-_pause} and {Pausable-_unpause} internal functions, with appropriate + * access control, e.g. using {AccessControl} or {Ownable}. Not doing so will + * make the contract pause mechanism of the contract unreachable, and thus unusable. + */ +abstract contract ERC6909Pausable is ERC6909, Pausable { + /** + * @dev See {ERC6909-_update}. + * + * Requirements: + * + * - the contract must not be paused. + */ + function _update(address from, address to, uint256 ids, uint256 values) internal virtual override whenNotPaused { + super._update(from, to, ids, values); + } +} diff --git a/test/token/ERC6909/extensions/ERC6909Pausable.test.js b/test/token/ERC6909/extensions/ERC6909Pausable.test.js new file mode 100644 index 00000000000..1f5ea1f9b16 --- /dev/null +++ b/test/token/ERC6909/extensions/ERC6909Pausable.test.js @@ -0,0 +1,81 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { shouldBehaveLikeERC6909 } = require('../ERC6909.behavior'); + +async function fixture() { + const [holder, operator, recipient, other] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC6909Pausable'); + return { token, holder, operator, recipient, other }; +} + +describe('ERC6909Pausable', function () { + const firstTokenId = 37n; + const firstTokenValue = 42n; + const secondTokenId = 19842n; + const secondTokenValue = 23n; + + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeERC6909(); + + describe('when token is paused', function () { + beforeEach(async function () { + await this.token.connect(this.holder).setOperator(this.operator, true); + await this.token.$_mint(this.holder, firstTokenId, firstTokenValue); + await this.token.$_pause(); + }); + + it('reverts when trying to transfer', async function () { + await expect( + this.token.connect(this.holder).transfer(this.recipient, firstTokenId, firstTokenValue), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); + }); + + it('reverts when trying to transferFrom from operator', async function () { + await expect( + this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, firstTokenValue), + ).to.be.revertedWithCustomError(this.token, 'EnforcedPause'); + }); + + it('reverts when trying to mint', async function () { + await expect(this.token.$_mint(this.holder, secondTokenId, secondTokenValue)).to.be.revertedWithCustomError( + this.token, + 'EnforcedPause', + ); + }); + + it('reverts when trying to burn', async function () { + await expect(this.token.$_burn(this.holder, firstTokenId, firstTokenValue)).to.be.revertedWithCustomError( + this.token, + 'EnforcedPause', + ); + }); + + describe('setOperator', function () { + it('approves an operator', async function () { + await this.token.connect(this.holder).setOperator(this.other, true); + expect(await this.token.isOperator(this.holder, this.other)).to.be.true; + }); + + it('disapproves an operator', async function () { + await this.token.connect(this.holder).setOperator(this.other, false); + expect(await this.token.isOperator(this.holder, this.other)).to.be.false; + }); + }); + + describe('balanceOf', function () { + it('returns the token value owned by the given address', async function () { + expect(await this.token.balanceOf(this.holder, firstTokenId)).to.equal(firstTokenValue); + }); + }); + + describe('isOperator', function () { + it('returns the approval of the operator', async function () { + expect(await this.token.isOperator(this.holder, this.operator)).to.be.true; + }); + }); + }); +});