diff --git a/content/confidential-contracts/api/finance.mdx b/content/confidential-contracts/api/finance.mdx
index 46be5ad3..082b6202 100644
--- a/content/confidential-contracts/api/finance.mdx
+++ b/content/confidential-contracts/api/finance.mdx
@@ -25,7 +25,7 @@ For convenience, this directory also includes:
## `BatcherConfidential`
-
+
@@ -36,8 +36,8 @@ import "@openzeppelin/confidential-contracts/finance/BatcherConfidential.sol";
```
`BatcherConfidential` is a batching primitive that enables routing between two [`ERC7984ERC20Wrapper`](/confidential-contracts/api/token#ERC7984ERC20Wrapper) contracts
-via a non-confidential route. Users deposit [`BatcherConfidential.fromToken`](#BatcherConfidential-fromToken--) into the batcher and receive [`BatcherConfidential.toToken`](#BatcherConfidential-toToken--) in exchange. Deposits are
-made by using `ERC7984` transfer and call functions such as [`ERC7984.confidentialTransferAndCall`](/confidential-contracts/api/token#ERC7984-confidentialTransferAndCall-address-euint64-bytes-).
+(with distinct underlying tokens) via a non-confidential route. Users deposit [`BatcherConfidential.fromToken`](#BatcherConfidential-fromToken--) into the batcher and receive
+[`BatcherConfidential.toToken`](#BatcherConfidential-toToken--) in exchange. Deposits are made by using `ERC7984` transfer and call functions such as [`ERC7984.confidentialTransferAndCall`](/confidential-contracts/api/token#ERC7984-confidentialTransferAndCall-address-euint64-bytes-).
Developers must implement the virtual function [`BatcherConfidential._executeRoute`](#BatcherConfidential-_executeRoute-uint256-uint256-) to perform the batch's route. This function is called
once the batch deposits are unwrapped into the underlying tokens. The function should swap the underlying [`BatcherConfidential.fromToken`](#BatcherConfidential-fromToken--) for
@@ -119,6 +119,8 @@ underlying tokens into [`BatcherConfidential.toToken`](#BatcherConfidential-toTo
- [InvalidExchangeRate(batchId, totalDeposits, exchangeRate)](#BatcherConfidential-InvalidExchangeRate-uint256-uint256-uint64-)
- [Unauthorized()](#BatcherConfidential-Unauthorized--)
- [InvalidWrapperToken(token)](#BatcherConfidential-InvalidWrapperToken-address-)
+- [DuplicateUnderlyingTokens()](#BatcherConfidential-DuplicateUnderlyingTokens--)
+- [IntermediateStepToTokenBalanceChanged(batchId)](#BatcherConfidential-IntermediateStepToTokenBalanceChanged-uint256-)
ReentrancyGuardTransient
@@ -485,7 +487,7 @@ the balance of the underlying [`BatcherConfidential.toToken`](#BatcherConfidenti
[`BatcherConfidential.dispatchBatchCallback`](#BatcherConfidential-dispatchBatchCallback-uint256-uint64-bytes-) (and in turn [`BatcherConfidential._executeRoute`](#BatcherConfidential-_executeRoute-uint256-uint256-)) can be repeatedly called until the route execution is complete.
If a multi-step route is necessary, intermediate steps should return `ExecuteOutcome.Partial`. Intermediate steps *must* not
-result in underlying [`BatcherConfidential.toToken`](#BatcherConfidential-toToken--) being transferred into the batcher.
+result in underlying [`BatcherConfidential.toToken`](#BatcherConfidential-toToken--) being transferred to or from the batcher.
@@ -771,13 +773,47 @@ The given `token` does not support `IERC7984ERC20Wrapper` via `ERC165`.
+
+
+
+
+Intermediate steps must not result in underlying [`BatcherConfidential.toToken`](#BatcherConfidential-toToken--) being transferred to or from the batcher.
+
+
+
+
## `ERC7821WithExecutor`
-
+
@@ -915,7 +951,7 @@ function _erc7821AuthorizedExecutor(
## `VestingWalletCliffConfidential`
-
+
@@ -1139,7 +1175,7 @@ The specified cliff duration is larger than the vesting duration.
## `VestingWalletConfidential`
-
+
@@ -1467,7 +1503,7 @@ Emitted when releasable vested tokens are released.
## `VestingWalletConfidentialFactory`
-
+
diff --git a/content/confidential-contracts/api/governance.mdx b/content/confidential-contracts/api/governance.mdx
index 4f0fce5a..6b6cfd19 100644
--- a/content/confidential-contracts/api/governance.mdx
+++ b/content/confidential-contracts/api/governance.mdx
@@ -16,7 +16,7 @@ This directory includes primitives for on-chain confidential governance.
## `VotesConfidential`
-
+
@@ -61,7 +61,7 @@ to activate checkpoints and have their voting power tracked.
HandleAccessManager
- [getHandleAllowance(handle, account, persistent)](#HandleAccessManager-getHandleAllowance-bytes32-address-bool-)
-- [_validateHandleAllowance(handle)](#HandleAccessManager-_validateHandleAllowance-bytes32-)
+- [_validateHandleAllowance()](#HandleAccessManager-_validateHandleAllowance-bytes32-)
diff --git a/content/confidential-contracts/api/interfaces.mdx b/content/confidential-contracts/api/interfaces.mdx
index 1a7711d5..4e37e175 100644
--- a/content/confidential-contracts/api/interfaces.mdx
+++ b/content/confidential-contracts/api/interfaces.mdx
@@ -23,7 +23,7 @@ These interfaces are available as `.sol` files and are useful to interact with t
## `IERC7984`
-
+
@@ -424,7 +424,7 @@ should be able to disclose the amount. This functionality is implementation spec
## `IERC7984ERC20Wrapper`
-
+
@@ -638,13 +638,123 @@ Emitted when an unwrap request is finalized for a given `receiver`, `unwrapReque
+
+Optionally emitted by a module to indicate the result of its validation (pre-transfer) hook.
+
+
+
+
## `IERC7984Receiver`
-
+
@@ -678,6 +788,10 @@ Interface for contracts that can receive ERC7984 transfers with a callback.
Called upon receiving a confidential token transfer. Returns an encrypted boolean indicating success
of the callback. If false is returned, the token contract will attempt to refund the transfer.
+
+The calling contract (token) must be granted ACL allowance to read the confidential return value.
+
+
Do not manually refund the transfer AND return false, as this can lead to double refunds.
@@ -691,7 +805,7 @@ Do not manually refund the transfer AND return false, as this can lead to double
## `IERC7984Rwa`
-
+
@@ -707,6 +821,8 @@ Interface for confidential RWA contracts.
+
+Emitted when the balance `amount` of `lostAccount` is recovered to `newAccount`.
+
+
+
+
diff --git a/content/confidential-contracts/api/token.mdx b/content/confidential-contracts/api/token.mdx
index dd268a47..0d48366b 100644
--- a/content/confidential-contracts/api/token.mdx
+++ b/content/confidential-contracts/api/token.mdx
@@ -13,7 +13,11 @@ This set of interfaces, contracts, and utilities are all related to `ERC7984`, a
* [`ERC7984Omnibus`](#ERC7984Omnibus): An extension of [`ERC7984`](#ERC7984) that emits additional events for omnibus transfers, which contain encrypted addresses for the sub-account sender and recipient.
* [`ERC7984Rwa`](#ERC7984Rwa): Extension of [`ERC7984`](#ERC7984) that supports confidential Real World Assets (RWAs) by providing compliance checks, transfer controls and enforcement actions.
* [`ERC7984Votes`](#ERC7984Votes): An extension of [`ERC7984`](#ERC7984) that supports confidential vote tracking and delegation via [`VotesConfidential`](/confidential-contracts/api/governance#VotesConfidential).
+* [`ERC7984Hooked`](#ERC7984Hooked): An extension of [`ERC7984`](#ERC7984) that calls pre and post transfer hooks on installed modules.
* [`ERC7984Utils`](#ERC7984Utils): A library that provides the on-transfer callback check used by [`ERC7984`](#ERC7984).
+* [`ERC7984HookModule`](#ERC7984HookModule): A an abstract, base implementation for hook modules compatible with [`ERC7984Hooked`](#ERC7984Hooked).
+* [`ERC7984BalanceCapHookModule`](#ERC7984BalanceCapHookModule): An example hook module for [`ERC7984Hooked`](#ERC7984Hooked) that enforces a per-account encrypted balance cap.
+* [`ERC7984HolderCapHookModule`](#ERC7984HolderCapHookModule): An example hook module for [`ERC7984Hooked`](#ERC7984Hooked) that enforces a maximum number of holders. Must be installed before any tokens are minted.
## Core
[`ERC7984`](#ERC7984)
@@ -26,9 +30,13 @@ This set of interfaces, contracts, and utilities are all related to `ERC7984`, a
[`ERC7984Omnibus`](#ERC7984Omnibus)
[`ERC7984Rwa`](#ERC7984Rwa)
[`ERC7984Votes`](#ERC7984Votes)
+[`ERC7984Hooked`](#ERC7984Hooked)
## Utilities
[`ERC7984Utils`](#ERC7984Utils)
+[`ERC7984HookModule`](#ERC7984HookModule)
+[`ERC7984BalanceCapHookModule`](#ERC7984BalanceCapHookModule)
+[`ERC7984HolderCapHookModule`](#ERC7984HolderCapHookModule)
@@ -36,7 +44,7 @@ This set of interfaces, contracts, and utilities are all related to `ERC7984`, a
## `ERC7984`
-
+
@@ -113,10 +121,8 @@ Key features:
- [ERC7984InvalidReceiver(receiver)](#ERC7984-ERC7984InvalidReceiver-address-)
- [ERC7984InvalidSender(sender)](#ERC7984-ERC7984InvalidSender-address-)
- [ERC7984UnauthorizedSpender(holder, spender)](#ERC7984-ERC7984UnauthorizedSpender-address-address-)
-- [ERC7984ZeroBalance(holder)](#ERC7984-ERC7984ZeroBalance-address-)
- [ERC7984UnauthorizedUseOfEncryptedAmount(amount, user)](#ERC7984-ERC7984UnauthorizedUseOfEncryptedAmount-euint64-address-)
- [ERC7984UnauthorizedCaller(caller)](#ERC7984-ERC7984UnauthorizedCaller-address-)
-- [ERC7984InvalidGatewayRequest(requestId)](#ERC7984-ERC7984InvalidGatewayRequest-uint256-)
@@ -288,11 +294,8 @@ Returns true if `spender` is currently an operator for `holder`.
-Sets `operator` as an operator for `holder` until the timestamp `until`.
-
-
-An operator may transfer any amount of tokens on behalf of a holder while approved.
-
+See [`IERC7984.setOperator`](/confidential-contracts/api/interfaces#IERC7984-setOperator-address-uint48-). Operators are given ACL allowance (ability to decrypt) for the transferred amount
+of transfers they initiate.
@@ -338,7 +341,7 @@ Similar to `confidentialTransfer-address-externalEuint64-bytes` but without an i
confidentialTransferFromAndCall(address from, address to, euint64 amount, bytes data) → euint64
public
#
@@ -563,6 +566,28 @@ May not be tied to a prior request via [`ERC7984.requestDiscloseEncryptedAmount`
+Transfers the given amount of tokens from `from` to `to` and calls the `onConfidentialTransferReceived`
+function on the recipient.
+
+The token contract initiates a second transfer refunding the tokens from the recipient to the sender--the
+amount is 0 if the callback succeeds, otherwise the amount is the amount that was transferred.
+
+The returned `transferred` amount is a fresh ciphertext computed as `sent - refund`
+and `msg.sender` only receives a transient FHE allowance for it. This value is generally
+intended to be processed only in the same transaction. Event observers see `sent` and `refund` individually.
+
+
+The refund triggered when [`IERC7984Receiver.onConfidentialTransferReceived`](/confidential-contracts/api/interfaces#IERC7984Receiver-onConfidentialTransferReceived-address-address-euint64-bytes-) returns an encrypted
+false is best-effort only. A receiver that transfers, burns, or otherwise reduces its balance during
+the hook can still return false, in which case the refund transfers zero tokens. The sender's tokens
+end up with the recipient rather than being refunded.
+
+
+
+Refunds are subject to the same validation flow as a normal transfer--they may fail for a variety of
+reasons (such as failed hook validation in [`ERC7984Hooked`](#ERC7984Hooked)). In these cases, the tokens do not return to the sender.
+
+
@@ -578,6 +603,9 @@ May not be tied to a prior request via [`ERC7984.requestDiscloseEncryptedAmount`
+Safely moves up to `amount` from `from` to `to`, or mints/burns if `from`/`to` is the zero address.
+Emits a [`IERC7984.ConfidentialTransfer`](/confidential-contracts/api/interfaces#IERC7984-ConfidentialTransfer-address-address-euint64-) event with the successfully transferred amount.
+
@@ -650,23 +678,6 @@ The given holder `holder` is not authorized to spend on behalf of `spender`.
-
-The given gateway request ID `requestId` is invalid.
-
-
-
-
## `ERC7984ERC20Wrapper`
-
+
@@ -837,10 +831,8 @@ tokens such as fee-on-transfer or other deflationary-type tokens are not support
- [ERC7984InvalidReceiver(receiver)](#ERC7984-ERC7984InvalidReceiver-address-)
- [ERC7984InvalidSender(sender)](#ERC7984-ERC7984InvalidSender-address-)
- [ERC7984UnauthorizedSpender(holder, spender)](#ERC7984-ERC7984UnauthorizedSpender-address-address-)
-- [ERC7984ZeroBalance(holder)](#ERC7984-ERC7984ZeroBalance-address-)
- [ERC7984UnauthorizedUseOfEncryptedAmount(amount, user)](#ERC7984-ERC7984UnauthorizedUseOfEncryptedAmount-euint64-address-)
- [ERC7984UnauthorizedCaller(caller)](#ERC7984-ERC7984UnauthorizedCaller-address-)
-- [ERC7984InvalidGatewayRequest(requestId)](#ERC7984-ERC7984InvalidGatewayRequest-uint256-)
@@ -1100,8 +1092,8 @@ Returns the maximum total supply of wrapped tokens supported by the encrypted da
-Get the address that has a pending unwrap request for the given `unwrapAmount`. Returns `address(0)` if no pending
-unwrap request for the amount `unwrapAmount` exists.
+Gets the address that will receive the ERC-20 tokens associated with a pending unwrap request identified by
+`unwrapRequestId`. Returns `address(0)` if there is no pending unwrap request with id `unwrapRequestId`.
@@ -1141,6 +1133,9 @@ not overflow.
+Safely moves up to `amount` from `from` to `to`, or mints/burns if `from`/`to` is the zero address.
+Emits a [`IERC7984.ConfidentialTransfer`](/confidential-contracts/api/interfaces#IERC7984-ConfidentialTransfer-address-address-euint64-) event with the successfully transferred amount.
+
@@ -1233,7 +1228,7 @@ Returns the maximum number that will be used for [`ERC7984.decimals`](#ERC7984-d
## `ERC7984Freezable`
-
+
@@ -1323,10 +1318,8 @@ Inspired by https://github.com/OpenZeppelin/openzeppelin-community-contracts/blo
- [ERC7984InvalidReceiver(receiver)](#ERC7984-ERC7984InvalidReceiver-address-)
- [ERC7984InvalidSender(sender)](#ERC7984-ERC7984InvalidSender-address-)
- [ERC7984UnauthorizedSpender(holder, spender)](#ERC7984-ERC7984UnauthorizedSpender-address-address-)
-- [ERC7984ZeroBalance(holder)](#ERC7984-ERC7984ZeroBalance-address-)
- [ERC7984UnauthorizedUseOfEncryptedAmount(amount, user)](#ERC7984-ERC7984UnauthorizedUseOfEncryptedAmount-euint64-address-)
- [ERC7984UnauthorizedCaller(caller)](#ERC7984-ERC7984UnauthorizedCaller-address-)
-- [ERC7984InvalidGatewayRequest(requestId)](#ERC7984-ERC7984InvalidGatewayRequest-uint256-)
@@ -1441,31 +1434,64 @@ Emitted when a confidential amount of token is frozen for an account
```solidity
-import "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984ObserverAccess.sol";
+import "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984Hooked.sol";
```
-Extension of [`ERC7984`](#ERC7984) that allows each account to add an observer who is given
-permanent ACL access to its transfer and balance amounts. An observer can be added or removed at any point in time.
+Extension of [`ERC7984`](#ERC7984) that supports hook modules. Inspired by ERC-7579 modules.
+
+Modules are called before and after transfers. Before the transfer, modules
+conduct checks to see if they approve the given transfer and return an encrypted boolean. If any module
+returns false, the transferred amount becomes 0. After the transfer, modules are notified of the final transfer
+amount and may do accounting as necessary. Modules may revert on either call, which will propagate
+and revert the entire transaction.
+
+
+Hook modules are trusted contracts--they have access to any private state the token has access to. This arbitrary
+ACL access allows hook modules to grant themselves (or any other address) allowance to view any handle the token has access to.
+ACL allowances granted by the hook module persist even after the module is uninstalled.
+
+
+
-- [setObserver(account, newObserver)](#ERC7984ObserverAccess-setObserver-address-address-)
-- [observer(account)](#ERC7984ObserverAccess-observer-address-)
-- [_update(from, to, amount)](#ERC7984ObserverAccess-_update-address-address-euint64-)
+- [isModuleInstalled(module)](#ERC7984Hooked-isModuleInstalled-address-)
+- [installModule(module, initData)](#ERC7984Hooked-installModule-address-bytes-)
+- [uninstallModule(module)](#ERC7984Hooked-uninstallModule-address-)
+- [modules(start, end)](#ERC7984Hooked-modules-uint256-uint256-)
+- [maxModules()](#ERC7984Hooked-maxModules--)
+- [_authorizeModuleChange()](#ERC7984Hooked-_authorizeModuleChange--)
+- [_installModule(module, initData)](#ERC7984Hooked-_installModule-address-bytes-)
+- [_uninstallModule(module)](#ERC7984Hooked-_uninstallModule-address-)
+- [_update(from, to, encryptedAmount)](#ERC7984Hooked-_update-address-address-euint64-)
+- [_runPreTransferHooks(from, to, encryptedAmount)](#ERC7984Hooked-_runPreTransferHooks-address-address-euint64-)
+- [_runPostTransferHooks(from, to, encryptedAmount)](#ERC7984Hooked-_runPostTransferHooks-address-address-euint64-)
+- [_validateHandleAllowance(handle)](#ERC7984Hooked-_validateHandleAllowance-bytes32-)
+
+HandleAccessManager
+
+- [getHandleAllowance(handle, account, persistent)](#HandleAccessManager-getHandleAllowance-bytes32-address-bool-)
+
+ERC7984
@@ -1501,7 +1527,8 @@ permanent ACL access to its transfer and balance amounts. An observer can be add
Events
-- [ERC7984ObserverAccessObserverSet(account, oldObserver, newObserver)](#ERC7984ObserverAccess-ERC7984ObserverAccessObserverSet-address-address-address-)
+- [ERC7984HookedModuleInstalled(module)](#ERC7984Hooked-ERC7984HookedModuleInstalled-address-)
+- [ERC7984HookedModuleUninstalled(module)](#ERC7984Hooked-ERC7984HookedModuleUninstalled-address-)
ERC7984
@@ -1522,469 +1549,387 @@ permanent ACL access to its transfer and balance amounts. An observer can be add
-Sets the observer for the given account `account` to `newObserver`. Can be called by the
-account or the existing observer to abdicate the observer role (may only set to `address(0)`).
+
+Installs a hook module.
+
+Consider gas footprint of the module before adding it since all modules will perform
+both steps (pre-hook, post-hook) on all transfers.
+
-
-```solidity
-import "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984Omnibus.sol";
-```
-
-Extension of [`ERC7984`](#ERC7984) that emits additional events for omnibus transfers.
-These events contain encrypted addresses for the sub-account sender and recipient.
+Returns a slice of the list of modules installed on the token with inclusive start and exclusive end.
-There is no onchain accounting for sub-accounts--integrators must track sub-account
-balances externally.
+Use an end value of type(uint256).max to get the entire list of modules.
-
-- [ERC7984UnauthorizedUseOfEncryptedAddress(addr, user)](#ERC7984Omnibus-ERC7984UnauthorizedUseOfEncryptedAddress-eaddress-address-)
-
-ERC7984
-
-- [ERC7984InvalidReceiver(receiver)](#ERC7984-ERC7984InvalidReceiver-address-)
-- [ERC7984InvalidSender(sender)](#ERC7984-ERC7984InvalidSender-address-)
-- [ERC7984UnauthorizedSpender(holder, spender)](#ERC7984-ERC7984UnauthorizedSpender-address-address-)
-- [ERC7984ZeroBalance(holder)](#ERC7984-ERC7984ZeroBalance-address-)
-- [ERC7984UnauthorizedUseOfEncryptedAmount(amount, user)](#ERC7984-ERC7984UnauthorizedUseOfEncryptedAmount-euint64-address-)
-- [ERC7984UnauthorizedCaller(caller)](#ERC7984-ERC7984UnauthorizedCaller-address-)
-- [ERC7984InvalidGatewayRequest(requestId)](#ERC7984-ERC7984InvalidGatewayRequest-uint256-)
+Returns the maximum number of modules that can be installed.
-
-Wraps the `confidentialTransfer-address-externalEuint64-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+Authorization logic for installing and uninstalling modules. Must be implemented by the concrete contract.
-Wraps the `confidentialTransfer-address-euint64` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+Internal function which installs a hook module.
-Wraps the `confidentialTransferFrom-address-address-externalEuint64-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+Internal function which uninstalls a module.
-Wraps the `confidentialTransferFrom-address-address-euint64` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+See [`ERC7984._update`](#ERC7984-_update-address-address-euint64-).
+
+Modified to run pre and post transfer hooks. Zero tokens are transferred if a module does not approve
+the transfer.
-Wraps the `confidentialTransferAndCall-address-externalEuint64-bytes-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+Runs the pre-transfer hooks for all modules.
-Wraps the `confidentialTransferAndCall-address-euint64-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+Runs the post-transfer hooks for all modules.
-Wraps the `confidentialTransferFromAndCall-address-address-externalEuint64-bytes-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+See [`HandleAccessManager._validateHandleAllowance`](/confidential-contracts/api/utils#HandleAccessManager-_validateHandleAllowance-bytes32-). Allow modules to access any handle the token has access to.
-Wraps the `confidentialTransferFromAndCall-address-address-euint64-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+Emitted when a module is installed.
-Handles the ACL allowances, does the transfer without a callback, and emits [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-).
+Emitted when a module is uninstalled.
-Handles the ACL allowances, does the transfer with a callback, and emits [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-).
+The address is not a valid module.
-Emitted when a confidential transfer is made representing the onchain settlement of
-an omnibus transfer from `sender` to `recipient` of amount `amount`. Settlement occurs between
-`omnibusFrom` and `omnibusTo` and is represented in a matching [`IERC7984.ConfidentialTransfer`](/confidential-contracts/api/interfaces#IERC7984-ConfidentialTransfer-address-address-euint64-) event.
-
-
-`omnibusFrom` and `omnibusTo` get permanent ACL allowances for `sender` and `recipient`.
-
+The module is already installed.
-The caller `user` does not have access to the encrypted address `addr`.
+The module is not installed.
-
-Try using the equivalent transfer function with an input proof.
-
+
```solidity
-import "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984Restricted.sol";
+import "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984ObserverAccess.sol";
```
-Extension of [`ERC7984`](#ERC7984) that implements user account transfer restrictions through the
-[`ERC7984Restricted.canTransact`](#ERC7984Restricted-canTransact-address-) function. Inspired by
-https://github.com/OpenZeppelin/openzeppelin-community-contracts/blob/master/contracts/token/ERC20/extensions/ERC20Restricted.sol.
-
-By default, each account has no explicit restriction. The [`ERC7984Restricted.canTransact`](#ERC7984Restricted-canTransact-address-) function acts as
-a blocklist. Developers can override [`ERC7984Restricted.canTransact`](#ERC7984Restricted-canTransact-address-) to check that `restriction == ALLOWED`
-to implement an allowlist.
+Extension of [`ERC7984`](#ERC7984) that allows each account to add an observer who is given
+permanent ACL access to its transfer and balance amounts. An observer can be added or removed at any point in time.
-Returns the restriction of a user account.
+Sets the observer for the given account `account` to `newObserver`. Can be called by the
+account or the existing observer to abdicate the observer role (may only set to `address(0)`).
-Returns whether a user account is allowed to interact with the token.
-
-Default implementation only disallows explicitly BLOCKED accounts (i.e. a blocklist).
+Returns the observer for the given account `account`.
-
+
-
_update(address from, address to, euint64 value) → euint64
+
_update(address from, address to, euint64 amount) → euint64 transferred
-See [`ERC7984._update`](#ERC7984-_update-address-address-euint64-). Enforces transfer restrictions (excluding minting and burning).
-
-Requirements:
-
-* `from` must be allowed to transfer tokens (see [`ERC7984Restricted.canTransact`](#ERC7984Restricted-canTransact-address-)).
-* `to` must be allowed to receive tokens (see [`ERC7984Restricted.canTransact`](#ERC7984Restricted-canTransact-address-)).
-
-The default restriction behavior can be changed (for a pass-through for instance) by overriding
-[`ERC7984Restricted._checkSenderRestriction`](#ERC7984Restricted-_checkSenderRestriction-address-) and/or [`ERC7984Restricted._checkRecipientRestriction`](#ERC7984Restricted-_checkRecipientRestriction-address-).
+Safely moves up to `amount` from `from` to `to`, or mints/burns if `from`/`to` is the zero address.
+Emits a [`IERC7984.ConfidentialTransfer`](/confidential-contracts/api/interfaces#IERC7984-ConfidentialTransfer-address-address-euint64-) event with the successfully transferred amount.
-Convenience function to block a user account (set to BLOCKED).
+Thrown when an account tries to set a `newObserver` for a given `account` without proper authority.
-Convenience function to allow a user account (set to ALLOWED).
+```solidity
+import "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984Omnibus.sol";
+```
+
+Extension of [`ERC7984`](#ERC7984) that emits additional events for omnibus transfers.
+These events contain encrypted addresses for the sub-account sender and recipient.
+
+
+There is no onchain accounting for sub-accounts--integrators must track sub-account
+balances externally.
+
+
+
-Checks if a user account is restricted. Reverts with [`ERC7984Restricted.UserRestricted`](#ERC7984Restricted-UserRestricted-address-) if so.
+Wraps the `confidentialTransfer-address-externalEuint64-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+
+Wraps the `confidentialTransfer-address-euint64` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+
+
+
+Wraps the `confidentialTransferFrom-address-address-externalEuint64-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+
+
+
+Wraps the `confidentialTransferFrom-address-address-euint64` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+
+
+
+Wraps the `confidentialTransferAndCall-address-externalEuint64-bytes-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+
+
+
+Wraps the `confidentialTransferAndCall-address-euint64-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+
+
+
+Wraps the `confidentialTransferFromAndCall-address-address-externalEuint64-bytes-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+
+
+
+Wraps the `confidentialTransferFromAndCall-address-address-euint64-bytes` function and emits the [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-) event.
+
+
+
+Handles the ACL allowances, does the transfer without a callback, and emits [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-).
+
+
+
+Handles the ACL allowances, does the transfer with a callback, and emits [`ERC7984Omnibus.OmnibusConfidentialTransfer`](#ERC7984Omnibus-OmnibusConfidentialTransfer-address-address-eaddress-eaddress-euint64-).
+
+
+
+Emitted when a confidential transfer is made representing the onchain settlement of
+an omnibus transfer from `sender` to `recipient` of amount `amount`. Settlement occurs between
+`omnibusFrom` and `omnibusTo` and is represented in a matching [`IERC7984.ConfidentialTransfer`](/confidential-contracts/api/interfaces#IERC7984-ConfidentialTransfer-address-address-euint64-) event.
+
+
+`omnibusFrom` and `omnibusTo` get permanent ACL allowances for `sender` and `recipient`.
+
+
+
+
+```solidity
+import "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984Restricted.sol";
+```
+
+Extension of [`ERC7984`](#ERC7984) that implements user account transfer restrictions through the
+[`ERC7984Restricted.canTransact`](#ERC7984Restricted-canTransact-address-) function. Inspired by
+https://github.com/OpenZeppelin/openzeppelin-community-contracts/blob/master/contracts/token/ERC20/extensions/ERC20Restricted.sol.
+
+By default, each account has no explicit restriction. The [`ERC7984Restricted.canTransact`](#ERC7984Restricted-canTransact-address-) function acts as
+a blocklist. Developers can override [`ERC7984Restricted.canTransact`](#ERC7984Restricted-canTransact-address-) to check that `restriction == ALLOWED`
+to implement an allowlist.
+
+
+
+Returns whether a user account is allowed to receive or send tokens.
+
+Default implementation only disallows explicitly BLOCKED accounts (i.e. a blocklist).
+
+
+
+
+
+
+
+
+
_update(address from, address to, euint64 value) → euint64
+
+See [`ERC7984._update`](#ERC7984-_update-address-address-euint64-). Enforces transfer restrictions.
+
+Requirements:
+
+* `from` must be allowed to transfer tokens (see [`ERC7984Restricted.canTransact`](#ERC7984Restricted-canTransact-address-)).
+* `to` must be allowed to receive tokens (see [`ERC7984Restricted.canTransact`](#ERC7984Restricted-canTransact-address-)).
+
+The default restriction behavior can be changed (for a pass-through for instance) by overriding
+[`ERC7984Restricted._checkSenderRestriction`](#ERC7984Restricted-_checkSenderRestriction-address-) and/or [`ERC7984Restricted._checkRecipientRestriction`](#ERC7984Restricted-_checkRecipientRestriction-address-).
+
+
+
+Internal function which checks restriction of the `from` account before a transfer.
+Working with [`ERC7984._update`](#ERC7984-_update-address-address-euint64-) function.
+
+
+
+Internal function which checks restriction of the `to` account before a transfer.
+Working with [`ERC7984._update`](#ERC7984-_update-address-address-euint64-) function.
+
+
+
+```solidity
+import "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984Rwa.sol";
+```
+
+Extension of [`ERC7984`](#ERC7984) that supports confidential Real World Assets (RWAs).
+This interface provides compliance checks, transfer controls and enforcement actions.
+
+
+
+Force transfer callable by the role [`ERC7984Rwa.AGENT_ROLE`](#ERC7984Rwa-AGENT_ROLE-bytes32) which transfers tokens from `from` to `to` and
+bypasses the [`ERC7984Restricted`](#ERC7984Restricted) (only on from) and [`++Pausable++`](https://docs.openzeppelin.com/contracts/api/utils#pausable)
+checks. Frozen tokens are not transferred and must be unfrozen first.
+
+
+
+Returns whether a user account is allowed to receive or send tokens.
+
+Default implementation only disallows explicitly BLOCKED accounts (i.e. a blocklist).
+
+
+
+
+
+
+
+
+
_update(address from, address to, euint64 encryptedAmount) → euint64
+
+Bypasses `Pausable` check when performing a [`ERC7984Rwa.forceConfidentialTransferFrom`](#ERC7984Rwa-forceConfidentialTransferFrom-address-address-euint64-).
+
+
-Internal function which checks restriction of the `from` account before a transfer.
-Working with [`ERC7984._update`](#ERC7984-_update-address-address-euint64-) function.
+Internal function which checks if the current function call should be treated as a force transfer.
-Internal function which checks restriction of the `to` account before a transfer.
-Working with [`ERC7984._update`](#ERC7984-_update-address-address-euint64-) function.
+Restrict overrides of `Context._msgSender`. Please use other account abstraction methods instead.
-Emitted when a user account's restriction is updated.
+Accounts granted the agent role have the following permissioned abilities:
+
+* Mint/Burn to/from a given address (does not require permission)
+* Force transfer from a given address (does not require permission)
+** Bypasses pause and restriction checks (not frozen)
+* Pause/Unpause the contract
+* Block/Unblock a given account
+* Set frozen amount of tokens for a given account.
```solidity
-import "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984Rwa.sol";
+import "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984Votes.sol";
```
-Extension of [`ERC7984`](#ERC7984) that supports confidential Real World Assets (RWAs).
-This interface provides compliance checks, transfer controls and enforcement actions.
+Extension of [`ERC7984`](#ERC7984) supporting confidential votes tracking and delegation.
-
+The amount of confidential voting units an account has is equal to the balance of
+that account. Voting power is taken into account when an account delegates votes to itself or to another
+account.
+
+Safely moves up to `amount` from `from` to `to`, or mints/burns if `from`/`to` is the zero address.
+Emits a [`IERC7984.ConfidentialTransfer`](/confidential-contracts/api/interfaces#IERC7984-ConfidentialTransfer-address-address-euint64-) event with the successfully transferred amount.
+
+
-- [TokensFrozen(account, encryptedAmount)](#ERC7984Freezable-TokensFrozen-address-euint64-)
+```solidity
+import "@openzeppelin/confidential-contracts/token/ERC7984/utils/ERC7984BalanceCapHookModule.sol";
+```
-
-
-ERC7984
+An ERC-7984 hook module that limits the balance of each investor.
-- [AmountDiscloseRequested(encryptedAmount, requester)](#ERC7984-AmountDiscloseRequested-euint64-address-)
+The cap is stored as an encrypted `euint64` value. The pre-transfer hook compares the recipient's prospective balance to the
+encrypted cap and emits an encrypted compliance result via [`ERC7984HookModule._emitPreTransferResults`](#ERC7984HookModule-_emitPreTransferResults-address-address-address-euint64-ebool-bytes32-).
-
-
-IERC7984
+This module is compatible with [`ERC7984Hooked`](#ERC7984Hooked).
-- [OperatorSet(holder, operator, until)](#IERC7984-OperatorSet-address-address-uint48-)
-- [ConfidentialTransfer(from, to, amount)](#IERC7984-ConfidentialTransfer-address-address-euint64-)
-- [AmountDisclosed(encryptedAmount, amount)](#IERC7984-AmountDisclosed-euint64-uint64-)
+
+This module notifies senders of the result of the pre-transfer hook. This can be used to leak
+information about the balance of the recipient. This is a potential security risk and should be used
+with caution. Production use-cases may want to remove this notification.
+
-
+
-Checks if the sender is an admin.
+Sets the max balance for a given token `token` to the encrypted value `newMaxBalance`.
+
+`msg.sender` must have the agent role on `token`.
+Internal function which runs before a transfer. Transient access is already granted to the module
+for `encryptedAmount`. If additional handle access is needed from the token, call [`ERC7984HookModule._getTokenHandleAllowance`](#ERC7984HookModule-_getTokenHandleAllowance-address-euint64-bool-).
+
+
+ACL allowance on `encryptedAmount` is already checked for `msg.sender` in [`ERC7984HookModule.preTransfer`](#ERC7984HookModule-preTransfer-address-address-euint64-).
+
+
-Returns true if has admin role, false otherwise.
+See [`ERC7984HookModule._onInstall`](#ERC7984HookModule-_onInstall-address-bytes-). The `initData` must contain the initial max balance for the token
+along with the input proof for the max balance. These are encoded using standard ABI encoding.
+
+```solidity
+import "@openzeppelin/confidential-contracts/token/ERC7984/utils/ERC7984HolderCapHookModule.sol";
+```
+
+An ERC-7984 hook module that limits the number of holders for a given token.
+
+
+This module must be installed prior to minting any tokens. After the total supply is initialized,
+it is not possible to guarantee that the number of holders is 0, so the module can not be installed.
+
+
+
+This module may not function correctly with non-standard tokens such as fee on transfer.
+
+
+
-Unblocks a user account.
+Internal function which runs before a transfer. Transient access is already granted to the module
+for `encryptedAmount`. If additional handle access is needed from the token, call [`ERC7984HookModule._getTokenHandleAllowance`](#ERC7984HookModule-_getTokenHandleAllowance-address-euint64-bool-).
+
+
+ACL allowance on `encryptedAmount` is already checked for `msg.sender` in [`ERC7984HookModule.preTransfer`](#ERC7984HookModule-preTransfer-address-address-euint64-).
+
-Sets confidential frozen for an account with proof.
+Internal function which performs operations after transfers. Transient access is already granted to the module
+for `encryptedAmount`. If additional handle access is needed from the token, call [`ERC7984HookModule._getTokenHandleAllowance`](#ERC7984HookModule-_getTokenHandleAllowance-address-euint64-bool-).
+
+
+ACL allowance on `encryptedAmount` is already checked for `msg.sender` in [`ERC7984HookModule.postTransfer`](#ERC7984HookModule-postTransfer-address-address-euint64-).
+
-Sets confidential frozen for an account.
+See [`ERC7984HookModule._onInstall`](#ERC7984HookModule-_onInstall-address-bytes-). The `initData` must contain the initial max holder count for the token
+as a standard ABI encoded uint64.
-
+
-
confidentialMint(address to, externalEuint64 encryptedAmount, bytes inputProof) → euint64
+
+```solidity
+import "@openzeppelin/confidential-contracts/token/ERC7984/utils/ERC7984HookModule.sol";
+```
+
+An abstract base contract for building ERC-7984 hook modules. Compatible with [`ERC7984Hooked`](#ERC7984Hooked).
+
-Variant of `forceConfidentialTransferFrom-address-address-euint64` with an input proof.
+Hook that runs before a transfer. Should not mutate token state. Module is already
+granted transient access to `encryptedAmount`.
-
+
-
forceConfidentialTransferFrom(address from, address to, euint64 encryptedAmount) → euint64 transferred
+
postTransfer(address from, address to, euint64 encryptedAmount)
-Force transfer callable by the role [`ERC7984Rwa.AGENT_ROLE`](#ERC7984Rwa-AGENT_ROLE-bytes32) which transfers tokens from `from` to `to` and
-bypasses the [`ERC7984Restricted`](#ERC7984Restricted) (only on from) and [`++Pausable++`](https://docs.openzeppelin.com/contracts/api/utils#pausable)
-checks. Frozen tokens are not transferred and must be unfrozen first.
+Performs operation after transfer.
-Returns the confidential frozen balance of an account.
+Returns true if this contract implements the interface defined by
+`interfaceId`. See the corresponding
+[ERC section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified)
+to learn more about how these ids are created.
+
+This function call must use less than 30 000 gas.
-Returns true if the contract is paused, and false otherwise.
+Internal function which may be overridden by the derived contract to perform actions
+when the module is installed. Should clean up dirty state from possible previous installations.
-Returns whether a user account is allowed to interact with the token.
+Internal function which runs before a transfer. Transient access is already granted to the module
+for `encryptedAmount`. If additional handle access is needed from the token, call [`ERC7984HookModule._getTokenHandleAllowance`](#ERC7984HookModule-_getTokenHandleAllowance-address-euint64-bool-).
-Default implementation only disallows explicitly BLOCKED accounts (i.e. a blocklist).
+
+ACL allowance on `encryptedAmount` is already checked for `msg.sender` in [`ERC7984HookModule.preTransfer`](#ERC7984HookModule-preTransfer-address-address-euint64-).
+
-
+
-
_update(address from, address to, euint64 encryptedAmount) → euint64
-Internal function which updates confidential balances while performing frozen and restriction compliance checks.
+Internal function which performs operations after transfers. Transient access is already granted to the module
+for `encryptedAmount`. If additional handle access is needed from the token, call [`ERC7984HookModule._getTokenHandleAllowance`](#ERC7984HookModule-_getTokenHandleAllowance-address-euint64-bool-).
+
+
+ACL allowance on `encryptedAmount` is already checked for `msg.sender` in [`ERC7984HookModule.postTransfer`](#ERC7984HookModule-postTransfer-address-address-euint64-).
+
-
+
-
_forceUpdate(address from, address to, euint64 encryptedAmount) → euint64
-Internal function which forces transfer of confidential amount of tokens from account to account by skipping compliance checks.
+Allow modules to get access to token handles during transaction.
-Bypasses the `from` restriction check when performing a [`ERC7984Rwa.forceConfidentialTransferFrom`](#ERC7984Rwa-forceConfidentialTransferFrom-address-address-euint64-).
+Allow modules to get access to token handles.
-Accounts granted the agent role have the following permissioned abilities:
-
-- Mint/Burn to/from a given address (does not require permission)
-- Force transfer from a given address (does not require permission)
- - Bypasses pause and restriction checks (not frozen)
-- Pause/Unpause the contract
-- Block/Unblock a given account
-- Set frozen amount of tokens for a given account.
-
-
-
-```solidity
-import "@openzeppelin/confidential-contracts/token/ERC7984/extensions/ERC7984Votes.sol";
-```
-
-Extension of [`ERC7984`](#ERC7984) supporting confidential votes tracking and delegation.
-
-The amount of confidential voting units an account has is equal to the balance of
-that account. Voting power is taken into account when an account delegates votes to itself or to another
-account.
-
-
-Returns the confidential total supply of the token.
+Get transient ACL allowance for the given handle from a contract that inherits [`HandleAccessManager`](/confidential-contracts/api/utils#HandleAccessManager).
+
+Additionally verifies that the token is authorized to access the handle.
-
+
-
_update(address from, address to, euint64 amount) → euint64 transferred
+The caller `user` does not have access to the encrypted amount `amount`.
+
@@ -3206,7 +4529,7 @@ Returns the confidential total supply of the token.
## `ERC7984Utils`
-
+
diff --git a/content/confidential-contracts/api/utils.mdx b/content/confidential-contracts/api/utils.mdx
index c66cc535..d7323025 100644
--- a/content/confidential-contracts/api/utils.mdx
+++ b/content/confidential-contracts/api/utils.mdx
@@ -26,7 +26,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
## `FHESafeMath`
-
+
@@ -51,6 +51,8 @@ This library may return an uninitialized value if all inputs are uninitialized.
- [tryDecrease(oldValue, delta)](#FHESafeMath-tryDecrease-euint64-euint64-)
- [tryAdd(a, b)](#FHESafeMath-tryAdd-euint64-euint64-)
- [trySub(a, b)](#FHESafeMath-trySub-euint64-euint64-)
+- [saturatingAdd(a, b)](#FHESafeMath-saturatingAdd-euint64-euint64-)
+- [saturatingSub(a, b)](#FHESafeMath-saturatingSub-euint64-euint64-)
@@ -128,13 +130,49 @@ will be `a - b`. Otherwise, `success` will be false, and `res` will be 0.
+
+Add `a` and `b` saturating at `type(uint64).max` on overflow. The returned value is the sum
+of `a` and `b` if it does not overflow, otherwise `type(uint64).max`.
+
+
@@ -185,7 +223,7 @@ This function call is validated by [`HandleAccessManager._validateHandleAllowanc
-
_validateHandleAllowance(bytes32 handle) → bool
+
_validateHandleAllowance(bytes32) → bool
internal
#
@@ -193,7 +231,7 @@ This function call is validated by [`HandleAccessManager._validateHandleAllowanc
-Unimplemented function that must return true if the message sender is allowed to call
+Validation function that must return true if the message sender is allowed to call
[`HandleAccessManager.getHandleAllowance`](#HandleAccessManager-getHandleAllowance-bytes32-address-bool-) for the given handle.
@@ -220,7 +258,7 @@ Unimplemented function that must return true if the message sender is allowed to
## `CheckpointsConfidential`
-
+
diff --git a/content/confidential-contracts/changelog.mdx b/content/confidential-contracts/changelog.mdx
index d1ef1a63..1b8dadaf 100644
--- a/content/confidential-contracts/changelog.mdx
+++ b/content/confidential-contracts/changelog.mdx
@@ -2,6 +2,57 @@
title: Changelog
---
+
+# [v0.5.1](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/releases/tag/v0.5.1) - 2026-06-22
+
+- `BatcherConfidential`: Initialize the zero value before unwrapping when dispatching a batch with no contributions.
+
+
+
+[Changes][v0.5.1]
+
+
+
+# [v0.5.0](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/releases/tag/v0.5.0) - 2026-06-17
+
+### Token
+
+- `ERC7984`: Remove revert on transfer where the sender has an uninitialized balance. ([#357](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/357))
+- `ERC7984Hooked`: Add an `ERC7984` extension that calls external hooks before and after transfer of confidential tokens. ([#332](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/332))
+- `ERC7984HookModule`: Add a base hook module for building modules compatible with `ERC7984Hooked`. ([#351](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/351))
+- `ERC7984BalanceCapHookModule`: Add an example hook module that enforces a confidential balance cap for the token. ([#351](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/351))
+- `ERC7984HolderCapHookModule`: Add an example hook module that enforces a maximum number of holders for the token. ([#351](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/351))
+- `ERC7984Rwa`: Always call `_update` on transfers (even force). Bypass restriction via restriction override. ([#339](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/339))
+- `ERC7984Rwa`: Add token recovery functionality. ([#341](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/341))
+- `ERC7984Rwa`: Bypass recipient on RWA force transfer in addition to sender. ([#372](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/372))
+- `ERC7984Rwa`: Block overrides of `Context` functions (`_msgSender()`, `_msgData()`). ([#382](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/382))
+- `IERC7984Rwa`: Add token recovery function and event. ([#341](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/341))
+
+### Finance
+
+- `BatcherConfidential`: Revert if underlying `toToken` balance changes during a partial route execution. ([#385](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/385))
+
+### Utils
+
+- `FHESafeMath`: Add `saturatingAdd` and `saturatingSub` functions. ([#341](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/341))
+- `HandleAccessManager`: Return false by default in `_validateHandleAllowance`. ([#338](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/338))
+
+
+
+[Changes][v0.5.0]
+
+
+
+# [v0.4.1](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/releases/tag/v0.4.1) - 2026-06-08
+
+### Bug Fixes
+- `BatcherConfidential`: Enable decryption of the `joinedAmount` in `BatcherConfidential`. ([#387](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/pull/387))
+
+
+
+[Changes][v0.4.1]
+
+
# [v0.4.0](https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/releases/tag/v0.4.0) - 2026-03-30
@@ -111,6 +162,9 @@ Note: Confidential contracts are currently in a phase of rapid development--futu
[Changes][v0.1.0]
+[v0.5.1]: https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/compare/v0.5.0...v0.5.1
+[v0.5.0]: https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/compare/v0.4.1...v0.5.0
+[v0.4.1]: https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/compare/v0.4.0...v0.4.1
[v0.4.0]: https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/compare/v0.3.1...v0.4.0
[v0.3.1]: https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/compare/v0.3.0...v0.3.1
[v0.3.0]: https://github.com/OpenZeppelin/openzeppelin-confidential-contracts/compare/v0.2.0...v0.3.0
diff --git a/content/confidential-contracts/token.mdx b/content/confidential-contracts/token.mdx
index 011b49de..09ad397c 100644
--- a/content/confidential-contracts/token.mdx
+++ b/content/confidential-contracts/token.mdx
@@ -41,7 +41,7 @@ Setting an operator for any amount of time allows the operator to _***take all o
The token standard exposes transfer functions with and without callbacks. It is up to the caller to decide if a callback is necessary for the transfer. For smart contracts that support it, callbacks allow the operator approval step to be skipped and directly invoke the receiver contract via a callback.
-Smart contracts that are the target of a callback must implement [`IERC7984Receiver`](/confidential-contracts/api/interfaces#IERC7984Receiver). After balances are updated for a transfer, the callback is triggered by calling the [`onConfidentialTransferReceived`](/confidential-contracts/api/interfaces#IERC7984Receiver-onConfidentialTransferReceived-address-address-euint64-bytes-) function. The function must either revert or return an `ebool` indicating success. If the callback returns false, the token transfer is reversed.
+Smart contracts that are the target of a callback must implement [`IERC7984Receiver`](/confidential-contracts/api/interfaces#IERC7984Receiver). After balances are updated for a transfer, the callback is triggered by calling the [`onConfidentialTransferReceived`](/confidential-contracts/api/interfaces#IERC7984Receiver-onConfidentialTransferReceived-address-address-euint64-bytes-) function. The function must either revert or return an `ebool` indicating success. If the callback returns false, the ERC-7984 token contract attempts to refund the tokens from the recipient to the sender.
## Examples