Wrap ERC-20 into Confidential ERC-7984 ​
An ERC-7984 wrapper lets users convert any existing ERC-20 token into a confidential ERC-7984 token and back. The underlying ERC-20 tokens are held by the wrapper contract, while confidential tokens with encrypted balances are minted 1:1.
How it works ​
Key concepts ​
One-step wrap ​
Wrapping is straightforward: the ERC-20 amount is public, so the wrapper can transfer and mint in a single transaction. The user approves the wrapper, calls wrap(), and their confidential balance is updated immediately.
Two-step unwrap ​
Unwrapping is more complex because the burn amount is encrypted. The wrapper cannot transfer ERC-20 tokens without knowing the plaintext amount. This requires two steps:
- Request unwrap: the user calls
unwrap(), which burns the encrypted ERC-7984 tokens - Finalize unwrap: after the burnt amount is decrypted off-chain (via the Nox protocol), the user calls
finalizeUnwrap()with the decrypted value and a decryption proof. The wrapper then transfers the corresponding ERC-20 tokens.
Deploying a wrapper ​
To deploy a wrapper, you only need the address of the ERC-20 token you want to wrap:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;
import {ERC20ToERC7984Wrapper} from "@iexec-nox/nox-confidential-contracts/contracts/token/extensions/ERC20ToERC7984Wrapper.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract WrappedUSDC is ERC20ToERC7984Wrapper {
constructor(IERC20 usdc)
ERC20ToERC7984Wrapper(usdc)
ERC7984("Wrapped Confidential USDC", "wcUSDC", "")
{}
}The wrapper inherits from both ERC7984 and ERC20ToERC7984Wrapper. All ERC-7984 features (confidential transfers, operators, callbacks) work on the wrapped token.
Swap ERC-20 to ERC-7984 ​
Swapping from a plaintext ERC-20 to a confidential ERC-7984 is done via the wrap function. It transfers ERC-20 tokens from the caller to the wrapper, then mints the equivalent confidential tokens:
function wrap(address to, uint256 amount) public virtual returns (euint256) {
SafeERC20.safeTransferFrom(IERC20(underlying()), msg.sender, address(this), amount);
euint256 wrappedAmount = _mint(to, Nox.toEuint256(amount));
Nox.allowTransient(wrappedAmount, msg.sender);
return wrappedAmount;
}From the caller's perspective:
// 1. Approve the wrapper
usdc.approve(address(wrappedUSDC), 100e18);
// 2. Wrap into confidential tokens
wrappedUSDC.wrap(msg.sender, 100e18);
// Balance is now encrypted, nobody can see how much you holdThe ERC-20 transfer would revert on failure (insufficient balance, missing approval). The _mint is guaranteed to succeed since it only adds to balances.
Swap ERC-7984 to ERC-20 ​
Swapping from a confidential token back to a plaintext ERC-20 is more complex. The wrapper needs to know the plaintext amount to transfer ERC-20 tokens, but the burn amount is encrypted. This requires two steps:
Step 1: Request unwrap ​
The user burns their confidential tokens. The burnt amount is recorded as an encrypted handle, and the function returns an unwrapRequestId needed for finalization:
// Encrypt the amount to unwrap
// (off-chain via JS SDK, then call the contract)
euint256 unwrapRequestId = wrappedUSDC.unwrap(
msg.sender, // burn from
msg.sender, // send ERC-20 to
encryptedAmount,
inputProof
);Step 2: Finalize with decryption proof ​
After the Nox protocol decrypts the burnt amount off-chain, the user calls finalizeUnwrap with the unwrapRequestId and the decrypted amount bundled with its proof:
wrappedUSDC.finalizeUnwrap(
unwrapRequestId,
decryptedAmountAndProof
);
// ERC-20 tokens are transferred to the recipientThe wrapper verifies the decryption proof, then transfers the plaintext amount of the underlying ERC-20 to the recipient.
Next steps ​
- ERC-7984 Token: create a native confidential token from scratch
- Demo: confidential token swap application
- JS SDK: encrypt inputs and decrypt balances from JavaScript
