Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b4ce8d9
Add `ERC7984Freezable` extension.
james-toussaint Jul 25, 2025
f04162b
Update pragma
james-toussaint Jul 25, 2025
ec16cdf
Merge remote-tracking branch 'origin/master' into feature/confidentia…
james-toussaint Aug 6, 2025
ff2a74f
Add freezable test
james-toussaint Aug 6, 2025
f485ec0
Remove unused functions in freezable mock
james-toussaint Aug 6, 2025
a961e33
Bring back previous HandleAccessManager
james-toussaint Aug 19, 2025
3c759fb
Move allow available to access function in mock
james-toussaint Aug 19, 2025
cd81483
Swap event & error order
james-toussaint Aug 19, 2025
3bb3f2e
Update inline documentation
james-toussaint Aug 19, 2025
ac9ae06
Remove useless var
james-toussaint Aug 19, 2025
4095083
Merge branch 'master' into feature/confidential-freezable-token
arr00 Aug 19, 2025
0286f57
Apply suggestions from review
james-toussaint Aug 20, 2025
c790dc5
Move freezable test file to extensions dir
james-toussaint Aug 20, 2025
754ecec
update tests
arr00 Aug 20, 2025
8e08500
Apply suggestions
james-toussaint Aug 21, 2025
7e45893
Remove account from `_validateHandleAllowance` (#178)
arr00 Aug 21, 2025
ed13e2f
Call internal when setting frozen with proof
james-toussaint Aug 22, 2025
9ae4074
Update doc
james-toussaint Aug 22, 2025
6a52710
Update pragma & remove import in freezable mock
james-toussaint Aug 22, 2025
bc71379
Base freezable mock on ERC7984Mock
james-toussaint Aug 22, 2025
4466341
Merge branch 'master' into feature/confidential-freezable-token
arr00 Aug 22, 2025
7f9b646
Apply suggestions
james-toussaint Aug 25, 2025
9d8aa7d
Check transfer amount is zero if transferring more than available
james-toussaint Aug 25, 2025
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/seven-books-dig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-confidential-contracts': minor
---

`ERC7984Freezable`: Add an extension to `ERC7984`, which allows an account granted the "freezer" role to freeze and unfreeze a confidential amount of tokens for holders.
50 changes: 50 additions & 0 deletions contracts/mocks/token/ERC7984FreezableMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {SepoliaConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
import {FHE, euint64, externalEuint64} from "@fhevm/solidity/lib/FHE.sol";
import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol";
import {ERC7984} from "../../token/ERC7984/ERC7984.sol";
import {ERC7984Freezable} from "../../token/ERC7984/extensions/ERC7984Freezable.sol";
import {HandleAccessManager} from "../../utils/HandleAccessManager.sol";
import {ERC7984Mock} from "./ERC7984Mock.sol";

// solhint-disable func-name-mixedcase
contract ERC7984FreezableMock is ERC7984Mock, ERC7984Freezable, AccessControl, HandleAccessManager {
bytes32 public constant FREEZER_ROLE = keccak256("FREEZER_ROLE");

error UnallowedHandleAccess(bytes32 handle, address account);

constructor(
string memory name,
string memory symbol,
string memory tokenUri,
address freezer
) ERC7984Mock(name, symbol, tokenUri) {
_grantRole(FREEZER_ROLE, freezer);
}

function _update(
address from,
address to,
euint64 amount
) internal virtual override(ERC7984Mock, ERC7984Freezable) returns (euint64) {
return super._update(from, to, amount);
}

function createEncryptedAmount(uint64 amount) public returns (euint64 encryptedAmount) {
FHE.allowThis(encryptedAmount = FHE.asEuint64(amount));
FHE.allow(encryptedAmount, msg.sender);
}

function confidentialAvailableAccess(address account) public {
euint64 available = confidentialAvailable(account);
FHE.allowThis(available);
getHandleAllowance(euint64.unwrap(available), account, true);
}

function _validateHandleAllowance(bytes32 handle) internal view override {}

function _checkFreezer() internal override onlyRole(FREEZER_ROLE) {}
}
83 changes: 83 additions & 0 deletions contracts/token/ERC7984/extensions/ERC7984Freezable.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.27;

import {FHE, ebool, euint64, externalEuint64} from "@fhevm/solidity/lib/FHE.sol";
import {FHESafeMath} from "../../../utils/FHESafeMath.sol";
import {ERC7984} from "../ERC7984.sol";

/**
* @dev Extension of {ERC7984} that allows to implement a confidential
* freezing mechanism that can be managed by an authorized account with
* {setConfidentialFrozen} functions.
*
* The freezing mechanism provides the guarantee to the contract owner
* (e.g. a DAO or a well-configured multisig) that a specific confidential
* amount of tokens held by an account won't be transferable until those
* tokens are unfrozen.
*
* Inspired by https://github.com/OpenZeppelin/openzeppelin-community-contracts/blob/master/contracts/token/ERC20/extensions/ERC20Freezable.sol
*/
abstract contract ERC7984Freezable is ERC7984 {
/// @dev Confidential frozen amount of tokens per address.
mapping(address account => euint64 encryptedAmount) private _frozenBalances;

/// @dev Emitted when a confidential amount of token is frozen for an account
event TokensFrozen(address indexed account, euint64 encryptedAmount);

/// @dev Returns the confidential frozen balance of an account.
function confidentialFrozen(address account) public view virtual returns (euint64) {
return _frozenBalances[account];
}

/// @dev Returns the confidential available (unfrozen) balance of an account. Up to {confidentialBalanceOf}.
function confidentialAvailable(address account) public virtual returns (euint64) {
(ebool success, euint64 unfrozen) = FHESafeMath.tryDecrease(
confidentialBalanceOf(account),
confidentialFrozen(account)
);
return FHE.select(success, unfrozen, FHE.asEuint64(0));
}

/// @dev Freezes a confidential amount of tokens for an account with a proof.
function setConfidentialFrozen(
address account,
externalEuint64 encryptedAmount,
bytes calldata inputProof
) public virtual {
_setConfidentialFrozen(account, FHE.fromExternal(encryptedAmount, inputProof));
}

/// @dev Freezes a confidential amount of tokens for an account.
function setConfidentialFrozen(address account, euint64 encryptedAmount) public virtual {
require(
FHE.isAllowed(encryptedAmount, msg.sender),
ERC7984UnauthorizedUseOfEncryptedAmount(encryptedAmount, msg.sender)
);
_setConfidentialFrozen(account, encryptedAmount);
}

/// @dev Internal function to freeze a confidential amount of tokens for an account.
function _setConfidentialFrozen(address account, euint64 encryptedAmount) internal virtual {
_checkFreezer();
FHE.allowThis(encryptedAmount);
FHE.allow(encryptedAmount, account);
_frozenBalances[account] = encryptedAmount;
emit TokensFrozen(account, encryptedAmount);
}

/// @dev Unimplemented function that must revert if `msg.sender` is not authorized as a freezer.
function _checkFreezer() internal virtual;

/**
* @dev See {ERC7984-_update}. The `from` account must have sufficient unfrozen balance,
* otherwise 0 tokens are transferred.
*/
function _update(address from, address to, euint64 encryptedAmount) internal virtual override returns (euint64) {
if (from != address(0)) {
euint64 unfrozen = confidentialAvailable(from);
encryptedAmount = FHE.select(FHE.le(encryptedAmount, unfrozen), encryptedAmount, FHE.asEuint64(0));
}
return super._update(from, to, encryptedAmount);
}
}
2 changes: 2 additions & 0 deletions contracts/token/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This set of interfaces, contracts, and utilities are all related to `ERC7984`, a

- {ERC7984}: Implementation of {IERC7984}.
- {ERC7984ERC20Wrapper}: Extension of {ERC7984} which wraps an `ERC20` into a confidential token. The wrapper allows for free conversion in both directions at a fixed rate.
- {ERC7984Freezable}: An extension for {ERC7984}, which allows an account granted the "freezer" role to freeze and unfreeze a confidential amount of tokens for holders.
- {ERC7984ObserverAccess}: An extension for {ERC7984}, which allows each account to add an observer who is given access to their transfer and balance amounts.
- {ERC7984Utils}: A library that provides the on-transfer callback check used by {ERC7984}.

Expand All @@ -15,6 +16,7 @@ This set of interfaces, contracts, and utilities are all related to `ERC7984`, a

== Extensions
{{ERC7984ERC20Wrapper}}
{{ERC7984Freezable}}
{{ERC7984ObserverAccess}}

== Utilities
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/helpers/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Addressable, Signer, ethers } from 'ethers';
import fs from 'fs';
import { HardhatRuntimeEnvironment } from 'hardhat/types';

const ACL_ADDRESS = constants.ACL_CONTRACT_ADDRESS;
export const ACL_ADDRESS = constants.ACL_CONTRACT_ADDRESS;

const DEFAULT_BALANCE: bigint = 10000n * ethers.WeiPerEther;

Expand Down
Loading