From f7b38a786f0441d92375fb5966d24a87c6735f0a Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:19:01 -0800 Subject: [PATCH 1/5] Move `contractIURI()` to extension --- contracts/interfaces/IERC7984.sol | 3 -- contracts/interfaces/IERC7984Metadata.sol | 11 +++++++ .../mocks/docs/ERC7984MintableBurnable.sol | 7 +--- .../mocks/token/ERC7984ERC20WrapperMock.sol | 5 ++- .../mocks/token/ERC7984FreezableMock.sol | 2 +- contracts/mocks/token/ERC7984MetadataMock.sol | 29 ++++++++++++++++ contracts/mocks/token/ERC7984Mock.sol | 6 +--- .../mocks/token/ERC7984ObserverAccessMock.sol | 6 +--- .../mocks/token/ERC7984ReentrantMock.sol | 6 +--- contracts/mocks/token/ERC7984RwaMock.sol | 7 +--- contracts/mocks/token/ERC7984VotesMock.sol | 6 +--- contracts/token/ERC7984/ERC7984.sol | 9 +---- .../ERC7984/extensions/ERC7984Metadata.sol | 28 ++++++++++++++++ test/finance/ERC7821WithExecutor.test.ts | 3 +- .../VestingWalletCliffConfidential.test.ts | 3 +- .../VestingWalletConfidential.behavior.ts | 2 +- .../finance/VestingWalletConfidential.test.ts | 3 +- .../VestingWalletConfidentialFactory.test.ts | 3 +- test/helpers/interface.ts | 2 +- test/token/ERC7984/ERC7984.test.ts | 7 +--- test/token/ERC7984/ERC7984Votes.test.ts | 3 +- .../extensions/ERC7984ERC20Wrapper.test.ts | 13 ++++---- .../extensions/ERC7984Freezable.test.ts | 2 -- .../extensions/ERC7984Metadata.test.ts | 33 +++++++++++++++++++ .../extensions/ERC7984ObserverAccess.test.ts | 3 +- .../ERC7984/extensions/ERC7984Omnibus.test.ts | 7 +--- .../extensions/ERC7984Restricted.test.ts | 2 +- .../ERC7984/extensions/ERC7984Rwa.test.ts | 2 +- 28 files changed, 129 insertions(+), 84 deletions(-) create mode 100644 contracts/interfaces/IERC7984Metadata.sol create mode 100644 contracts/mocks/token/ERC7984MetadataMock.sol create mode 100644 contracts/token/ERC7984/extensions/ERC7984Metadata.sol create mode 100644 test/token/ERC7984/extensions/ERC7984Metadata.test.ts diff --git a/contracts/interfaces/IERC7984.sol b/contracts/interfaces/IERC7984.sol index 1aee1d22..388967eb 100644 --- a/contracts/interfaces/IERC7984.sol +++ b/contracts/interfaces/IERC7984.sol @@ -33,9 +33,6 @@ interface IERC7984 is IERC165 { /// @dev Returns the number of decimals of the token. Recommended to be 6. function decimals() external view returns (uint8); - /// @dev Returns the contract URI. See https://eips.ethereum.org/EIPS/eip-7572[ERC-7572] for details. - function contractURI() external view returns (string memory); - /// @dev Returns the confidential total supply of the token. function confidentialTotalSupply() external view returns (euint64); diff --git a/contracts/interfaces/IERC7984Metadata.sol b/contracts/interfaces/IERC7984Metadata.sol new file mode 100644 index 00000000..54f7d725 --- /dev/null +++ b/contracts/interfaces/IERC7984Metadata.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Confidential Contracts (token/ERC7984/IERC7984Metadata.sol) +pragma solidity ^0.8.24; + +import {IERC7984} from "./IERC7984.sol"; + +/// @dev Interface for optional metadata functions for {IERC7984}. +interface IERC7984Metadata is IERC7984 { + /// @dev Returns the contract URI. Should be formatted as described in https://eips.ethereum.org/EIPS/eip-7572[ERC-7572]. + function contractURI() external view returns (string memory); +} diff --git a/contracts/mocks/docs/ERC7984MintableBurnable.sol b/contracts/mocks/docs/ERC7984MintableBurnable.sol index d3f7d070..97d1c725 100644 --- a/contracts/mocks/docs/ERC7984MintableBurnable.sol +++ b/contracts/mocks/docs/ERC7984MintableBurnable.sol @@ -6,12 +6,7 @@ import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {ERC7984} from "../../token/ERC7984/ERC7984.sol"; contract ERC7984MintableBurnable is ERC7984, Ownable { - constructor( - address owner, - string memory name, - string memory symbol, - string memory uri - ) ERC7984(name, symbol, uri) Ownable(owner) {} + constructor(address owner, string memory name, string memory symbol) ERC7984(name, symbol) Ownable(owner) {} function mint(address to, externalEuint64 amount, bytes memory inputProof) public onlyOwner { _mint(to, FHE.fromExternal(amount, inputProof)); diff --git a/contracts/mocks/token/ERC7984ERC20WrapperMock.sol b/contracts/mocks/token/ERC7984ERC20WrapperMock.sol index 12c34591..c1c78c89 100644 --- a/contracts/mocks/token/ERC7984ERC20WrapperMock.sol +++ b/contracts/mocks/token/ERC7984ERC20WrapperMock.sol @@ -9,7 +9,6 @@ contract ERC7984ERC20WrapperMock is ERC7984ERC20Wrapper, ZamaEthereumConfig { constructor( IERC20 token, string memory name, - string memory symbol, - string memory uri - ) ERC7984ERC20Wrapper(token) ERC7984(name, symbol, uri) {} + string memory symbol + ) ERC7984ERC20Wrapper(token) ERC7984(name, symbol) {} } diff --git a/contracts/mocks/token/ERC7984FreezableMock.sol b/contracts/mocks/token/ERC7984FreezableMock.sol index e0b768ba..f2500691 100644 --- a/contracts/mocks/token/ERC7984FreezableMock.sol +++ b/contracts/mocks/token/ERC7984FreezableMock.sol @@ -12,7 +12,7 @@ import {ERC7984Mock} from "./ERC7984Mock.sol"; contract ERC7984FreezableMock is ERC7984Mock, ERC7984Freezable, HandleAccessManager { error UnallowedHandleAccess(bytes32 handle, address account); - constructor(string memory name, string memory symbol, string memory tokenUri) ERC7984Mock(name, symbol, tokenUri) {} + constructor(string memory name, string memory symbol) ERC7984Mock(name, symbol) {} function _update( address from, diff --git a/contracts/mocks/token/ERC7984MetadataMock.sol b/contracts/mocks/token/ERC7984MetadataMock.sol new file mode 100644 index 00000000..446a85a2 --- /dev/null +++ b/contracts/mocks/token/ERC7984MetadataMock.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.27; + +import {euint64} from "@fhevm/solidity/lib/FHE.sol"; +import {ERC7984Metadata} from "../../token/ERC7984/extensions/ERC7984Metadata.sol"; +import {ERC7984Mock, ERC7984} from "./ERC7984Mock.sol"; + +contract ERC7984MetadataMock is ERC7984Metadata, ERC7984Mock { + constructor( + string memory name_, + string memory symbol_, + string memory contractURI_ + ) ERC7984Mock(name_, symbol_) ERC7984Metadata(contractURI_) {} + + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC7984Metadata, ERC7984) returns (bool) { + return super.supportsInterface(interfaceId); + } + + function _update( + address from, + address to, + euint64 amount + ) internal virtual override(ERC7984Mock, ERC7984) returns (euint64) { + return super._update(from, to, amount); + } +} diff --git a/contracts/mocks/token/ERC7984Mock.sol b/contracts/mocks/token/ERC7984Mock.sol index 63f75192..45e113d6 100644 --- a/contracts/mocks/token/ERC7984Mock.sol +++ b/contracts/mocks/token/ERC7984Mock.sol @@ -13,11 +13,7 @@ contract ERC7984Mock is ERC7984, ZamaEthereumConfig { event EncryptedAmountCreated(euint64 amount); event EncryptedAddressCreated(eaddress addr); - constructor( - string memory name_, - string memory symbol_, - string memory tokenURI_ - ) ERC7984(name_, symbol_, tokenURI_) { + constructor(string memory name_, string memory symbol_) ERC7984(name_, symbol_) { _OWNER = msg.sender; } diff --git a/contracts/mocks/token/ERC7984ObserverAccessMock.sol b/contracts/mocks/token/ERC7984ObserverAccessMock.sol index 21f828a1..c5a184a9 100644 --- a/contracts/mocks/token/ERC7984ObserverAccessMock.sol +++ b/contracts/mocks/token/ERC7984ObserverAccessMock.sol @@ -7,11 +7,7 @@ import {ERC7984ObserverAccess} from "../../token/ERC7984/extensions/ERC7984Obser import {ERC7984Mock} from "./ERC7984Mock.sol"; contract ERC7984ObserverAccessMock is ERC7984ObserverAccess, ERC7984Mock { - constructor( - string memory name_, - string memory symbol_, - string memory tokenURI_ - ) ERC7984Mock(name_, symbol_, tokenURI_) {} + constructor(string memory name_, string memory symbol_) ERC7984Mock(name_, symbol_) {} function _update( address from, diff --git a/contracts/mocks/token/ERC7984ReentrantMock.sol b/contracts/mocks/token/ERC7984ReentrantMock.sol index 53e45375..b03332d6 100644 --- a/contracts/mocks/token/ERC7984ReentrantMock.sol +++ b/contracts/mocks/token/ERC7984ReentrantMock.sol @@ -6,11 +6,7 @@ import {euint64} from "@fhevm/solidity/lib/FHE.sol"; import {ERC7984Mock} from "./ERC7984Mock.sol"; contract ERC7984ReentrantMock is ERC7984Mock { - constructor( - string memory name_, - string memory symbol_, - string memory tokenURI_ - ) ERC7984Mock(name_, symbol_, tokenURI_) {} + constructor(string memory name_, string memory symbol_) ERC7984Mock(name_, symbol_) {} function confidentialTransfer(address, euint64) public override returns (euint64 transferred) { IVestingWalletConfidential(msg.sender).release(address(this)); diff --git a/contracts/mocks/token/ERC7984RwaMock.sol b/contracts/mocks/token/ERC7984RwaMock.sol index 85056f3c..9caa7963 100644 --- a/contracts/mocks/token/ERC7984RwaMock.sol +++ b/contracts/mocks/token/ERC7984RwaMock.sol @@ -10,12 +10,7 @@ import {ERC7984Mock} from "./ERC7984Mock.sol"; // solhint-disable func-name-mixedcase contract ERC7984RwaMock is ERC7984Rwa, ERC7984Mock, HandleAccessManager { - constructor( - string memory name, - string memory symbol, - string memory tokenUri, - address admin - ) ERC7984Rwa(admin) ERC7984Mock(name, symbol, tokenUri) {} + constructor(string memory name, string memory symbol, address admin) ERC7984Rwa(admin) ERC7984Mock(name, symbol) {} function supportsInterface(bytes4 interfaceId) public view virtual override(ERC7984Rwa, ERC7984) returns (bool) { return super.supportsInterface(interfaceId); diff --git a/contracts/mocks/token/ERC7984VotesMock.sol b/contracts/mocks/token/ERC7984VotesMock.sol index 95dd3961..e66be878 100644 --- a/contracts/mocks/token/ERC7984VotesMock.sol +++ b/contracts/mocks/token/ERC7984VotesMock.sol @@ -11,11 +11,7 @@ abstract contract ERC7984VotesMock is ERC7984Mock, ERC7984Votes { uint48 private _clockOverrideVal; - constructor( - string memory name_, - string memory symbol_, - string memory tokenURI_ - ) ERC7984Mock(name_, symbol_, tokenURI_) EIP712(name_, "1.0.0") { + constructor(string memory name_, string memory symbol_) ERC7984Mock(name_, symbol_) EIP712(name_, "1.0.0") { _OWNER = msg.sender; } diff --git a/contracts/token/ERC7984/ERC7984.sol b/contracts/token/ERC7984/ERC7984.sol index 0f8eafaa..5679e464 100644 --- a/contracts/token/ERC7984/ERC7984.sol +++ b/contracts/token/ERC7984/ERC7984.sol @@ -30,7 +30,6 @@ abstract contract ERC7984 is IERC7984, ERC165 { euint64 private _totalSupply; string private _name; string private _symbol; - string private _contractURI; /// @dev Emitted when an encrypted amount `encryptedAmount` is requested for disclosure by `requester`. event AmountDiscloseRequested(euint64 indexed encryptedAmount, address indexed requester); @@ -60,10 +59,9 @@ abstract contract ERC7984 is IERC7984, ERC165 { /// @dev The given gateway request ID `requestId` is invalid. error ERC7984InvalidGatewayRequest(uint256 requestId); - constructor(string memory name_, string memory symbol_, string memory contractURI_) { + constructor(string memory name_, string memory symbol_) { _name = name_; _symbol = symbol_; - _contractURI = contractURI_; } /// @inheritdoc ERC165 @@ -86,11 +84,6 @@ abstract contract ERC7984 is IERC7984, ERC165 { return 6; } - /// @inheritdoc IERC7984 - function contractURI() public view virtual returns (string memory) { - return _contractURI; - } - /// @inheritdoc IERC7984 function confidentialTotalSupply() public view virtual returns (euint64) { return _totalSupply; diff --git a/contracts/token/ERC7984/extensions/ERC7984Metadata.sol b/contracts/token/ERC7984/extensions/ERC7984Metadata.sol new file mode 100644 index 00000000..41e732e0 --- /dev/null +++ b/contracts/token/ERC7984/extensions/ERC7984Metadata.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Confidential Contracts (token/ERC7984/extensions/ERC7984Metadata.sol) +pragma solidity ^0.8.27; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC7984Metadata} from "../../../interfaces/IERC7984Metadata.sol"; +import {ERC7984} from "../ERC7984.sol"; + +/** + * @dev Extension of {ERC7984} that adds a {contractURI} function. + */ +abstract contract ERC7984Metadata is IERC7984Metadata, ERC7984 { + string private _contractURI; + + constructor(string memory contractURI_) { + _contractURI = contractURI_; + } + + /// @inheritdoc IERC7984Metadata + function contractURI() public view virtual returns (string memory) { + return _contractURI; + } + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC7984, IERC165) returns (bool) { + return interfaceId == type(IERC7984Metadata).interfaceId || super.supportsInterface(interfaceId); + } +} diff --git a/test/finance/ERC7821WithExecutor.test.ts b/test/finance/ERC7821WithExecutor.test.ts index 702be4b7..005b2629 100644 --- a/test/finance/ERC7821WithExecutor.test.ts +++ b/test/finance/ERC7821WithExecutor.test.ts @@ -6,13 +6,12 @@ import { ethers, fhevm } from 'hardhat'; const name = 'ConfidentialFungibleToken'; const symbol = 'CFT'; -const uri = 'https://example.com/metadata'; describe('ERC7821WithExecutor', function () { beforeEach(async function () { const [recipient, executor, ...accounts] = await ethers.getSigners(); - const token = await ethers.deployContract('$ERC7984Mock', [name, symbol, uri]); + const token = await ethers.deployContract('$ERC7984Mock', [name, symbol]); const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), recipient.address) diff --git a/test/finance/VestingWalletCliffConfidential.test.ts b/test/finance/VestingWalletCliffConfidential.test.ts index ebd9bbc1..2952da9e 100644 --- a/test/finance/VestingWalletCliffConfidential.test.ts +++ b/test/finance/VestingWalletCliffConfidential.test.ts @@ -6,14 +6,13 @@ import { ethers, fhevm } from 'hardhat'; const name = 'ConfidentialFungibleToken'; const symbol = 'CFT'; -const uri = 'https://example.com/metadata'; describe(`VestingWalletCliffConfidential`, function () { beforeEach(async function () { const accounts = (await ethers.getSigners()).slice(3); const [holder, recipient] = accounts; - const token = await ethers.deployContract('$ERC7984Mock', [name, symbol, uri]); + const token = await ethers.deployContract('$ERC7984Mock', [name, symbol]); const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), holder.address) diff --git a/test/finance/VestingWalletConfidential.behavior.ts b/test/finance/VestingWalletConfidential.behavior.ts index 18e0969f..73be1ff2 100644 --- a/test/finance/VestingWalletConfidential.behavior.ts +++ b/test/finance/VestingWalletConfidential.behavior.ts @@ -46,7 +46,7 @@ function shouldBehaveLikeVestingConfidential() { }); it('should not release if reentrancy', async function () { - const reentrantToken = await ethers.deployContract('$ERC7984ReentrantMock', ['name', 'symbol', 'uri']); + const reentrantToken = await ethers.deployContract('$ERC7984ReentrantMock', ['name', 'symbol']); const encryptedInput = await fhevm .createEncryptedInput(await reentrantToken.getAddress(), this.holder.address) .add64(1000) diff --git a/test/finance/VestingWalletConfidential.test.ts b/test/finance/VestingWalletConfidential.test.ts index 09d3014e..2f2078c9 100644 --- a/test/finance/VestingWalletConfidential.test.ts +++ b/test/finance/VestingWalletConfidential.test.ts @@ -4,14 +4,13 @@ import { ethers, fhevm } from 'hardhat'; const name = 'ConfidentialFungibleToken'; const symbol = 'CFT'; -const uri = 'https://example.com/metadata'; describe('VestingWalletConfidential', function () { beforeEach(async function () { const accounts = (await ethers.getSigners()).slice(3); const [holder, recipient] = accounts; - const token = await ethers.deployContract('$ERC7984Mock', [name, symbol, uri]); + const token = await ethers.deployContract('$ERC7984Mock', [name, symbol]); const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), holder.address) diff --git a/test/finance/VestingWalletConfidentialFactory.test.ts b/test/finance/VestingWalletConfidentialFactory.test.ts index a2f334de..c7c85ba1 100644 --- a/test/finance/VestingWalletConfidentialFactory.test.ts +++ b/test/finance/VestingWalletConfidentialFactory.test.ts @@ -9,7 +9,6 @@ import { ethers, fhevm } from 'hardhat'; const name = 'ConfidentialFungibleToken'; const symbol = 'CFT'; -const uri = 'https://example.com/metadata'; const startTimestamp = 9876543210; const duration = 1234; const cliff = 10; @@ -20,7 +19,7 @@ describe('VestingWalletCliffExecutorConfidentialFactory', function () { beforeEach(async function () { const [holder, recipient, recipient2, operator, executor, ...accounts] = await ethers.getSigners(); - const token = (await ethers.deployContract('$ERC7984Mock', [name, symbol, uri])) as any as $ERC7984Mock; + const token = (await ethers.deployContract('$ERC7984Mock', [name, symbol])) as any as $ERC7984Mock; const encryptedInput = await fhevm .createEncryptedInput(await token.getAddress(), holder.address) diff --git a/test/helpers/interface.ts b/test/helpers/interface.ts index caaccf9a..10ebcc79 100644 --- a/test/helpers/interface.ts +++ b/test/helpers/interface.ts @@ -15,13 +15,13 @@ export const SIGNATURES = { 'confidentialTransferFrom(address,address,bytes32)', 'confidentialTransferFromAndCall(address,address,bytes32,bytes,bytes)', 'confidentialTransferFromAndCall(address,address,bytes32,bytes)', - 'contractURI()', 'decimals()', 'isOperator(address,address)', 'name()', 'setOperator(address,uint48)', 'symbol()', ], + ERC7984Metadata: ['contractURI()'], ERC7984ERC20Wrapper: ['underlying()', 'unwrap(address,address,bytes32,bytes)', 'wrap(address,uint256)'], ERC7984RWA: [ 'blockUser(address)', diff --git a/test/token/ERC7984/ERC7984.test.ts b/test/token/ERC7984/ERC7984.test.ts index 5b430c00..3060e6ea 100644 --- a/test/token/ERC7984/ERC7984.test.ts +++ b/test/token/ERC7984/ERC7984.test.ts @@ -7,7 +7,6 @@ import hre, { ethers, fhevm } from 'hardhat'; const name = 'ConfidentialFungibleToken'; const symbol = 'CFT'; -const uri = 'https://example.com/metadata'; /* eslint-disable no-unexpected-multiline */ describe('ERC7984', function () { @@ -15,7 +14,7 @@ describe('ERC7984', function () { const accounts = await ethers.getSigners(); const [holder, recipient, operator] = accounts; - const token = await ethers.deployContract('$ERC7984Mock', [name, symbol, uri]); + const token = await ethers.deployContract('$ERC7984Mock', [name, symbol]); this.accounts = accounts.slice(3); this.holder = holder; this.recipient = recipient; @@ -41,10 +40,6 @@ describe('ERC7984', function () { await expect(this.token.symbol()).to.eventually.equal(symbol); }); - it('sets the uri', async function () { - await expect(this.token.contractURI()).to.eventually.equal(uri); - }); - it('decimals is 6', async function () { await expect(this.token.decimals()).to.eventually.equal(6); }); diff --git a/test/token/ERC7984/ERC7984Votes.test.ts b/test/token/ERC7984/ERC7984Votes.test.ts index 3e5be5de..eb7b8d97 100644 --- a/test/token/ERC7984/ERC7984Votes.test.ts +++ b/test/token/ERC7984/ERC7984Votes.test.ts @@ -7,14 +7,13 @@ import { ethers, fhevm } from 'hardhat'; const name = 'ConfidentialFungibleTokenVotes'; const symbol = 'CFT'; -const uri = 'https://example.com/metadata'; describe('ERC7984Votes', function () { beforeEach(async function () { const accounts = await ethers.getSigners(); const [holder, recipient, operator] = accounts; - const token = await ethers.deployContract('$ERC7984VotesMock', [name, symbol, uri]); + const token = await ethers.deployContract('$ERC7984VotesMock', [name, symbol]); this.accounts = accounts.slice(3); this.holder = holder; diff --git a/test/token/ERC7984/extensions/ERC7984ERC20Wrapper.test.ts b/test/token/ERC7984/extensions/ERC7984ERC20Wrapper.test.ts index d9ef5ca9..1969b5c1 100644 --- a/test/token/ERC7984/extensions/ERC7984ERC20Wrapper.test.ts +++ b/test/token/ERC7984/extensions/ERC7984ERC20Wrapper.test.ts @@ -8,7 +8,6 @@ import { ethers, fhevm } from 'hardhat'; const name = 'ConfidentialFungibleToken'; const symbol = 'CFT'; -const uri = 'https://example.com/metadata'; /* eslint-disable no-unexpected-multiline */ describe('ERC7984ERC20Wrapper', function () { @@ -17,7 +16,7 @@ describe('ERC7984ERC20Wrapper', function () { const [holder, recipient, operator] = accounts; const token = await ethers.deployContract('$ERC20Mock', ['Public Token', 'PT', 18]); - const wrapper = await ethers.deployContract('$ERC7984ERC20WrapperMock', [token, name, symbol, uri]); + const wrapper = await ethers.deployContract('$ERC7984ERC20WrapperMock', [token, name, symbol]); this.accounts = accounts.slice(3); this.holder = holder; @@ -346,7 +345,7 @@ describe('ERC7984ERC20Wrapper', function () { describe('decimals', function () { it('when underlying has 6 decimals', async function () { const token = await ethers.deployContract('ERC20Mock', ['Public Token', 'PT', 6]); - const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol, uri]); + const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol]); await expect(wrapper.decimals()).to.eventually.equal(6); await expect(wrapper.rate()).to.eventually.equal(1); @@ -354,7 +353,7 @@ describe('ERC7984ERC20Wrapper', function () { it('when underlying has more than 9 decimals', async function () { const token = await ethers.deployContract('ERC20Mock', ['Public Token', 'PT', 18]); - const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol, uri]); + const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol]); await expect(wrapper.decimals()).to.eventually.equal(6); await expect(wrapper.rate()).to.eventually.equal(10n ** 12n); @@ -362,7 +361,7 @@ describe('ERC7984ERC20Wrapper', function () { it('when underlying has less than 6 decimals', async function () { const token = await ethers.deployContract('ERC20Mock', ['Public Token', 'PT', 4]); - const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol, uri]); + const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol]); await expect(wrapper.decimals()).to.eventually.equal(4); await expect(wrapper.rate()).to.eventually.equal(1); @@ -370,7 +369,7 @@ describe('ERC7984ERC20Wrapper', function () { it('when underlying decimals are not available', async function () { const token = await ethers.deployContract('ERC20RevertDecimalsMock'); - const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol, uri]); + const wrapper = await ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol]); await expect(wrapper.decimals()).to.eventually.equal(6); await expect(wrapper.rate()).to.eventually.equal(10n ** 12n); @@ -378,7 +377,7 @@ describe('ERC7984ERC20Wrapper', function () { it('when decimals are over `type(uint8).max`', async function () { const token = await ethers.deployContract('ERC20ExcessDecimalsMock'); - await expect(ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol, uri])).to.be.reverted; + await expect(ethers.deployContract('ERC7984ERC20WrapperMock', [token, name, symbol])).to.be.reverted; }); }); }); diff --git a/test/token/ERC7984/extensions/ERC7984Freezable.test.ts b/test/token/ERC7984/extensions/ERC7984Freezable.test.ts index c105e1d5..e7b5f29c 100644 --- a/test/token/ERC7984/extensions/ERC7984Freezable.test.ts +++ b/test/token/ERC7984/extensions/ERC7984Freezable.test.ts @@ -8,7 +8,6 @@ import { ethers, fhevm } from 'hardhat'; const name = 'ConfidentialFungibleToken'; const symbol = 'CFT'; -const uri = 'https://example.com/metadata'; describe('ERC7984Freezable', function () { async function deployFixture() { @@ -16,7 +15,6 @@ describe('ERC7984Freezable', function () { const token = (await ethers.deployContract('$ERC7984FreezableMock', [ name, symbol, - uri, ])) as any as $ERC7984FreezableMock; const acl = IACL__factory.connect(await getAclAddress(), ethers.provider); return { token, acl, holder, recipient, freezer, operator, anyone }; diff --git a/test/token/ERC7984/extensions/ERC7984Metadata.test.ts b/test/token/ERC7984/extensions/ERC7984Metadata.test.ts new file mode 100644 index 00000000..c76032c8 --- /dev/null +++ b/test/token/ERC7984/extensions/ERC7984Metadata.test.ts @@ -0,0 +1,33 @@ +import { INTERFACE_IDS, INVALID_ID } from '../../../helpers/interface'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +const name = 'ConfidentialFungibleToken'; +const symbol = 'CFT'; +const uri = 'https://example.com/metadata'; + +describe('ERC7984Metadata', function () { + beforeEach(async function () { + this.token = await ethers.deployContract('ERC7984MetadataMock', [name, symbol, uri]); + }); + + describe('constructor', function () { + it('sets the contract URI', async function () { + await expect(this.token.contractURI()).to.eventually.equal(uri); + }); + }); + + describe('ERC165', function () { + it('supports IERC7984Metadata', async function () { + await expect(this.token.supportsInterface(INTERFACE_IDS.ERC7984Metadata)).to.eventually.be.true; + }); + + it('supports IERC7984', async function () { + await expect(this.token.supportsInterface(INTERFACE_IDS.ERC7984)).to.eventually.be.true; + }); + + it('does not support invalid interface', async function () { + await expect(this.token.supportsInterface(INVALID_ID)).to.eventually.be.false; + }); + }); +}); diff --git a/test/token/ERC7984/extensions/ERC7984ObserverAccess.test.ts b/test/token/ERC7984/extensions/ERC7984ObserverAccess.test.ts index 2d4d681c..27b83751 100644 --- a/test/token/ERC7984/extensions/ERC7984ObserverAccess.test.ts +++ b/test/token/ERC7984/extensions/ERC7984ObserverAccess.test.ts @@ -5,14 +5,13 @@ import { ethers, fhevm } from 'hardhat'; const name = 'Observer Access Token'; const symbol = 'OAT'; -const uri = 'https://example.com/metadata'; describe('ERC7984ObserverAccess', function () { beforeEach(async function () { const accounts = await ethers.getSigners(); const [holder, recipient, operator] = accounts; - const token = await ethers.deployContract('$ERC7984ObserverAccessMock', [name, symbol, uri]); + const token = await ethers.deployContract('$ERC7984ObserverAccessMock', [name, symbol]); this.holder = holder; this.recipient = recipient; this.token = token; diff --git a/test/token/ERC7984/extensions/ERC7984Omnibus.test.ts b/test/token/ERC7984/extensions/ERC7984Omnibus.test.ts index 70863203..dbe37faa 100644 --- a/test/token/ERC7984/extensions/ERC7984Omnibus.test.ts +++ b/test/token/ERC7984/extensions/ERC7984Omnibus.test.ts @@ -7,16 +7,11 @@ import { ethers, fhevm } from 'hardhat'; const name = 'OmnibusToken'; const symbol = 'OBT'; -const uri = 'https://example.com/metadata'; describe('ERC7984Omnibus', function () { beforeEach(async function () { const [holder, recipient, operator, subaccount] = await ethers.getSigners(); - const token = (await ethers.deployContract('$ERC7984OmnibusMock', [ - name, - symbol, - uri, - ])) as any as $ERC7984OmnibusMock; + const token = (await ethers.deployContract('$ERC7984OmnibusMock', [name, symbol])) as any as $ERC7984OmnibusMock; const acl = IACL__factory.connect(await getAclAddress(), ethers.provider); Object.assign(this, { token, acl, holder, recipient, operator, subaccount }); diff --git a/test/token/ERC7984/extensions/ERC7984Restricted.test.ts b/test/token/ERC7984/extensions/ERC7984Restricted.test.ts index a596d167..7031b948 100644 --- a/test/token/ERC7984/extensions/ERC7984Restricted.test.ts +++ b/test/token/ERC7984/extensions/ERC7984Restricted.test.ts @@ -7,7 +7,7 @@ const initialSupply = 1000n; async function fixture() { const [holder, recipient, approved] = await ethers.getSigners(); - const token = await ethers.deployContract('$ERC7984RestrictedMock', ['token', 'tk', 'uri']); + const token = await ethers.deployContract('$ERC7984RestrictedMock', ['token', 'tk']); await token['$_mint(address,uint64)'](holder, initialSupply); return { holder, recipient, approved, token }; diff --git a/test/token/ERC7984/extensions/ERC7984Rwa.test.ts b/test/token/ERC7984/extensions/ERC7984Rwa.test.ts index bb80cf8d..b977f051 100644 --- a/test/token/ERC7984/extensions/ERC7984Rwa.test.ts +++ b/test/token/ERC7984/extensions/ERC7984Rwa.test.ts @@ -11,7 +11,7 @@ const agentRole = ethers.id('AGENT_ROLE'); const fixture = async () => { const [admin, agent1, agent2, recipient, anyone] = await ethers.getSigners(); - const token = await ethers.deployContract('ERC7984RwaMock', ['name', 'symbol', 'uri', admin.address]); + const token = await ethers.deployContract('ERC7984RwaMock', ['name', 'symbol', admin.address]); await token.connect(admin).addAgent(agent1); token.connect(anyone); return { token, admin, agent1, agent2, recipient, anyone }; From 89c4dde3418849adcee3c8f58be337f7c25c8df4 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:20:38 -0800 Subject: [PATCH 2/5] remove incorrect comment --- contracts/interfaces/IERC7984Metadata.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/interfaces/IERC7984Metadata.sol b/contracts/interfaces/IERC7984Metadata.sol index 54f7d725..d6fba230 100644 --- a/contracts/interfaces/IERC7984Metadata.sol +++ b/contracts/interfaces/IERC7984Metadata.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (token/ERC7984/IERC7984Metadata.sol) + pragma solidity ^0.8.24; import {IERC7984} from "./IERC7984.sol"; From f317bc02a4a0882ef4ed461ee8c065dad29064e5 Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:54:47 -0800 Subject: [PATCH 3/5] add `_setContractURI` internal function --- .../token/ERC7984/extensions/ERC7984Metadata.sol | 16 +++++++++++++++- .../ERC7984/extensions/ERC7984Metadata.test.ts | 12 +++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/contracts/token/ERC7984/extensions/ERC7984Metadata.sol b/contracts/token/ERC7984/extensions/ERC7984Metadata.sol index 41e732e0..bb98299a 100644 --- a/contracts/token/ERC7984/extensions/ERC7984Metadata.sol +++ b/contracts/token/ERC7984/extensions/ERC7984Metadata.sol @@ -10,10 +10,13 @@ import {ERC7984} from "../ERC7984.sol"; * @dev Extension of {ERC7984} that adds a {contractURI} function. */ abstract contract ERC7984Metadata is IERC7984Metadata, ERC7984 { + /// @dev Event emitted when the contract URI is changed. + event ContractURIUpdated(); + string private _contractURI; constructor(string memory contractURI_) { - _contractURI = contractURI_; + _setContractURI(contractURI_); } /// @inheritdoc IERC7984Metadata @@ -25,4 +28,15 @@ abstract contract ERC7984Metadata is IERC7984Metadata, ERC7984 { function supportsInterface(bytes4 interfaceId) public view virtual override(ERC7984, IERC165) returns (bool) { return interfaceId == type(IERC7984Metadata).interfaceId || super.supportsInterface(interfaceId); } + + /** + * @dev Sets the {contractURI} for the contract. + * + * Emits a {ContractURIUpdated} event. + */ + function _setContractURI(string memory newContractURI) internal virtual { + _contractURI = newContractURI; + + emit ContractURIUpdated(); + } } diff --git a/test/token/ERC7984/extensions/ERC7984Metadata.test.ts b/test/token/ERC7984/extensions/ERC7984Metadata.test.ts index c76032c8..620e754a 100644 --- a/test/token/ERC7984/extensions/ERC7984Metadata.test.ts +++ b/test/token/ERC7984/extensions/ERC7984Metadata.test.ts @@ -8,12 +8,18 @@ const uri = 'https://example.com/metadata'; describe('ERC7984Metadata', function () { beforeEach(async function () { - this.token = await ethers.deployContract('ERC7984MetadataMock', [name, symbol, uri]); + this.token = await ethers.deployContract('$ERC7984MetadataMock', [name, symbol, uri]); + await expect(this.token.contractURI()).to.eventually.equal(uri); }); - describe('constructor', function () { + describe('_setContractURI', function () { it('sets the contract URI', async function () { - await expect(this.token.contractURI()).to.eventually.equal(uri); + await this.token.$_setContractURI('new URI'); + await expect(this.token.contractURI()).to.eventually.equal('new URI'); + }); + + it('emits a ContractURIUpdated event', async function () { + await expect(this.token.$_setContractURI(uri)).to.emit(this.token, 'ContractURIUpdated'); }); }); From 317627345ed5975bec65cec70f7f0aaa371c699a Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:24:03 -0500 Subject: [PATCH 4/5] rename `ERC7984ContractURI` to `ERC7984ContractURI` --- .../{IERC7984Metadata.sol => IERC7984ContractURI.sol} | 4 ++-- ...7984MetadataMock.sol => ERC7984ContractURIMock.sol} | 8 ++++---- .../{ERC7984Metadata.sol => ERC7984ContractURI.sol} | 10 +++++----- test/helpers/interface.ts | 2 +- ...7984Metadata.test.ts => ERC7984ContractURI.test.ts} | 8 ++++---- 5 files changed, 16 insertions(+), 16 deletions(-) rename contracts/interfaces/{IERC7984Metadata.sol => IERC7984ContractURI.sol} (71%) rename contracts/mocks/token/{ERC7984MetadataMock.sol => ERC7984ContractURIMock.sol} (66%) rename contracts/token/ERC7984/extensions/{ERC7984Metadata.sol => ERC7984ContractURI.sol} (76%) rename test/token/ERC7984/extensions/{ERC7984Metadata.test.ts => ERC7984ContractURI.test.ts} (83%) diff --git a/contracts/interfaces/IERC7984Metadata.sol b/contracts/interfaces/IERC7984ContractURI.sol similarity index 71% rename from contracts/interfaces/IERC7984Metadata.sol rename to contracts/interfaces/IERC7984ContractURI.sol index d6fba230..a2bed7e0 100644 --- a/contracts/interfaces/IERC7984Metadata.sol +++ b/contracts/interfaces/IERC7984ContractURI.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.24; import {IERC7984} from "./IERC7984.sol"; -/// @dev Interface for optional metadata functions for {IERC7984}. -interface IERC7984Metadata is IERC7984 { +/// @dev Interface for the optional {contractURI} function for {IERC7984}. +interface IERC7984ContractURI is IERC7984 { /// @dev Returns the contract URI. Should be formatted as described in https://eips.ethereum.org/EIPS/eip-7572[ERC-7572]. function contractURI() external view returns (string memory); } diff --git a/contracts/mocks/token/ERC7984MetadataMock.sol b/contracts/mocks/token/ERC7984ContractURIMock.sol similarity index 66% rename from contracts/mocks/token/ERC7984MetadataMock.sol rename to contracts/mocks/token/ERC7984ContractURIMock.sol index 446a85a2..70175f6b 100644 --- a/contracts/mocks/token/ERC7984MetadataMock.sol +++ b/contracts/mocks/token/ERC7984ContractURIMock.sol @@ -3,19 +3,19 @@ pragma solidity ^0.8.27; import {euint64} from "@fhevm/solidity/lib/FHE.sol"; -import {ERC7984Metadata} from "../../token/ERC7984/extensions/ERC7984Metadata.sol"; +import {ERC7984ContractURI} from "../../token/ERC7984/extensions/ERC7984ContractURI.sol"; import {ERC7984Mock, ERC7984} from "./ERC7984Mock.sol"; -contract ERC7984MetadataMock is ERC7984Metadata, ERC7984Mock { +contract ERC7984ContractURIMock is ERC7984ContractURI, ERC7984Mock { constructor( string memory name_, string memory symbol_, string memory contractURI_ - ) ERC7984Mock(name_, symbol_) ERC7984Metadata(contractURI_) {} + ) ERC7984Mock(name_, symbol_) ERC7984ContractURI(contractURI_) {} function supportsInterface( bytes4 interfaceId - ) public view virtual override(ERC7984Metadata, ERC7984) returns (bool) { + ) public view virtual override(ERC7984ContractURI, ERC7984) returns (bool) { return super.supportsInterface(interfaceId); } diff --git a/contracts/token/ERC7984/extensions/ERC7984Metadata.sol b/contracts/token/ERC7984/extensions/ERC7984ContractURI.sol similarity index 76% rename from contracts/token/ERC7984/extensions/ERC7984Metadata.sol rename to contracts/token/ERC7984/extensions/ERC7984ContractURI.sol index bb98299a..177e66aa 100644 --- a/contracts/token/ERC7984/extensions/ERC7984Metadata.sol +++ b/contracts/token/ERC7984/extensions/ERC7984ContractURI.sol @@ -1,15 +1,15 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Confidential Contracts (token/ERC7984/extensions/ERC7984Metadata.sol) +// OpenZeppelin Confidential Contracts (token/ERC7984/extensions/ERC7984ContractURI.sol) pragma solidity ^0.8.27; import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; -import {IERC7984Metadata} from "../../../interfaces/IERC7984Metadata.sol"; +import {IERC7984ContractURI} from "../../../interfaces/IERC7984ContractURI.sol"; import {ERC7984} from "../ERC7984.sol"; /** * @dev Extension of {ERC7984} that adds a {contractURI} function. */ -abstract contract ERC7984Metadata is IERC7984Metadata, ERC7984 { +abstract contract ERC7984ContractURI is IERC7984ContractURI, ERC7984 { /// @dev Event emitted when the contract URI is changed. event ContractURIUpdated(); @@ -19,14 +19,14 @@ abstract contract ERC7984Metadata is IERC7984Metadata, ERC7984 { _setContractURI(contractURI_); } - /// @inheritdoc IERC7984Metadata + /// @inheritdoc IERC7984ContractURI function contractURI() public view virtual returns (string memory) { return _contractURI; } /// @inheritdoc IERC165 function supportsInterface(bytes4 interfaceId) public view virtual override(ERC7984, IERC165) returns (bool) { - return interfaceId == type(IERC7984Metadata).interfaceId || super.supportsInterface(interfaceId); + return interfaceId == type(IERC7984ContractURI).interfaceId || super.supportsInterface(interfaceId); } /** diff --git a/test/helpers/interface.ts b/test/helpers/interface.ts index 10ebcc79..7d403b01 100644 --- a/test/helpers/interface.ts +++ b/test/helpers/interface.ts @@ -21,7 +21,7 @@ export const SIGNATURES = { 'setOperator(address,uint48)', 'symbol()', ], - ERC7984Metadata: ['contractURI()'], + ERC7984ContractURI: ['contractURI()'], ERC7984ERC20Wrapper: ['underlying()', 'unwrap(address,address,bytes32,bytes)', 'wrap(address,uint256)'], ERC7984RWA: [ 'blockUser(address)', diff --git a/test/token/ERC7984/extensions/ERC7984Metadata.test.ts b/test/token/ERC7984/extensions/ERC7984ContractURI.test.ts similarity index 83% rename from test/token/ERC7984/extensions/ERC7984Metadata.test.ts rename to test/token/ERC7984/extensions/ERC7984ContractURI.test.ts index 620e754a..9d33c33d 100644 --- a/test/token/ERC7984/extensions/ERC7984Metadata.test.ts +++ b/test/token/ERC7984/extensions/ERC7984ContractURI.test.ts @@ -6,9 +6,9 @@ const name = 'ConfidentialFungibleToken'; const symbol = 'CFT'; const uri = 'https://example.com/metadata'; -describe('ERC7984Metadata', function () { +describe('ERC7984ContractURI', function () { beforeEach(async function () { - this.token = await ethers.deployContract('$ERC7984MetadataMock', [name, symbol, uri]); + this.token = await ethers.deployContract('$ERC7984ContractURIMock', [name, symbol, uri]); await expect(this.token.contractURI()).to.eventually.equal(uri); }); @@ -24,8 +24,8 @@ describe('ERC7984Metadata', function () { }); describe('ERC165', function () { - it('supports IERC7984Metadata', async function () { - await expect(this.token.supportsInterface(INTERFACE_IDS.ERC7984Metadata)).to.eventually.be.true; + it('supports IERC7984ContractURI', async function () { + await expect(this.token.supportsInterface(INTERFACE_IDS.ERC7984ContractURI)).to.eventually.be.true; }); it('supports IERC7984', async function () { From 75906f1415e4438866123bc613c8a091e928f0df Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Thu, 26 Feb 2026 18:30:45 -0500 Subject: [PATCH 5/5] add extension to docs --- contracts/token/README.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/token/README.adoc b/contracts/token/README.adoc index 1c0c40d1..e9be2220 100644 --- a/contracts/token/README.adoc +++ b/contracts/token/README.adoc @@ -14,6 +14,7 @@ This set of interfaces, contracts, and utilities are all related to `ERC7984`, a - {ERC7984Rwa}: Extension of {ERC7984} that supports confidential Real World Assets (RWAs) by providing compliance checks, transfer controls and enforcement actions. - {ERC7984Votes}: An extension of {ERC7984} that supports confidential vote tracking and delegation via {VotesConfidential}. - {ERC7984Utils}: A library that provides the on-transfer callback check used by {ERC7984}. +- {ERC7984ContractURI}: An extension of {ERC7984} that adds the `contractURI()` function--allowing for additional metadata to be associated with the token. == Core {{ERC7984}} @@ -26,6 +27,7 @@ This set of interfaces, contracts, and utilities are all related to `ERC7984`, a {{ERC7984Omnibus}} {{ERC7984Rwa}} {{ERC7984Votes}} +{{ERC7984ContractURI}} == Utilities {{ERC7984Utils}} \ No newline at end of file