-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat(protocol-contracts): add ConfidentialTokensRegistry contract #1610
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
melanciani
merged 8 commits into
main
from
melanciani/792/protocol-contracts/add_confidential_tokens_registry
Dec 22, 2025
Merged
Changes from 1 commit
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
2dc2cdb
feat(protocol-contracts): add ConfidentialTokensRegistry contract
melanciani 221d7c3
chore(protocol-contracts): take reviews into acocunt
melanciani fba7db4
chore(protocol-contracts): add owner check test
melanciani 3249885
chore(protocol-contracts): add verify task
melanciani 4ff893f
chore(protocol-contracts): simplify verify task
melanciani bea4181
chore(protocol-contracts): simplify implementation and add supportsIn…
melanciani 70d51d3
chore(protocol-contracts): rename contract to ConfidentialTokenWrappe…
melanciani 61016d4
chore(protocol-contracts): update contract natspec comment
melanciani File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
protocol-contracts/confidential-tokens-registry/.env.example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| # Authentication (use one of these) | ||
| MNEMONIC= | ||
| PRIVATE_KEY= | ||
|
|
||
| # RPC URLs | ||
| MAINNET_RPC_URL= | ||
| SEPOLIA_RPC_URL= | ||
|
|
||
| # API Keys | ||
| ETHERSCAN_API_KEY= | ||
|
|
||
| # Gas Reporter | ||
| REPORT_GAS=false | ||
|
|
||
|
|
||
| # ---------------------------------------------------------------------------- | ||
| # ConfidentialTokensRegistry initial configuration | ||
| # ---------------------------------------------------------------------------- | ||
|
|
||
| # The number of tokens to register initially | ||
| INITIAL_NUM_TOKENS=2 | ||
|
|
||
| # Token and confidential token 1 | ||
| INITIAL_TOKEN_ADDRESS_0=0x0000000000000000000000000000000000000001 | ||
| INITIAL_CONFIDENTIAL_TOKEN_ADDRESS_0=0x0000000000000000000000000000000000000011 | ||
|
|
||
| # Token and confidential token 2 | ||
| INITIAL_TOKEN_ADDRESS_1=0x0000000000000000000000000000000000000002 | ||
| INITIAL_CONFIDENTIAL_TOKEN_ADDRESS_1=0x0000000000000000000000000000000000000022 |
24 changes: 24 additions & 0 deletions
24
protocol-contracts/confidential-tokens-registry/.gitignore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Dependencies | ||
| node_modules/ | ||
|
|
||
| # Build artifacts | ||
| artifacts/ | ||
| cache/ | ||
| cache_forge/ | ||
| out/ | ||
| types/ | ||
|
|
||
| # Coverage | ||
| coverage/ | ||
| coverage.json | ||
|
|
||
| # Environment | ||
| .env | ||
|
|
||
| # IDE | ||
| .idea/ | ||
| .vscode/ | ||
|
|
||
| # OS | ||
| .DS_Store | ||
|
|
22 changes: 22 additions & 0 deletions
22
protocol-contracts/confidential-tokens-registry/.prettierignore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| # directories | ||
| .coverage_artifacts | ||
| .coverage_cache | ||
| .coverage_contracts | ||
| artifacts | ||
| build | ||
| cache | ||
| coverage | ||
| dist | ||
| node_modules | ||
| types | ||
| lib | ||
|
|
||
| # files | ||
| *.env | ||
| *.log | ||
| .DS_Store | ||
| .pnp.* | ||
| coverage.json | ||
| package-lock.json | ||
| yarn.lock | ||
| README.md |
15 changes: 15 additions & 0 deletions
15
protocol-contracts/confidential-tokens-registry/.prettierrc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| { | ||
| "printWidth": 120, | ||
| "singleQuote": true, | ||
| "trailingComma": "all", | ||
| "arrowParens": "avoid", | ||
| "overrides": [ | ||
| { | ||
| "files": "*.sol", | ||
| "options": { | ||
| "singleQuote": false | ||
| } | ||
| } | ||
| ], | ||
| "plugins": ["prettier-plugin-solidity"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| include .env.example | ||
|
|
||
| ENV_PATH=.env.example | ||
|
|
||
| prettier: | ||
| npx prettier . --write | ||
|
|
||
| lint: | ||
| npm run lint | ||
|
|
||
| compile: | ||
| npx hardhat compile | ||
|
|
||
| clean: | ||
| npx hardhat clean | ||
|
|
||
| get-accounts: | ||
| DOTENV_CONFIG_PATH=$(ENV_PATH) npx hardhat get-accounts | ||
|
|
||
| # Define it as a phony target to avoid conflicts with the test directory | ||
| .PHONY: test | ||
| test: clean | ||
| DOTENV_CONFIG_PATH=$(ENV_PATH) npx hardhat test --network hardhat $(if $(GREP),--grep '$(GREP)',) | ||
|
|
||
| # Conform to pre-commit checks | ||
| conformance: prettier lint |
193 changes: 193 additions & 0 deletions
193
protocol-contracts/confidential-tokens-registry/contracts/ConfidentialTokensRegistry.sol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| // SPDX-License-Identifier: BSD-3-Clause-Clear | ||
| pragma solidity ^0.8.27; | ||
|
|
||
| import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; | ||
| import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol"; | ||
|
|
||
| /** | ||
| * @title ConfidentialTokensRegistry | ||
| * @notice A registry contract to map ERC20 token addresses to their ERC7984 confidential token addresses. | ||
| * @dev This contract allows an owner to register new entries and flag revoked ones. | ||
| */ | ||
| contract ConfidentialTokensRegistry is Ownable2StepUpgradeable, UUPSUpgradeable { | ||
| /// @custom:storage-location erc7201:fhevm_protocol.storage.ConfidentialTokensRegistry | ||
| struct ConfidentialTokensRegistryStorage { | ||
| mapping(address tokenAddress => address confidentialTokenAddress) _tokensToConfidentialTokens; | ||
| mapping(address confidentialTokenAddress => address tokenAddress) _confidentialTokensToTokens; | ||
| mapping(address confidentialTokenAddress => bool isRevoked) _revokedConfidentialTokens; | ||
| } | ||
|
|
||
| // keccak256(abi.encode(uint256(keccak256("fhevm_protocol.storage.ConfidentialTokensRegistry")) - 1)) & ~bytes32(uint256(0xff)) | ||
| bytes32 private constant CONFIDENTIAL_TOKENS_REGISTRY_STORAGE_LOCATION = | ||
| 0x25094496394205337c7da64eaca0c35bf780125467d04de96cd0ee9b701d6c00; | ||
|
|
||
| /// @notice Error thrown when the token address is zero. | ||
| error TokenZeroAddress(); | ||
|
|
||
| /// @notice Error thrown when the confidential token address is zero. | ||
| error ConfidentialTokenZeroAddress(); | ||
|
|
||
| /// @notice Error thrown when a confidential token is already associated with a token. | ||
| error ConfidentialTokenAlreadyAssociatedWithToken(address tokenAddress, address existingConfidentialTokenAddress); | ||
|
|
||
| /// @notice Error thrown when a token is already associated with a confidential token. | ||
| error TokenAlreadyAssociatedWithConfidentialToken(address tokenAddress, address existingConfidentialTokenAddress); | ||
|
|
||
| /// @notice Error thrown when a confidential token is revoked. | ||
| error RevokedConfidentialToken(address confidentialTokenAddress); | ||
|
|
||
| /// @notice Error thrown when no token is associated with a confidential token. | ||
| error NoTokenAssociatedWithConfidentialToken(address confidentialTokenAddress); | ||
|
|
||
| /// @notice Error thrown when a confidential token is not revoked. | ||
| error ConfidentialTokenNotRevoked(address confidentialTokenAddress); | ||
|
|
||
| /// @notice Emitted when a token is registered and associated with a confidential token. | ||
| event ConfidentialTokenRegistered(address indexed tokenAddress, address indexed confidentialTokenAddress); | ||
|
|
||
| /// @notice Emitted when a confidential token is revoked and the association with a token is removed. | ||
| event ConfidentialTokenRevoked(address indexed tokenAddress, address indexed confidentialTokenAddress); | ||
|
|
||
| /// @notice Emitted when a confidential token no longer revoked and thus is allowed to be | ||
| /// associated with a token again. | ||
| event ConfidentialTokenReinstated(address indexed confidentialTokenAddress); | ||
|
|
||
| /// @custom:oz-upgrades-unsafe-allow constructor | ||
| constructor() { | ||
| _disableInitializers(); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Initialize the ConfidentialTokensRegistry contract. | ||
| * @param initialOwner The initial owner of the contract. | ||
| */ | ||
| function initialize(address initialOwner) public initializer { | ||
| __Ownable_init(initialOwner); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Register a new ERC20 token and associate it with a validated corresponding ERC7984 confidential token. | ||
| * @param tokenAddress The address of the ERC20 token contract to register. | ||
| */ | ||
| function registerConfidentialToken(address tokenAddress, address confidentialTokenAddress) external onlyOwner { | ||
| if (tokenAddress == address(0)) { | ||
| revert TokenZeroAddress(); | ||
| } | ||
| if (confidentialTokenAddress == address(0)) { | ||
| revert ConfidentialTokenZeroAddress(); | ||
| } | ||
|
|
||
| // The confidential token must not be revoked. | ||
| if (isConfidentialTokenRevoked(confidentialTokenAddress)) { | ||
melanciani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| revert RevokedConfidentialToken(confidentialTokenAddress); | ||
| } | ||
|
|
||
| // The confidential token must not be already associated with a token. | ||
| address existingTokenAddress = getTokenAddress(confidentialTokenAddress); | ||
| if (existingTokenAddress != address(0)) { | ||
| revert ConfidentialTokenAlreadyAssociatedWithToken(confidentialTokenAddress, existingTokenAddress); | ||
| } | ||
|
|
||
| // The token must not be already associated with a confidential token. | ||
| address existingConfidentialTokenAddress = getConfidentialTokenAddress(tokenAddress); | ||
| if (existingConfidentialTokenAddress != address(0)) { | ||
| revert TokenAlreadyAssociatedWithConfidentialToken(tokenAddress, existingConfidentialTokenAddress); | ||
| } | ||
|
|
||
| ConfidentialTokensRegistryStorage storage $ = _getConfidentialTokensRegistryStorage(); | ||
|
|
||
| $._tokensToConfidentialTokens[tokenAddress] = confidentialTokenAddress; | ||
| $._confidentialTokensToTokens[confidentialTokenAddress] = tokenAddress; | ||
|
|
||
| emit ConfidentialTokenRegistered(tokenAddress, confidentialTokenAddress); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Revoke an ERC7984 confidential token. | ||
| * @param confidentialTokenAddress The address of the ERC7984 confidential token to revoke. | ||
| */ | ||
| function revokeConfidentialToken(address confidentialTokenAddress) external onlyOwner { | ||
| if (confidentialTokenAddress == address(0)) { | ||
| revert ConfidentialTokenZeroAddress(); | ||
| } | ||
|
|
||
| // The confidential token must not be already revoked. | ||
| if (isConfidentialTokenRevoked(confidentialTokenAddress)) { | ||
| revert RevokedConfidentialToken(confidentialTokenAddress); | ||
| } | ||
|
|
||
| // The confidential token must be associated with a token. | ||
| address tokenAddress = getTokenAddress(confidentialTokenAddress); | ||
| if (tokenAddress == address(0)) { | ||
| revert NoTokenAssociatedWithConfidentialToken(confidentialTokenAddress); | ||
| } | ||
|
|
||
| ConfidentialTokensRegistryStorage storage $ = _getConfidentialTokensRegistryStorage(); | ||
|
|
||
| delete $._confidentialTokensToTokens[confidentialTokenAddress]; | ||
| delete $._tokensToConfidentialTokens[tokenAddress]; | ||
| $._revokedConfidentialTokens[confidentialTokenAddress] = true; | ||
|
|
||
| emit ConfidentialTokenRevoked(tokenAddress, confidentialTokenAddress); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Reinstate an ERC7984 confidential token in case it was revoked. It does not restore | ||
| * the association with a token, it only allows it to be associated with one again. | ||
| * @param confidentialTokenAddress The address of the ERC7984 confidential token to reinstate. | ||
| */ | ||
| function reinstateConfidentialToken(address confidentialTokenAddress) external onlyOwner { | ||
melanciani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| ConfidentialTokensRegistryStorage storage $ = _getConfidentialTokensRegistryStorage(); | ||
| if (confidentialTokenAddress == address(0)) { | ||
| revert ConfidentialTokenZeroAddress(); | ||
| } | ||
|
|
||
| // The confidential token must be revoked. | ||
| if (!isConfidentialTokenRevoked(confidentialTokenAddress)) { | ||
| revert ConfidentialTokenNotRevoked(confidentialTokenAddress); | ||
| } | ||
|
|
||
| $._revokedConfidentialTokens[confidentialTokenAddress] = false; | ||
| emit ConfidentialTokenReinstated(confidentialTokenAddress); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Returns the address of the confidential token associated with a token. | ||
| * @param tokenAddress The address of the token. | ||
| * @return The address of the confidential token. | ||
| */ | ||
| function getConfidentialTokenAddress(address tokenAddress) public view returns (address) { | ||
melanciani marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return _getConfidentialTokensRegistryStorage()._tokensToConfidentialTokens[tokenAddress]; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Returns the address of the token associated with a confidential token. | ||
| * @param confidentialTokenAddress The address of the confidential token. | ||
| * @return The address of the token. | ||
| */ | ||
| function getTokenAddress(address confidentialTokenAddress) public view returns (address) { | ||
| return _getConfidentialTokensRegistryStorage()._confidentialTokensToTokens[confidentialTokenAddress]; | ||
| } | ||
|
|
||
| /** | ||
| * @notice Returns true if a confidential token is revoked, false otherwise. A revoked confidential token | ||
| * is not allowed to be associated with a token again. | ||
| * @param confidentialTokenAddress The address of the confidential token. | ||
| * @return True if the confidential token is revoked, false otherwise. | ||
| */ | ||
| function isConfidentialTokenRevoked(address confidentialTokenAddress) public view returns (bool) { | ||
| return _getConfidentialTokensRegistryStorage()._revokedConfidentialTokens[confidentialTokenAddress]; | ||
| } | ||
|
|
||
| function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} | ||
|
|
||
| function _getConfidentialTokensRegistryStorage() | ||
| private | ||
| pure | ||
| returns (ConfidentialTokensRegistryStorage storage $) | ||
| { | ||
| assembly { | ||
| $.slot := CONFIDENTIAL_TOKENS_REGISTRY_STORAGE_LOCATION | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| [profile.default] | ||
| src = 'contracts' | ||
| libs = ['node_modules'] | ||
| solc = '0.8.27' | ||
| optimizer_runs = 200 | ||
|
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.