Skip to content

Commit db7377d

Browse files
committed
feat: updated conf token implementation and updated the deployment scripts
1 parent 1f15ff0 commit db7377d

8 files changed

Lines changed: 504 additions & 81 deletions

File tree

contracts/ConfidentialUSDC.sol

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity 0.8.28;
3+
4+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5+
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";
6+
import {ERC7984} from "./vendor/token/ERC7984/ERC7984.sol";
7+
import {ERC7984ERC20Wrapper} from "./vendor/token/ERC7984/extensions/ERC7984ERC20Wrapper.sol";
8+
9+
/// @title ConfidentialUSDC
10+
/// @notice A confidential wrapper for USDC using the ERC7984 standard backed by
11+
/// the OpenZeppelin ERC7984ERC20Wrapper implementation.
12+
///
13+
/// Wrapping: wrap(address to, uint256 amount) — pull USDC, mint cUSDC to `to`
14+
/// Unwrapping (two-step, trustless via Zama FHE coprocessor):
15+
/// 1. unwrap(from, to, encAmount, inputProof) — burn cUSDC, mark handle for decryption
16+
/// 2. finalizeUnwrap(handle, clearAmount, decryptionProof) — verify KMS proof, release USDC
17+
///
18+
/// @dev On Sepolia: after unwrap() is mined, query https://relayer.testnet.zama.org to obtain
19+
/// clearAmount + decryptionProof, then call finalizeUnwrap(). Anyone may call finalizeUnwrap.
20+
/// On Hardhat: use hre.fhevm.publicDecrypt([handle]) to simulate the relayer in tests.
21+
///
22+
/// Since USDC has 6 decimals and _maxDecimals() returns 6, the rate is 1:1 with no scaling.
23+
contract ConfidentialUSDC is ZamaEthereumConfig, ERC7984ERC20Wrapper {
24+
constructor(address usdc)
25+
ERC7984("Confidential USDC", "cUSDC", "")
26+
ERC7984ERC20Wrapper(IERC20(usdc))
27+
{}
28+
}

contracts/test/MockConfidentialERC20.sol

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Confidential Contracts (last updated v0.3.1)
3+
4+
pragma solidity ^0.8.24;
5+
6+
import {externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol";
7+
import {IERC7984} from "./IERC7984.sol";
8+
9+
/// @dev Interface for ERC7984ERC20Wrapper contract.
10+
interface IERC7984ERC20Wrapper is IERC7984 {
11+
/**
12+
* @dev Wraps `amount` of the underlying token into a confidential token and sends it to `to`.
13+
*
14+
* Returns amount of wrapped token sent.
15+
*/
16+
function wrap(address to, uint256 amount) external returns (euint64);
17+
18+
/**
19+
* @dev Unwraps tokens from `from` and sends the underlying tokens to `to`. The caller must be `from`
20+
* or be an approved operator for `from`.
21+
*
22+
* Returns amount unwrapped.
23+
*
24+
* NOTE: The caller *must* already be approved by ACL for the given `amount`.
25+
*/
26+
function unwrap(
27+
address from,
28+
address to,
29+
externalEuint64 encryptedAmount,
30+
bytes calldata inputProof
31+
) external returns (euint64);
32+
33+
/// @dev Returns the address of the underlying ERC-20 token that is being wrapped.
34+
function underlying() external view returns (address);
35+
}
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Confidential Contracts (last updated v0.3.1) (token/ERC7984/extensions/ERC7984ERC20Wrapper.sol)
3+
4+
pragma solidity ^0.8.27;
5+
6+
import {FHE, externalEuint64, euint64} from "@fhevm/solidity/lib/FHE.sol";
7+
import {IERC1363Receiver} from "@openzeppelin/contracts/interfaces/IERC1363Receiver.sol";
8+
import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
9+
import {IERC20Metadata} from "@openzeppelin/contracts/interfaces/IERC20Metadata.sol";
10+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
11+
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
12+
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
13+
import {IERC7984} from "./../../../interfaces/IERC7984.sol";
14+
import {IERC7984ERC20Wrapper} from "./../../../interfaces/IERC7984ERC20Wrapper.sol";
15+
import {ERC7984} from "./../ERC7984.sol";
16+
17+
/**
18+
* @dev A wrapper contract built on top of {ERC7984} that allows wrapping an `ERC20` token
19+
* into an `ERC7984` token. The wrapper contract implements the `IERC1363Receiver` interface
20+
* which allows users to transfer `ERC1363` tokens directly to the wrapper with a callback to wrap the tokens.
21+
*
22+
* WARNING: Minting assumes the full amount of the underlying token transfer has been received, hence some non-standard
23+
* tokens such as fee-on-transfer or other deflationary-type tokens are not supported by this wrapper.
24+
*/
25+
abstract contract ERC7984ERC20Wrapper is ERC7984, IERC7984ERC20Wrapper, IERC1363Receiver {
26+
IERC20 private immutable _underlying;
27+
uint8 private immutable _decimals;
28+
uint256 private immutable _rate;
29+
30+
mapping(euint64 unwrapAmount => address recipient) private _unwrapRequests;
31+
32+
event UnwrapRequested(address indexed receiver, euint64 amount);
33+
event UnwrapFinalized(address indexed receiver, euint64 encryptedAmount, uint64 cleartextAmount);
34+
35+
error InvalidUnwrapRequest(euint64 amount);
36+
error ERC7984TotalSupplyOverflow();
37+
38+
constructor(IERC20 underlying_) {
39+
_underlying = underlying_;
40+
41+
uint8 tokenDecimals = _tryGetAssetDecimals(underlying_);
42+
uint8 maxDecimals = _maxDecimals();
43+
if (tokenDecimals > maxDecimals) {
44+
_decimals = maxDecimals;
45+
_rate = 10 ** (tokenDecimals - maxDecimals);
46+
} else {
47+
_decimals = tokenDecimals;
48+
_rate = 1;
49+
}
50+
}
51+
52+
/**
53+
* @dev `ERC1363` callback function which wraps tokens to the address specified in `data` or
54+
* the address `from` (if no address is specified in `data`). This function refunds any excess tokens
55+
* sent beyond the nearest multiple of {rate} to `from`. See {wrap} for more details on wrapping tokens.
56+
*/
57+
function onTransferReceived(
58+
address /*operator*/,
59+
address from,
60+
uint256 amount,
61+
bytes calldata data
62+
) public virtual returns (bytes4) {
63+
// check caller is the token contract
64+
require(underlying() == msg.sender, ERC7984UnauthorizedCaller(msg.sender));
65+
66+
// mint confidential token
67+
address to = data.length < 20 ? from : address(bytes20(data));
68+
_mint(to, FHE.asEuint64(SafeCast.toUint64(amount / rate())));
69+
70+
// transfer excess back to the sender
71+
uint256 excess = amount % rate();
72+
if (excess > 0) SafeERC20.safeTransfer(IERC20(underlying()), from, excess);
73+
74+
// return magic value
75+
return IERC1363Receiver.onTransferReceived.selector;
76+
}
77+
78+
/**
79+
* @dev See {IERC7984ERC20Wrapper-wrap}. Tokens are exchanged at a fixed rate specified by {rate} such that
80+
* `amount / rate()` confidential tokens are sent. The amount transferred in is rounded down to the nearest
81+
* multiple of {rate}.
82+
*
83+
* Returns the amount of wrapped token sent.
84+
*/
85+
function wrap(address to, uint256 amount) public virtual override returns (euint64) {
86+
// take ownership of the tokens
87+
SafeERC20.safeTransferFrom(IERC20(underlying()), msg.sender, address(this), amount - (amount % rate()));
88+
89+
// mint confidential token
90+
euint64 wrappedAmountSent = _mint(to, FHE.asEuint64(SafeCast.toUint64(amount / rate())));
91+
FHE.allowTransient(wrappedAmountSent, msg.sender);
92+
93+
return wrappedAmountSent;
94+
}
95+
96+
/// @dev Unwrap without passing an input proof. See {unwrap-address-address-bytes32-bytes} for more details.
97+
function unwrap(address from, address to, euint64 amount) public virtual returns (euint64) {
98+
require(FHE.isAllowed(amount, msg.sender), ERC7984UnauthorizedUseOfEncryptedAmount(amount, msg.sender));
99+
return _unwrap(from, to, amount);
100+
}
101+
102+
/**
103+
* @dev See {IERC7984ERC20Wrapper-unwrap}. `amount * rate()` underlying tokens are sent to `to`.
104+
*
105+
* NOTE: The unwrap request created by this function must be finalized by calling {finalizeUnwrap}.
106+
*/
107+
function unwrap(
108+
address from,
109+
address to,
110+
externalEuint64 encryptedAmount,
111+
bytes calldata inputProof
112+
) public virtual returns (euint64) {
113+
return _unwrap(from, to, FHE.fromExternal(encryptedAmount, inputProof));
114+
}
115+
116+
/// @dev Fills an unwrap request for a given cipher-text `unwrapAmount` with the `cleartextAmount` and `decryptionProof`.
117+
function finalizeUnwrap(
118+
euint64 unwrapAmount,
119+
uint64 unwrapAmountCleartext,
120+
bytes calldata decryptionProof
121+
) public virtual {
122+
address to = unwrapRequester(unwrapAmount);
123+
require(to != address(0), InvalidUnwrapRequest(unwrapAmount));
124+
delete _unwrapRequests[unwrapAmount];
125+
126+
bytes32[] memory handles = new bytes32[](1);
127+
handles[0] = euint64.unwrap(unwrapAmount);
128+
129+
bytes memory cleartexts = abi.encode(unwrapAmountCleartext);
130+
131+
FHE.checkSignatures(handles, cleartexts, decryptionProof);
132+
133+
SafeERC20.safeTransfer(IERC20(underlying()), to, unwrapAmountCleartext * rate());
134+
135+
emit UnwrapFinalized(to, unwrapAmount, unwrapAmountCleartext);
136+
}
137+
138+
/// @inheritdoc ERC7984
139+
function decimals() public view virtual override(IERC7984, ERC7984) returns (uint8) {
140+
return _decimals;
141+
}
142+
143+
/**
144+
* @dev Returns the rate at which the underlying token is converted to the wrapped token.
145+
* For example, if the `rate` is 1000, then 1000 units of the underlying token equal 1 unit of the wrapped token.
146+
*/
147+
function rate() public view virtual returns (uint256) {
148+
return _rate;
149+
}
150+
151+
/// @inheritdoc IERC7984ERC20Wrapper
152+
function underlying() public view virtual override returns (address) {
153+
return address(_underlying);
154+
}
155+
156+
/// @inheritdoc IERC165
157+
function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC7984) returns (bool) {
158+
return
159+
interfaceId == type(IERC7984ERC20Wrapper).interfaceId ||
160+
interfaceId == type(IERC1363Receiver).interfaceId ||
161+
super.supportsInterface(interfaceId);
162+
}
163+
164+
/**
165+
* @dev Returns the underlying balance divided by the {rate}, a value greater or equal to the actual
166+
* {confidentialTotalSupply}.
167+
*/
168+
function inferredTotalSupply() public view virtual returns (uint256) {
169+
return IERC20(underlying()).balanceOf(address(this)) / rate();
170+
}
171+
172+
/// @dev Returns the maximum total supply of wrapped tokens supported by the encrypted datatype.
173+
function maxTotalSupply() public view virtual returns (uint256) {
174+
return type(uint64).max;
175+
}
176+
177+
/**
178+
* @dev Get the address that has a pending unwrap request for the given `unwrapAmount`.
179+
* Returns `address(0)` if no pending unwrap request exists for that handle.
180+
*/
181+
function unwrapRequester(euint64 unwrapAmount) public view virtual returns (address) {
182+
return _unwrapRequests[unwrapAmount];
183+
}
184+
185+
function _checkConfidentialTotalSupply() internal virtual {
186+
if (inferredTotalSupply() > maxTotalSupply()) {
187+
revert ERC7984TotalSupplyOverflow();
188+
}
189+
}
190+
191+
/// @inheritdoc ERC7984
192+
function _update(address from, address to, euint64 amount) internal virtual override returns (euint64) {
193+
if (from == address(0)) {
194+
_checkConfidentialTotalSupply();
195+
}
196+
return super._update(from, to, amount);
197+
}
198+
199+
/// @dev Internal logic for handling the creation of unwrap requests.
200+
function _unwrap(address from, address to, euint64 amount) internal virtual returns (euint64) {
201+
require(to != address(0), ERC7984InvalidReceiver(to));
202+
require(from == msg.sender || isOperator(from, msg.sender), ERC7984UnauthorizedSpender(from, msg.sender));
203+
204+
// try to burn, see how much we actually got
205+
euint64 unwrapAmount = _burn(from, amount);
206+
FHE.makePubliclyDecryptable(unwrapAmount);
207+
208+
assert(unwrapRequester(unwrapAmount) == address(0));
209+
210+
// WARNING: Storing unwrap requests in a mapping from cipher-text to address assumes that
211+
// cipher-texts are unique--this holds here but is not always true. Be cautious when assuming
212+
// cipher-text uniqueness.
213+
_unwrapRequests[unwrapAmount] = to;
214+
215+
emit UnwrapRequested(to, unwrapAmount);
216+
return unwrapAmount;
217+
}
218+
219+
function _fallbackUnderlyingDecimals() internal pure virtual returns (uint8) {
220+
return 18;
221+
}
222+
223+
function _maxDecimals() internal pure virtual returns (uint8) {
224+
return 6;
225+
}
226+
227+
function _tryGetAssetDecimals(IERC20 asset_) private view returns (uint8 assetDecimals) {
228+
(bool success, bytes memory encodedDecimals) = address(asset_).staticcall(
229+
abi.encodeCall(IERC20Metadata.decimals, ())
230+
);
231+
if (success && encodedDecimals.length == 32) {
232+
return abi.decode(encodedDecimals, (uint8));
233+
}
234+
return _fallbackUnderlyingDecimals();
235+
}
236+
}

scripts/deploy.ts

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,52 @@ async function main() {
44
const [deployer] = await ethers.getSigners();
55
console.log("Deploying with account:", deployer.address);
66

7-
// Deploy VaultFactory
7+
// ── Deploy MockUSDC ────────────────────────────────────────────────
8+
const usdc = await (await ethers.getContractFactory("MockERC20")).deploy("USD Coin", "USDC", 6);
9+
await usdc.waitForDeployment();
10+
const usdcAddr = await usdc.getAddress();
11+
console.log("MockUSDC deployed to:", usdcAddr);
12+
13+
// ── Deploy ConfidentialUSDC ────────────────────────────────────────
14+
// No gateway address needed — fulfillUnwrap is trustless via FHE.checkSignatures.
15+
// Anyone can call fulfillUnwrap with a valid KMS decryption proof.
16+
// On Sepolia: obtain proofs from https://relayer.testnet.zama.org
17+
const confidentialUsdc = await (await ethers.getContractFactory("ConfidentialUSDC")).deploy(usdcAddr);
18+
await confidentialUsdc.waitForDeployment();
19+
const confidentialUsdcAddr = await confidentialUsdc.getAddress();
20+
console.log("ConfidentialUSDC deployed to:", confidentialUsdcAddr);
21+
22+
// ── Deploy VaultFactory ────────────────────────────────────────────
823
const factory = await (await ethers.getContractFactory("VaultFactory")).deploy();
924
await factory.waitForDeployment();
1025
const factoryAddr = await factory.getAddress();
1126
console.log("VaultFactory deployed to:", factoryAddr);
1227

13-
// Register ERC20Vault type
14-
const VAULT_TYPE = ethers.keccak256(ethers.toUtf8Bytes("ERC20Vault"));
15-
const vaultCreationCode = (await ethers.getContractFactory("ERC20Vault")).bytecode;
16-
const tx = await factory.registerVaultType(VAULT_TYPE, vaultCreationCode);
17-
await tx.wait();
18-
console.log("ERC20Vault type registered:", VAULT_TYPE);
28+
// ── Register ERC20Vault type ───────────────────────────────────────
29+
const ERC20_VAULT_TYPE = ethers.keccak256(ethers.toUtf8Bytes("ERC20Vault"));
30+
const erc20VaultCode = (await ethers.getContractFactory("ERC20Vault")).bytecode;
31+
const tx1 = await factory.registerVaultType(ERC20_VAULT_TYPE, erc20VaultCode);
32+
await tx1.wait();
33+
console.log("ERC20Vault type registered:", ERC20_VAULT_TYPE);
34+
35+
// ── Register ConfidentialVault type ───────────────────────────────
36+
const CONF_VAULT_TYPE = ethers.keccak256(ethers.toUtf8Bytes("ConfidentialVault"));
37+
const confVaultCode = (await ethers.getContractFactory("ConfidentialVault")).bytecode;
38+
const tx2 = await factory.registerVaultType(CONF_VAULT_TYPE, confVaultCode);
39+
await tx2.wait();
40+
console.log("ConfidentialVault type registered:", CONF_VAULT_TYPE);
41+
42+
// ── Summary ────────────────────────────────────────────────────────
43+
console.log("\nDeployment complete:");
44+
console.log(" MockUSDC: ", usdcAddr);
45+
console.log(" ConfidentialUSDC: ", confidentialUsdcAddr);
46+
console.log(" VaultFactory: ", factoryAddr);
47+
console.log(" ERC20Vault type: ", ERC20_VAULT_TYPE);
48+
console.log(" ConfVault type: ", CONF_VAULT_TYPE);
1949
}
2050

2151
main().catch((error) => {
2252
console.error(error);
2353
process.exitCode = 1;
2454
});
55+

0 commit comments

Comments
 (0)