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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pink-areas-double.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-confidential-contracts': minor
---

`ERC7984ERC20Wrapper`: return the amount of wrapped token sent on wrap calls.
8 changes: 6 additions & 2 deletions contracts/interfaces/IERC7984ERC20Wrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ import {IERC7984} from "./IERC7984.sol";

/// @dev Interface for ERC7984ERC20Wrapper contract.
interface IERC7984ERC20Wrapper is IERC7984 {
/// @dev Wraps `amount` of the underlying token into a confidential token and sends it to `to`.
function wrap(address to, uint256 amount) external;
/**
* @dev Wraps `amount` of the underlying token into a confidential token and sends it to `to`.
*
* Returns amount of wrapped token sent.
*/
function wrap(address to, uint256 amount) external returns (euint64);

/**
* @dev Unwraps tokens from `from` and sends the underlying tokens to `to`. The caller must be `from`
Expand Down
9 changes: 7 additions & 2 deletions contracts/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,18 @@ abstract contract ERC7984ERC20Wrapper is ERC7984, IERC7984ERC20Wrapper, IERC1363
* @dev See {IERC7984ERC20Wrapper-wrap}. Tokens are exchanged at a fixed rate specified by {rate} such that
* `amount / rate()` confidential tokens are sent. The amount transferred in is rounded down to the nearest
* multiple of {rate}.
*
* Returns the amount of wrapped token sent.
*/
function wrap(address to, uint256 amount) public virtual override {
function wrap(address to, uint256 amount) public virtual override returns (euint64) {
// take ownership of the tokens
SafeERC20.safeTransferFrom(IERC20(underlying()), msg.sender, address(this), amount - (amount % rate()));

// mint confidential token
_mint(to, FHE.asEuint64(SafeCast.toUint64(amount / rate())));
euint64 wrappedAmountSent = _mint(to, FHE.asEuint64(SafeCast.toUint64(amount / rate())));
FHE.allowTransient(wrappedAmountSent, msg.sender);

return wrappedAmountSent;
Comment on lines +82 to +93
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the main ERC7984 contract file
fd -a '^ERC7984\.sol$' --type f contracts/

Repository: OpenZeppelin/openzeppelin-confidential-contracts

Length of output: 343


🏁 Script executed:

#!/bin/bash
# Find and examine _mint implementation
rg -n "function _mint" contracts/token/ERC7984/ -A 10

Repository: OpenZeppelin/openzeppelin-confidential-contracts

Length of output: 343


🏁 Script executed:

#!/bin/bash
# Also check _update implementation since _mint often delegates to it
rg -n "function _update" contracts/token/ERC7984/ -A 10

Repository: OpenZeppelin/openzeppelin-confidential-contracts

Length of output: 343


🏁 Script executed:

#!/bin/bash
# Check the wrap function documentation more thoroughly
cat -n contracts/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol | sed -n '75,95p'

Repository: OpenZeppelin/openzeppelin-confidential-contracts

Length of output: 343


🌐 Web query:

ERC7984 _mint return semantics OpenZeppelin confidential contracts

💡 Result:

In OpenZeppelin Confidential Contracts (@openzeppelin/confidential-contracts), ERC7984’s internal mint is not “ERC20-style (bool success / revert)”. Its signature is:

  • function _mint(address to, euint64 amount) internal returns (euint64 transferred) — it returns the encrypted amount actually minted (“transferred”), not a boolean. [1]

Return semantics (important):

  • The transferred return value can be less than requested, including 0.
  • In particular, in versions <= 0.3.0, if minting would make the confidential euint64 total supply overflow, _mint can fail silently (i.e., not revert) and effectively mint 0, so callers must not assume mint succeeded unless they check the returned transferred. This behavior is the root cause of GHSA-hqf9-8xv5-x8xw. [2]

Practical implication:

  • If your logic needs “mint must succeed or revert” (e.g., wrappers), you should check the returned euint64 transferred (or upgrade to >= 0.3.1, which patches the wrapper-side issue). [2]

Sources: OpenZeppelin ERC7984 API docs [1], OpenZeppelin advisory GHSA-hqf9-8xv5-x8xw (patched in 0.3.1) [2].


The wrap function must handle potential silent mint failures.

_mint in ERC7984 returns the encrypted amount actually minted (not the updated balance), so the return semantics are correct. However, in versions <= 0.3.0, _mint can silently fail to mint the full amount or return 0 due to overflow, without reverting. The wrap function should either:

  • Validate that wrappedAmountSent equals the requested amount (or handle underflow)
  • Upgrade to version >= 0.3.1, which patches this issue
  • Or explicitly document this failure mode for callers
🤖 Prompt for AI Agents
In `@contracts/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol` around lines 82 -
93, The wrap function may silently receive a short or zero mint from _mint
(which returns the encrypted amount minted), so update wrap to validate the
returned euint64 wrappedAmountSent against the expected minted quantity (compute
expected as FHE.asEuint64(SafeCast.toUint64(amount / rate())) or equivalent) and
revert if wrappedAmountSent is zero or not equal to the expected value; ensure
you perform this check before calling FHE.allowTransient and returning, or
alternatively upgrade the ERC7984 implementation to >=0.3.1 to eliminate the
silent-failure bug (references: wrap, _mint, wrappedAmountSent, rate(),
FHE.allowTransient).

}

/// @dev Unwrap without passing an input proof. See {unwrap-address-address-bytes32-bytes} for more details.
Expand Down