Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/full-chicken-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': patch
---

Implements ERC6909Pausable
33 changes: 33 additions & 0 deletions contracts/token/ERC6909/extensions/ERC6909Pausable.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
81 changes: 81 additions & 0 deletions test/token/ERC6909/extensions/ERC6909Pausable.test.js
Original file line number Diff line number Diff line change
@@ -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;
});
});
Comment on lines +57 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

disapproves an operator test never makes other an operator

In this block, this.other is never approved before calling setOperator(this.other, false), so the test will pass even if disapproval is a no-op. To actually exercise revocation while paused, first approve other, then disapprove:

     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;
+        await this.token.connect(this.holder).setOperator(this.other, true);
+        await this.token.connect(this.holder).setOperator(this.other, false);
+        expect(await this.token.isOperator(this.holder, this.other)).to.be.false;
       });
     });

This way the test guarantees that operator revocation still works while the token is paused.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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('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, true);
await this.token.connect(this.holder).setOperator(this.other, false);
expect(await this.token.isOperator(this.holder, this.other)).to.be.false;
});
});
🤖 Prompt for AI Agents
In test/token/ERC6909/extensions/ERC6909Pausable.test.js around lines 57 to 67,
the "disapproves an operator" test never approves this.other before attempting
to disapprove it, so it can pass even if revocation is a no-op; update the test
to first call setOperator(this.other, true) (or otherwise ensure this.other is
an operator), then call setOperator(this.other, false) and assert isOperator
returns false to verify revocation works while paused.


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;
});
});
});
});